gsd-pi 2.76.0-dev.707772f58 → 2.76.0-dev.7218806ab

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 (202) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  6. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-start.js +27 -18
  8. package/dist/resources/extensions/gsd/auto.js +13 -17
  9. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  11. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  12. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  16. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  17. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  18. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  20. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  21. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  22. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  23. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  24. package/dist/resources/extensions/gsd/model-router.js +36 -3
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  26. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  28. package/dist/resources/extensions/gsd/preferences.js +17 -17
  29. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  30. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  31. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  32. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  33. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  34. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  35. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  36. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/required-server-files.json +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.html +1 -1
  60. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  67. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  69. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  70. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  71. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  72. package/dist/web/standalone/server.js +1 -1
  73. package/package.json +1 -1
  74. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  75. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  76. package/packages/mcp-server/dist/remote-questions.js +732 -0
  77. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  78. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  79. package/packages/mcp-server/dist/server.js +18 -1
  80. package/packages/mcp-server/dist/server.js.map +1 -1
  81. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  83. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  84. package/packages/mcp-server/package.json +2 -1
  85. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  86. package/packages/mcp-server/src/remote-questions.ts +916 -0
  87. package/packages/mcp-server/src/server.ts +19 -1
  88. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  89. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  90. package/packages/mcp-server/tsconfig.test.json +19 -0
  91. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  93. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  94. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  95. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  96. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  98. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  99. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  100. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  101. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  104. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  105. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  108. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  114. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  116. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  119. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  121. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  133. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  134. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  135. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  136. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  137. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  138. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  139. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  140. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  142. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  143. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  144. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  145. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  146. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  147. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -1
  148. package/src/resources/extensions/gsd/auto-start.ts +29 -19
  149. package/src/resources/extensions/gsd/auto.ts +12 -17
  150. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  151. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  152. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  153. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  154. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  155. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  156. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  157. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  158. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  159. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  160. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  161. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  162. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  163. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  164. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  165. package/src/resources/extensions/gsd/model-router.ts +42 -1
  166. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  167. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  168. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  169. package/src/resources/extensions/gsd/preferences.ts +17 -17
  170. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  171. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  172. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  174. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  175. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  176. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  177. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  178. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  179. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  180. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  181. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  182. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  183. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  184. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  185. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  186. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  187. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  188. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  189. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  190. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  191. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  192. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  193. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  194. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  195. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  196. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  197. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  198. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  199. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  200. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  201. /package/dist/web/standalone/.next/static/{7iJDIT41wyL1lOgpDXINh → 5qAwYhcU5Fs2VOq_R8lOc}/_buildManifest.js +0 -0
  202. /package/dist/web/standalone/.next/static/{7iJDIT41wyL1lOgpDXINh → 5qAwYhcU5Fs2VOq_R8lOc}/_ssgManifest.js +0 -0
@@ -12,12 +12,41 @@ import { execFileSync } from 'node:child_process';
12
12
  * `src/resources/extensions/gsd/pre-execution-checks.ts`.
13
13
  */
14
14
  export const CLAUDE_COMMAND = process.platform === 'win32' ? 'claude.cmd' : 'claude';
15
+ /**
16
+ * Ordered list of binary names to probe for the Claude Code CLI.
17
+ *
18
+ * Windows installs vary: npm-global installs produce a `claude.cmd` shim,
19
+ * direct binary installs produce `claude.exe`, and Git Bash wrappers may
20
+ * expose a bare `claude` shim. Try all three so no valid install is missed.
21
+ */
22
+ const CLAUDE_COMMAND_CANDIDATES = process.platform === 'win32' ? [CLAUDE_COMMAND, 'claude.exe', 'claude'] : [CLAUDE_COMMAND];
23
+ /**
24
+ * Try to run `args` against each candidate binary.
25
+ * Returns the output buffer on first success, throws the last error if all fail.
26
+ */
27
+ function execClaudeCheck(args) {
28
+ let lastError;
29
+ for (const command of CLAUDE_COMMAND_CANDIDATES) {
30
+ try {
31
+ return execFileSync(command, args, { timeout: 5_000, stdio: 'pipe' });
32
+ }
33
+ catch (error) {
34
+ lastError = error;
35
+ const code = error?.code;
36
+ // EINVAL can surface on Windows Git Bash for .cmd spawn failures.
37
+ if (code === 'ENOENT' || code === 'EINVAL')
38
+ continue;
39
+ throw error;
40
+ }
41
+ }
42
+ throw lastError ?? new Error(`Claude CLI not found (tried: ${CLAUDE_COMMAND_CANDIDATES.join(', ')})`);
43
+ }
15
44
  /**
16
45
  * Check if the `claude` binary is installed (regardless of auth state).
17
46
  */
