@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.d2070876

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 (177) hide show
  1. package/dist/headless.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +24 -4
  4. package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
  5. package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
  8. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
  9. package/dist/resources/extensions/gsd/auto-start.js +29 -9
  10. package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
  11. package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
  12. package/dist/resources/extensions/gsd/auto.js +15 -5
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
  14. package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
  15. package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
  16. package/dist/resources/extensions/gsd/db/engine.js +5 -2
  17. package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
  18. package/dist/resources/extensions/gsd/error-classifier.js +12 -2
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
  20. package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
  21. package/dist/resources/extensions/gsd/gsd-db.js +75 -31
  22. package/dist/resources/extensions/gsd/guided-flow.js +2 -2
  23. package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
  24. package/dist/resources/extensions/gsd/paths.js +17 -7
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  26. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
  29. package/dist/resources/extensions/gsd/stop-notice.js +9 -0
  30. package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
  31. package/dist/resources/extensions/shared/interview-ui.js +43 -6
  32. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  61. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  62. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  63. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/package.json +2 -1
  69. package/packages/cloud-mcp-gateway/package.json +2 -2
  70. package/packages/contracts/package.json +1 -1
  71. package/packages/daemon/package.json +4 -4
  72. package/packages/gsd-agent-core/package.json +5 -5
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/gsd-cloud/README.md +9 -3
  75. package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
  76. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
  77. package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
  78. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
  79. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  80. package/packages/gsd-cloud/dist/cli.js +121 -15
  81. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  82. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  83. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  84. package/packages/gsd-cloud/dist/cloud-config.js +7 -1
  85. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  86. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
  87. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
  89. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  90. package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
  91. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
  92. package/packages/gsd-cloud/dist/runtime-process.js +272 -0
  93. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
  94. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
  95. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
  96. package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
  97. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
  98. package/packages/gsd-cloud/package.json +2 -2
  99. package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
  100. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  101. package/packages/mcp-server/dist/cli-runner.js +119 -33
  102. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  103. package/packages/mcp-server/package.json +4 -4
  104. package/packages/native/package.json +1 -1
  105. package/packages/pi-agent-core/package.json +1 -1
  106. package/packages/pi-ai/dist/models.generated.d.ts +17 -0
  107. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.js +17 -0
  109. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
  112. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  113. package/packages/pi-ai/package.json +1 -1
  114. package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
  115. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/skills.js +54 -0
  117. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  118. package/packages/pi-coding-agent/package.json +7 -7
  119. package/packages/pi-tui/package.json +2 -2
  120. package/packages/rpc-client/package.json +2 -2
  121. package/pkg/package.json +1 -1
  122. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  123. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  124. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  126. package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
  127. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  128. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  129. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  130. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  131. package/src/resources/extensions/gsd/auto.ts +20 -7
  132. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
  133. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  134. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  135. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  136. package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
  137. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  138. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  139. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  140. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  141. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  142. package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
  143. package/src/resources/extensions/gsd/paths.ts +17 -7
  144. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  145. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  146. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  147. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  148. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  149. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  150. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  151. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  152. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  153. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  154. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  155. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  156. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  157. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  158. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  159. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  160. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  161. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  162. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  163. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
  164. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  165. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  167. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  168. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  169. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  170. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  171. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  172. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  173. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  174. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  175. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  176. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_buildManifest.js +0 -0
  177. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_ssgManifest.js +0 -0
package/dist/headless.js CHANGED
@@ -29,7 +29,8 @@ export function buildHeadlessSlashCommand(options) {
29
29
  if (options.command === 'new-milestone' && options.auto && !commandArgs.includes(HEADLESS_CHAIN_AUTO_FLAG)) {
30
30
  commandArgs.push(HEADLESS_CHAIN_AUTO_FLAG);
31
31
  }
32
- return `/gsd ${options.command}${commandArgs.length > 0 ? ' ' + commandArgs.join(' ') : ''}`;
32
+ const serializedArgs = commandArgs.map(arg => /\s/.test(arg) ? JSON.stringify(arg) : arg);
33
+ return `/gsd ${options.command}${serializedArgs.length > 0 ? ' ' + serializedArgs.join(' ') : ''}`;
33
34
  }
