gsd-pi 2.80.0-dev.e51d2c88c → 2.80.0-dev.e6c48c3af

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +30 -6
  3. package/dist/resources/extensions/gsd/auto/run-unit.js +4 -1
  4. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -1
  5. package/dist/resources/extensions/gsd/auto.js +18 -1
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +54 -4
  7. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  8. package/dist/resources/extensions/gsd/git-service.js +36 -4
  9. package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
  10. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  11. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  12. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  13. package/dist/web/standalone/.next/BUILD_ID +1 -1
  14. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  15. package/dist/web/standalone/.next/build-manifest.json +2 -2
  16. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  17. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.html +1 -1
  34. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  41. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  43. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  44. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  45. package/package.json +1 -1
  46. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +15 -0
  47. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  49. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/agent-session.js +4 -3
  51. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  53. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  55. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
  56. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  58. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +18 -0
  59. package/packages/pi-coding-agent/src/core/agent-session.ts +6 -3
  60. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -0
  61. package/packages/pi-coding-agent/src/core/extensions/types.ts +5 -0
  62. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  63. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  64. package/src/resources/extensions/gsd/auto/phases.ts +50 -15
  65. package/src/resources/extensions/gsd/auto/run-unit.ts +4 -1
  66. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -1
  67. package/src/resources/extensions/gsd/auto.ts +18 -1
  68. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +66 -4
  69. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  70. package/src/resources/extensions/gsd/git-service.ts +46 -8
  71. package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
  72. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  73. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +144 -1
  74. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +166 -4
  75. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  76. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  77. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  78. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
  79. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
  80. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  81. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  82. /package/dist/web/standalone/.next/static/{8F5YpnZNBaooIWGF4GBV3 → 4dQ9NTZJ8pEvFwBgDUX93}/_buildManifest.js +0 -0
  83. /package/dist/web/standalone/.next/static/{8F5YpnZNBaooIWGF4GBV3 → 4dQ9NTZJ8pEvFwBgDUX93}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- 4a20b588f749081c
1
+ 533c696bf1b57db9
@@ -129,6 +129,22 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
129
129
  }
130
130
  await deps.stopAuto(ctx, pi, reason);
131
131
  }
132
+ async function stopOnPostflightRecoveryNeeded(ic, result, milestoneId) {
133
+ if (!result.needsManualRecovery)
134
+ return null;
135
+ const { ctx, pi, deps } = ic;
136
+ const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
137
+ ctx.ui.notify(`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`, "error");
138
+ await deps.stopAuto(ctx, pi, reason);
139
+ return { action: "break", reason: "postflight-stash-restore-failed" };
140
+ }
141
+ async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
142
+ if (!preflight.stashPushed)
143
+ return null;
144
+ const { ctx, s, deps } = ic;
145
+ const result = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
146
+ return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
147
+ }
132
148
  async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
