Merge pull request #264 from frizynn/fix/gsd-merge-all-conflicts

fix: auto-resolve .gsd/ planning artifact conflicts during slice merge
This commit is contained in:
TÂCHES 2026-03-13 15:40:49 -06:00 committed by GitHub
commit 40e30f61dc
2 changed files with 49 additions and 3 deletions

View file

@ -673,10 +673,11 @@ export class GitServiceImpl {
try {
this.git(mergeArgs);
} catch (mergeError) {
// Check if conflicts are limited to runtime files we can auto-resolve (#189)
// Check if conflicts can be auto-resolved (#189, #218)
const conflicted = this.git(["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
if (conflicted) {
const conflictedFiles = conflicted.split("\n").filter(Boolean);
const allGsd = conflictedFiles.every(f => f.startsWith(".gsd/"));
const allRuntime = conflictedFiles.every(f =>
RUNTIME_EXCLUSION_PATHS.some(excl => f.startsWith(excl.replace(/\/$/, ""))),
);
@ -688,12 +689,21 @@ export class GitServiceImpl {
}
this.git(["add", "-A"], { allowFailure: true });
// Don't throw — let the merge proceed
} else if (allGsd) {
// Non-runtime .gsd/ conflicts (DECISIONS.md, REQUIREMENTS.md, ROADMAP.md, etc.):
// The slice branch has the authoritative .gsd/ state since the LLM just finished
// updating these artifacts during complete-slice. Take theirs (the slice branch).
for (const f of conflictedFiles) {
this.git(["checkout", "--theirs", "--", f], { allowFailure: true });
}
this.git(["add", "-A"], { allowFailure: true });
// Don't throw — let the merge proceed
} else {
// Non-runtime conflicts: reset and throw as before
// Non-.gsd/ conflicts: reset and throw as before
this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
throw new Error(
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts. ` +
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts in non-.gsd/ files. ` +
`Working tree has been reset to a clean state. ` +
`Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
`Original error: ${msg}`,

View file

@ -953,6 +953,42 @@ async function main(): Promise<void> {
rmSync(repo, { recursive: true, force: true });
}
// ─── mergeSliceToMain: auto-resolve .gsd/ planning artifact conflicts ──
console.log("\n=== mergeSliceToMain: auto-resolve .gsd/ planning conflicts ===");
{
const repo = initBranchTestRepo();
const svc = new GitServiceImpl(repo);
// Create a .gsd/ planning artifact on main (simulates reassess-roadmap)
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n");
run("git add -A", repo);
run("git commit -m 'add decisions on main'", repo);
// Create slice branch and modify the same .gsd/ file differently
svc.ensureSliceBranch("M001", "S01");
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n- D002: New decision from slice\n");
createFile(repo, "src/feature.ts", "export const x = 1;");
run("git add -A", repo);
run("git commit -m 'slice work with .gsd/ changes'", repo);
// Back on main, modify the same .gsd/ file to create a conflict
svc.switchToMain();
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Updated decision on main\n");
run("git add -A", repo);
run("git commit -m 'update decisions on main'", repo);
// Merge should auto-resolve .gsd/ conflicts by taking theirs (slice branch)
const result = svc.mergeSliceToMain("M001", "S01", "Feature with .gsd/ conflicts");
assertEq(result.deletedBranch, true, ".gsd/ conflict auto-resolved: branch deleted");
// Verify the merge succeeded and src file is present
assert(existsSync(join(repo, "src/feature.ts")), ".gsd/ conflict auto-resolved: src file merged");
rmSync(repo, { recursive: true, force: true });
}
// ═══════════════════════════════════════════════════════════════════════
// S05: Enhanced features — merge guards, snapshots, auto-push, rich commits
// ═══════════════════════════════════════════════════════════════════════