34
35
  /**
35
36
  * Commands classified as multi-turn in headless mode: they involve multiple
@@ -1 +1 @@
1
- 1252616a01ee43bc
1
+ 5f0f1c788a126951
@@ -261,7 +261,22 @@ async function enforceMinRequestInterval(s, prefs) {
261
261
  await new Promise(r => setTimeout(r, decision.waitMs));
262
262
  }
263
263
  }
264
- function closeOutCrashedUnit(s, iterData, err) {
264
+ async function snapshotCrashedUnitWork(ctx, s, deps, iterData) {
265
+ try {
266
+ const commitMsg = await deps.autoCommitUnit?.(s.basePath, iterData.unitType, iterData.unitId, ctx);
267
+ if (commitMsg) {
268
+ debugLog("autoLoop", {
269
+ phase: "crash-snapshot-commit",
270
+ unitType: iterData.unitType,
271
+ unitId: iterData.unitId,
272
+ });
273
+ }
274
+ }
275
+ catch (snapshotErr) {
276
+ logWarning("dispatch", `unit crash snapshot failed: ${snapshotErr instanceof Error ? snapshotErr.message : String(snapshotErr)}`);
277
+ }
278
+ }
279
+ async function closeOutCrashedUnit(ctx, s, deps, iterData, err) {
265
280
  const summary = formatDispatchExceptionSummary({ error: err });
266
281
  try {
267
282
  emitOpenUnitEndForUnit(s.basePath, iterData.unitType, iterData.unitId, "cancelled", {
@@ -278,6 +293,7 @@ function closeOutCrashedUnit(s, iterData, err) {
278
293
  catch (closeoutErr) {
279
294
  logWarning("dispatch", `unit crash closeout failed: ${closeoutErr instanceof Error ? closeoutErr.message : String(closeoutErr)}`);
280
295
  }
296
+ await snapshotCrashedUnitWork(ctx, s, deps, iterData);
281
297
  }
282
298
  /**
283
299
  * Main auto-mode execution loop. Iterates: orchestration.advance → guards →
@@ -400,11 +416,15 @@ export async function autoLoop(ctx, pi, s, deps, options) {
400
416
  active: s.active,
401
417
  iteration,
402
418
  maxIterations: MAX_LOOP_ITERATIONS,
403
- hasCommandContext: Boolean(s.cmdCtx),
419
+ hasCommandContext: typeof s.cmdCtx?.newSession === "function",
404
420
  sessionLockValid: true,
405
421
  });
406
422
  if (commandContextDecision.action === "stop" && commandContextDecision.reason === "missing-command-context") {
407
423
  debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
424
+ if (s.currentUnit) {
425
+ await deps.autoCommitUnit?.(s.basePath, s.currentUnit.type, s.currentUnit.id, ctx);
426
+ }
427
+ await deps.stopAuto(ctx, pi, commandContextDecision.message, { preserveWorktree: true });
408
428
  finishTurn("stopped", "manual-attention", commandContextDecision.reason);
409
429
  break;
410
430
  }
@@ -586,7 +606,7 @@ export async function autoLoop(ctx, pi, s, deps, options) {
586
606
  if (err instanceof ModelPolicyDispatchBlockedError) {
587
607
  throw err;
588
608
  }
589
- closeOutCrashedUnit(s, iterData, err);
609
+ await closeOutCrashedUnit(ctx, s, deps, iterData, err);
590
610
  throw err;
591
611
  }
592
612
  if (unitPhaseResult.action === "next") {
@@ -1109,7 +1129,7 @@ export async function autoLoop(ctx, pi, s, deps, options) {
1109
1129
  if (err instanceof ModelPolicyDispatchBlockedError) {
1110
1130
  throw err;
1111
1131
  }
1112
- closeOutCrashedUnit(s, iterData, err);
1132
+ await closeOutCrashedUnit(ctx, s, deps, iterData, err);
1113
1133
  dispatchSettled = settleDispatchFailed(dispatchId, formatDispatchExceptionSummary({ error: err }), {
1114
1134
  markFailed: markDispatchFailed,
1115
1135
  logWriteFailure: logDispatchLedgerWriteFailure,
@@ -58,6 +58,24 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
58
58
  debugLog("runUnit", { phase: "start", unitType, unitId });
59
59
  // ── Session creation with timeout ──
60
60
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
61
+ const cmdCtx = s.cmdCtx;
62
+ if (typeof cmdCtx?.newSession !== "function") {
63
+ const msg = "command context is missing newSession";
64
+ debugLog("runUnit", {
65
+ phase: "session-error",
66
+ unitType,
67
+ unitId,
68
+ error: msg,
69
+ });
70
+ return {
71
+ status: "cancelled",
72
+ errorContext: {
73
+ message: `Session creation failed: ${msg}`,
74
+ category: "session-failed",
75
+ isTransient: false,
76
+ },
77
+ };
78
+ }
61
79
  let sessionResult;
62
80
  let sessionTimeoutHandle;
63
81
  const mySessionSwitchGeneration = ++sessionSwitchGeneration;
@@ -67,7 +85,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
67
85
  const sessionAbortController = new AbortController();
68
86
  _setSessionSwitchInFlight(true);
69
87
  try {
70
- const sessionPromise = s.cmdCtx.newSession({
88
+ const sessionPromise = cmdCtx.newSession({
71
89
  abortSignal: sessionAbortController.signal,
72
90
  workspaceRoot: s.basePath,
73
91
  }).finally(() => {
@@ -30,6 +30,25 @@ import { isIsolatedWorktreeSession, _resolveCurrentUnitStartedAtForTest, emitCan
30
30
  import { MAX_RECOVERY_CHARS } from "./types.js";
31
31
  const ZERO_TOOL_PROVIDER_ERROR_PREFIX_RE = /^(?:api error(?::|$|\s*\()|provider error(?::|$|\s*\()|request failed\b|(?:http\s*)?(?:429|500|502|503)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|socket hang up\b|fetch failed\b|(?:network|connection|server) error(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|dns\b.*(?:fail|error|timeout)|unexpected eof\b|stream idle timeout\b|partial response received\b|stream_exhausted\b|terminated(?::|$)|(?:connection|stream|request)\b.{0,40}\bterminated\b|other side closed\b|rate.?limit(?:ed| exceeded| reached| error)|too many requests\b|you(?:'ve| have) (?:hit|reached) your (?:\w+ )?limit\b|.*\b(?:usage|session|weekly|daily|monthly|quota) limit\b|limit\b.{0,40}\bresets?\b|out of extra usage\b|service.?unavailable\b|internal(?: server)? error(?::|$)|internal(?:[_-]server)?[_-]error\b|server[_-]error\b|(?:provider|server|api|model|codex|claude|openai|anthropic|gemini)\b.{0,80}\boverloaded\b|overloaded\b.{0,80}\b(?:provider|server|api|model)\b|context (?:window|length) exceed|context window exceed)/i;
32
32
  const ZERO_TOOL_PROVIDER_ERROR_SIGNAL_RE = /(?:\b(?:http|status(?: code)?|code|error:)\s*(?:429|500|502|503)\b|\b(?:api|provider) error\s*[:(]?\s*(?:429|500|502|503)\b|\b(?:typeerror|error):\s*(?:fetch failed\b|socket hang up\b|terminated(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|(?:network|connection|server) error(?::|$)|stream idle timeout\b|partial response received\b|unexpected eof\b)|\b(?:server_error|api_error|stream_exhausted(?:_without_result)?)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|context (?:window|length) exceed|context window exceed)/i;
33
+ const ZERO_TOOL_PSEUDO_TOOL_CALL_RE = /<arg_key>[\s\S]*?<\/arg_key>\s*<arg_value>[\s\S]*?<\/arg_value>[\s\S]*?<\/tool_call>/i;
34
+ const ZERO_TOOL_PSEUDO_TOOL_CALL_SNIPPET_CHARS = 200;
35
+ function zeroToolMessageSnippet(message) {
36
+ const compact = message.trim().replace(/\s+/g, " ");
37
+ if (compact.length <= ZERO_TOOL_PSEUDO_TOOL_CALL_SNIPPET_CHARS)
38
+ return compact;
39
+ return `${compact.slice(0, ZERO_TOOL_PSEUDO_TOOL_CALL_SNIPPET_CHARS - 3)}...`;
40
+ }
41
+ // Provider serialization drift: the model emitted a tool call as plain assistant
42
+ // text (`toolName<arg_key>…</arg_key><arg_value>…</arg_value></tool_call>`)
43
+ // instead of a structured tool call. Returns an operator-visible snippet when
44
+ // detected. Kept separate from error classification so a drift snippet is never
45
+ // conflated with an ErrorClass (which would force a fragile union whose
46
+ // narrowing the root and extensions tsconfigs disagree on).
47
+ function zeroToolPseudoToolCallSnippet(message) {
48
+ if (!ZERO_TOOL_PSEUDO_TOOL_CALL_RE.test(message))
49
+ return null;
50
+ return zeroToolMessageSnippet(message);
51
+ }
33
52
  function classifyZeroToolProviderMessage(message) {
34
53
  const firstLine = message.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
35
54
  if (!firstLine ||
@@ -39,6 +58,7 @@ function classifyZeroToolProviderMessage(message) {
39
58
  return classifyError(firstLine);
40
59
  }
41
60
  export const _classifyZeroToolProviderMessageForTest = classifyZeroToolProviderMessage;
61
+ export const _zeroToolPseudoToolCallSnippetForTest = zeroToolPseudoToolCallSnippet;
42
62
  export function resolveDispatchRecoveryAttempts(unitRecoveryCount, unitType, unitId) {
43
63
  return (unitRecoveryCount.get(`${unitType}/${unitId}`) ?? 0) > 0
44
64
  ? 0
@@ -572,11 +592,23 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
572
592
  const lastUnit = [...currentLedger.units].reverse().find((u) => u.type === unitType && u.id === unitId && u.startedAt === _resolveCurrentUnitStartedAtForTest(s.currentUnit));
573
593
  if (lastUnit && lastUnit.toolCalls === 0) {
574
594
  const lastAssistantMessage = lastAssistantText(s.lastUnitAgentEndMessages);
575
- const providerMessageClass = classifyZeroToolProviderMessage(lastAssistantMessage);
576
- if (providerMessageClass && isTransient(providerMessageClass)) {
577
- const retryAfterMs = "retryAfterMs" in providerMessageClass ? providerMessageClass.retryAfterMs : 15_000;
595
+ const pseudoToolCallSnippet = zeroToolPseudoToolCallSnippet(lastAssistantMessage);
596
+ if (pseudoToolCallSnippet) {
597
+ debugLog("runUnitPhase", {
598
+ phase: "zero-tool-serialization-drift",
599
+ unitType,
600
+ unitId,
601
+ snippet: pseudoToolCallSnippet,
602
+ });
603
+ ctx.ui.notify(`${unitType} ${unitId} completed with 0 tool calls - provider serialization drift: model emitted pseudo-tool-call text. Snippet: ${pseudoToolCallSnippet}`, "error");
604
+ await deps.pauseAuto(ctx, pi);
605
+ return { action: "break", reason: "zero-tool-serialization-drift" };
606
+ }
607
+ const providerErrorClass = classifyZeroToolProviderMessage(lastAssistantMessage);
608
+ if (providerErrorClass && isTransient(providerErrorClass)) {
609
+ const retryAfterMs = "retryAfterMs" in providerErrorClass ? providerErrorClass.retryAfterMs : 15_000;
578
610
  await pauseAutoForProviderError(ctx.ui, ` for ${unitType} ${unitId}`, () => deps.pauseAuto(ctx, pi), {
579
- isRateLimit: providerMessageClass.kind === "rate-limit",
611
+ isRateLimit: providerErrorClass.kind === "rate-limit",
580
612
  isTransient: true,
581
613
  retryAfterMs,
582
614
  resume: () => {
@@ -593,7 +625,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
593
625
  });
594
626
  return {
595
627
  action: "break",
596
- reason: providerMessageClass.kind === "rate-limit" ? "rate-limit" : "api-timeout",
628
+ reason: providerErrorClass.kind === "rate-limit" ? "rate-limit" : "api-timeout",
597
629
  };
598
630
  }
599
631
  if (USER_DRIVEN_DEEP_UNITS.has(unitType) && isAwaitingUserInput(s.lastUnitAgentEndMessages ?? undefined)) {
@@ -1603,6 +1603,8 @@ function applyLanguageDirectiveToDispatch(action, prefs) {
1603
1603
  const directive = renderLanguageDirectiveForPrompt(prefs);
1604
1604
  if (!directive)
1605
1605
  return action;
1606
+ if (action.prompt.startsWith(directive))
1607
+ return action;
1606
1608
  return { ...action, prompt: `${directive}\n\n${action.prompt}` };
1607
1609
  }
1608
1610
  // ─── Resolver ─────────────────────────────────────────────────────────────
@@ -11,7 +11,7 @@ import { loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverr
11
11
  import { extractVerdict } from "./verdict-parser.js";
12
12
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
13
13
  import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, taskIdFromTaskFileName, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, relTaskFile, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
14
- import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
14
+ import { resolveInlineLevel, loadEffectiveGSDPreferences, renderLanguageDirectiveForPrompt } from "./preferences.js";
15
15
  import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
16
16
  import { isContextModeEnabled } from "./preferences-types.js";
17
17
  import { parseRoadmap } from "./parsers-legacy.js";
@@ -47,6 +47,12 @@ export { buildSkillActivationBlock, buildSkillDiscoveryVars };
47
47
  * from their configured executor window.
48
48
  */
