@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea

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 (183) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
  3. package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
  6. package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
  7. package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
  9. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
  10. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
  13. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  15. package/dist/resources/extensions/gsd/commands-context.js +19 -1
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
  17. package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
  18. package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
  19. package/dist/resources/extensions/gsd/db/queries.js +60 -0
  20. package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
  22. package/dist/resources/extensions/gsd/forensics.js +2 -32
  23. package/dist/resources/extensions/gsd/git-service.js +4 -4
  24. package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
  25. package/dist/resources/extensions/gsd/health-widget.js +55 -29
  26. package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
  27. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
  28. package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
  29. package/dist/resources/extensions/gsd/quick.js +45 -2
  30. package/dist/resources/extensions/gsd/session-forensics.js +11 -1
  31. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
  32. package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
  33. package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
  34. package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
  35. package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
  36. package/dist/resources/extensions/gsd/unit-registry.js +25 -3
  37. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
  38. package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
  39. package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
  40. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  41. package/dist/web/standalone/.next/BUILD_ID +1 -1
  42. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  43. package/dist/web/standalone/.next/build-manifest.json +3 -3
  44. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  45. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
  77. package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/dist/workflow.d.ts +1 -0
  81. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  82. package/packages/contracts/dist/workflow.js +2 -0
  83. package/packages/contracts/dist/workflow.js.map +1 -1
  84. package/packages/contracts/package.json +1 -1
  85. package/packages/daemon/package.json +4 -4
  86. package/packages/gsd-agent-core/package.json +5 -5
  87. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
  89. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  90. package/packages/gsd-agent-modes/package.json +7 -7
  91. package/packages/mcp-server/README.md +1 -1
  92. package/packages/mcp-server/dist/server.d.ts +1 -1
  93. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/server.js +3 -3
  95. package/packages/mcp-server/dist/server.js.map +1 -1
  96. package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
  97. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/workflow-tools.js +34 -20
  99. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  100. package/packages/mcp-server/package.json +4 -4
  101. package/packages/native/package.json +1 -1
  102. package/packages/pi-agent-core/package.json +1 -1
  103. package/packages/pi-ai/package.json +1 -1
  104. package/packages/pi-coding-agent/package.json +7 -7
  105. package/packages/pi-tui/package.json +2 -2
  106. package/packages/rpc-client/package.json +2 -2
  107. package/pkg/package.json +1 -1
  108. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
  109. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
  110. package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
  111. package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
  112. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
  113. package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
  114. package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
  115. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
  116. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
  117. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
  119. package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
  120. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
  121. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  122. package/src/resources/extensions/gsd/commands-context.ts +18 -1
  123. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
  124. package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
  125. package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
  126. package/src/resources/extensions/gsd/db/queries.ts +79 -0
  127. package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
  128. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  129. package/src/resources/extensions/gsd/forensics.ts +2 -33
  130. package/src/resources/extensions/gsd/git-service.ts +5 -5
  131. package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
  132. package/src/resources/extensions/gsd/health-widget.ts +69 -32
  133. package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
  134. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
  135. package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
  136. package/src/resources/extensions/gsd/quick.ts +43 -2
  137. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  138. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
  139. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
  140. package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
  141. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
  142. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
  143. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
  144. package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
  145. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
  146. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
  147. package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
  148. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
  149. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
  150. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
  151. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
  152. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
  153. package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
  154. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
  155. package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
  156. package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
  157. package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
  158. package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
  159. package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
  160. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
  162. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
  163. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
  164. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
  165. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
  167. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
  168. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
  169. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
  170. package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
  171. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
  172. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
  173. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
  174. package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
  175. package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
  176. package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
  177. package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
  178. package/src/resources/extensions/gsd/unit-registry.ts +25 -3
  179. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
  180. package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
  181. package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
  182. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- 0b507856bc29955a
1
+ 3da89e4dca62c288
@@ -285,8 +285,10 @@ export function buildPromptFromContext(context, toolContext = {}) {
285
285
  ? "- GSD workflow tools (gsd_exec, gsd_slice_complete, gsd_task_complete, gsd_plan_slice, gsd_save_gate_result, etc.) " +
286
286
  `are MCP tools — call them as mcp__${toolContext.workflowMcpServerName}__<tool_name> ` +
287
287
  `(e.g. mcp__${toolContext.workflowMcpServerName}__gsd_exec, mcp__${toolContext.workflowMcpServerName}__gsd_save_gate_result)\n` +
288
- `- Structured user input: call mcp__${toolContext.workflowMcpServerName}__ask_user_questions. ` +
289
- "Do not call bare ask_user_questions. Do not call native AskUserQuestion.\n"
288
+ (toolContext.questionToolAvailable !== false
289
+ ? `- Structured user input: call mcp__${toolContext.workflowMcpServerName}__ask_user_questions. ` +
290
+ "Do not call bare ask_user_questions. Do not call native AskUserQuestion.\n"
291
+ : "")
290
292
  : "- GSD workflow MCP tools are unavailable in this Claude Code run.\n";
291
293
  const toolSearchLine = toolContext.workflowMcpServerName
292
294
  ? "- ToolSearch is available only for Claude Code deferred workflow MCP hydration. " +
@@ -1413,6 +1415,12 @@ function workflowMcpServerNameFromAllowedTools(allowedTools) {
1413
1415
  }
1414
1416
  return undefined;
1415
1417
  }
