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

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 (173) 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 +10 -10
  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 +10 -10
  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/package.json +7 -7
  115. package/packages/pi-tui/package.json +2 -2
  116. package/packages/rpc-client/package.json +2 -2
  117. package/pkg/package.json +1 -1
  118. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  119. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  120. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  121. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  122. package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
  123. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  124. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  125. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  126. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  127. package/src/resources/extensions/gsd/auto.ts +20 -7
  128. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
  129. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  130. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  131. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  132. package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
  133. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  134. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  135. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  136. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  137. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  138. package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
  139. package/src/resources/extensions/gsd/paths.ts +17 -7
  140. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  141. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  142. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  143. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  144. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  145. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  146. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  147. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  148. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  149. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  150. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  151. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  152. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  153. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  154. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  155. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  156. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  157. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  158. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  159. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
  160. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  161. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  162. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  163. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  164. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  165. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  166. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  167. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  168. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  169. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  170. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  171. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  172. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_buildManifest.js +0 -0
  173. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/rpc-client",
3
- "version": "1.9.0-dev.5c1a3ff2",
3
+ "version": "1.9.0-dev.97ef11e3",
4
4
  "description": "Standalone RPC client SDK for GSD — zero internal dependencies",
5
5
  "license": "MIT",
6
6
  "gsd": {
@@ -34,7 +34,7 @@
34
34
  "test": "node --test dist/rpc-client.test.js"
35
35
  },
36
36
  "dependencies": {
37
- "@opengsd/contracts": "^1.9.0-dev.5c1a3ff2"
37
+ "@opengsd/contracts": "^1.9.0-dev.97ef11e3"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=22.0.0"
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "1.9.0-dev.5c1a3ff2",
3
+ "version": "1.9.0-dev.97ef11e3",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -355,7 +355,41 @@ async function enforceMinRequestInterval(s: AutoSession, prefs: IterationContext
355
355
  }
356
356
  }
357
357
 