133
149
  ic.deps.emitJournalEvent({
134
150
  ts: new Date().toISOString(),
@@ -475,6 +491,8 @@ export async function runPreDispatch(ic, loopState) {
475
491
  const preflightTransition = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
476
492
  try {
477
493
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
494
+ // Prevent stopAuto() from attempting the same merge again if postflight recovery stops here.
495
+ s.milestoneMergedInPhases = true;
478
496
  }
479
497
  catch (mergeErr) {
480
498
  if (mergeErr instanceof MergeConflictError) {
@@ -490,8 +508,10 @@ export async function runPreDispatch(ic, loopState) {
490
508
  return { action: "break", reason: "merge-failed" };
491
509
  }
492
510
  // #2909: postflight — restore stashed changes after successful merge
493
- if (preflightTransition.stashPushed) {
494
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightTransition.stashMarker, ctx.ui.notify.bind(ctx.ui));
511
+ {
512
+ const postflightStop = await restorePreflightStashOrStop(ic, preflightTransition, s.currentMilestoneId);
513
+ if (postflightStop)
514
+ return postflightStop;
495
515
  }
496
516
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
497
517
  deps.invalidateAllCaches();
@@ -566,8 +586,10 @@ export async function runPreDispatch(ic, loopState) {
566
586
  return { action: "break", reason: "merge-failed" };
567
587
  }
568
588
  // #2909: postflight — restore stashed changes after successful merge
569
- if (preflightAllComplete.stashPushed) {
570
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightAllComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
589
+ {
590
+ const postflightStop = await restorePreflightStashOrStop(ic, preflightAllComplete, s.currentMilestoneId);
591
+ if (postflightStop)
592
+ return postflightStop;
571
593
  }
572
594
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
573
595
  }
@@ -652,8 +674,10 @@ export async function runPreDispatch(ic, loopState) {
652
674
  return { action: "break", reason: "merge-failed" };
653
675
  }
654
676
  // #2909: postflight — restore stashed changes after successful merge
655
- if (preflightComplete.stashPushed) {
656
- deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
677
+ {
678
+ const postflightStop = await restorePreflightStashOrStop(ic, preflightComplete, s.currentMilestoneId);
679
+ if (postflightStop)
680
+ return postflightStop;
657
681
  }
658
682
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
659
683
  }
@@ -55,7 +55,10 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
55
55
  const sessionAbortController = new AbortController();
56
56
  _setSessionSwitchInFlight(true);
57
57
  try {
58
- const sessionPromise = s.cmdCtx.newSession({ abortSignal: sessionAbortController.signal }).finally(() => {
58
+ const sessionPromise = s.cmdCtx.newSession({
59
+ abortSignal: sessionAbortController.signal,
60
+ cwd: s.basePath,
61
+ }).finally(() => {
59
62
  if (sessionSwitchGeneration === mySessionSwitchGeneration) {
60
63
  _setSessionSwitchInFlight(false);
61
64
  }
@@ -246,7 +246,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
246
246
  ctx.ui.notify(`${msg}. Cancelling dispatch to avoid running in the wrong directory.`, "error");
247
247
  return;
248
248
  }
249
- const result = await ctx.newSession();
249
+ const result = await ctx.newSession({ cwd: dispatchBase });
250
250
  if (result.cancelled) {
251
251
  ctx.ui.notify("Session creation cancelled.", "warning");
252
252
  return;
@@ -720,6 +720,8 @@ export async function stopAuto(ctx, pi, reason) {
720
720
  if (s.workerId) {
721
721
  markWorkerStopping(s.workerId);
722
722
  }
723
+ s.workerId = null;
724
+ s.milestoneLeaseToken = null;
723
725
  }
724
726
  catch (e) {
725
727
  debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
@@ -852,6 +854,21 @@ export async function stopAuto(ctx, pi, reason) {
852
854
  catch (e) {
853
855
  debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
854
856
  }
857
+ // Re-root the active command session/tool runtime after worktree teardown.
858
+ // mergeAndExit restores process.cwd(), but AgentSession has already captured
859
+ // its own cwd for tools and system prompt; refresh it before returning to the
860
+ // user so follow-up commands do not target a removed milestone worktree.
861
+ if (s.originalBasePath && ctx && s.cmdCtx) {
862
+ try {
863
+ const result = await s.cmdCtx.newSession({ cwd: s.basePath });
864
+ if (result.cancelled) {
865
+ logWarning("engine", "post-stop session re-root was cancelled", { file: "auto.ts", basePath: s.basePath });
866
+ }
867
+ }
868
+ catch (err) {
869
+ logWarning("engine", `post-stop session re-root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts", basePath: s.basePath });
870
+ }
871
+ }
855
872
  // ── Step 8: Ledger notification ──
856
873
  try {
857
874
  const ledger = getLedger();
@@ -1831,7 +1848,7 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
1831
1848
  }
1832
1849
  return false;
1833
1850
  }
1834
- const result = await s.cmdCtx.newSession();
1851
+ const result = await s.cmdCtx.newSession({ cwd: s.basePath });
1835
1852
  if (result.cancelled) {
1836
1853
  await stopAuto(ctx, pi);
1837
1854
  return false;
@@ -23,6 +23,7 @@ import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserAp
23
23
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
24
24
  let isFirstSession = true;
25
25
  let approvalQuestionAbortInFlight = false;
26
+ let deferredApprovalGate = null;
26
27
  async function deriveGsdState(basePath) {
27
28
  const { deriveState } = await import("../state.js");
28
29
  return deriveState(basePath);
@@ -71,6 +72,43 @@ async function applyCompactionThresholdOverride(ctx) {
71
72
  // Non-fatal: leave any existing override in place.
72
73
  }
73
74
  }
75
+ function clearDeferredApprovalGate(basePath) {
76
+ if (!basePath || deferredApprovalGate?.basePath === basePath) {
77
+ deferredApprovalGate = null;
78
+ }
79
+ }
80
+ function deferApprovalGate(gateId, basePath) {
81
+ deferredApprovalGate = { gateId, basePath };
82
+ }
83
+ function activateDeferredApprovalGate(basePath) {
84
+ if (deferredApprovalGate?.basePath !== basePath)
85
+ return;
86
+ setPendingGate(deferredApprovalGate.gateId, basePath);
87
+ deferredApprovalGate = null;
88
+ }
89
+ function isContextDraftSummarySave(toolName, input) {
90
+ if (toolName !== "gsd_summary_save" && toolName !== "summary_save")
91
+ return false;
92
+ if (!input || typeof input !== "object")
93
+ return false;
94
+ return input.artifact_type === "CONTEXT-DRAFT";
95
+ }
96
+ function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
97
+ if (deferredApprovalGate?.basePath !== basePath)
98
+ return { block: false };
99
+ if (toolName === "ask_user_questions")
100
+ return { block: false };
101
+ if (isContextDraftSummarySave(toolName, input))
102
+ return { block: false };
103
+ return {
104
+ block: true,
105
+ reason: [
106
+ `HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
107
+ `Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
108
+ `Wait for the user's answer before calling additional tools.`,
109
+ ].join(" "),
110
+ };
111
+ }
74
112
  export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
75
113
  return resolveWorktreeProjectRoot(cwd);
76
114
  }
@@ -117,6 +155,7 @@ export function registerHooks(pi, ecosystemHandlers) {
117
155
  resetWriteGateState(process.cwd());
118
156
  resetToolCallLoopGuard();
119
157
  approvalQuestionAbortInFlight = false;
158
+ clearDeferredApprovalGate();
120
159
  await resetAskUserQuestionsTurnCache();
121
160
  await syncServiceTierStatus(ctx);
122
161
  await applyDisabledModelProviderPolicy(ctx);
@@ -165,6 +204,7 @@ export function registerHooks(pi, ecosystemHandlers) {
165
204
  initSessionNotifications(ctx);
166
205
  resetWriteGateState(process.cwd());
167
206
  resetToolCallLoopGuard();
207
+ clearDeferredApprovalGate();
168
208
  await resetAskUserQuestionsTurnCache();
169
209
  clearDiscussionFlowState(process.cwd());
170
210
  await syncServiceTierStatus(ctx);
@@ -201,6 +241,7 @@ export function registerHooks(pi, ecosystemHandlers) {
201
241
  markDepthVerified(milestoneId, beforeAgentBasePath);
202
242
  clearPendingGate(beforeAgentBasePath);
203
243
  }
244
+ clearDeferredApprovalGate(beforeAgentBasePath);
204
245
  // GSD's own context injection (existing behavior — unchanged).
205
246
  const { buildBeforeAgentStartResult } = await import("./system-context.js");
206
247
  const gsdResult = await buildBeforeAgentStartResult(event, ctx);
@@ -244,7 +285,12 @@ export function registerHooks(pi, ecosystemHandlers) {
244
285
  resetToolCallLoopGuard();
245
286
  await resetAskUserQuestionsTurnCache();
246
287
  const { handleAgentEnd } = await import("./agent-end-recovery.js");
247
- await handleAgentEnd(pi, event, ctx);
288
+ try {
289
+ await handleAgentEnd(pi, event, ctx);
290
+ }
291
+ finally {
292
+ activateDeferredApprovalGate(process.cwd());
293
+ }
248
294
  });
249
295
  // Squash-merge quick-task branch back to the original branch after the
250
296
  // agent turn completes (#2668). cleanupQuickBranch is a no-op when no
@@ -342,11 +388,12 @@ export function registerHooks(pi, ecosystemHandlers) {
342
388
  return;
343
389
  const gateId = approvalGateIdForUnit(unitType, unitId);
344
390
  if (gateId)
345
- setPendingGate(gateId, process.cwd());
391
+ deferApprovalGate(gateId, process.cwd());
346
392
  approvalQuestionAbortInFlight = true;
347
393
  ctx.ui.notify(`${unitType}${unitId ? ` ${unitId}` : ""} is waiting for your approval - pausing before more tool calls run.`, "info");
348
- // The pending gate set above blocks subsequent non-read-only tool calls
349
- // via the tool_call hook below, so we do not abort the in-flight stream.
394
+ // The durable pending gate is activated at agent_end so same-turn
395
+ // CONTEXT-DRAFT persistence can finish after the text boundary streams.
396
+ // The tool_call hook below still blocks non-draft tools in this turn.
350
397
  // Aborting mid-stream eats the model's question text on external CLI
351
398
  // providers (Claude Code SDK) because lastTextContent isn't populated
352
399
  // from in-flight builder state — the user only ever sees "Claude Code
@@ -377,6 +424,9 @@ export function registerHooks(pi, ecosystemHandlers) {
377
424
  if (loopCheck.block) {
378
425
  return { block: true, reason: loopCheck.reason };
379
426
  }
427
+ const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
428
+ if (deferredGateGuard.block)
429
+ return deferredGateGuard;
380
430
  // ── Discussion gate enforcement: track pending gate questions ─────────
381
431
  // Only gate-shaped ask_user_questions calls should block execution.
382
432
  // The gate stays pending until the user selects the approval option.
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * #2909: Adds a fast-path git status check before milestone completion merges.
5
5
  * When the working tree is dirty the user is warned and changes are auto-stashed
6
- * so the merge can proceed cleanly. After the merge completes, postflightPopStash
7
- * restores the stashed changes.
6
+ * so the merge can proceed cleanly. After the merge completes, postflightPopStash
7
+ * restores the stashed changes and reports whether manual recovery is needed.
8
8
  *
9
9
  * Design constraints (from Trek-e approval):
10
10
  * - Warn the user before stashing (no silent surprises)
11
11
  * - git stash push / git stash pop only — no custom stash management layer
12
- * - Stash/pop errors are logged but MUST NOT block the merge
12
+ * - Stash/pop errors are logged but MUST NOT block the merge itself
13
13
  * - Fast-path status check — clean trees pay no extra cost
14
14
  */
15
15
  import { execFileSync } from "node:child_process";
@@ -98,7 +98,8 @@ export function preflightCleanRoot(basePath, milestoneId, notify) {
98
98
  *
99
99
  * Only called when preflightCleanRoot returned stashPushed=true.
100
100
  * Any pop error (e.g. conflict) is logged and notified but does NOT throw —
101
- * the merge already completed successfully.
101
+ * the merge already completed successfully. Callers must treat
102
+ * needsManualRecovery=true as a dirty workspace stop, not a clean completion.
102
103
  */
103
104
  export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
104
105
  let stashRef = null;
@@ -108,7 +109,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
108
109
  const msg = `No matching GSD preflight stash found for milestone ${milestoneId}; leaving stash list untouched.`;
109
110
  logWarning("preflight", msg);
110
111
  notify(msg, "warning");
111
- return;
112
+ return {
113
+ restored: false,
114
+ needsManualRecovery: true,
115
+ message: msg,
116
+ };
112
117
  }
113
118
  execFileSync("git", ["stash", "pop", stashRef], {
114
119
  cwd: basePath,
@@ -116,7 +121,14 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
116
121
  encoding: "utf-8",
117
122
  env: GIT_NO_PROMPT_ENV,
118
123
  });
119
- notify(`Restored stashed changes after milestone ${milestoneId} merge.`, "info");
124
+ const msg = `Restored stashed changes after milestone ${milestoneId} merge.`;
125
+ notify(msg, "info");
126
+ return {
127
+ restored: true,
128
+ needsManualRecovery: false,
129
+ message: msg,
130
+ stashRef,
131
+ };
120
132
  }
121
133
  catch (err) {
122
134
  // Pop conflicts mean the merged code collides with the stashed changes.
@@ -127,5 +139,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
127
139
  const msg = `git stash pop ${stashRef ?? ""}`.trim() + ` failed after merge of milestone ${milestoneId}: ${err instanceof Error ? err.message : String(err)}. ${restoreHint}`;
128
140
  logWarning("preflight", msg);
129
141
  notify(msg, "warning");
142
+ return {
143
+ restored: false,
144
+ needsManualRecovery: true,
145
+ message: msg,
146
+ ...(stashRef ? { stashRef } : {}),
147
+ };
130
148
  }
131
149
  }
@@ -13,6 +13,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
13
13
  import { gsdRoot } from "./paths.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
+ import { logWarning } from "./workflow-logger.js";
16
17
  import { detectWorktreeName, } from "./worktree.js";
17
18
  import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
18
19
  import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeAddPaths, nativeResetSoft, nativeCommitSubject, _resetHasChangesCache, } from "./native-git-bridge.js";
@@ -535,14 +536,45 @@ export class GitServiceImpl {
535
536
  if (keyFiles.length === 0)
536
537
  return false;
537
538
  const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
538
- const paths = Array.from(new Set(keyFiles
539
+ const normalized = keyFiles
539
540
  .map(file => normalizeRepoRelativePath(this.basePath, file))
540
541
  .filter((file) => file !== null)
541
- .filter(file => !isExcludedScopedPath(file, allExclusions))));
542
+ .filter(file => !isExcludedScopedPath(file, allExclusions));
543
+ // Drop entries that don't exist on disk. The LLM occasionally lists files
544
+ // it intended to write but didn't (or names them with wrong casing/path).
545
+ // Pre-`b304f738b` `git add -A` swallowed these silently; the scoped
546
+ // pathspec form passes each path explicitly, so a single bad entry made
547
+ // the whole commit fail (see #5500). Filter so valid paths still commit.
548
+ const missing = [];
549
+ const existing = [];
550
+ for (const path of normalized) {
551
+ if (existsSync(join(this.basePath, path))) {
552
+ existing.push(path);
553
+ }
554
+ else {
555
+ missing.push(path);
556
+ }
557
+ }
558
+ if (missing.length > 0) {
559
+ logWarning("engine", `scoped stage: dropping ${missing.length} non-existent keyFile(s) from task commit: ${missing.join(", ")}`, { file: "git-service.ts" });
560
+ }
561
+ const paths = Array.from(new Set(existing));
542
562
  if (paths.length === 0)
543
563
  return false;
544
- nativeAddPaths(this.basePath, paths);
545
- return true;
564
+ try {
565
+ nativeAddPaths(this.basePath, paths);
566
+ return true;
567
+ }
568
+ catch (err) {
569
+ // Defense-in-depth: even after existence filtering, libgit2/git can
570
+ // still reject paths (gitignore matches, case-only differences on
571
+ // case-insensitive FS, submodule boundaries). Returning false lets
572
+ // autoCommit fall through to smartStage so the commit still goes out
573
+ // — restoring the resilience the unscoped path used to provide.
574
+ const msg = err instanceof Error ? err.message : String(err);
575
+ logWarning("engine", `scoped stage failed (${msg}); falling back to smartStage`, { file: "git-service.ts" });
576
+ return false;
577
+ }
546
578
  }
547
579
  /** Tracks whether runtime file cleanup has run this session. */
548
580
  _runtimeFilesCleanedUp = false;
@@ -328,6 +328,8 @@ function shouldValidateInputAsPath(raw) {
328
328
  const trimmed = raw.trim();
329
329
  if (!trimmed)
330
330
  return false;
331
+ if (isRuntimeOnlyInput(trimmed))
332
+ return false;
331
333
  const candidate = extractPathFromAnnotation(trimmed);
332
334
  if (!candidate)
333
335
  return false;
@@ -349,6 +351,9 @@ function shouldValidateInputAsPath(raw) {
349
351
  /[\\/]/.test(candidate) ||
350
352
  /[*?[\]{}]/.test(candidate));
351
353
  }
354
+ function isRuntimeOnlyInput(raw) {
355
+ return /\(\s*runtime\s*\)/i.test(raw);
356
+ }
352
357
  function containsGlobPattern(candidate) {
353
358
  return ["*", "?", "[", "]", "{", "}"].some((char) => candidate.includes(char));
354
359
  }
@@ -461,6 +466,8 @@ export function checkTaskOrdering(tasks, basePath) {
461
466
  const task = tasks[i];
462
467
  const filesToCheck = [...task.inputs];
463
468
  for (const file of filesToCheck) {
469
+ if (isRuntimeOnlyInput(file))
470
+ continue;
464
471
  if (!shouldValidateInputAsPath(file))
465
472
  continue;
466
473
  const normalizedFile = normalizeFilePath(file);
@@ -6,6 +6,8 @@ You are executing GSD auto-mode.
6
6
 
7
7
  Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
8
8
 
9
+ If any inlined plan, summary, verification command, or prior artifact names an absolute path outside `{{workingDirectory}}`, treat that path as stale context. Convert it to the equivalent relative path under `{{workingDirectory}}` before reading, writing, or executing. If no equivalent path exists under `{{workingDirectory}}`, record a verification failure and stop; do not edit or run commands in another checkout.
10
+
9
11
  ## Mission
10
12
 
11
13
  All slices are complete. Verify the integrated work, persist milestone completion, refresh project state, and write the final record future milestones will rely on.
@@ -22,7 +22,7 @@ import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js
22
22
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
23
23
  import { loadEffectiveGSDPreferences } from "./preferences.js";
24
24
  import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
25
- import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
25
+ import { claimMilestoneLease, refreshMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
26
26
  // ─── Path Comparison Helper ────────────────────────────────────────────────
27
27
  /**
28
28
  * Compare two paths for physical identity, tolerating trailing slashes,
@@ -119,24 +119,40 @@ export class WorktreeResolver {
119
119
  // milestone (re-entry within the same session).
120
120
  if (this.s.workerId) {
121
121
  if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
122
- // Already held no-op, the heartbeat in loop.ts refreshes TTL.
123
- }
124
- else {
125
- // If we held a different milestone, release it first so other
126
- // workers don't have to wait for TTL.
127
- if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
128
- try {
129
- releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
130
- }
131
- catch (err) {
132
- debugLog("WorktreeResolver", {
133
- action: "enterMilestone",
134
- milestoneId,
135
- releasePriorLeaseError: err instanceof Error ? err.message : String(err),
136
- });
137
- }
122
+ const refreshed = refreshMilestoneLease(this.s.workerId, milestoneId, this.s.milestoneLeaseToken);
123
+ if (refreshed) {
124
+ debugLog("WorktreeResolver", {
125
+ action: "enterMilestone",
126
+ milestoneId,
127
+ leaseRefreshed: true,
128
+ fencingToken: this.s.milestoneLeaseToken,
129
+ });
130
+ }
131
+ else {
132
+ debugLog("WorktreeResolver", {
133
+ action: "enterMilestone",
134
+ milestoneId,
135
+ staleLeaseToken: this.s.milestoneLeaseToken,
136
+ });
138
137
  this.s.milestoneLeaseToken = null;
139
138
  }
139
+ }
140
+ // If we held a different milestone, release it first so other
141
+ // workers don't have to wait for TTL.
142
+ if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
143
+ try {
144
+ releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
145
+ }
146
+ catch (err) {
147
+ debugLog("WorktreeResolver", {
148
+ action: "enterMilestone",
149
+ milestoneId,
150
+ releasePriorLeaseError: err instanceof Error ? err.message : String(err),
151
+ });
152
+ }
153
+ this.s.milestoneLeaseToken = null;
154
+ }
155
+ if (this.s.milestoneLeaseToken === null) {
140
156
  try {
141
157
  const claim = claimMilestoneLease(this.s.workerId, milestoneId);
142
158
  if (claim.ok) {