1418
+ function workflowQuestionToolAvailableFromAllowedTools(allowedTools, workflowMcpServerName, gsdPhase) {
1419
+ if (!workflowMcpServerName || !gsdPhase)
1420
+ return undefined;
1421
+ return Array.isArray(allowedTools)
1422
+ && allowedTools.includes(`mcp__${workflowMcpServerName}__ask_user_questions`);
1423
+ }
1416
1424
  function isRecord(value) {
1417
1425
  return !!value && typeof value === "object" && !Array.isArray(value);
1418
1426
  }
@@ -1821,6 +1829,7 @@ async function pumpSdkMessages(model, context, options, stream) {
1821
1829
  const prompt = buildPromptFromContext(context, {
1822
1830
  workflowMcpServerName,
1823
1831
  browserMcpServerName: browserMcpServerNameFromAllowedTools(sdkOpts.allowedTools),
1832
+ questionToolAvailable: workflowQuestionToolAvailableFromAllowedTools(sdkOpts.allowedTools, workflowMcpServerName, gsdPhase),
1824
1833
  });
1825
1834
  const queryPrompt = buildSdkQueryPrompt(context, prompt);
1826
1835
  // Emit start with an empty partial
@@ -8,6 +8,64 @@ const ZERO_USAGE = {
8
8
  totalTokens: 0,
9
9
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
10
10
  };
11
+ const WINDOWS_CHILD_ENV_KEYS = new Set([
12
+ "ALLUSERSPROFILE",
13
+ "APPDATA",
14
+ "COMSPEC",
15
+ "COMMONPROGRAMFILES",
16
+ "COMMONPROGRAMFILES(X86)",
17
+ "FORCE_COLOR",
18
+ "HOME",
19
+ "HOMEDRIVE",
20
+ "HOMEPATH",
21
+ "LANG",
22
+ "LC_ALL",
23
+ "LOCALAPPDATA",
24
+ "NODE_EXTRA_CA_CERTS",
25
+ "NO_COLOR",
26
+ "NO_PROXY",
27
+ "PATHEXT",
28
+ "PATH",
29
+ "PROGRAMDATA",
30
+ "PROGRAMFILES",
31
+ "PROGRAMFILES(X86)",
32
+ "SSL_CERT_FILE",
33
+ "SYSTEMROOT",
34
+ "TEMP",
35
+ "TERM",
36
+ "TMP",
37
+ "TMPDIR",
38
+ "USER",
39
+ "USERNAME",
40
+ "USERPROFILE",
41
+ "WINDIR",
42
+ "XDG_CACHE_HOME",
43
+ "XDG_CONFIG_HOME",
44
+ "HTTP_PROXY",
45
+ "HTTPS_PROXY",
46
+ ]);
47
+ const WINDOWS_CHILD_ENV_PREFIXES = [
48
+ "AGY_",
49
+ "ANTIGRAVITY_",
50
+ "CLOUDSDK_",
51
+ "GEMINI_",
52
+ "GOOGLE_",
53
+ ];
54
+ export function buildGoogleCliChildEnv(env = process.env, platform = process.platform) {
55
+ if (platform !== "win32")
56
+ return env;
57
+ const childEnv = {};
58
+ for (const [key, value] of Object.entries(env)) {
59
+ if (typeof value !== "string")
60
+ continue;
61
+ const upperKey = key.toUpperCase();
62
+ if (WINDOWS_CHILD_ENV_KEYS.has(upperKey) ||
63
+ WINDOWS_CHILD_ENV_PREFIXES.some((prefix) => upperKey.startsWith(prefix))) {
64
+ childEnv[key] = value;
65
+ }
66
+ }
67
+ return childEnv;
68
+ }
11
69
  function textBlocks(content) {
12
70
  return content
13
71
  .map((block) => block.type === "text" ? block.text : `[${block.type} omitted]`)
@@ -87,16 +145,16 @@ function extractGeminiJsonResponse(raw) {
87
145
  function commandForProvider(provider) {
88
146
  return provider === "google-gemini-cli" ? "gemini" : "agy";
89
147
  }
90
- function argsForProvider(provider, model, prompt) {
148
+ function argsForProvider(provider, modelId, prompt) {
91
149
  if (provider === "google-gemini-cli") {
92
- const args = ["-p", prompt, "--output-format", "json"];
93
- if (model.id !== "default")
94
- args.unshift("-m", model.id);
150
+ const args = prompt === undefined ? ["--output-format", "json"] : ["-p", prompt, "--output-format", "json"];
151
+ if (modelId !== "default")
152
+ args.unshift("-m", modelId);
95
153
  return args;
96
154
  }
97
- const args = ["-p", prompt];
98
- if (model.id !== "default")
99
- args.unshift("-m", model.id);
155
+ const args = prompt === undefined ? [] : ["-p", prompt];
156
+ if (modelId !== "default")
157
+ args.unshift("-m", modelId);
100
158
  return args;
101
159
  }
102
160
  export function buildGoogleCliSpawnInvocation(command, args, platform = process.platform) {
@@ -105,13 +163,20 @@ export function buildGoogleCliSpawnInvocation(command, args, platform = process.
105
163
  }
106
164
  return { command, args };
107
165
  }
108
- function runCli(command, args, options) {
166
+ export function buildGoogleCliRunPlan(provider, modelId, prompt, platform = process.platform) {
167
+ const pipePrompt = platform === "win32";
168
+ const args = argsForProvider(provider, modelId, pipePrompt ? undefined : prompt);
169
+ return {
170
+ ...buildGoogleCliSpawnInvocation(commandForProvider(provider), args, platform),
171
+ ...(pipePrompt ? { stdin: prompt } : {}),
172
+ };
173
+ }
174
+ function runCli(plan, options) {
109
175
  return new Promise((resolve, reject) => {
110
- const invocation = buildGoogleCliSpawnInvocation(command, args);
111
- const child = spawn(invocation.command, invocation.args, {
176
+ const child = spawn(plan.command, plan.args, {
112
177
  cwd: options?.cwd || process.cwd(),
113
- env: process.env,
114
- stdio: ["ignore", "pipe", "pipe"],
178
+ env: buildGoogleCliChildEnv(),
179
+ stdio: [plan.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
115
180
  });
116
181
  let stdout = "";
117
182
  let stderr = "";
@@ -132,6 +197,10 @@ function runCli(command, args, options) {
132
197
  return;
133
198
  }
134
199
  options?.signal?.addEventListener("abort", onAbort);
200
+ if (plan.stdin !== undefined) {
201
+ child.stdin?.on("error", () => { });
202
+ child.stdin?.end(plan.stdin);
203
+ }
135
204
  child.stdout.setEncoding("utf8");
136
205
  child.stderr.setEncoding("utf8");
137
206
  child.stdout.on("data", (chunk) => {
@@ -185,9 +254,7 @@ export function streamViaGoogleCli(model, context, options) {
185
254
  queueMicrotask(async () => {
186
255
  try {
187
256
  const prompt = buildGoogleCliPrompt(context);
188
- const command = commandForProvider(provider);
189
- const args = argsForProvider(provider, model, prompt);
190
- const result = await runCli(command, args, options);
257
+ const result = await runCli(buildGoogleCliRunPlan(provider, model.id, prompt), options);
191
258
  if (result.code !== 0) {
192
259
  const detail = (result.stderr || result.stdout || `CLI exited with code ${result.code}`).trim();
193
260
  throw new Error(formatGoogleCliError(detail, provider));
@@ -396,14 +396,17 @@ export class AutoOrchestrator {
396
396
  }
397
397
  }
398
398
  // ── UokGateAdapter (folded) ──────────────────────────────────────────────
399
- async emitUokGate(input) {
399
+ resolveUokGateContext() {
400
400
  const activeBasePath = this.getLiveDispatchBasePath();
401
401
  const prefs = loadEffectiveGSDPreferencesWithRegistry(this.ctx.modelRegistry, activeBasePath, resolveProfileAnchorProvider(this.ctx.model?.provider, this.s.autoModeStartModel?.provider), this.s.autoModeStartModel
402
402
  ? `${this.s.autoModeStartModel.provider}/${this.s.autoModeStartModel.id}`
403
403
  : undefined)?.preferences;
404
- const uokFlags = resolveUokFlags(prefs);
405
- if (!uokFlags.gates)
404
+ return { activeBasePath, uokFlags: resolveUokFlags(prefs) };
405
+ }
406
+ async emitUokGate(input) {
407
+ if (!input.uokFlags.gates)
406
408
  return;
409
+ const activeBasePath = input.activeBasePath;
407
410
  const milestoneId = input.milestoneId ?? this.s.currentMilestoneId ?? undefined;
408
411
  try {
409
412
  const { UokGateRunner } = await import("../uok/gate-runner.js");
@@ -760,9 +763,11 @@ export class AutoOrchestrator {
760
763
  const stopAdvanceTimer = debugTime("orchestrator-advance");
761
764
  try {
762
765
  this.ensureLockOwnership();
766
+ const uokGateContext = this.resolveUokGateContext();
763
767
  const staleMsg = this.checkResourcesStale();
764
768
  if (staleMsg) {
765
769
  await this.emitUokGate({
770
+ ...uokGateContext,
766
771
  gateId: "resource-version-guard",
767
772
  gateType: "policy",
768
773
  outcome: "fail",
@@ -776,6 +781,7 @@ export class AutoOrchestrator {
776
781
  return blocked;
777
782
  }
778
783
  await this.emitUokGate({
784
+ ...uokGateContext,
779
785
  gateId: "resource-version-guard",
780
786
  gateType: "policy",
781
787
  outcome: "pass",
@@ -785,6 +791,7 @@ export class AutoOrchestrator {
785
791
  const gate = await this.preAdvanceGate();
786
792
  if (gate.kind === "fail") {
787
793
  await this.emitUokGate({
794
+ ...uokGateContext,
788
795
  gateId: "pre-dispatch-health-gate",
789
796
  gateType: "execution",
790
797
  outcome: "manual-attention",
@@ -803,6 +810,7 @@ export class AutoOrchestrator {
803
810
  }
804
811
  if (gate.kind === "threw") {
805
812
  await this.emitUokGate({
813
+ ...uokGateContext,
806
814
  gateId: "pre-dispatch-health-gate",
807
815
  gateType: "execution",
808
816
  outcome: "manual-attention",
@@ -814,6 +822,7 @@ export class AutoOrchestrator {
814
822
  }
815
823
  else {
816
824
  await this.emitUokGate({
825
+ ...uokGateContext,
817
826
  gateId: "pre-dispatch-health-gate",
818
827
  gateType: "execution",
819
828
  outcome: "pass",
@@ -2,7 +2,7 @@
2
2
  // File Purpose: Declarative auto-mode dispatch rules and dispatch resolver.
3
3
  import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
4
4
  import { getUatBrowserToolSupportError } from "./uat-policy.js";
5
- import { isDbAvailable, getMilestoneSlices, getMilestoneSliceSummaries, getClosedSliceIds, getPendingGatesForTurn, markPendingGatesOmittedForTurn, getMilestone, insertArtifact, insertAssessment, setSliceSketchFlag, transaction, getAssessment, } from "./gsd-db.js";
5
+ import { isDbAvailable, getMilestoneSlices, getMilestoneSliceSummaries, getClosedSliceIds, getPendingGatesForTurn, markPendingGatesOmittedForTurn, getMilestone, insertArtifact, insertAssessment, setSliceSketchFlag, transaction, getAssessment, getSliceRunUatAssessment, } from "./gsd-db.js";
6
6
  import { isClosedStatus } from "./status-guards.js";
7
7
  import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
8
8
  import { gsdRoot, resolveGsdPathContract, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relTaskFile, relSliceFile, relMilestoneFile, buildTaskFileName, gsdProjectionRoot, } from "./paths.js";
@@ -155,6 +155,18 @@ export async function readUatGateVerdict(basePath, mid, sliceId) {
155
155
  return { verdict: legacyUatVerdict, uatType };
156
156
  }
157
157
  }
158
+ // ADR-017 DB fallback: when the ASSESSMENT markdown is missing or orphaned
159
+ // from its canonical path (e.g. after a milestone artifact-layout migration
160
+ // moves slice artifacts from `phases/…` to `milestones/…`), consult the
161
+ // authoritative assessments table by (mid, slice) identity instead of path.
162
+ // `gsd_uat_result_save` always writes this row, so it is the source of truth.
163
+ const runUatAssessment = getSliceRunUatAssessment(mid, sliceId);
164
+ if (runUatAssessment?.status) {
165
+ return {
166
+ verdict: runUatAssessment.status,
167
+ uatType: uatType ?? extractUatType(runUatAssessment.fullContent),
168
+ };
169
+ }
158
170
  return null;
159
171
  }
160
172
  /**
@@ -919,16 +931,13 @@ export const DISPATCH_RULES = [
919
931
  const dbSlices = getMilestoneSliceSummaries(mid);
920
932
  if (dbSlices.length === 0)
921
933
  return null;
922
- // Find slices that need research (no RESEARCH file, dependencies done)
923
- const milestoneResearchFile = resolveExistingExpectedArtifact("research-milestone", mid, basePath) ??
924
- resolveMilestoneFile(basePath, mid, "RESEARCH");
934
+ // Find slices that need research (no RESEARCH file, dependencies done).
935
+ // Milestone research informs slice research; it does not satisfy the
936
+ // per-slice RESEARCH artifact contract.
925
937
  const researchReadySlices = [];
926
938
  for (const slice of dbSlices) {
927
939
  if (slice.done)
928
940
  continue;
929
- // Skip S01 when milestone research exists
930
- if (milestoneResearchFile && slice.id === "S01")
931
- continue;
932
941
  // Skip if already has research
933
942
  if (resolveExistingExpectedArtifact("research-slice", `${mid}/${slice.id}`, basePath))
934
943
  continue;
@@ -957,7 +966,7 @@ export const DISPATCH_RULES = [
957
966
  },
958
967
  },
959
968
  {
960
- name: "planning (no research, not S01) → research-slice",
969
+ name: "planning (no research) → research-slice",
961
970
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
962
971
  if (state.phase !== "planning")
963
972
  return null;
@@ -975,12 +984,6 @@ export const DISPATCH_RULES = [
975
984
  resolveSliceFile(basePath, mid, sid, "RESEARCH");
976
985
  if (researchFile)
977
986
  return null; // has research, fall through
978
- // Skip slice research for S01 when milestone research already exists —
979
- // the milestone research already covers the same ground for the first slice.
980
- const milestoneResearchFile = resolveExistingExpectedArtifact("research-milestone", mid, basePath) ??
981
- resolveMilestoneFile(basePath, mid, "RESEARCH");
982
- if (milestoneResearchFile && sid === "S01")
983
- return null; // fall through to plan-slice
984
987
  return {
985
988
  action: "dispatch",
986
989
  unitType: "research-slice",
@@ -14,7 +14,7 @@ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksD
14
14
  import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
15
15
  import { isContextModeEnabled } from "./preferences-types.js";
16
16
  import { parseRoadmap } from "./parsers-legacy.js";
17
- import { join } from "node:path";
17
+ import { join, relative } from "node:path";
18
18
  import { existsSync } from "node:fs";
19
19
  import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
20
20
  import { getPendingGatesForTurn } from "./gsd-db.js";
@@ -23,6 +23,7 @@ import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-
23
23
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
24
24
  import { composeContextModeInstructions, composeContractedUnitContext, composeInlinedContext, composeToolSurfaceInstructions, composeUnitContext, } from "./unit-context-composer.js";
25
25
  import { resolveManifest } from "./unit-context-manifest.js";
26
+ import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
26
27
  import { compileUnitContextContract } from "./tool-contract.js";
27
28
  import { readCompactionSnapshot } from "./compaction-snapshot.js";
28
29
  import { logWarning } from "./workflow-logger.js";
@@ -247,14 +248,37 @@ function requireComposedArtifactBlock(blocks, unitType, key) {
247
248
  return block.body;
248
249
  }
249
250
  function renderExecuteTaskOnDemandContext(base, mid, sid, artifacts) {
250
- if (!artifacts.includes("slice-research"))
251
- return "";
252
- const researchPath = relSliceFile(base, mid, sid, "RESEARCH");
253
- return [
254
- "## On-demand Context",
255
- "",
256
- `Slice research is available at \`${researchPath}\`. Read it only if the inlined task plan, slice plan excerpt, and carry-forward context do not explain a required implementation detail.`,
257
- ].join("\n");
251
+ if (!artifacts.includes("slice-research")) {
252
+ return { text: "", skipReason: "not declared by contract" };
253
+ }
254
+ // Mirror dispatch's dual-resolution logic: worktree projection path first,
255
+ // then the authoritative project-root path via resolveExpectedArtifactPath.
256
+ // In worktree layouts the RESEARCH file may live under the project-root .gsd
257
+ // and not have been copied into the worktree projection, so resolveSliceFile
258
+ // (which looks only at gsdProjectionRoot) would miss it while dispatch would
259
+ // still treat research as satisfied.
260
+ const projectedFile = resolveSliceFile(base, mid, sid, "RESEARCH");
261
+ const researchFile = projectedFile ?? (() => {
262
+ const p = resolveExpectedArtifactPath("research-slice", `${mid}/${sid}`, base);
263
+ return p && existsSync(p) ? p : null;
264
+ })();
265
+ if (!researchFile) {
266
+ return { text: "", skipReason: "missing" };
267
+ }
268
+ // Use the layout-aware relative path when the file is in the worktree
269
+ // projection (relSliceFile), or fall back to node:path relative() when the
270
+ // file was only found via the project-root resolution path.
271
+ const researchPath = projectedFile
272
+ ? relSliceFile(base, mid, sid, "RESEARCH")
273
+ : relative(base, researchFile);
274
+ return {
275
+ text: [
276
+ "## On-demand Context",
277
+ "",
278
+ `Slice research is available at \`${researchPath}\`. Read it only if the inlined task plan, slice plan excerpt, and carry-forward context do not explain a required implementation detail.`,
279
+ ].join("\n"),
280
+ skipReason: null,
281
+ };
258
282
  }
259
283
  // ─── Executor Constraints ─────────────────────────────────────────────────────
260
284
  /**
@@ -1459,7 +1483,13 @@ export async function buildDiscussMilestonePrompt(mid, midTitle, base, structure
1459
1483
  const draftPath = resolveMilestoneFile(base, mid, "CONTEXT-DRAFT");
1460
1484
  const draftContent = draftPath ? await loadFile(draftPath) : null;
1461
1485
  if (includeDraftSeed && draftContent) {
1462
- 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${draftContent}`;
1486
+ const draftRelPath = relMilestoneFile(base, mid, "CONTEXT-DRAFT");
1487
+ const draftSeed = `### Prior Discussion Draft\nSource: \`${draftRelPath}\`\n\n${draftContent.trim()}`;
1488
+ const cappedDraftSeed = capPreamble(draftSeed);
1489
+ const truncationNote = cappedDraftSeed !== draftSeed
1490
+ ? `\n\n_(Draft seed truncated; read the full draft at \`${draftRelPath}\` if needed.)_`
1491
+ : "";
1492
+ 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}`;
1463
1493
  }
1464
1494
  return promptWithContextMode;
1465
1495
  }
@@ -2405,8 +2435,9 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
2405
2435
  const slicePlanExcerpt = requireComposedArtifactBlock(contractedContext.blocks, "execute-task", "slice-plan");
2406
2436
  const contractedCarryForward = requireComposedArtifactBlock(contractedContext.blocks, "execute-task", "prior-task-summaries");
2407
2437
  const contractedTemplates = requireComposedArtifactBlock(contractedContext.blocks, "execute-task", "templates");
2408
- const onDemandContext = renderExecuteTaskOnDemandContext(base, mid, sid, contractedContext.onDemand);
2409
- trackPromptContext(contextTelemetry, "slice-research", onDemandContext ? "on-demand" : "skipped", onDemandContext, onDemandContext ? undefined : "not declared by contract");
2438
+ const onDemandResult = renderExecuteTaskOnDemandContext(base, mid, sid, contractedContext.onDemand);
2439
+ const onDemandContext = onDemandResult.text;
2440
+ trackPromptContext(contextTelemetry, "slice-research", onDemandContext ? "on-demand" : "skipped", onDemandContext, onDemandContext ? undefined : onDemandResult.skipReason ?? undefined);
2410
2441
  const prompt = loadPrompt("execute-task", {
2411
2442
  overridesSection,
2412
2443
  runtimeContext,
@@ -452,14 +452,21 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
452
452
  // discuss-milestone unit with CONTEXT at phases/NN-slug/ in the project root
453
453
  // but not in the worktree resolves to null → "resolveExpectedArtifactPath
454
454
  // returned null" → finalize-retry loop (#852).
455
- if (artifactBase !== base) {
455
+ //
456
+ // #870: the fallback must fire whenever the artifact is missing at
457
+ // `artifactBase`, NOT only when `artifactBase !== base`. When the real call
458
+ // site passes the worktree path as `base` (auto-post-unit.ts:1726 uses
459
+ // `s.currentUnit.workspaceRoot ?? s.basePath`), resolveCanonicalMilestoneRoot
460
+ // round-trips the worktree path back to itself, so `artifactBase === base`
461
+ // while still being a worktree path that lacks the projected artifact. The
462
+ // old `if (artifactBase !== base)` guard skipped the fallback in exactly that
463
+ // case, stranding CONTEXT at the project root and producing a stuck-loop.
464
+ if (!absPath || !existsSync(absPath)) {
456
465
  const projectRoot = resolve(resolveWorktreeProjectRoot(artifactBase));
457
466
  if (projectRoot && projectRoot !== artifactBase) {
458
- if (!absPath || !existsSync(absPath)) {
459
- const projectPath = resolveExpectedArtifactPath(unitType, unitId, projectRoot);
460
- if (projectPath && existsSync(projectPath)) {
461
- absPath = projectPath;
462
- }
467
+ const projectPath = resolveExpectedArtifactPath(unitType, unitId, projectRoot);
468
+ if (projectPath && existsSync(projectPath)) {
469
+ absPath = projectPath;
463
470
  }
464
471
  }
465
472
  }
@@ -1,7 +1,9 @@
1
1
  // gsd-pi + src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts - Handles provider and agent-end recovery for GSD auto-mode.
2
+ import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import { logWarning } from "../workflow-logger.js";
3
5
  import { checkDeepProjectSetupAfterTurn, checkAutoStartAfterDiscuss, maybeHandleReadyPhraseWithoutFiles, maybeHandleEmptyIntentTurn, resetEmptyTurnCounter, } from "../guided-flow.js";
4
- import { clearPathCache } from "../paths.js";
6
+ import { clearPathCache, gsdRoot } from "../paths.js";
5
7
  import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, isAutoCompletionStopInProgress, pauseAuto, setCurrentDispatchedModelId, setCurrentUnitModelForRecovery, } from "../auto.js";
6
8
  import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
7
9
  import { pauseAutoForProviderError } from "../provider-error-pause.js";
@@ -11,7 +13,7 @@ import { resolveModelId } from "../auto-model-selection.js";
11
13
  import { resolveProjectRoot } from "../worktree.js";
12
14
  import { clearDiscussionFlowState } from "./write-gate.js";
13
15
  import { scheduleFallbackContinuation } from "./fallback-continuation.js";
14
- import { clearGuidedUnitContext } from "../guided-unit-context.js";
16
+ import { clearGuidedUnitContext, getGuidedUnitContext } from "../guided-unit-context.js";
15
17
  import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
16
18
  import { classifyError, createRetryState, resetRetryState, isTransient, } from "../error-classifier.js";
17
19
  import { blockModel, blockModelUntil, isModelBlocked, isModelTemporarilyUnavailable } from "../blocked-models.js";
@@ -78,10 +80,15 @@ export function isUserInitiatedAbortMessage(message) {
78
80
  return false;
79
81
  return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
80
82
  }
81
- export function shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg) {
83
+ export function shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg, deferCheckMsg = rawErrorMsg) {
82
84
  if (!isTransient(cls) || cls.kind === "rate-limit")
83
85
  return false;
84
- return !/retry failed after \d+ attempts:/i.test(rawErrorMsg);
86
+ // Empty rawErrorMsg means the SDK terminated the session without providing an
87
+ // error string — core is done, not mid-retry. GSD must schedule its own
88
+ // retry rather than silently deferring to a core that has already exited.
89
+ if (!rawErrorMsg)
90
+ return false;
91
+ return !/retry failed after \d+ attempts:/i.test(deferCheckMsg);
85
92
  }
86
93
  /** Try configured fallbacks, then the auto-mode start model. Returns true if switched. */
87
94
  async function tryProviderModelFallback(params) {
@@ -277,6 +284,86 @@ export function suppressTerminalDeletedWorktreeMessageEnd(event) {
277
284
  logWarning("bootstrap", `Suppressing stale deleted-worktree provider error during terminal completion reroot: ${displayMsg || rawErrorMsg}`);
278
285
  return true;
279
286
  }
287
+ function modelLabel(ctx) {
288
+ const provider = ctx.model?.provider;
289
+ const id = ctx.model?.id;
290
+ return provider && id ? `${provider}/${id}` : "unknown model";
291
+ }
292
+ function isFatalManualGuidedTerminalFailure(lastMsg) {
293
+ if (!isObjectRecord(lastMsg) || !("stopReason" in lastMsg))
294
+ return false;
295
+ if (lastMsg.stopReason === "error") {
296
+ const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
297
+ if (isUserInitiatedAbortMessage(rawErrorMsg))
298
+ return false;
299
+ return true;
300
+ }
301
+ if (lastMsg.stopReason !== "aborted")
302
+ return false;
303
+ const content = "content" in lastMsg ? lastMsg.content : undefined;
304
+ const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
305
+ return hasErrorMessage || !_hasEmptyAgentEndContent(content);
306
+ }
307
+ function terminalFailureDetail(lastMsg) {
308
+ if (!isObjectRecord(lastMsg))
309
+ return "Provider turn ended with an unknown terminal error.";
310
+ const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
311
+ const content = "content" in lastMsg ? lastMsg.content : undefined;
312
+ const displayMsg = resolveAgentEndErrorDisplay(rawErrorMsg, content).replace(/\s+/g, " ").trim();
313
+ if (displayMsg)
314
+ return displayMsg.length > 300 ? `${displayMsg.slice(0, 300)}...` : displayMsg;
315
+ return lastMsg.stopReason === "aborted"
316
+ ? "Provider turn aborted with error context."
317
+ : "Provider stream ended with stopReason=error.";
318
+ }
319
+ function nextManualActivitySequence(activityDir) {
320
+ let maxSeq = 0;
321
+ try {
322
+ for (const file of readdirSync(activityDir)) {
323
+ const match = /^(\d+)-/.exec(file);
324
+ if (match)
325
+ maxSeq = Math.max(maxSeq, Number.parseInt(match[1], 10));
326
+ }
327
+ }
328
+ catch {
329
+ return "001";
330
+ }
331
+ return String(maxSeq + 1).padStart(3, "0");
332
+ }
333
+ function writeManualGuidedTerminalErrorActivity(basePath, unitType, model, detail, stopReason) {
334
+ const activityDir = join(gsdRoot(basePath), "activity");
335
+ mkdirSync(activityDir, { recursive: true });
336
+ const seq = nextManualActivitySequence(activityDir);
337
+ const safeUnitType = unitType.replace(/[^a-z0-9_.-]+/gi, "-");
338
+ const markerPath = join(activityDir, `${seq}-${safeUnitType}-manual-terminal-provider-error.jsonl`);
339
+ const message = `Manual guided ${unitType} turn ended with provider ${String(stopReason)} on ${model}: ${detail}`;
340
+ writeFileSync(markerPath, JSON.stringify({
341
+ type: "message",
342
+ message: {
343
+ role: "toolResult",
344
+ toolCallId: "manual-guided-terminal-provider-error",
345
+ toolName: "provider",
346
+ isError: true,
347
+ content: [{ type: "text", text: message }],
348
+ },
349
+ }) + "\n", "utf-8");
350
+ }
351
+ function observeManualDiscussTerminalError(ctx, lastMsg, guidedUnit) {
352
+ if (!guidedUnit?.unitType.startsWith("discuss-"))
353
+ return;
354
+ if (!isFatalManualGuidedTerminalFailure(lastMsg))
355
+ return;
356
+ const model = modelLabel(ctx);
357
+ const detail = terminalFailureDetail(lastMsg);
358
+ ctx.ui.notify(`Manual /gsd discuss ${guidedUnit.unitType} ended with a provider error on ${model}: ${detail}`, "warning");
359
+ try {
360
+ writeManualGuidedTerminalErrorActivity(guidedUnit.basePath, guidedUnit.unitType, model, detail, isObjectRecord(lastMsg) ? lastMsg.stopReason : "unknown");
361
+ }
362
+ catch (err) {
363
+ const message = err instanceof Error ? err.message : String(err);
364
+ logWarning("bootstrap", `Failed to write manual guided terminal-error activity marker: ${message}`);
365
+ }
366
+ }
280
367
  async function pauseTransientWithBackoff(cls, pi, ctx, errorDetail, isRateLimit) {
281
368
  retryState.consecutiveTransientCount += 1;
282
369
  const baseRetryAfterMs = "retryAfterMs" in cls ? cls.retryAfterMs : 15_000;
@@ -315,7 +402,9 @@ export async function handleAgentEnd(pi, event, ctx) {
315
402
  // rejected" loop even though the files are on disk.
316
403
  clearPathCache();
317
404
  const basePath = resolveAgentEndBasePath();
318
- clearGuidedUnitContext(basePath);
405
+ const lastMsg = event.messages[event.messages.length - 1];
406
+ const guidedUnit = basePath ? getGuidedUnitContext(basePath) ?? getGuidedUnitContext() : getGuidedUnitContext();
407
+ clearGuidedUnitContext(guidedUnit?.basePath ?? basePath);
319
408
  try {
320
409
  if (await checkDeepProjectSetupAfterTurn(event, ctx, basePath)) {
321
410
  return;
@@ -342,12 +431,13 @@ export async function handleAgentEnd(pi, event, ctx) {
342
431
  // discussions (where isAutoActive may be false) still get recovered.
343
432
  if (maybeHandleEmptyIntentTurn(event, isAutoActive(), basePath))
344
433
  return;
345
- if (!isAutoActive())
434
+ if (!isAutoActive()) {
435
+ observeManualDiscussTerminalError(ctx, lastMsg, guidedUnit);
346
436
  return;
437
+ }
347
438
  if (shouldIgnoreAgentEndForActiveUnit(event)) {
348
439
  return;
349
440
  }
350
- const lastMsg = event.messages[event.messages.length - 1];
351
441
  if (isSessionSwitchInFlight()) {
352
442
  _handleSessionSwitchAgentEnd(lastMsg, resolveAgentEndCancelled);
353
443
  return;
@@ -435,9 +525,9 @@ export async function handleAgentEnd(pi, event, ctx) {
435
525
  });
436
526
  return;
437
527
  }
438
- // #3588: When errorMessage is uninformative, extract the real error from
439
- // the assistant message text content for display purposes only.
440
- // Classification still uses rawErrorMsg to avoid false positives from prose.
528
+ // #3588/#956: When errorMessage is uninformative, extract the real error
529
+ // from assistant text. Prefer rawErrorMsg for classification to avoid
530
+ // prose false-positives, but use display text when rawErrorMsg is empty.
441
531
  const displayMsg = resolveAgentEndErrorDisplay(rawErrorMsg, "content" in lastMsg ? lastMsg.content : undefined);
442
532
  if (isAutoCompletionStopInProgress() &&
443
533
  isTerminalDeletedWorktreeProviderError(`${rawErrorMsg}\n${displayMsg}`)) {
@@ -447,8 +537,8 @@ export async function handleAgentEnd(pi, event, ctx) {
447
537
  }
448
538
  const errorDetail = displayMsg ? `: ${displayMsg}` : "";
449
539
  const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
450
- // ── 1. Classify using rawErrorMsg to avoid prose false-positives ────
451
- const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
540
+ // ── 1. Classify, preserving non-empty errorMessage precedence ──────
541
+ const cls = classifyError(rawErrorMsg || displayMsg, explicitRetryAfterMs);
452
542
  // ── 1a. Unsupported-model: provider rejected this model for the current
453
543
  // account/plan at request time (#4513). Persist a block so the
454
544
  // same dead model isn't reselected on the next /gsd auto restart,
@@ -517,7 +607,7 @@ export async function handleAgentEnd(pi, event, ctx) {
517
607
  // Core retries transient failures in-session after this handler.
518
608
  // Keep that behavior for non-rate-limit classes to avoid pause/retry races,
519
609
  // but let rate-limit continue into model fallback logic below (#4373).
520
- if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg)) {
610
+ if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg, rawErrorMsg || displayMsg)) {
521
611
  return;
522
612
  }
523
613
  // ── Tool-schema overload: the active model repeatedly emitted tool-call
@@ -2,6 +2,7 @@
2
2
  // File Purpose: Registers DB-backed GSD workflow tools and compatibility aliases.
3
3
  import { Type, StringEnum } from "@gsd/pi-ai";
4
4
  import { Text } from "@gsd/pi-tui";
5
+ import { SUMMARY_SAVE_CONTENT_MAX_LENGTH } from "@opengsd/contracts";
5
6
  import { loadEffectiveGSDPreferences } from "../preferences.js";
6
7
  import { ensureDbOpen, resolveCtxCwd, resolveWorkflowToolBasePath } from "./dynamic-tools.js";
7
8
  import { importWorkflowExecutorsModule } from "../workflow-mcp.js";
@@ -373,13 +374,17 @@ export function registerDbTools(pi) {
373
374
  "Root-level artifact paths are PROJECT.md, PROJECT-DRAFT.md, REQUIREMENTS.md, and REQUIREMENTS-DRAFT.md.",
374
375
  "artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT, PROJECT, PROJECT-DRAFT, REQUIREMENTS, REQUIREMENTS-DRAFT.",
375
376
  "Use CONTEXT-DRAFT for incremental draft persistence; use CONTEXT for the final milestone context after depth verification.",
377
+ `Keep each content payload under ${SUMMARY_SAVE_CONTENT_MAX_LENGTH} characters; save large context incrementally with CONTEXT-DRAFT/PROJECT-DRAFT/REQUIREMENTS-DRAFT instead of one oversized call.`,
376
378
  ],
377
379
  parameters: Type.Object({
378
380
  milestone_id: Type.Optional(Type.String({ description: "Milestone ID (e.g. M001). Omit only for root-level PROJECT/PROJECT-DRAFT/REQUIREMENTS/REQUIREMENTS-DRAFT artifacts." })),
379
381
  slice_id: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
380
382
  task_id: Type.Optional(Type.String({ description: "Task ID (e.g. T01)" })),
381
383
  artifact_type: StringEnum(["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT", "CONTEXT-DRAFT", "PROJECT", "PROJECT-DRAFT", "REQUIREMENTS", "REQUIREMENTS-DRAFT"], { description: "Artifact type to save" }),
382
- content: Type.String({ description: "The full markdown content of the artifact" }),
384
+ content: Type.String({
385
+ description: `The full markdown content of the artifact. Maximum ${SUMMARY_SAVE_CONTENT_MAX_LENGTH} characters per save.`,
386
+ maxLength: SUMMARY_SAVE_CONTENT_MAX_LENGTH,
387
+ }),
383
388
  }),
384
389
  execute: summarySaveExecute,
385
390
  renderCall(args, theme) {