49
49
  const MAX_PREAMBLE_CHARS = 20_000;
50
+ function prependLanguageDirectiveForBase(base, prompt) {
51
+ const directive = renderLanguageDirectiveForPrompt(loadEffectiveGSDPreferences(base)?.preferences);
52
+ if (!directive || prompt.startsWith(directive))
53
+ return prompt;
54
+ return `${directive}\n\n${prompt}`;
55
+ }
50
56
  /**
51
57
  * Resolve prompt budgets from the configured executor context window.
52
58
  *
@@ -1463,7 +1469,7 @@ export async function buildDiscussMilestonePrompt(mid, midTitle, base, structure
1463
1469
  if (headless) {
1464
1470
  const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
1465
1471
  const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
1466
- return loadPrompt("discuss-headless", {
1472
+ const prompt = loadPrompt("discuss-headless", {
1467
1473
  seedContext: roadmapContent ?? "",
1468
1474
  inlinedTemplates: contextTemplate,
1469
1475
  workingDirectory: base,
@@ -1472,6 +1478,7 @@ export async function buildDiscussMilestonePrompt(mid, midTitle, base, structure
1472
1478
  commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
1473
1479
  multiMilestoneCommitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
1474
1480
  });
1481
+ return prependLanguageDirectiveForBase(base, prompt);
1475
1482
  }
1476
1483
  const rawInlinedContext = await buildDiscussMilestoneInlinedContext(mid, base);
1477
1484
  const cappedInlinedContext = capPreamble(rawInlinedContext);
@@ -1498,9 +1505,10 @@ export async function buildDiscussMilestonePrompt(mid, midTitle, base, structure
1498
1505
  const truncationNote = cappedDraftSeed !== draftSeed
1499
1506
  ? `\n\n_(Draft seed truncated; read the full draft at \`${draftRelPath}\` if needed.)_`
1500
1507
  : "";
1501
- return `${promptWithContextMode}\n\n## Prior Discussion (Draft Seed)\n\nThe following draft was captured from a prior multi-milestone discussion. Use it as seed material — the user has already provided this context. Start with a brief reflection on what the draft covers, then probe for any gaps or open questions before writing the full CONTEXT.md.\n\n${cappedDraftSeed}${truncationNote}`;
1508
+ const promptWithDraftSeed = `${promptWithContextMode}\n\n## Prior Discussion (Draft Seed)\n\nThe following draft was captured from a prior multi-milestone discussion. Use it as seed material — the user has already provided this context. Start with a brief reflection on what the draft covers, then probe for any gaps or open questions before writing the full CONTEXT.md.\n\n${cappedDraftSeed}${truncationNote}`;
1509
+ return prependLanguageDirectiveForBase(base, promptWithDraftSeed);
1502
1510
  }
1503
- return promptWithContextMode;
1511
+ return prependLanguageDirectiveForBase(base, promptWithContextMode);
1504
1512
  }
1505
1513
  /**
1506
1514
  * Build a prompt for the workflow-preferences unit type (deep mode).
@@ -1,6 +1,6 @@
1
1
  // GSD auto-mode runtime state
2
2
  import { AutoSession } from "./auto/session.js";
3
- import { isDeterministicPolicyError, isQueuedUserMessageSkip, isToolInvocationError, markToolEnd as markTrackedToolEnd, markToolStart as markTrackedToolStart, } from "./auto-tool-tracking.js";
3
+ import { markToolEnd as markTrackedToolEnd, markToolStart as markTrackedToolStart, shouldClearToolInvocationErrorAfterSuccess, updateToolInvocationError, } from "./auto-tool-tracking.js";
4
4
  // Re-exported as a pure pass-through. Must stay UNGATED (no autoSession.active
5
5
  // argument, unlike markToolStart at the bottom of this file) so it is true in
6
6
  // foreground where the foreground approval-gate pause consults it.
@@ -61,13 +61,13 @@ export function markToolEnd(toolCallId) {
61
61
  export function recordToolInvocationError(toolName, errorMsg) {
62
62
  if (!autoSession.active)
63
63
  return;
64
- if (isToolInvocationError(errorMsg) || isQueuedUserMessageSkip(errorMsg) || isDeterministicPolicyError(errorMsg)) {
65
- autoSession.lastToolInvocationError = `${toolName}: ${errorMsg}`;
66
- }
64
+ autoSession.lastToolInvocationError = updateToolInvocationError(autoSession.lastToolInvocationError, toolName, errorMsg);
67
65
  }
68
- export function clearToolInvocationError() {
66
+ export function clearToolInvocationError(successfulToolName) {
69
67
  if (!autoSession.active)
70
68
  return;
69
+ if (!shouldClearToolInvocationErrorAfterSuccess(autoSession.lastToolInvocationError, successfulToolName))
70
+ return;
71
71
  autoSession.lastToolInvocationError = null;
72
72
  }
73
73
  export function getSourceObservationStore() {
@@ -46,9 +46,10 @@ import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.
46
46
  import { extractVerdict } from "./verdict-parser.js";
47
47
  import { auditOrphanedPreflightStashes } from "./orphan-stash-audit.js";
48
48
  import { parseProject } from "./schemas/parsers.js";
49
+ import { LAYOUT_SEGMENTS } from "./layout-policy.js";
49
50
  import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
50
51
  import { logWarning, logError } from "./workflow-logger.js";
51
- import { existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs";
52
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, } from "node:fs";
52
53
  import { join } from "node:path";
53
54
  import { validateDirectory } from "./validate-directory.js";
54
55
  import { isCustomProvider, resolveDefaultSessionModel, resolveDynamicRoutingConfig, } from "./preferences-models.js";
@@ -73,6 +74,30 @@ const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
73
74
  export function hasGitIndexLockForTest(basePath) {
74
75
  return existsSync(join(basePath, ".git", "index.lock"));
75
76
  }
77
+ function isEmptyDirectory(dirPath) {
78
+ try {
79
+ return existsSync(dirPath) && readdirSync(dirPath).length === 0;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ export function reconcileFlatPhaseBootstrapLayout(basePath) {
86
+ const gsdDir = join(basePath, ".gsd");
87
+ const phasesPath = join(gsdDir, LAYOUT_SEGMENTS.level1);
88
+ const milestonesPath = join(gsdDir, "milestones");
89
+ if (isEmptyDirectory(milestonesPath)) {
90
+ if (!existsSync(phasesPath))
91
+ mkdirSync(phasesPath, { recursive: true });
92
+ rmSync(milestonesPath, { recursive: true, force: true });
93
+ return true;
94
+ }
95
+ if (!existsSync(phasesPath) && !existsSync(milestonesPath)) {
96
+ mkdirSync(phasesPath, { recursive: true });
97
+ return true;
98
+ }
99
+ return false;
100
+ }
76
101
  export function _shouldAbortBootstrapForUnavailableDbForTest(gsdDbPath, dbAvailable, pathExists = existsSync) {
77
102
  return pathExists(gsdDbPath) && !dbAvailable;
78
103
  }
@@ -868,20 +893,15 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
868
893
  ensureGitignore(base, { manageGitignore });
869
894
  if (manageGitignore !== false)
870
895
  untrackRuntimeFiles(base);
871
- // Bootstrap milestones/ if it doesn't exist.
872
- // Check milestones/ directly — ensureGsdSymlink above already created .gsd/,
873
- // so checking .gsd/ existence would be dead code (#2942).
874
- const gsdDir = join(base, ".gsd");
875
- const milestonesPath = join(gsdDir, "milestones");
876
- if (!existsSync(milestonesPath)) {
877
- mkdirSync(milestonesPath, { recursive: true });
896
+ // Bootstrap the flat-phase projection root and clean empty legacy scaffolds.
897
+ if (reconcileFlatPhaseBootstrapLayout(base)) {
878
898
  try {
879
899
  nativeAddAll(base);
880
900
  nativeCommit(base, "chore: init gsd");
881
901
  }
882
902
  catch (err) {
883
903
  /* nothing to commit */