358
- function closeOutCrashedUnit(s: AutoSession, iterData: IterationData, err: unknown): void {
358
+ async function snapshotCrashedUnitWork(
359
+ ctx: ExtensionContext,
360
+ s: AutoSession,
361
+ deps: LoopDeps,
362
+ iterData: IterationData,
363
+ ): Promise<void> {
364
+ try {
365
+ const commitMsg = await deps.autoCommitUnit?.(
366
+ s.basePath,
367
+ iterData.unitType,
368
+ iterData.unitId,
369
+ ctx,
370
+ );
371
+ if (commitMsg) {
372
+ debugLog("autoLoop", {
373
+ phase: "crash-snapshot-commit",
374
+ unitType: iterData.unitType,
375
+ unitId: iterData.unitId,
376
+ });
377
+ }
378
+ } catch (snapshotErr) {
379
+ logWarning(
380
+ "dispatch",
381
+ `unit crash snapshot failed: ${snapshotErr instanceof Error ? snapshotErr.message : String(snapshotErr)}`,
382
+ );
383
+ }
384
+ }
385
+
386
+ async function closeOutCrashedUnit(
387
+ ctx: ExtensionContext,
388
+ s: AutoSession,
389
+ deps: LoopDeps,
390
+ iterData: IterationData,
391
+ err: unknown,
392
+ ): Promise<void> {
359
393
  const summary = formatDispatchExceptionSummary({ error: err });
360
394
  try {
361
395
  emitOpenUnitEndForUnit(
@@ -383,6 +417,7 @@ function closeOutCrashedUnit(s: AutoSession, iterData: IterationData, err: unkno
383
417
  } catch (closeoutErr) {
384
418
  logWarning("dispatch", `unit crash closeout failed: ${closeoutErr instanceof Error ? closeoutErr.message : String(closeoutErr)}`);
385
419
  }
420
+ await snapshotCrashedUnitWork(ctx, s, deps, iterData);
386
421
  }
387
422
 
388
423
  /**
@@ -526,11 +561,20 @@ export async function autoLoop(
526
561
  active: s.active,
527
562
  iteration,
528
563
  maxIterations: MAX_LOOP_ITERATIONS,
529
- hasCommandContext: Boolean(s.cmdCtx),
564
+ hasCommandContext: typeof s.cmdCtx?.newSession === "function",
530
565
  sessionLockValid: true,
531
566
  });
532
567
  if (commandContextDecision.action === "stop" && commandContextDecision.reason === "missing-command-context") {
533
568
  debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
569
+ if (s.currentUnit) {
570
+ await deps.autoCommitUnit?.(
571
+ s.basePath,
572
+ s.currentUnit.type,
573
+ s.currentUnit.id,
574
+ ctx,
575
+ );
576
+ }
577
+ await deps.stopAuto(ctx, pi, commandContextDecision.message, { preserveWorktree: true });
534
578
  finishTurn("stopped", "manual-attention", commandContextDecision.reason);
535
579
  break;
536
580
  }
@@ -730,7 +774,7 @@ export async function autoLoop(
730
774
  if (err instanceof ModelPolicyDispatchBlockedError) {
731
775
  throw err;
732
776
  }
733
- closeOutCrashedUnit(s, iterData, err);
777
+ await closeOutCrashedUnit(ctx, s, deps, iterData, err);
734
778
  throw err;
735
779
  }
736
780
  if (unitPhaseResult.action === "next") {
@@ -1285,7 +1329,7 @@ export async function autoLoop(
1285
1329
  if (err instanceof ModelPolicyDispatchBlockedError) {
1286
1330
  throw err;
1287
1331
  }
1288
- closeOutCrashedUnit(s, iterData, err);
1332
+ await closeOutCrashedUnit(ctx, s, deps, iterData, err);
1289
1333
  dispatchSettled = settleDispatchFailed(
1290
1334
  dispatchId,
1291
1335
  formatDispatchExceptionSummary({ error: err }),
@@ -91,6 +91,25 @@ export async function runUnit(
91
91
  // ── Session creation with timeout ──
92
92
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
93
93
 
94
+ const cmdCtx = s.cmdCtx;
95
+ if (typeof cmdCtx?.newSession !== "function") {
96
+ const msg = "command context is missing newSession";
97
+ debugLog("runUnit", {
98
+ phase: "session-error",
99
+ unitType,
100
+ unitId,
101
+ error: msg,
102
+ });
103
+ return {
104
+ status: "cancelled",
105
+ errorContext: {
106
+ message: `Session creation failed: ${msg}`,
107
+ category: "session-failed",
108
+ isTransient: false,
109
+ },
110
+ };
111
+ }
112
+
94
113
  let sessionResult: { cancelled: boolean };
95
114
  let sessionTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
96
115
  const mySessionSwitchGeneration = ++sessionSwitchGeneration;
@@ -100,7 +119,7 @@ export async function runUnit(
100
119
  const sessionAbortController = new AbortController();
101
120
  _setSessionSwitchInFlight(true);
102
121
  try {
103
- const sessionPromise = s.cmdCtx!.newSession({
122
+ const sessionPromise = cmdCtx.newSession({
104
123
  abortSignal: sessionAbortController.signal,
105
124
  workspaceRoot: s.basePath,
106
125
  }).finally(() => {
@@ -56,6 +56,26 @@ const ZERO_TOOL_PROVIDER_ERROR_PREFIX_RE =
56
56
  /^(?: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;
57
57
  const ZERO_TOOL_PROVIDER_ERROR_SIGNAL_RE =
58
58
  /(?:\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;
59
+ const ZERO_TOOL_PSEUDO_TOOL_CALL_RE =
60
+ /<arg_key>[\s\S]*?<\/arg_key>\s*<arg_value>[\s\S]*?<\/arg_value>[\s\S]*?<\/tool_call>/i;
61
+ const ZERO_TOOL_PSEUDO_TOOL_CALL_SNIPPET_CHARS = 200;
62
+
63
+ function zeroToolMessageSnippet(message: string): string {
64
+ const compact = message.trim().replace(/\s+/g, " ");
65
+ if (compact.length <= ZERO_TOOL_PSEUDO_TOOL_CALL_SNIPPET_CHARS) return compact;
66
+ return `${compact.slice(0, ZERO_TOOL_PSEUDO_TOOL_CALL_SNIPPET_CHARS - 3)}...`;
67
+ }
68
+
69
+ // Provider serialization drift: the model emitted a tool call as plain assistant
70
+ // text (`toolName<arg_key>…</arg_key><arg_value>…</arg_value></tool_call>`)
71
+ // instead of a structured tool call. Returns an operator-visible snippet when
72
+ // detected. Kept separate from error classification so a drift snippet is never
73
+ // conflated with an ErrorClass (which would force a fragile union whose
74
+ // narrowing the root and extensions tsconfigs disagree on).
75
+ function zeroToolPseudoToolCallSnippet(message: string): string | null {
76
+ if (!ZERO_TOOL_PSEUDO_TOOL_CALL_RE.test(message)) return null;
77
+ return zeroToolMessageSnippet(message);
78
+ }
59
79
 
60
80
  function classifyZeroToolProviderMessage(message: string): ReturnType<typeof classifyError> | null {
61
81
  const firstLine = message.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
@@ -68,6 +88,7 @@ function classifyZeroToolProviderMessage(message: string): ReturnType<typeof cla
68
88
  }
69
89
 
70
90
  export const _classifyZeroToolProviderMessageForTest = classifyZeroToolProviderMessage;
91
+ export const _zeroToolPseudoToolCallSnippetForTest = zeroToolPseudoToolCallSnippet;
71
92
 
72
93
  export function resolveDispatchRecoveryAttempts(
73
94
  unitRecoveryCount: Map<string, number>,
@@ -767,15 +788,30 @@ export async function runUnitPhase(
767
788
  );
768
789
  if (lastUnit && lastUnit.toolCalls === 0) {
769
790
  const lastAssistantMessage = lastAssistantText(s.lastUnitAgentEndMessages);
770
- const providerMessageClass = classifyZeroToolProviderMessage(lastAssistantMessage);
771
- if (providerMessageClass && isTransient(providerMessageClass)) {
772
- const retryAfterMs = "retryAfterMs" in providerMessageClass ? providerMessageClass.retryAfterMs : 15_000;
791
+ const pseudoToolCallSnippet = zeroToolPseudoToolCallSnippet(lastAssistantMessage);
792
+ if (pseudoToolCallSnippet) {
793
+ debugLog("runUnitPhase", {
794
+ phase: "zero-tool-serialization-drift",
795
+ unitType,
796
+ unitId,
797
+ snippet: pseudoToolCallSnippet,
798
+ });
799
+ ctx.ui.notify(
800
+ `${unitType} ${unitId} completed with 0 tool calls - provider serialization drift: model emitted pseudo-tool-call text. Snippet: ${pseudoToolCallSnippet}`,
801
+ "error",
802
+ );
803
+ await deps.pauseAuto(ctx, pi);
804
+ return { action: "break", reason: "zero-tool-serialization-drift" };
805
+ }
806
+ const providerErrorClass = classifyZeroToolProviderMessage(lastAssistantMessage);
807
+ if (providerErrorClass && isTransient(providerErrorClass)) {
808
+ const retryAfterMs = "retryAfterMs" in providerErrorClass ? providerErrorClass.retryAfterMs : 15_000;
773
809
  await pauseAutoForProviderError(
774
810
  ctx.ui,
775
811
  ` for ${unitType} ${unitId}`,
776
812
  () => deps.pauseAuto(ctx, pi),
777
813
  {
778
- isRateLimit: providerMessageClass.kind === "rate-limit",
814
+ isRateLimit: providerErrorClass.kind === "rate-limit",
779
815
  isTransient: true,
780
816
  retryAfterMs,
781
817
  resume: () => {
@@ -793,7 +829,7 @@ export async function runUnitPhase(
793
829
  });
794
830
  return {
795
831
  action: "break",
796
- reason: providerMessageClass.kind === "rate-limit" ? "rate-limit" : "api-timeout",
832
+ reason: providerErrorClass.kind === "rate-limit" ? "rate-limit" : "api-timeout",
797
833
  };
798
834
  }
799
835
  if (USER_DRIVEN_DEEP_UNITS.has(unitType) && isAwaitingUserInput(s.lastUnitAgentEndMessages ?? undefined)) {
@@ -1956,6 +1956,7 @@ function applyLanguageDirectiveToDispatch(
1956
1956
  if (action.action !== "dispatch" || !action.prompt) return action;
1957
1957
  const directive = renderLanguageDirectiveForPrompt(prefs);
1958
1958
  if (!directive) return action;
1959
+ if (action.prompt.startsWith(directive)) return action;
1959
1960
  return { ...action, prompt: `${directive}\n\n${action.prompt}` };
1960
1961
  }
1961
1962
 
@@ -20,7 +20,7 @@ import {
20
20
  relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
21
21
  relTaskFile, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
22
22
  } from "./paths.js";
23
- import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
23
+ import { resolveInlineLevel, loadEffectiveGSDPreferences, renderLanguageDirectiveForPrompt } from "./preferences.js";
24
24
  import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
25
25
  import { isContextModeEnabled } from "./preferences-types.js";
26
26
  import { parseRoadmap } from "./parsers-legacy.js";
@@ -78,6 +78,14 @@ export { buildSkillActivationBlock, buildSkillDiscoveryVars };
78
78
  */
79
79
  const MAX_PREAMBLE_CHARS = 20_000;
80
80
 
81
+ function prependLanguageDirectiveForBase(base: string, prompt: string): string {
82
+ const directive = renderLanguageDirectiveForPrompt(
83
+ loadEffectiveGSDPreferences(base)?.preferences,
84
+ );
85
+ if (!directive || prompt.startsWith(directive)) return prompt;
86
+ return `${directive}\n\n${prompt}`;
87
+ }
88
+
81
89
  /**
82
90
  * Resolve prompt budgets from the configured executor context window.
83
91
  *
@@ -1730,7 +1738,7 @@ export async function buildDiscussMilestonePrompt(
1730
1738
  if (headless) {
1731
1739
  const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
1732
1740
  const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
1733
- return loadPrompt("discuss-headless", {
1741
+ const prompt = loadPrompt("discuss-headless", {
1734
1742
  seedContext: roadmapContent ?? "",
1735
1743
  inlinedTemplates: contextTemplate,
1736
1744
  workingDirectory: base,
@@ -1739,6 +1747,7 @@ export async function buildDiscussMilestonePrompt(
1739
1747
  commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
1740
1748
  multiMilestoneCommitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
1741
1749
  });
1750
+ return prependLanguageDirectiveForBase(base, prompt);
1742
1751
  }
1743
1752
 
1744
1753
  const rawInlinedContext = await buildDiscussMilestoneInlinedContext(mid, base);
@@ -1769,10 +1778,11 @@ export async function buildDiscussMilestonePrompt(
1769
1778
  const truncationNote = cappedDraftSeed !== draftSeed
1770
1779
  ? `\n\n_(Draft seed truncated; read the full draft at \`${draftRelPath}\` if needed.)_`
1771
1780
  : "";
1772
- 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}`;
1781
+ 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}`;
1782
+ return prependLanguageDirectiveForBase(base, promptWithDraftSeed);
1773
1783
  }
1774
1784
 
1775
- return promptWithContextMode;
1785
+ return prependLanguageDirectiveForBase(base, promptWithContextMode);
1776
1786
  }
1777
1787
 
1778
1788
  /**
@@ -3,11 +3,10 @@ import { AutoSession } from "./auto/session.js";
3
3
  import type { CurrentUnit } from "./auto/session.js";
4
4
  import type { SourceObservationStore } from "./source-observations.js";
5
5
  import {
6
- isDeterministicPolicyError,
7
- isQueuedUserMessageSkip,
8
- isToolInvocationError,
9
6
  markToolEnd as markTrackedToolEnd,
10
7
  markToolStart as markTrackedToolStart,
8
+ shouldClearToolInvocationErrorAfterSuccess,
9
+ updateToolInvocationError,
11
10
  } from "./auto-tool-tracking.js";
12
11
  // Re-exported as a pure pass-through. Must stay UNGATED (no autoSession.active
13
12
  // argument, unlike markToolStart at the bottom of this file) so it is true in
@@ -99,13 +98,19 @@ export function markToolEnd(toolCallId: string): void {
99
98
 
100
99
  export function recordToolInvocationError(toolName: string, errorMsg: string): void {
101
100
  if (!autoSession.active) return;
102
- if (isToolInvocationError(errorMsg) || isQueuedUserMessageSkip(errorMsg) || isDeterministicPolicyError(errorMsg)) {
103
- autoSession.lastToolInvocationError = `${toolName}: ${errorMsg}`;
104
- }
101
+ autoSession.lastToolInvocationError = updateToolInvocationError(
102
+ autoSession.lastToolInvocationError,
103
+ toolName,
104
+ errorMsg,
105
+ );
105
106
  }
106
107
 
107
- export function clearToolInvocationError(): void {
108
+ export function clearToolInvocationError(successfulToolName?: string): void {
108
109
  if (!autoSession.active) return;
110
+ if (!shouldClearToolInvocationErrorAfterSuccess(
111
+ autoSession.lastToolInvocationError,
112
+ successfulToolName,
113
+ )) return;
109
114
  autoSession.lastToolInvocationError = null;
110
115
  }
111
116
 
@@ -88,6 +88,7 @@ import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.
88
88
  import { extractVerdict } from "./verdict-parser.js";
89
89
  import { auditOrphanedPreflightStashes } from "./orphan-stash-audit.js";
90
90
  import { parseProject } from "./schemas/parsers.js";
91
+ import { LAYOUT_SEGMENTS } from "./layout-policy.js";
91
92
 
92
93
  import {
93
94
  debugLog,
@@ -152,6 +153,33 @@ export function hasGitIndexLockForTest(basePath: string): boolean {
152
153
  return existsSync(join(basePath, ".git", "index.lock"));
153
154
  }
154
155
 
156
+ function isEmptyDirectory(dirPath: string): boolean {
157
+ try {
158
+ return existsSync(dirPath) && readdirSync(dirPath).length === 0;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ export function reconcileFlatPhaseBootstrapLayout(basePath: string): boolean {
165
+ const gsdDir = join(basePath, ".gsd");
166
+ const phasesPath = join(gsdDir, LAYOUT_SEGMENTS.level1);
167
+ const milestonesPath = join(gsdDir, "milestones");
168
+
169
+ if (isEmptyDirectory(milestonesPath)) {
170
+ if (!existsSync(phasesPath)) mkdirSync(phasesPath, { recursive: true });
171
+ rmSync(milestonesPath, { recursive: true, force: true });
172
+ return true;
173
+ }
174
+
175
+ if (!existsSync(phasesPath) && !existsSync(milestonesPath)) {
176
+ mkdirSync(phasesPath, { recursive: true });
177
+ return true;
178
+ }
179
+
180
+ return false;
181
+ }
182
+
155
183
  export function _shouldAbortBootstrapForUnavailableDbForTest(
156
184
  gsdDbPath: string,
157
185
  dbAvailable: boolean,
@@ -1152,19 +1180,14 @@ export async function bootstrapAutoSession(
1152
1180
  ensureGitignore(base, { manageGitignore });
1153
1181
  if (manageGitignore !== false) untrackRuntimeFiles(base);
1154
1182
 
1155
- // Bootstrap milestones/ if it doesn't exist.
1156
- // Check milestones/ directly — ensureGsdSymlink above already created .gsd/,
1157
- // so checking .gsd/ existence would be dead code (#2942).
1158
- const gsdDir = join(base, ".gsd");
1159
- const milestonesPath = join(gsdDir, "milestones");
1160
- if (!existsSync(milestonesPath)) {
1161
- mkdirSync(milestonesPath, { recursive: true });
1183
+ // Bootstrap the flat-phase projection root and clean empty legacy scaffolds.
1184
+ if (reconcileFlatPhaseBootstrapLayout(base)) {
1162
1185
  try {
1163
1186
  nativeAddAll(base);
1164
1187
  nativeCommit(base, "chore: init gsd");
1165
1188
  } catch (err) {
1166
1189
  /* nothing to commit */
1167
- logWarning("engine", `mkdir failed: ${err instanceof Error ? err.message : String(err)}`);
1190
+ logWarning("engine", `layout bootstrap commit failed: ${err instanceof Error ? err.message : String(err)}`);
1168
1191
  }
1169
1192
  }
1170
1193
 
@@ -132,6 +132,8 @@ export function clearInFlightTools(): void {
132
132
  */
133
133
  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;
134
134
  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;
135
+ const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
136
+ const SCHEDULE_WAKEUP_CONTINUATION_ERROR = "`prompt` is required when `stop` is not true";
135
137
 
136
138
  /**
137
139
  * Matches the runtime's "tool not registered" error. Unlike the deterministic
@@ -151,6 +153,60 @@ export function isToolInvocationError(errorMsg: string): boolean {
151
153
  return TOOL_INVOCATION_ERROR_RE.test(errorMsg) || isDeterministicPolicyError(errorMsg);
152
154
  }
153
155
 
156
+ /**
157
+ * Claude Code validates its native ScheduleWakeup tool outside GSD's schema.
158
+ * Keep this exact and tool-scoped so similarly worded business validation from
159
+ * unrelated tools does not pause auto-mode.
160
+ */
161
+ export function isScheduleWakeupContinuationError(toolName: string, errorMsg: string): boolean {
162
+ return stripMcpToolPrefix(toolName) === SCHEDULE_WAKEUP_TOOL_NAME
163
+ && errorMsg.trim() === SCHEDULE_WAKEUP_CONTINUATION_ERROR;
164
+ }
165
+
166
+ function shouldRecordToolInvocationError(toolName: string, errorMsg: string): boolean {
167
+ return isScheduleWakeupContinuationError(toolName, errorMsg)
168
+ || isToolInvocationError(errorMsg)
169
+ || isQueuedUserMessageSkip(errorMsg);
170
+ }
171
+
172
+ function isRecordedScheduleWakeupContinuationError(recordedError: string | null): boolean {
173
+ if (!recordedError) return false;
174
+
175
+ const separator = recordedError.indexOf(": ");
176
+ return separator >= 0 && isScheduleWakeupContinuationError(
177
+ recordedError.slice(0, separator),
178
+ recordedError.slice(separator + 2),
179
+ );
180
+ }
181
+
182
+ export function updateToolInvocationError(
183
+ recordedError: string | null,
184
+ toolName: string,
185
+ errorMsg: string,
186
+ ): string | null {
187
+ if (!shouldRecordToolInvocationError(toolName, errorMsg)) return recordedError;
188
+ if (
189
+ isRecordedScheduleWakeupContinuationError(recordedError)
190
+ && !isScheduleWakeupContinuationError(toolName, errorMsg)
191
+ ) return recordedError;
192
+
193
+ return `${toolName}: ${errorMsg}`;
194
+ }
195
+
196
+ /**
197
+ * A failed continuation must survive successful diagnostic/read calls in the
198
+ * same turn. Only a later successful ScheduleWakeup proves the continuation
199
+ * was armed; all other invocation errors retain their existing clear-on-success
200
+ * behavior.
201
+ */
202
+ export function shouldClearToolInvocationErrorAfterSuccess(
203
+ recordedError: string | null,
204
+ successfulToolName?: string,
205
+ ): boolean {
206
+ return !isRecordedScheduleWakeupContinuationError(recordedError)
207
+ || stripMcpToolPrefix(successfulToolName ?? "") === SCHEDULE_WAKEUP_TOOL_NAME;
208
+ }
209
+
154
210
  /**
155
211
  * Returns true if the error message indicates the called tool was not on the
156
212
  * session's tool surface (MCP startup race — see TOOL_UNAVAILABLE_ERROR_RE).
@@ -16,7 +16,7 @@ import {
16
16
  _isSamePath as isSamePath,
17
17
  _shouldReconcileWorktreeDb,
18
18
  } from "./auto-worktree-cleanup.js";
19
- import { resolveGsdPathContract } from "./paths.js";
19
+ import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
20
20
  import type { MilestoneScope } from "./workspace.js";
21
21
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
22
22
  import { logWarning } from "./workflow-logger.js";
@@ -211,10 +211,17 @@ function syncMilestoneLayout(
211
211
  if (!existsSync(mainMilestonesDir)) return;
212
212
 
213
213
  try {
214
- mkdirSync(wtMilestonesDir, { recursive: true });
215
214
  const mainMilestones = readdirSync(mainMilestonesDir, { withFileTypes: true })
216
215
  .filter((entry) => entry.isDirectory())
217
- .map((entry) => entry.name);
216
+ .map((entry) => entry.name)
217
+ .filter((name) =>
218
+ layoutSegment !== "milestones" ||
219
+ dirIsContentBearingLegacyMilestone(join(mainMilestonesDir, name)),
220
+ );
221
+
222
+ if (mainMilestones.length === 0) return;
223
+
224
+ mkdirSync(wtMilestonesDir, { recursive: true });
218
225
 
219
226
  for (const milestoneId of mainMilestones) {
220
227
  syncMilestoneDirectory(
@@ -110,9 +110,7 @@ import {
110
110
  getOldestInFlightToolStart,
111
111
  hasInteractiveToolInFlight,
112
112
  clearInFlightTools,
113
- isToolInvocationError,
114
- isQueuedUserMessageSkip,
115
- isDeterministicPolicyError,
113
+ updateToolInvocationError,
116
114
  } from "./auto-tool-tracking.js";
117
115
  import { closeoutUnit } from "./auto-unit-closeout.js";
118
116
  import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
@@ -1001,14 +999,17 @@ export function markToolEnd(toolCallId: string): void {
1001
999
  * Called from tool_execution_end when a GSD tool fails with isError.
1002
1000
  * Stores the error if it matches:
1003
1001
  * - tool-invocation-error pattern (malformed/truncated JSON)
1002
+ * - failed ScheduleWakeup continuation validation (#1379)
1004
1003
  * - queued-user-message skip pattern
1005
1004
  * - deterministic policy rejection (#4973, e.g. context_write_blocked)
1006
1005
  */
1007
1006
  export function recordToolInvocationError(toolName: string, errorMsg: string): void {
1008
1007
  if (!s.active) return;
1009
- if (isToolInvocationError(errorMsg) || isQueuedUserMessageSkip(errorMsg) || isDeterministicPolicyError(errorMsg)) {
1010
- s.lastToolInvocationError = `${toolName}: ${errorMsg}`;
1011
- }
1008
+ s.lastToolInvocationError = updateToolInvocationError(
1009
+ s.lastToolInvocationError,
1010
+ toolName,
1011
+ errorMsg,
1012
+ );
1012
1013
  }
1013
1014
 
1014
1015
  export function getOldestInFlightToolAgeMs(): number {
@@ -3162,7 +3163,19 @@ export async function dispatchHookUnit(
3162
3163
  workspaceRoot: s.basePath,
3163
3164
  });
3164
3165
 
3165
- const result = await s.cmdCtx!.newSession({ workspaceRoot: s.basePath });
3166
+ const cmdCtx = s.cmdCtx;
3167
+ if (typeof cmdCtx?.newSession !== "function") {
3168
+ const message = "Auto-mode has no command context for dispatch.";
3169
+ ctx.ui.notify(message, "error");
3170
+ const unitToCommit = previousCurrentUnit ?? s.currentUnit;
3171
+ if (unitToCommit) {
3172
+ await autoCommitUnit(s.basePath, unitToCommit.type, unitToCommit.id, ctx);
3173
+ }
3174
+ await stopAuto(ctx, pi, message, { preserveWorktree: true });
3175
+ return false;
3176
+ }
3177
+
3178
+ const result = await cmdCtx.newSession({ workspaceRoot: s.basePath });
3166
3179
  if (result.cancelled) {
3167
3180
  await stopAuto(ctx, pi);
3168
3181
  return false;
@@ -1769,7 +1769,7 @@ export function registerHooks(
1769
1769
  recordToolInvocationError(event.toolName, errorText);
1770
1770
  recordRetryableHarnessToolError(toolName, resultPayload, errorText);
1771
1771
  } else if (isAutoActive()) {
1772
- clearToolInvocationError();
1772
+ clearToolInvocationError(event.toolName);
1773
1773
  }
1774
1774
  // Interactive Closeout adapter (ADR-032): auto-mode owns closeout for its
1775
1775
  // own units; interactive completions get the durable git subset (commit +
@@ -1980,7 +1980,7 @@ export function registerHooks(
1980
1980
  recordToolInvocationError(event.toolName, errorText);
1981
1981
  recordRetryableHarnessToolError(toolName, event.result, errorText);
1982
1982
  } else if (isAutoActive()) {
1983
- clearToolInvocationError();
1983
+ clearToolInvocationError(event.toolName);
1984
1984
  }
1985
1985
  // Safety harness: record tool execution results for evidence cross-referencing
1986
1986
  if (isAutoActive()) {
@@ -11,6 +11,7 @@ import { executeValidateMilestone } from "./tools/workflow-tool-executors.js";
11
11
  import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
12
12
  import { getLatestAssessmentByScope } from "./gsd-db.js";
13
13
  import { checkpointWorkflowDatabase } from "./db-workspace.js";
14
+ import { formatVerdictRecordedNotice, formatVerdictRejectedNotice } from "./stop-notice.js";
14
15
  import {
15
16
  VALIDATION_VERDICTS,
16
17
  extractVerdict,
@@ -165,17 +166,17 @@ export async function handleVerdict(
165
166
  basePath: string,
166
167
  ): Promise<void> {
167
168
  if (!rawArgs.trim()) {
168
- ctx.ui.notify(USAGE, "warning");
169
+ ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
169
170
  return;
170
171
  }
171
172
 
172
173
  const parsed = parseArgs(rawArgs);
173
174
  if ("error" in parsed) {
174
- ctx.ui.notify(`${parsed.error}\n${USAGE}`, "warning");
175
+ ctx.ui.notify(formatVerdictRejectedNotice(`${parsed.error}\n${USAGE}`), "warning");
175
176
  return;
176
177
  }
177
178
  if (!parsed.verdict) {
178
- ctx.ui.notify(USAGE, "warning");
179
+ ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
179
180
  return;
180
181
  }
181
182
 
@@ -184,7 +185,7 @@ export async function handleVerdict(
184
185
  const state = await deriveState(basePath);
185
186
  if (!state.activeMilestone) {
186
187
  ctx.ui.notify(
187
- "No active milestone — pass --milestone Mxxx to target a specific milestone.",
188
+ formatVerdictRejectedNotice("No active milestone — pass --milestone Mxxx to target a specific milestone."),
188
189
  "warning",
189
190
  );
190
191
  return;
@@ -195,7 +196,9 @@ export async function handleVerdict(
195
196
  const existingValidation = await loadExistingValidation(basePath, milestoneId);
196
197
  if (!existingValidation) {
197
198
  ctx.ui.notify(
198
- `No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`,
199
+ formatVerdictRejectedNotice(
200
+ `No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`,
201
+ ),
199
202
  "warning",
200
203
  );
201
204
  return;
@@ -205,7 +208,7 @@ export async function handleVerdict(
205
208
 
206
209
  if (parsed.verdict !== "pass" && !parsed.rationale) {
207
210
  ctx.ui.notify(
208
- `--rationale is required when overriding to ${parsed.verdict}.`,
211
+ formatVerdictRejectedNotice(`--rationale is required when overriding to ${parsed.verdict}.`),
209
212
  "warning",
210
213
  );
211
214
  return;
@@ -234,7 +237,7 @@ export async function handleVerdict(
234
237
  if (result.isError) {
235
238
  const msg =
236
239
  result.content[0]?.type === "text" ? result.content[0].text : "Unknown error";
237
- ctx.ui.notify(msg, "error");
240
+ ctx.ui.notify(formatVerdictRejectedNotice(msg), "error");
238
241
  return;
239
242
  }
240
243
 
@@ -244,12 +247,16 @@ export async function handleVerdict(
244
247
  const effectiveVerdict = extractEffectiveVerdict(result.details, parsed.verdict);
245
248
  if (effectiveVerdict !== parsed.verdict) {
246
249
  ctx.ui.notify(
247
- `Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`,
250
+ formatVerdictRecordedNotice(
251
+ `Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`,
252
+ ),
248
253
  "warning",
249
254
  );
250
255
  } else {
251
256
  ctx.ui.notify(
252
- `Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`,
257
+ formatVerdictRecordedNotice(
258
+ `Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`,
259
+ ),
253
260
  "success",
254
261
  );
255
262
  }