18
47
  export function isClaudeBinaryInstalled() {
19
48
  try {
20
- execFileSync(CLAUDE_COMMAND, ['--version'], { timeout: 5_000, stdio: 'pipe' });
49
+ execClaudeCheck(['--version']);
21
50
  return true;
22
51
  }
23
52
  catch {
@@ -29,13 +58,13 @@ export function isClaudeBinaryInstalled() {
29
58
  */
30
59
  export function isClaudeCliReady() {
31
60
  try {
32
- execFileSync(CLAUDE_COMMAND, ['--version'], { timeout: 5_000, stdio: 'pipe' });
61
+ execClaudeCheck(['--version']);
33
62
  }
34
63
  catch {
35
64
  return false;
36
65
  }
37
66
  try {
38
- const output = execFileSync(CLAUDE_COMMAND, ['auth', 'status'], { timeout: 5_000, stdio: 'pipe' })
67
+ const output = execClaudeCheck(['auth', 'status'])
39
68
  .toString()
40
69
  .toLowerCase();
41
70
  return !(/not logged in|no credentials|unauthenticated|not authenticated/i.test(output));
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Minimal tool interface matching GSD's AgentTool shape.
3
3
  * Avoids a direct dependency on @gsd/pi-agent-core from this compiled module.
4
+ *
5
+ * `details` and `isError` are optional fields that runtime tool implementations
6
+ * may populate. The MCP transport drops non-standard fields, so the wrapper at
7
+ * the call site mirrors `details` into `structuredContent` and forwards
8
+ * `isError` directly. See #4472.
4
9
  */
5
10
  export interface McpToolDef {
6
11
  name: string;
@@ -13,6 +18,8 @@ export interface McpToolDef {
13
18
  data?: string;
14
19
  mimeType?: string;
15
20
  }>;
21
+ details?: Record<string, unknown>;
22
+ isError?: boolean;
16
23
  }>;
17
24
  }
18
25
  /**
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Strict plain-object guard. True only for object literals and
3
+ * `Object.create(null)` — not for `Date`, `URL`, `Map`, `Set`, class instances,
4
+ * or arrays. Used to gate `structuredContent` forwarding so the MCP transport
5
+ * receives only true JSON objects (the protocol contract). See #4477 review.
6
+ *
7
+ * Mirrored in `packages/mcp-server/src/workflow-tools.ts` for the
8
+ * `adaptExecutorResult` adapter on the workflow path. Keep both copies in
9
+ * sync if the contract definition needs to evolve.
10
+ */
11
+ function isPlainObject(value) {
12
+ if (value === null || typeof value !== 'object')
13
+ return false;
14
+ if (Array.isArray(value))
15
+ return false;
16
+ const proto = Object.getPrototypeOf(value);
17
+ return proto === null || proto === Object.prototype;
18
+ }
1
19
  // MCP SDK subpath imports use wildcard exports (./*) in @modelcontextprotocol/sdk's
2
20
  // package.json export map. The wildcard maps "./foo" → "./dist/cjs/foo" (no .js
3
21
  // suffix), so bare subpath specifiers like `${MCP_PKG}/server/stdio` resolve to
@@ -83,7 +101,23 @@ export async function startMcpServer(options) {
83
101
  // by stringifying into a text block so clients see the payload.
84
102
  return { type: 'text', text: JSON.stringify(block) };
85
103
  });
86
- return { content };
104
+ // Forward a tool's runtime `details` field to MCP's `structuredContent`
105
+ // channel. The protocol drops non-standard fields on the wire, so tools
106
+ // that populate `details` for client-side renderers (e.g. save_gate_result)
107
+ // would otherwise arrive empty on the other side. See #4472.
108
+ //
109
+ // Use a strict plain-object guard (prototype-chain check) rather than just
110
+ // `typeof === 'object' && !Array.isArray()` — Date, URL, Map, Set, and
111
+ // class instances would otherwise pass through and end up as
112
+ // `structuredContent`, violating the protocol's JSON-object contract.
113
+ // The mirror discipline applies in `workflow-tools.ts adaptExecutorResult`.
114
+ const base = { content };
115
+ if (isPlainObject(result.details)) {
116
+ base.structuredContent = result.details;
117
+ }
118
+ if (result.isError === true)
119
+ base.isError = true;
120
+ return base;
87
121
  }
88
122
  catch (err) {
89
123
  // AbortError from a cancelled tool surfaces as a normal error — MCP
@@ -18,10 +18,11 @@ import { execFileSync } from "node:child_process";
18
18
  const CLAUDE_COMMAND = process.platform === "win32" ? "claude.cmd" : "claude";
19
19
  /**
20
20
  * Windows installs vary: some environments expose `claude.cmd` (npm shim),
21
- * others expose a `claude` shim on PATH (for example Git Bash wrappers).
22
- * Try both to avoid false "not installed" results in readiness checks.
21
+ * `claude.exe` (direct binary install), or a bare `claude` shim on PATH
22
+ * (for example Git Bash wrappers). Try all three to avoid false "not
23
+ * installed" results in readiness checks.
23
24
  */
24
- const CLAUDE_COMMAND_CANDIDATES = process.platform === "win32" ? [CLAUDE_COMMAND, "claude"] : [CLAUDE_COMMAND];
25
+ const CLAUDE_COMMAND_CANDIDATES = process.platform === "win32" ? [CLAUDE_COMMAND, "claude.exe", "claude"] : [CLAUDE_COMMAND];
25
26
  function execClaude(args) {
26
27
  let lastError;
27
28
  for (const command of CLAUDE_COMMAND_CANDIDATES) {
@@ -484,25 +484,23 @@ export function makeAbortedMessage(model, lastTextContent) {
484
484
  /**
485
485
  * Resolve the Claude Code permission mode for the current run.
486
486
  *
487
- * GSD subagents run underneath a host Claude Code session the user has
488
- * already consented to, and their work (edits, shell inspection, MCP calls)
489
- * spans the full workflow toolset. Defaulting the inner SDK to
490
- * `bypassPermissions` avoids per-tool approval prompts that offer no
491
- * meaningful safety beyond what the host session and the subagent prompts
492
- * already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
493
- * users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
487
+ * Defaults to `acceptEdits`, which auto-approves file reads/edits but
488
+ * surfaces a permission dialog for dangerous operations (e.g. general Bash,
489
+ * Agent, WebFetch). This prevents tools outside the allowlist from being
490
+ * silently denied the SDK emits an `extension_ui_request` event so the
491
+ * user sees a prompt instead of a silent refusal that Claude Code mistakes
492
+ * for user rejection (#4383).
494
493
  *
495
- * Tradeoff: bypass means a prompt-injection payload read from an untrusted
496
- * file could trigger tool calls without a second gate. Accepted for GSD
497
- * because the workflow is explicit user intent and the alternative
498
- * (#4099) is continuous approval fatigue that blocks real work.
494
+ * Set `GSD_CLAUDE_CODE_PERMISSION_MODE` to `bypassPermissions` to restore
495
+ * the old always-approve behaviour, or to `default` / `plan` for stricter
496
+ * modes.
499
497
  */
500
498
  export async function resolveClaudePermissionMode(env = process.env) {
501
499
  const override = env.GSD_CLAUDE_CODE_PERMISSION_MODE?.trim();
502
500
  if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
503
501
  return override;
504
502
  }
505
- return "bypassPermissions";
503
+ return "acceptEdits";
506
504
  }
507
505
  // NOTE: These helpers intentionally mirror @gsd/pi-ai anthropic-shared
508
506
  // behavior so this extension remains typecheck-stable even when the published
@@ -554,7 +552,7 @@ function mapThinkingLevelToAnthropicEffort(level, modelId) {
554
552
  export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
555
553
  const { reasoning, ...sdkExtraOptions } = extraOptions;
556
554
  const mcpServers = buildWorkflowMcpServers();
557
- const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
555
+ const permissionMode = overrides?.permissionMode ?? "acceptEdits";
558
556
  const disallowedTools = ["AskUserQuestion"];
559
557
  // Pre-authorize the safe built-ins and every registered workflow MCP
560
558
  // server's tools. `acceptEdits` mode (the interactive default) only
@@ -637,6 +635,68 @@ function normalizeToolResultContent(content) {
637
635
  }
638
636
  return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
639
637
  }
638
+ /**
639
+ * Extract a `details` payload from an MCP tool-result block.
640
+ *
641
+ * MCP's `CallToolResult` carries structured data in `structuredContent` — the
642
+ * protocol's supported channel for non-text payloads. Claude Code's synthetic
643
+ * user message may surface that field in one of two shapes depending on SDK
644
+ * version: as a sibling on the `mcp_tool_result` block itself, or as a
645
+ * dedicated content sub-block with `type: "structuredContent"`. Snake-case
646
+ * (`structured_content`) is accepted defensively in case a transport hop
647
+ * rewrites casing. All other shapes fall back to an empty object so callers
648
+ * can rely on `details` being present.
649
+ */
650
+ function extractStructuredDetailsFromBlock(block) {
651
+ const sibling = block.structuredContent ?? block.structured_content;
652
+ if (sibling && typeof sibling === "object" && !Array.isArray(sibling)) {
653
+ return sibling;
654
+ }
655
+ if (Array.isArray(block.content)) {
656
+ for (const item of block.content) {
657
+ if (!item || typeof item !== "object")
658
+ continue;
659
+ const sub = item;
660
+ if (sub.type !== "structuredContent" && sub.type !== "structured_content")
661
+ continue;
662
+ const payload = sub.structuredContent ?? sub.structured_content ?? sub.data ?? sub.value;
663
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
664
+ return payload;
665
+ }
666
+ }
667
+ }
668
+ // Return undefined (not {}) when no structured payload is present, matching
669
+ // the pre-#4477 contract where `details` was nullable. An empty-object
670
+ // sentinel is truthy and breaks downstream consumers that gate on
671
+ // `if (details)`. `undefined` matches the type of the field these results
672
+ // flow into (`Record<string, unknown> | undefined`).
673
+ return undefined;
674
+ }
675
+ /**
676
+ * True for items that are MCP `structuredContent` pseudo-blocks living inside
677
+ * a tool-result `content[]` array. These blocks carry the structured payload
678
+ * (extracted separately by `extractStructuredDetailsFromBlock`) and must NOT
679
+ * leak into the visible content rendered to the user — otherwise the renderer
680
+ * stringifies the JSON pseudo-block and shows it next to the actual tool
681
+ * output. See PR #4477 review (CodeRabbit, post-fix-round).
682
+ */
683
+ function isStructuredContentPseudoBlock(item) {
684
+ if (!item || typeof item !== "object")
685
+ return false;
686
+ const type = item.type;
687
+ return type === "structuredContent" || type === "structured_content";
688
+ }
689
+ /**
690
+ * Strip `structuredContent` pseudo-blocks from a tool-result content array
691
+ * before normalization. The structured payload is extracted via the sibling
692
+ * `structuredContent` field (or a dedicated extractor pass on the raw block);
693
+ * the visible content path must not include the pseudo-block itself.
694
+ */
695
+ function stripStructuredContentPseudoBlocks(content) {
696
+ if (!Array.isArray(content))
697
+ return content;
698
+ return content.filter((item) => !isStructuredContentPseudoBlock(item));
699
+ }
640
700
  /** Extract tool result payloads from an SDK synthetic user message, keyed by tool-use ID. */
641
701
  export function extractToolResultsFromSdkUserMessage(message) {
642
702
  const extracted = [];
@@ -657,8 +717,8 @@ export function extractToolResultsFromSdkUserMessage(message) {
657
717
  extracted.push({
658
718
  toolUseId,
659
719
  result: {
660
- content: normalizeToolResultContent(block.content),
661
- details: {},
720
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(block.content)),
721
+ details: extractStructuredDetailsFromBlock(block),
662
722
  isError: block.is_error === true,
663
723
  },
664
724
  });
@@ -672,8 +732,8 @@ export function extractToolResultsFromSdkUserMessage(message) {
672
732
  extracted.push({
673
733
  toolUseId,
674
734
  result: {
675
- content: normalizeToolResultContent(toolResult.content),
676
- details: {},
735
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(toolResult.content)),
736
+ details: extractStructuredDetailsFromBlock(toolResult),
677
737
  isError: toolResult.is_error === true,
678
738
  },
679
739
  });
@@ -291,7 +291,7 @@ autoModeStartThinkingLevel) {
291
291
  // ADR-005: Adjust active tool set for the selected model's provider capabilities.
292
292
  // Hard-filter incompatible tools, then let extensions override via adjust_tool_set hook.
293
293
  const activeToolNames = pi.getActiveTools();
294
- const { toolNames: compatibleTools, removedTools } = adjustToolSet(activeToolNames, model.api);
294
+ const { toolNames: compatibleTools, removedTools } = adjustToolSet(activeToolNames, model.api, model.provider);
295
295
  let finalToolNames = compatibleTools;
296
296
  // Fire adjust_tool_set hook — extensions can override the filtered tool set
297
297
  if (routingConfig.hooks !== false) {
@@ -30,8 +30,7 @@ import { initRoutingHistory } from "./routing-history.js";
30
30
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
31
31
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
32
32
  import { snapshotSkills } from "./skill-discovery.js";
33
- import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
34
- import { hideFooter } from "./auto-dashboard.js";
33
+ import { isDbAvailable, getMilestone, openDatabase, getDbStatus } from "./gsd-db.js";
35
34
  import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
36
35
  import { logWarning, logError } from "./workflow-logger.js";
37
36
  import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } from "node:fs";
@@ -247,7 +246,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
247
246
  // the parent git root). See #2393 and related issue.
248
247
  const hasLocalGit = existsSync(join(base, ".git"));
249
248
  if (!hasLocalGit || isInheritedRepo(base)) {
250
- const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
249
+ const mainBranch = loadEffectiveGSDPreferences(base)?.preferences?.git?.main_branch || "main";
251
250
  nativeInit(base, mainBranch);
252
251
  }
253
252
  // Migrate legacy in-project .gsd/ to external state directory.
@@ -263,7 +262,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
263
262
  // Ensure .gitignore has baseline patterns.
264
263
  // ensureGitignore checks for git-tracked .gsd/ files and skips the
265
264
  // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
266
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
265
+ const gitPrefs = loadEffectiveGSDPreferences(base)?.preferences?.git;
267
266
  const manageGitignore = gitPrefs?.manage_gitignore;
268
267
  ensureGitignore(base, { manageGitignore });
269
268
  if (manageGitignore !== false)
@@ -289,7 +288,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
289
288
  prepareWorkflowMcpForProject(ctx, base);
290
289
  }
291
290
  // Initialize GitServiceImpl
292
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
291
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences(base)?.preferences?.git ?? {});
293
292
  // ── Debug mode ──
294
293
  if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
295
294
  enableDebug(base);
@@ -322,7 +321,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
322
321
  // was lost due to session ending between completion and teardown.
323
322
  // Must run after DB open and before worktree entry.
324
323
  try {
325
- const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode());
324
+ const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode(base));
326
325
  for (const msg of auditResult.recovered) {
327
326
  ctx.ui.notify(`Orphan audit: ${msg}`, "info");
328
327
  }
@@ -340,7 +339,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
340
339
  let state = await deriveState(base);
341
340
  // Stale worktree state recovery (#654)
342
341
  if (state.activeMilestone &&
343
- shouldUseWorktreeIsolation() &&
342
+ shouldUseWorktreeIsolation(base) &&
344
343
  !detectWorktreeName(base)) {
345
344
  const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
346
345
  if (wtPath) {
@@ -355,7 +354,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
355
354
  let hasSurvivorBranch = false;
356
355
  if (state.activeMilestone &&
357
356
  (state.phase === "pre-planning" || state.phase === "complete") &&
358
- getIsolationMode() !== "none" &&
357
+ getIsolationMode(base) !== "none" &&
359
358
  !detectWorktreeName(base) &&
360
359
  !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)) {
361
360
  const milestoneBranch = `milestone/${state.activeMilestone.id}`;
@@ -516,7 +515,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
516
515
  registerSigtermHandler(base);
517
516
  // Capture integration branch
518
517
  if (s.currentMilestoneId) {
519
- if (getIsolationMode() !== "none") {
518
+ if (getIsolationMode(base) !== "none") {
520
519
  captureIntegrationBranch(base, s.currentMilestoneId);
521
520
  }
522
521
  setActiveMilestoneId(base, s.currentMilestoneId);
@@ -524,7 +523,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
524
523
  // Guard against stale milestone branch when isolation:none (#3613).
525
524
  // A prior session with isolation:branch/worktree may have left HEAD on
526
525
  // milestone/<MID>. Auto-checkout back to the integration branch.
527
- if (getIsolationMode() === "none" && nativeIsRepo(base)) {
526
+ if (getIsolationMode(base) === "none" && nativeIsRepo(base)) {
528
527
  try {
529
528
  const currentBranch = nativeGetCurrentBranch(base);
530
529
  if (currentBranch.startsWith("milestone/")) {
@@ -552,7 +551,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
552
551
  return symlinkRe.test(p);
553
552
  };
554
553
  if (s.currentMilestoneId &&
555
- getIsolationMode() !== "none" &&
554
+ getIsolationMode(base) !== "none" &&
556
555
  !detectWorktreeName(base) &&
557
556
  !isUnderGsdWorktrees(base)) {
558
557
  buildResolver().enterMilestone(s.currentMilestoneId, {
@@ -597,8 +596,21 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
597
596
  // call returns "db_unavailable", triggering artifact-retry which
598
597
  // re-dispatches the same task — producing an infinite loop (#2419).
599
598
  if (existsSync(gsdDbPath) && !isDbAvailable()) {
600
- ctx.ui.notify("SQLite database exists but failed to open. Auto-mode cannot proceed without a working database provider. " +
601
- "Check for corrupt gsd.db or missing native SQLite bindings.", "error");
599
+ const dbStatus = getDbStatus();
600
+ const phaseHint = dbStatus.lastPhase === "open"
601
+ ? "The database file could not be opened"
602
+ : dbStatus.lastPhase === "initSchema"
603
+ ? "The database schema could not be initialized"
604
+ : dbStatus.lastPhase === "vacuum-recovery"
605
+ ? "Corruption recovery (VACUUM) failed"
606
+ : dbStatus.attempted
607
+ ? "The database could not be opened (phase unknown)"
608
+ : "The database provider could not be loaded";
609
+ const errorDetail = dbStatus.lastError ? ` (${dbStatus.lastError.message})` : "";
610
+ const providerHint = dbStatus.provider
611
+ ? ` Provider: ${dbStatus.provider}.`
612
+ : " No SQLite provider available — check Node >= 22 or install better-sqlite3.";
613
+ ctx.ui.notify(`SQLite database exists but failed to open: ${gsdDbPath}. ${phaseHint}${errorDetail}.${providerHint}`, "error");
602
614
  return releaseLockAndReturn();
603
615
  }
604
616
  // Initialize metrics
@@ -633,13 +645,10 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
633
645
  }
634
646
  }
635
647
  // Snapshot installed skills
636
- if (resolveSkillDiscoveryMode() !== "off") {
648
+ if (resolveSkillDiscoveryMode(base) !== "off") {
637
649
  snapshotSkills();
638
650
  }
639
651
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
640
- ctx.ui.setFooter(hideFooter);
641
- // Hide gsd-health during AUTO — gsd-progress is the single source of truth
642
- // for last-commit / cost / health signal while auto is running.
643
652
  ctx.ui.setWidget("gsd-health", undefined);
644
653
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
645
654
  const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
@@ -661,7 +670,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
661
670
  // FlatRateContext used by selectAndApplyModel so user-declared
662
671
  // flat-rate providers and externalCli auto-detection are respected.
663
672
  const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
664
- const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
673
+ const bannerPrefs = loadEffectiveGSDPreferences(base)?.preferences;
665
674
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
666
675
  const effectivelyEnabled = routingConfig.enabled
667
676
  && (routingConfig.allow_flat_rate_providers
@@ -55,7 +55,7 @@ import { getErrorMessage } from "./error-utils.js";
55
55
  import { recoverFailedMigration } from "./migrate-external.js";
56
56
  import { initRegistry, convertDispatchRules } from "./rule-registry.js";
57
57
  import { emitJournalEvent as _emitJournalEvent } from "./journal.js";
58
- import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache, clearSliceProgressCache, hideFooter, } from "./auto-dashboard.js";
58
+ import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache, clearSliceProgressCache, } from "./auto-dashboard.js";
59
59
  import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
60
60
  import { isDbAvailable, getMilestone } from "./gsd-db.js";
61
61
  import { countPendingCaptures } from "./captures.js";
@@ -145,8 +145,8 @@ export function startAutoDetached(ctx, pi, base, verboseMode, options) {
145
145
  });
146
146
  }
147
147
  /** Returns true if the project is configured for `isolation:worktree` mode. */
148
- export function shouldUseWorktreeIsolation() {
149
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
148
+ export function shouldUseWorktreeIsolation(basePath) {
149
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
150
150
  if (prefs?.isolation === "worktree")
151
151
  return true;
152
152
  // Default is false — worktree isolation requires explicit opt-in
@@ -215,7 +215,7 @@ export function getAutoDashboardData() {
215
215
  const rtkSavings = sessionId && s.basePath
216
216
  ? getRtkSessionSavings(s.basePath, sessionId)
217
217
  : null;
218
- const rtkEnabled = loadEffectiveGSDPreferences()?.preferences.experimental?.rtk === true;
218
+ const rtkEnabled = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences.experimental?.rtk === true;
219
219
  // Pending capture count — lazy check, non-fatal
220
220
  let pendingCaptureCount = 0;
221
221
  try {
@@ -393,7 +393,7 @@ function clearUnitTimeout() {
393
393
  }
394
394
  /** Build snapshot metric opts. */
395
395
  function buildSnapshotOpts(_unitType, _unitId) {
396
- const prefs = loadEffectiveGSDPreferences()?.preferences;
396
+ const prefs = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
397
397
  const uokFlags = resolveUokFlags(prefs);
398
398
  return {
399
399
  ...(s.autoStartTime > 0 ? { autoSessionKey: String(s.autoStartTime) } : {}),
@@ -427,7 +427,7 @@ function handleLostSessionLock(ctx, lockStatus) {
427
427
  restoreProjectRootEnv();
428
428
  restoreMilestoneLockEnv();
429
429
  deregisterSigtermHandler();
430
- clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
430
+ clearCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences);
431
431
  const base = lockBase();
432
432
  const lockFilePath = base ? join(gsdRoot(base), "auto.lock") : "unknown";
433
433
  const recoverySuggestion = "\nTo recover, run: gsd doctor --fix";
@@ -443,7 +443,6 @@ function handleLostSessionLock(ctx, lockStatus) {
443
443
  ctx?.ui.notify(message, "error");
444
444
  ctx?.ui.setStatus("gsd-auto", undefined);
445
445
  ctx?.ui.setWidget("gsd-progress", undefined);
446
- ctx?.ui.setFooter(undefined);
447
446
  if (ctx)
448
447
  initHealthWidget(ctx);
449
448
  }
@@ -480,7 +479,6 @@ function cleanupAfterLoopExit(ctx) {
480
479
  if (!s.paused) {
481
480
  ctx.ui.setStatus("gsd-auto", undefined);
482
481
  ctx.ui.setWidget("gsd-progress", undefined);
483
- ctx.ui.setFooter(undefined);
484
482
  initHealthWidget(ctx);
485
483
  }
486
484
  // Restore CWD out of worktree back to original project root
@@ -498,7 +496,7 @@ function cleanupAfterLoopExit(ctx) {
498
496
  export async function stopAuto(ctx, pi, reason) {
499
497
  if (!s.active && !s.paused)
500
498
  return;
501
- const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
499
+ const loadedPreferences = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
502
500
  const reasonSuffix = reason ? ` — ${reason}` : "";
503
501
  try {
504
502
  // ── Step 1: Timers and locks ──
@@ -743,7 +741,6 @@ export async function stopAuto(ctx, pi, reason) {
743
741
  // UI cleanup
744
742
  ctx?.ui.setStatus("gsd-auto", undefined);
745
743
  ctx?.ui.setWidget("gsd-progress", undefined);
746
- ctx?.ui.setFooter(undefined);
747
744
  if (ctx)
748
745
  initHealthWidget(ctx);
749
746
  restoreProjectRootEnv();
@@ -832,7 +829,6 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
832
829
  s.verificationRetryCount.clear();
833
830
  ctx?.ui.setStatus("gsd-auto", "paused");
834
831
  ctx?.ui.setWidget("gsd-progress", undefined);
835
- ctx?.ui.setFooter(undefined);
836
832
  if (ctx)
837
833
  initHealthWidget(ctx);
838
834
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
@@ -1164,7 +1160,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1164
1160
  });
1165
1161
  // ── Auto-worktree / branch-mode: re-enter on resume ──
1166
1162
  if (s.currentMilestoneId &&
1167
- getIsolationMode() !== "none" &&
1163
+ getIsolationMode(s.originalBasePath || s.basePath) !== "none" &&
1168
1164
  s.originalBasePath &&
1169
1165
  !isInAutoWorktree(s.basePath) &&
1170
1166
  !detectWorktreeName(s.basePath) &&
@@ -1175,7 +1171,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1175
1171
  }
1176
1172
  registerSigtermHandler(lockBase());
1177
1173
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1178
- ctx.ui.setFooter(hideFooter);
1174
+ ctx.ui.setWidget("gsd-health", undefined);
1179
1175
  ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
1180
1176
  restoreHookState(s.basePath);
1181
1177
  // Re-sync managed resources on resume so long-lived auto sessions pick up
@@ -1197,7 +1193,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1197
1193
  await openProjectDbIfPresent(s.basePath);
1198
1194
  try {
1199
1195
  await rebuildState(s.basePath);
1200
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1196
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1201
1197
  }
1202
1198
  catch (e) {
1203
1199
  debugLog("resume-rebuild-state-failed", {
@@ -1227,7 +1223,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1227
1223
  }
1228
1224
  updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown");
1229
1225
  writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown");
1230
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1226
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1231
1227
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1232
1228
  startAutoCommandPolling(s.basePath);
1233
1229
  await runAutoLoopWithUok({
@@ -1253,13 +1249,13 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1253
1249
  return;
1254
1250
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1255
1251
  try {
1256
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1252
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1257
1253
  }
1258
1254
  catch (err) {
1259
1255
  // Best-effort only — sidebar sync must never block auto-mode startup
1260
1256
  logWarning("engine", `cmux sync failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1261
1257
  }
1262
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1258
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1263
1259
  startAutoCommandPolling(s.basePath);
1264
1260
  // Dispatch the first unit
1265
1261
  await runAutoLoopWithUok({
@@ -1,5 +1,5 @@
1
1
  import { logWarning } from "../workflow-logger.js";
2
- import { checkAutoStartAfterDiscuss } from "../guided-flow.js";
2
+ import { checkAutoStartAfterDiscuss, maybeHandleReadyPhraseWithoutFiles, maybeHandleEmptyIntentTurn, resetEmptyTurnCounter, } from "../guided-flow.js";
3
3
  import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, pauseAuto, setCurrentDispatchedModelId } from "../auto.js";
4
4
  import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
5
5
  import { pauseAutoForProviderError } from "../provider-error-pause.js";
@@ -53,6 +53,19 @@ export async function handleAgentEnd(pi, event, ctx) {
53
53
  clearDiscussionFlowState();
54
54
  return;
55
55
  }
56
+ // #4573 — When the LLM emits "Milestone X ready." but the required files
57
+ // are missing, `checkAutoStartAfterDiscuss` returns false silently. Surface
58
+ // that and nudge the LLM to complete the writes before the user hits the
59
+ // downstream "All milestones complete" warning loop.
60
+ if (maybeHandleReadyPhraseWithoutFiles(event))
61
+ return;
62
+ // #4573 — Empty-turn recovery: if the LLM announced intent in prose but
63
+ // emitted no tool calls, nudge it to execute. Fires only when auto-mode is
64
+ // active or a discussion autostart is pending (non-auto interactive discuss
65
+ // is user-driven). Runs before `isAutoActive` early return so pending
66
+ // discussions (where isAutoActive may be false) still get recovered.
67
+ if (maybeHandleEmptyIntentTurn(event, isAutoActive()))
68
+ return;
56
69
  if (!isAutoActive())
57
70
  return;
58
71
  if (isSessionSwitchInFlight())
@@ -286,6 +299,9 @@ export async function handleAgentEnd(pi, event, ctx) {
286
299
  // ── Success path ─────────────────────────────────────────────────────────
287
300
  try {
288
301
  resetRetryState(retryState);
302
+ // #4573 — Reset the empty-turn counter on any successful agent turn so
303
+ // transient stalls don't accumulate across independent units.
304
+ resetEmptyTurnCounter();
289
305
  resolveAgentEnd(event);
290
306
  }
291
307
  catch (err) {