884
- logWarning("engine", `mkdir failed: ${err instanceof Error ? err.message : String(err)}`);
904
+ logWarning("engine", `layout bootstrap commit failed: ${err instanceof Error ? err.message : String(err)}`);
885
905
  }
886
906
  }
887
907
  {
@@ -118,6 +118,8 @@ export function clearInFlightTools() {
118
118
  */
119
119
  const TOOL_INVOCATION_ERROR_RE = /Validation failed for tool|Input validation error|Invalid arguments for tool|MCP error -32602|No such tool available|Expected ',' or '\}'(?: after property value)?(?: in JSON)?|Unexpected end of JSON|Unexpected token.*in JSON|does not provide an export named|Named export .* not found|Cannot find module|ERR_MODULE_NOT_FOUND|ERR_MODULE_NOT_EXPORTED|ERR_PACKAGE_PATH_NOT_EXPORTED/i;
120
120
  const DETERMINISTIC_POLICY_ERROR_RE = /(?:^|\b)(?:HARD BLOCK:|Blocked: \/gsd queue is a planning tool|Direct writes to \.gsd\/STATE\.md and \.gsd\/gsd\.db are blocked|This is a mechanical gate)/i;
121
+ const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
122
+ const SCHEDULE_WAKEUP_CONTINUATION_ERROR = "`prompt` is required when `stop` is not true";
121
123
  /**
122
124
  * Matches the runtime's "tool not registered" error. Unlike the deterministic
123
125
  * invocation failures above, this one is usually transient: the workflow MCP
@@ -135,6 +137,44 @@ export function isToolInvocationError(errorMsg) {
135
137
  return false;
136
138
  return TOOL_INVOCATION_ERROR_RE.test(errorMsg) || isDeterministicPolicyError(errorMsg);
137
139
  }
140
+ /**
141
+ * Claude Code validates its native ScheduleWakeup tool outside GSD's schema.
142
+ * Keep this exact and tool-scoped so similarly worded business validation from
143
+ * unrelated tools does not pause auto-mode.
144
+ */
145
+ export function isScheduleWakeupContinuationError(toolName, errorMsg) {
146
+ return stripMcpToolPrefix(toolName) === SCHEDULE_WAKEUP_TOOL_NAME
147
+ && errorMsg.trim() === SCHEDULE_WAKEUP_CONTINUATION_ERROR;
148
+ }
149
+ function shouldRecordToolInvocationError(toolName, errorMsg) {
150
+ return isScheduleWakeupContinuationError(toolName, errorMsg)
151
+ || isToolInvocationError(errorMsg)
152
+ || isQueuedUserMessageSkip(errorMsg);
153
+ }
154
+ function isRecordedScheduleWakeupContinuationError(recordedError) {
155
+ if (!recordedError)
156
+ return false;
157
+ const separator = recordedError.indexOf(": ");
158
+ return separator >= 0 && isScheduleWakeupContinuationError(recordedError.slice(0, separator), recordedError.slice(separator + 2));
159
+ }
160
+ export function updateToolInvocationError(recordedError, toolName, errorMsg) {
161
+ if (!shouldRecordToolInvocationError(toolName, errorMsg))
162
+ return recordedError;
163
+ if (isRecordedScheduleWakeupContinuationError(recordedError)
164
+ && !isScheduleWakeupContinuationError(toolName, errorMsg))
165
+ return recordedError;
166
+ return `${toolName}: ${errorMsg}`;
167
+ }
168
+ /**
169
+ * A failed continuation must survive successful diagnostic/read calls in the
170
+ * same turn. Only a later successful ScheduleWakeup proves the continuation
171
+ * was armed; all other invocation errors retain their existing clear-on-success
172
+ * behavior.
173
+ */
174
+ export function shouldClearToolInvocationErrorAfterSuccess(recordedError, successfulToolName) {
175
+ return !isRecordedScheduleWakeupContinuationError(recordedError)
176
+ || stripMcpToolPrefix(successfulToolName ?? "") === SCHEDULE_WAKEUP_TOOL_NAME;
177
+ }
138
178
  /**
139
179
  * Returns true if the error message indicates the called tool was not on the
140
180
  * session's tool surface (MCP startup race — see TOOL_UNAVAILABLE_ERROR_RE).
@@ -5,7 +5,7 @@
5
5
  import { cpSync, existsSync, lstatSync as lstatSyncFn, mkdirSync, readdirSync, } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { _isSamePath as isSamePath, } from "./auto-worktree-cleanup.js";
8
- import { resolveGsdPathContract } from "./paths.js";
8
+ import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
9
9
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
10
10
  import { logWarning } from "./workflow-logger.js";
11
11
  const PROJECT_PREFERENCES_FILE = "PREFERENCES.md";
@@ -143,10 +143,14 @@ function syncMilestoneLayout(mainGsd, wtGsd, layoutSegment, synced) {
143
143
  if (!existsSync(mainMilestonesDir))
144
144
  return;
145
145
  try {
146
- mkdirSync(wtMilestonesDir, { recursive: true });
147
146
  const mainMilestones = readdirSync(mainMilestonesDir, { withFileTypes: true })
148
147
  .filter((entry) => entry.isDirectory())
149
- .map((entry) => entry.name);
148
+ .map((entry) => entry.name)
149
+ .filter((name) => layoutSegment !== "milestones" ||
150
+ dirIsContentBearingLegacyMilestone(join(mainMilestonesDir, name)));
151
+ if (mainMilestones.length === 0)
152
+ return;
153
+ mkdirSync(wtMilestonesDir, { recursive: true });
150
154
  for (const milestoneId of mainMilestones) {
151
155
  syncMilestoneDirectory(mainMilestonesDir, wtMilestonesDir, milestoneId, synced);
152
156
  }
@@ -28,7 +28,7 @@ import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSes
28
28
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, loadEffectiveGSDPreferencesWithRegistry, resolveProfileAnchorProvider, getIsolationMode, resolveEffectiveUnitIsolationMode, } from "./preferences.js";
29
29
  import { playNotificationBell, sendDesktopNotification } from "./notifications.js";
30
30
  import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, HARD_CONTEXT_REROOT_THRESHOLD_PERCENT, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, shouldWarnStepSessionForContext, } from "./auto-budget.js";
31
- import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, isToolInvocationError, isQueuedUserMessageSkip, isDeterministicPolicyError, } from "./auto-tool-tracking.js";
31
+ import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, updateToolInvocationError, } from "./auto-tool-tracking.js";
32
32
  import { closeoutUnit } from "./auto-unit-closeout.js";
33
33
  import { selectAndApplyModel, resolveModelId, clearToolBaseline, isModelUnavailable, applyThinkingLevelForModel } from "./auto-model-selection.js";
34
34
  import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
@@ -617,15 +617,14 @@ export function markToolEnd(toolCallId) {
617
617
  * Called from tool_execution_end when a GSD tool fails with isError.
618
618
  * Stores the error if it matches:
619
619
  * - tool-invocation-error pattern (malformed/truncated JSON)
620
+ * - failed ScheduleWakeup continuation validation (#1379)
620
621
  * - queued-user-message skip pattern
621
622
  * - deterministic policy rejection (#4973, e.g. context_write_blocked)
622
623
  */
623
624
  export function recordToolInvocationError(toolName, errorMsg) {
624
625
  if (!s.active)
625
626
  return;
626
- if (isToolInvocationError(errorMsg) || isQueuedUserMessageSkip(errorMsg) || isDeterministicPolicyError(errorMsg)) {
627
- s.lastToolInvocationError = `${toolName}: ${errorMsg}`;
628
- }
627
+ s.lastToolInvocationError = updateToolInvocationError(s.lastToolInvocationError, toolName, errorMsg);
629
628
  }
630
629
  export function getOldestInFlightToolAgeMs() {
631
630
  return _getOldestInFlightToolAgeMs();
@@ -2441,7 +2440,18 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
2441
2440
  startedAt: hookStartedAt,
2442
2441
  workspaceRoot: s.basePath,
2443
2442
  });
2444
- const result = await s.cmdCtx.newSession({ workspaceRoot: s.basePath });
2443
+ const cmdCtx = s.cmdCtx;
2444
+ if (typeof cmdCtx?.newSession !== "function") {
2445
+ const message = "Auto-mode has no command context for dispatch.";
2446
+ ctx.ui.notify(message, "error");
2447
+ const unitToCommit = previousCurrentUnit ?? s.currentUnit;
2448
+ if (unitToCommit) {
2449
+ await autoCommitUnit(s.basePath, unitToCommit.type, unitToCommit.id, ctx);
2450
+ }
2451
+ await stopAuto(ctx, pi, message, { preserveWorktree: true });
2452
+ return false;
2453
+ }
2454
+ const result = await cmdCtx.newSession({ workspaceRoot: s.basePath });
2445
2455
  if (result.cancelled) {
2446
2456
  await stopAuto(ctx, pi);
2447
2457
  return false;
@@ -1448,7 +1448,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1448
1448
  recordRetryableHarnessToolError(toolName, resultPayload, errorText);
1449
1449
  }
1450
1450
  else if (isAutoActive()) {
1451
- clearToolInvocationError();
1451
+ clearToolInvocationError(event.toolName);
1452
1452
  }
1453
1453
  // Interactive Closeout adapter (ADR-032): auto-mode owns closeout for its
1454
1454
  // own units; interactive completions get the durable git subset (commit +
@@ -1631,7 +1631,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1631
1631
  recordRetryableHarnessToolError(toolName, event.result, errorText);
1632
1632
  }
1633
1633
  else if (isAutoActive()) {
1634
- clearToolInvocationError();
1634
+ clearToolInvocationError(event.toolName);
1635
1635
  }
1636
1636
  // Safety harness: record tool execution results for evidence cross-referencing
1637
1637
  if (isAutoActive()) {
@@ -8,6 +8,7 @@ import { executeValidateMilestone } from "./tools/workflow-tool-executors.js";
8
8
  import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
9
9
  import { getLatestAssessmentByScope } from "./gsd-db.js";
10
10
  import { checkpointWorkflowDatabase } from "./db-workspace.js";
11
+ import { formatVerdictRecordedNotice, formatVerdictRejectedNotice } from "./stop-notice.js";
11
12
  import { VALIDATION_VERDICTS, extractVerdict, isValidMilestoneVerdict, } from "./verdict-parser.js";
12
13
  const USAGE = 'Usage: /gsd verdict <pass|needs-attention|needs-remediation> [--milestone Mxxx] [--rationale "..."]';
13
14
  function tokenize(raw) {
@@ -123,35 +124,35 @@ async function loadExistingValidation(basePath, milestoneId) {
123
124
  }
124
125
  export async function handleVerdict(rawArgs, ctx, basePath) {
125
126
  if (!rawArgs.trim()) {
126
- ctx.ui.notify(USAGE, "warning");
127
+ ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
127
128
  return;
128
129
  }
129
130
  const parsed = parseArgs(rawArgs);
130
131
  if ("error" in parsed) {
131
- ctx.ui.notify(`${parsed.error}\n${USAGE}`, "warning");
132
+ ctx.ui.notify(formatVerdictRejectedNotice(`${parsed.error}\n${USAGE}`), "warning");
132
133
  return;
133
134
  }
134
135
  if (!parsed.verdict) {
135
- ctx.ui.notify(USAGE, "warning");
136
+ ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
136
137
  return;
137
138
  }
138
139
  let milestoneId = parsed.milestoneId;
139
140
  if (!milestoneId) {
140
141
  const state = await deriveState(basePath);
141
142
  if (!state.activeMilestone) {
142
- ctx.ui.notify("No active milestone — pass --milestone Mxxx to target a specific milestone.", "warning");
143
+ ctx.ui.notify(formatVerdictRejectedNotice("No active milestone — pass --milestone Mxxx to target a specific milestone."), "warning");
143
144
  return;
144
145
  }
145
146
  milestoneId = state.activeMilestone.id;
146
147
  }
147
148
  const existingValidation = await loadExistingValidation(basePath, milestoneId);
148
149
  if (!existingValidation) {
149
- ctx.ui.notify(`No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`, "warning");
150
+ ctx.ui.notify(formatVerdictRejectedNotice(`No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`), "warning");
150
151
  return;
151
152
  }
152
153
  const current = parseValidationFile(existingValidation.content);
153
154
  if (parsed.verdict !== "pass" && !parsed.rationale) {
154
- ctx.ui.notify(`--rationale is required when overriding to ${parsed.verdict}.`, "warning");
155
+ ctx.ui.notify(formatVerdictRejectedNotice(`--rationale is required when overriding to ${parsed.verdict}.`), "warning");
155
156
  return;
156
157
  }
157
158
  const verdictRationale = parsed.rationale ?? "Manually overridden via /gsd verdict";
@@ -169,17 +170,17 @@ export async function handleVerdict(rawArgs, ctx, basePath) {
169
170
  }, basePath, parsed.verdict === "pass" ? { skipBrowserEvidenceGate: true } : undefined);
170
171
  if (result.isError) {
171
172
  const msg = result.content[0]?.type === "text" ? result.content[0].text : "Unknown error";
172
- ctx.ui.notify(msg, "error");
173
+ ctx.ui.notify(formatVerdictRejectedNotice(msg), "error");
173
174
  return;
174
175
  }
175
176
  checkpointWorkflowDatabase();
176
177
  const prevVerdict = current.verdict ?? "unknown";
177
178
  const effectiveVerdict = extractEffectiveVerdict(result.details, parsed.verdict);
178
179
  if (effectiveVerdict !== parsed.verdict) {
179
- ctx.ui.notify(`Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`, "warning");
180
+ ctx.ui.notify(formatVerdictRecordedNotice(`Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`), "warning");
180
181
  }
181
182
  else {
182
- ctx.ui.notify(`Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`, "success");
183
+ ctx.ui.notify(formatVerdictRecordedNotice(`Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`), "success");
183
184
  }
184
185
  if (effectiveVerdict === "needs-remediation") {
185
186
  ctx.ui.notify("Follow up with /gsd dispatch reassess to add remediation slices, then re-run /gsd auto.", "info");