feat: Add merge conflict detection and agent resolution in initiative review
Pre-merge mergeability check via `git merge-tree --write-tree` (dry-run, no side effects). When conflicts exist the "Merge & Push" button is disabled and a ConflictResolutionPanel shows conflict files with options to resolve manually or spawn a conflict-resolution agent. Agent questions appear inline via QuestionForm; on completion the mergeability re-checks automatically. New server-side: MergeabilityResult type, BranchManager.checkMergeability, conflict-resolution prompt, checkInitiativeMergeability query, spawnConflictResolutionAgent mutation, getActiveConflictAgent query. New frontend: useConflictAgent hook, ConflictResolutionPanel component, mergeability badge + panel integration in InitiativeReview.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* a worktree to be checked out.
|
||||
*/
|
||||
|
||||
import type { MergeResult, BranchCommit } from './types.js';
|
||||
import type { MergeResult, MergeabilityResult, BranchCommit } from './types.js';
|
||||
|
||||
export interface BranchManager {
|
||||
/**
|
||||
@@ -68,4 +68,11 @@ export interface BranchManager {
|
||||
* Defaults to 'origin' if no remote specified.
|
||||
*/
|
||||
pushBranch(repoPath: string, branch: string, remote?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Dry-run merge check — determines if sourceBranch can be cleanly merged
|
||||
* into targetBranch without actually performing the merge.
|
||||
* Uses `git merge-tree --write-tree` (git 2.38+).
|
||||
*/
|
||||
checkMergeability(repoPath: string, sourceBranch: string, targetBranch: string): Promise<MergeabilityResult>;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
export type { WorktreeManager } from './types.js';
|
||||
|
||||
// Domain types
|
||||
export type { Worktree, WorktreeDiff, MergeResult } from './types.js';
|
||||
export type { Worktree, WorktreeDiff, MergeResult, MergeabilityResult } from './types.js';
|
||||
|
||||
// Adapters
|
||||
export { SimpleGitWorktreeManager } from './manager.js';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import type { BranchManager } from './branch-manager.js';
|
||||
import type { MergeResult, BranchCommit } from './types.js';
|
||||
import type { MergeResult, MergeabilityResult, BranchCommit } from './types.js';
|
||||
import { createModuleLogger } from '../logger/index.js';
|
||||
|
||||
const log = createModuleLogger('branch-manager');
|
||||
@@ -164,4 +164,41 @@ export class SimpleGitBranchManager implements BranchManager {
|
||||
await git.push(remote, branch);
|
||||
log.info({ repoPath, branch, remote }, 'branch pushed to remote');
|
||||
}
|
||||
|
||||
async checkMergeability(repoPath: string, sourceBranch: string, targetBranch: string): Promise<MergeabilityResult> {
|
||||
const git = simpleGit(repoPath);
|
||||
|
||||
try {
|
||||
// git merge-tree --write-tree merges source INTO target virtually.
|
||||
// Exit 0 = clean merge, non-zero = conflicts.
|
||||
await git.raw(['merge-tree', '--write-tree', targetBranch, sourceBranch]);
|
||||
log.debug({ repoPath, sourceBranch, targetBranch }, 'merge-tree check: clean');
|
||||
return { mergeable: true };
|
||||
} catch (err) {
|
||||
const stderr = err instanceof Error ? err.message : String(err);
|
||||
|
||||
// Parse conflict file names from "CONFLICT (content): Merge conflict in <path>"
|
||||
const conflictPattern = /CONFLICT \([^)]+\): (?:Merge conflict in|.* -> )(.+)/g;
|
||||
const conflicts: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = conflictPattern.exec(stderr)) !== null) {
|
||||
conflicts.push(match[1].trim());
|
||||
}
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
log.debug({ repoPath, sourceBranch, targetBranch, conflicts }, 'merge-tree check: conflicts');
|
||||
return { mergeable: false, conflicts };
|
||||
}
|
||||
|
||||
// If we couldn't parse conflicts but the command failed, it's still a conflict
|
||||
// (could be add/add, rename conflicts, etc.)
|
||||
if (stderr.includes('CONFLICT')) {
|
||||
log.debug({ repoPath, sourceBranch, targetBranch }, 'merge-tree check: unparsed conflicts');
|
||||
return { mergeable: false, conflicts: ['(unable to parse conflict details)'] };
|
||||
}
|
||||
|
||||
// Genuine error (not a conflict)
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,19 @@ export interface MergeResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mergeability Check
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Result of a dry-run merge check.
|
||||
* No side effects — only tells you whether the merge would succeed.
|
||||
*/
|
||||
export interface MergeabilityResult {
|
||||
mergeable: boolean;
|
||||
conflicts?: string[];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Branch Commit Info
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user