fix: force-add .gsd/ planning artifacts and guard handleAgentEnd reentrancy (#341)
Two bugs that interact to silently kill auto-mode: 1. smartStage() uses `git add -A` which respects .gitignore. When .gsd/ is gitignored (common — GSD's own baseline patterns only ignore runtime files, but many projects ignore all of .gsd/), new planning artifacts (CONTEXT.md, SUMMARY.md, PLAN.md, UAT.md, DECISIONS.md) are never staged. They exist on disk but not in git. Squash-merges then delete them on main because they appear as "removed relative to main." Fix: after `git add -A`, force-add `.gsd/milestones/` and root planning files. Runtime paths are still excluded by the subsequent `git reset HEAD` step. 2. handleAgentEnd() has no reentrancy guard. Background job notifications (async_bash results) trigger additional agent_end events while the first handler is still running (it yields at every await). Concurrent dispatchNextUnit() calls race on newSession() — one cancels the other, silently stopping auto-mode. Combined with bug #1, the second dispatchNextUnit call may find the active milestone's CONTEXT.md missing (never committed, lost during branch switch) and stop with "No context or roadmap yet." Fix: boolean guard prevents concurrent execution. Reset in stopAuto() so restarts aren't blocked. Fixes #TBD Co-authored-by: TÂCHES <afromanguy@me.com>
This commit is contained in:
parent
9c1bf837fb
commit
c13b1bfc6e
2 changed files with 48 additions and 0 deletions
|
|
@ -276,6 +276,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|||
currentMilestoneId = null;
|
||||
cachedSliceProgress = null;
|
||||
pendingCrashRecovery = null;
|
||||
_handlingAgentEnd = false;
|
||||
ctx?.ui.setStatus("gsd-auto", undefined);
|
||||
ctx?.ui.setWidget("gsd-progress", undefined);
|
||||
ctx?.ui.setFooter(undefined);
|
||||
|
|
@ -526,11 +527,23 @@ export async function startAuto(
|
|||
|
||||
// ─── Agent End Handler ────────────────────────────────────────────────────────
|
||||
|
||||
/** Guard against concurrent handleAgentEnd execution. Background job
|
||||
* notifications and other system messages can trigger multiple agent_end
|
||||
* events before the first handler finishes (the handler yields at every
|
||||
* await). Without this guard, concurrent dispatchNextUnit calls race on
|
||||
* newSession(), causing one to cancel the other and silently stopping
|
||||
* auto-mode. */
|
||||
let _handlingAgentEnd = false;
|
||||
|
||||
export async function handleAgentEnd(
|
||||
ctx: ExtensionContext,
|
||||
pi: ExtensionAPI,
|
||||
): Promise<void> {
|
||||
if (!active || !cmdCtx) return;
|
||||
if (_handlingAgentEnd) return;
|
||||
_handlingAgentEnd = true;
|
||||
|
||||
try {
|
||||
|
||||
// Unit completed — clear its timeout
|
||||
clearUnitTimeout();
|
||||
|
|
@ -580,6 +593,10 @@ export async function handleAgentEnd(
|
|||
}
|
||||
|
||||
await dispatchNextUnit(ctx, pi);
|
||||
|
||||
} finally {
|
||||
_handlingAgentEnd = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Step Mode Wizard ─────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -98,6 +98,22 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
|
|||
".gsd/STATE.md",
|
||||
];
|
||||
|
||||
/**
|
||||
* GSD planning artifact paths that must be force-added even when .gsd/
|
||||
* is in .gitignore. These are durable planning files that the agent writes
|
||||
* and that must survive squash-merges to main.
|
||||
*
|
||||
* `git add --force` is a no-op when the path doesn't exist or has no
|
||||
* changes, so this list is safe to apply unconditionally.
|
||||
*/
|
||||
const GSD_DURABLE_PATHS: readonly string[] = [
|
||||
".gsd/milestones/",
|
||||
".gsd/DECISIONS.md",
|
||||
".gsd/QUEUE.md",
|
||||
".gsd/PROJECT.md",
|
||||
".gsd/REQUIREMENTS.md",
|
||||
];
|
||||
|
||||
// ─── Integration Branch Metadata ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -291,6 +307,21 @@ export class GitServiceImpl {
|
|||
// git reset HEAD silently succeeds when the path isn't staged, so no
|
||||
// error handling is needed per-path.
|
||||
this.git(["add", "-A"]);
|
||||
|
||||
// Force-add GSD planning artifacts that live under .gsd/ but may be
|
||||
// blocked by a .gsd/ gitignore pattern. `git add -A` respects .gitignore,
|
||||
// so new files (CONTEXT.md, SUMMARY.md, PLAN.md, etc.) in gitignored
|
||||
// directories are silently skipped. Without this force-add, planning
|
||||
// artifacts are never committed — they exist on disk but not in git.
|
||||
// Squash-merges then delete them on main because they appear as "removed
|
||||
// relative to main" during the merge.
|
||||
//
|
||||
// Only force-add durable planning paths — runtime paths are excluded
|
||||
// by the reset step below.
|
||||
for (const durablePath of GSD_DURABLE_PATHS) {
|
||||
this.git(["add", "--force", "--", durablePath], { allowFailure: true });
|
||||
}
|
||||
|
||||
for (const exclusion of allExclusions) {
|
||||
this.git(["reset", "HEAD", "--", exclusion], { allowFailure: true });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue