gsd-pi 2.76.0-dev.4c866b677 → 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 (187) 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 +11 -15
  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 +3 -1
  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/token-counter.js +22 -5
  32. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  33. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  34. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  35. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  36. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  37. package/dist/web/standalone/.next/BUILD_ID +1 -1
  38. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  39. package/dist/web/standalone/.next/build-manifest.json +2 -2
  40. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  41. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.html +1 -1
  58. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  65. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/package.json +1 -1
  71. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  72. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  73. package/packages/mcp-server/dist/remote-questions.js +732 -0
  74. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  75. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  76. package/packages/mcp-server/dist/server.js +18 -1
  77. package/packages/mcp-server/dist/server.js.map +1 -1
  78. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  79. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  80. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  81. package/packages/mcp-server/package.json +2 -1
  82. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  83. package/packages/mcp-server/src/remote-questions.ts +916 -0
  84. package/packages/mcp-server/src/server.ts +19 -1
  85. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  86. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  87. package/packages/mcp-server/tsconfig.test.json +19 -0
  88. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  89. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  91. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  92. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  93. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  95. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  96. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  97. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  98. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  102. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  105. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  116. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  123. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  124. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  125. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  126. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  127. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  128. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  129. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  130. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  131. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  132. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  133. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  134. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -1
  135. package/src/resources/extensions/gsd/auto-start.ts +13 -16
  136. package/src/resources/extensions/gsd/auto.ts +12 -17
  137. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  138. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  139. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  140. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  141. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  142. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  143. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  144. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  145. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  146. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  147. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  148. package/src/resources/extensions/gsd/gsd-db.ts +3 -1
  149. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  150. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  151. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  152. package/src/resources/extensions/gsd/model-router.ts +42 -1
  153. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  154. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  155. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  156. package/src/resources/extensions/gsd/preferences.ts +17 -17
  157. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  158. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  159. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  160. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  161. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  162. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  163. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +64 -0
  164. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  165. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  166. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  167. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  168. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  171. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  172. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  173. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  174. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  175. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  176. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  177. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  178. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  179. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  180. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  181. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  182. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  183. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  184. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  185. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  186. /package/dist/web/standalone/.next/static/{jDqWYbuP_CG6Kjc-uKwkN → 5qAwYhcU5Fs2VOq_R8lOc}/_buildManifest.js +0 -0
  187. /package/dist/web/standalone/.next/static/{jDqWYbuP_CG6Kjc-uKwkN → 5qAwYhcU5Fs2VOq_R8lOc}/_ssgManifest.js +0 -0
@@ -21,10 +21,11 @@ const CLAUDE_COMMAND = process.platform === "win32" ? "claude.cmd" : "claude";
21
21
 
22
22
  /**
23
23
  * Windows installs vary: some environments expose `claude.cmd` (npm shim),
24
- * others expose a `claude` shim on PATH (for example Git Bash wrappers).
25
- * Try both to avoid false "not installed" results in readiness checks.
24
+ * `claude.exe` (direct binary install), or a bare `claude` shim on PATH
25
+ * (for example Git Bash wrappers). Try all three to avoid false "not
26
+ * installed" results in readiness checks.
26
27
  */
27
- const CLAUDE_COMMAND_CANDIDATES = process.platform === "win32" ? [CLAUDE_COMMAND, "claude"] : [CLAUDE_COMMAND];
28
+ const CLAUDE_COMMAND_CANDIDATES = process.platform === "win32" ? [CLAUDE_COMMAND, "claude.exe", "claude"] : [CLAUDE_COMMAND];
28
29
 
29
30
  function execClaude(args: string[]): Buffer {
30
31
  let lastError: unknown;
@@ -692,18 +692,16 @@ export function makeAbortedMessage(model: string, lastTextContent: string): Assi
692
692
  /**
693
693
  * Resolve the Claude Code permission mode for the current run.
694
694
  *
695
- * GSD subagents run underneath a host Claude Code session the user has
696
- * already consented to, and their work (edits, shell inspection, MCP calls)
697
- * spans the full workflow toolset. Defaulting the inner SDK to
698
- * `bypassPermissions` avoids per-tool approval prompts that offer no
699
- * meaningful safety beyond what the host session and the subagent prompts
700
- * already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
701
- * users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
695
+ * Defaults to `acceptEdits`, which auto-approves file reads/edits but
696
+ * surfaces a permission dialog for dangerous operations (e.g. general Bash,
697
+ * Agent, WebFetch). This prevents tools outside the allowlist from being
698
+ * silently denied the SDK emits an `extension_ui_request` event so the
699
+ * user sees a prompt instead of a silent refusal that Claude Code mistakes
700
+ * for user rejection (#4383).
702
701
  *
703
- * Tradeoff: bypass means a prompt-injection payload read from an untrusted
704
- * file could trigger tool calls without a second gate. Accepted for GSD
705
- * because the workflow is explicit user intent and the alternative
706
- * (#4099) is continuous approval fatigue that blocks real work.
702
+ * Set `GSD_CLAUDE_CODE_PERMISSION_MODE` to `bypassPermissions` to restore
703
+ * the old always-approve behaviour, or to `default` / `plan` for stricter
704
+ * modes.
707
705
  */
708
706
  export async function resolveClaudePermissionMode(
709
707
  env: NodeJS.ProcessEnv = process.env,
@@ -712,7 +710,7 @@ export async function resolveClaudePermissionMode(
712
710
  if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
713
711
  return override;
714
712
  }
715
- return "bypassPermissions";
713
+ return "acceptEdits";
716
714
  }
717
715
 
718
716
  // NOTE: These helpers intentionally mirror @gsd/pi-ai anthropic-shared
@@ -772,7 +770,7 @@ export function buildSdkOptions(
772
770
  ): Record<string, unknown> {
773
771
  const { reasoning, ...sdkExtraOptions } = extraOptions;
774
772
  const mcpServers = buildWorkflowMcpServers();
775
- const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
773
+ const permissionMode = overrides?.permissionMode ?? "acceptEdits";
776
774
  const disallowedTools = ["AskUserQuestion"];
777
775
  // Pre-authorize the safe built-ins and every registered workflow MCP
778
776
  // server's tools. `acceptEdits` mode (the interactive default) only
@@ -867,6 +865,69 @@ function normalizeToolResultContent(content: unknown): ExternalToolResultContent
867
865
  return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
868
866
  }
869
867
 
868
+ /**
869
+ * Extract a `details` payload from an MCP tool-result block.
870
+ *
871
+ * MCP's `CallToolResult` carries structured data in `structuredContent` — the
872
+ * protocol's supported channel for non-text payloads. Claude Code's synthetic
873
+ * user message may surface that field in one of two shapes depending on SDK
874
+ * version: as a sibling on the `mcp_tool_result` block itself, or as a
875
+ * dedicated content sub-block with `type: "structuredContent"`. Snake-case
876
+ * (`structured_content`) is accepted defensively in case a transport hop
877
+ * rewrites casing. All other shapes fall back to an empty object so callers
878
+ * can rely on `details` being present.
879
+ */
880
+ function extractStructuredDetailsFromBlock(block: Record<string, unknown>): Record<string, unknown> | undefined {
881
+ const sibling = block.structuredContent ?? (block as Record<string, unknown>).structured_content;
882
+ if (sibling && typeof sibling === "object" && !Array.isArray(sibling)) {
883
+ return sibling as Record<string, unknown>;
884
+ }
885
+
886
+ if (Array.isArray(block.content)) {
887
+ for (const item of block.content) {
888
+ if (!item || typeof item !== "object") continue;
889
+ const sub = item as Record<string, unknown>;
890
+ if (sub.type !== "structuredContent" && sub.type !== "structured_content") continue;
891
+ const payload = sub.structuredContent ?? sub.structured_content ?? sub.data ?? sub.value;
892
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
893
+ return payload as Record<string, unknown>;
894
+ }
895
+ }
896
+ }
897
+
898
+ // Return undefined (not {}) when no structured payload is present, matching
899
+ // the pre-#4477 contract where `details` was nullable. An empty-object
900
+ // sentinel is truthy and breaks downstream consumers that gate on
901
+ // `if (details)`. `undefined` matches the type of the field these results
902
+ // flow into (`Record<string, unknown> | undefined`).
903
+ return undefined;
904
+ }
905
+
906
+ /**
907
+ * True for items that are MCP `structuredContent` pseudo-blocks living inside
908
+ * a tool-result `content[]` array. These blocks carry the structured payload
909
+ * (extracted separately by `extractStructuredDetailsFromBlock`) and must NOT
910
+ * leak into the visible content rendered to the user — otherwise the renderer
911
+ * stringifies the JSON pseudo-block and shows it next to the actual tool
912
+ * output. See PR #4477 review (CodeRabbit, post-fix-round).
913
+ */
914
+ function isStructuredContentPseudoBlock(item: unknown): boolean {
915
+ if (!item || typeof item !== "object") return false;
916
+ const type = (item as Record<string, unknown>).type;
917
+ return type === "structuredContent" || type === "structured_content";
918
+ }
919
+
920
+ /**
921
+ * Strip `structuredContent` pseudo-blocks from a tool-result content array
922
+ * before normalization. The structured payload is extracted via the sibling
923
+ * `structuredContent` field (or a dedicated extractor pass on the raw block);
924
+ * the visible content path must not include the pseudo-block itself.
925
+ */
926
+ function stripStructuredContentPseudoBlocks(content: unknown): unknown {
927
+ if (!Array.isArray(content)) return content;
928
+ return content.filter((item) => !isStructuredContentPseudoBlock(item));
929
+ }
930
+
870
931
  /** Extract tool result payloads from an SDK synthetic user message, keyed by tool-use ID. */
871
932
  export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): Array<{
872
933
  toolUseId: string;
@@ -890,8 +951,8 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
890
951
  extracted.push({
891
952
  toolUseId,
892
953
  result: {
893
- content: normalizeToolResultContent(block.content),
894
- details: {},
954
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(block.content)),
955
+ details: extractStructuredDetailsFromBlock(block),
895
956
  isError: block.is_error === true,
896
957
  },
897
958
  });
@@ -906,8 +967,8 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
906
967
  extracted.push({
907
968
  toolUseId,
908
969
  result: {
909
- content: normalizeToolResultContent(toolResult.content),
910
- details: {},
970
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(toolResult.content)),
971
+ details: extractStructuredDetailsFromBlock(toolResult),
911
972
  isError: toolResult.is_error === true,
912
973
  },
913
974
  });
@@ -372,13 +372,146 @@ describe("stream-adapter — Claude Code external tool results", () => {
372
372
  toolUseId: "tool-bash-1",
373
373
  result: {
374
374
  content: [{ type: "text", text: "line 1\nline 2" }],
375
- details: {},
375
+ // extractStructuredDetailsFromBlock returns undefined when no
376
+ // structured payload exists, restoring the pre-#4477 nullable
377
+ // contract (#4477 review feedback).
378
+ details: undefined,
376
379
  isError: false,
377
380
  },
378
381
  },
379
382
  ]);
380
383
  });
381
384
 
385
+ test("extractToolResultsFromSdkUserMessage reads structuredContent as a sibling field (#4472)", () => {
386
+ const message: SDKUserMessage = {
387
+ type: "user",
388
+ session_id: "sess-1",
389
+ parent_tool_use_id: "tool-mcp-1",
390
+ message: {
391
+ role: "user",
392
+ content: [
393
+ {
394
+ type: "mcp_tool_result",
395
+ tool_use_id: "tool-mcp-1",
396
+ content: [{ type: "text", text: "Gate Q3 result saved: verdict=pass" }],
397
+ is_error: false,
398
+ structuredContent: { gateId: "Q3", verdict: "pass" },
399
+ } as unknown as Record<string, unknown>,
400
+ ],
401
+ },
402
+ };
403
+
404
+ const results = extractToolResultsFromSdkUserMessage(message);
405
+ assert.deepEqual(results[0].result.details, { gateId: "Q3", verdict: "pass" });
406
+ });
407
+
408
+ test("extractToolResultsFromSdkUserMessage reads structuredContent from a content sub-block (#4472)", () => {
409
+ const message: SDKUserMessage = {
410
+ type: "user",
411
+ session_id: "sess-1",
412
+ parent_tool_use_id: "tool-mcp-2",
413
+ message: {
414
+ role: "user",
415
+ content: [
416
+ {
417
+ type: "mcp_tool_result",
418
+ tool_use_id: "tool-mcp-2",
419
+ content: [
420
+ { type: "text", text: "Gate Q4 result saved: verdict=flag" },
421
+ { type: "structuredContent", structuredContent: { gateId: "Q4", verdict: "flag" } },
422
+ ],
423
+ is_error: false,
424
+ } as unknown as Record<string, unknown>,
425
+ ],
426
+ },
427
+ };
428
+
429
+ const results = extractToolResultsFromSdkUserMessage(message);
430
+ assert.deepEqual(results[0].result.details, { gateId: "Q4", verdict: "flag" });
431
+ });
432
+
433
+ test("#4477 extractToolResultsFromSdkUserMessage does NOT leak structuredContent pseudo-blocks into visible content", () => {
434
+ // Regression: when a content sub-block carries `type: "structuredContent"`,
435
+ // it carries the structured payload (extracted separately into `details`)
436
+ // and must NOT appear in the visible `content` array — otherwise the
437
+ // renderer stringifies the JSON pseudo-block and shows it next to the
438
+ // actual tool output. See PR #4477 review (CodeRabbit, post-fix-round).
439
+ const message: SDKUserMessage = {
440
+ type: "user",
441
+ session_id: "sess-1",
442
+ parent_tool_use_id: "tool-mcp-strip",
443
+ message: {
444
+ role: "user",
445
+ content: [
446
+ {
447
+ type: "mcp_tool_result",
448
+ tool_use_id: "tool-mcp-strip",
449
+ content: [
450
+ { type: "text", text: "Gate Q5 result saved: verdict=pass" },
451
+ { type: "structuredContent", structuredContent: { gateId: "Q5", verdict: "pass" } },
452
+ { type: "text", text: "second visible line" },
453
+ // snake_case variant — also a pseudo-block; also must be stripped
454
+ { type: "structured_content", structured_content: { extra: "data" } },
455
+ ],
456
+ is_error: false,
457
+ } as unknown as Record<string, unknown>,
458
+ ],
459
+ },
460
+ };
461
+
462
+ const results = extractToolResultsFromSdkUserMessage(message);
463
+ assert.equal(results.length, 1, "should extract one result");
464
+ const result = results[0].result;
465
+
466
+ // The structured payload IS extracted to `details`.
467
+ assert.deepEqual(result.details, { gateId: "Q5", verdict: "pass" });
468
+
469
+ // The visible content has the two text blocks but NEITHER pseudo-block.
470
+ const visibleTexts = result.content.map((c: any) => c.text);
471
+ assert.deepEqual(
472
+ visibleTexts,
473
+ ["Gate Q5 result saved: verdict=pass", "second visible line"],
474
+ "visible content must include only the two text blocks; both structuredContent variants must be stripped",
475
+ );
476
+
477
+ // Belt-and-suspenders: assert no rendered text shows the JSON serialization
478
+ // of a pseudo-block. We don't check for bare keys like "gateId" or "verdict"
479
+ // because those are legitimate words in the gate-result message text. The
480
+ // regression signature would be a JSON-shaped substring that could only
481
+ // appear via stringification.
482
+ const allText = visibleTexts.join("\n");
483
+ assert.ok(
484
+ !allText.includes('"structuredContent"'),
485
+ "rendered content must not include the pseudo-block type marker as JSON text",
486
+ );
487
+ assert.ok(
488
+ !allText.includes('"structured_content"'),
489
+ "rendered content must not include the snake_case pseudo-block type marker as JSON text",
490
+ );
491
+ });
492
+
493
+ test("extractToolResultsFromSdkUserMessage accepts snake_case structured_content defensively (#4472)", () => {
494
+ const message: SDKUserMessage = {
495
+ type: "user",
496
+ session_id: "sess-1",
497
+ parent_tool_use_id: "tool-mcp-3",
498
+ message: {
499
+ role: "user",
500
+ content: [
501
+ {
502
+ type: "mcp_tool_result",
503
+ tool_use_id: "tool-mcp-3",
504
+ content: [{ type: "text", text: "ok" }],
505
+ structured_content: { operation: "save_gate_result" },
506
+ } as unknown as Record<string, unknown>,
507
+ ],
508
+ },
509
+ };
510
+
511
+ const results = extractToolResultsFromSdkUserMessage(message);
512
+ assert.deepEqual(results[0].result.details, { operation: "save_gate_result" });
513
+ });
514
+
382
515
  test("extractToolResultsFromSdkUserMessage falls back to tool_use_result", () => {
383
516
  const message: SDKUserMessage = {
384
517
  type: "user",
@@ -398,7 +531,9 @@ describe("stream-adapter — Claude Code external tool results", () => {
398
531
  toolUseId: "tool-read-1",
399
532
  result: {
400
533
  content: [{ type: "text", text: "file contents" }],
401
- details: {},
534
+ // undefined (not {}) per the restored nullable contract — see
535
+ // the analogous assertion in the tool_result test above.
536
+ details: undefined,
402
537
  isError: true,
403
538
  },
404
539
  },
@@ -1081,11 +1216,15 @@ describe("stream-adapter — permission mode (F10)", () => {
1081
1216
  }
1082
1217
  }
1083
1218
 
1084
- test("buildSdkOptions defaults to bypassPermissions for backwards compatibility", () => {
1219
+ test("buildSdkOptions defaults to acceptEdits (#4383)", () => {
1085
1220
  clearWorkflowMcpEnv();
1086
1221
  const opts = buildSdkOptions("claude-sonnet-4-6", "test");
1087
- assert.equal(opts.permissionMode, "bypassPermissions");
1088
- assert.equal(opts.allowDangerouslySkipPermissions, true);
1222
+ assert.equal(opts.permissionMode, "acceptEdits");
1223
+ assert.equal(
1224
+ opts.allowDangerouslySkipPermissions,
1225
+ false,
1226
+ "allowDangerouslySkipPermissions must be false when permissionMode is acceptEdits",
1227
+ );
1089
1228
  });
1090
1229
 
1091
1230
  test("buildSdkOptions respects explicit acceptEdits override", () => {
@@ -1099,6 +1238,11 @@ describe("stream-adapter — permission mode (F10)", () => {
1099
1238
  );
1100
1239
  });
1101
1240
 
1241
+ test("resolveClaudePermissionMode defaults to acceptEdits when no env var is set (#4383)", async () => {
1242
+ const mode = await resolveClaudePermissionMode({});
1243
+ assert.equal(mode, "acceptEdits");
1244
+ });
1245
+
1102
1246
  test("resolveClaudePermissionMode honours the GSD_CLAUDE_CODE_PERMISSION_MODE env override", async () => {
1103
1247
  const env = { GSD_CLAUDE_CODE_PERMISSION_MODE: "acceptEdits" } as NodeJS.ProcessEnv;
1104
1248
  const mode = await resolveClaudePermissionMode(env);
@@ -395,7 +395,7 @@ export async function selectAndApplyModel(
395
395
  // ADR-005: Adjust active tool set for the selected model's provider capabilities.
396
396
  // Hard-filter incompatible tools, then let extensions override via adjust_tool_set hook.
397
397
  const activeToolNames = pi.getActiveTools();
398
- const { toolNames: compatibleTools, removedTools } = adjustToolSet(activeToolNames, model.api);
398
+ const { toolNames: compatibleTools, removedTools } = adjustToolSet(activeToolNames, model.api, model.provider);
399
399
  let finalToolNames = compatibleTools;
400
400
 
401
401
  // Fire adjust_tool_set hook — extensions can override the filtered tool set
@@ -107,7 +107,6 @@ import {
107
107
  updateProgressWidget as _updateProgressWidget,
108
108
  updateSliceProgressCache,
109
109
  unitVerb,
110
- hideFooter,
111
110
  describeNextUnit,
112
111
  } from "./auto-dashboard.js";
113
112
  import { existsSync, unlinkSync } from "node:fs";
@@ -61,7 +61,7 @@ import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
61
61
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
62
62
  import { snapshotSkills } from "./skill-discovery.js";
63
63
  import { isDbAvailable, getMilestone, openDatabase, getDbStatus } from "./gsd-db.js";
64
- import { hideFooter } from "./auto-dashboard.js";
64
+
65
65
  import {
66
66
  debugLog,
67
67
  enableDebug,
@@ -92,7 +92,7 @@ import type { WorktreeResolver } from "./worktree-resolver.js";
92
92
  import { getSessionModelOverride } from "./session-model-override.js";
93
93
 
94
94
  export interface BootstrapDeps {
95
- shouldUseWorktreeIsolation: () => boolean;
95
+ shouldUseWorktreeIsolation: (basePath?: string) => boolean;
96
96
  registerSigtermHandler: (basePath: string) => void;
97
97
  lockBase: () => string;
98
98
  buildResolver: () => WorktreeResolver;
@@ -343,7 +343,7 @@ export async function bootstrapAutoSession(
343
343
  const hasLocalGit = existsSync(join(base, ".git"));
344
344
  if (!hasLocalGit || isInheritedRepo(base)) {
345
345
  const mainBranch =
346
- loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
346
+ loadEffectiveGSDPreferences(base)?.preferences?.git?.main_branch || "main";
347
347
  nativeInit(base, mainBranch);
348
348
  }
349
349
 
@@ -361,7 +361,7 @@ export async function bootstrapAutoSession(
361
361
  // Ensure .gitignore has baseline patterns.
362
362
  // ensureGitignore checks for git-tracked .gsd/ files and skips the
363
363
  // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
364
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
364
+ const gitPrefs = loadEffectiveGSDPreferences(base)?.preferences?.git;
365
365
  const manageGitignore = gitPrefs?.manage_gitignore;
366
366
  ensureGitignore(base, { manageGitignore });
367
367
  if (manageGitignore !== false) untrackRuntimeFiles(base);
@@ -390,7 +390,7 @@ export async function bootstrapAutoSession(
390
390
  // Initialize GitServiceImpl
391
391
  s.gitService = new GitServiceImpl(
392
392
  s.basePath,
393
- loadEffectiveGSDPreferences()?.preferences?.git ?? {},
393
+ loadEffectiveGSDPreferences(base)?.preferences?.git ?? {},
394
394
  );
395
395
 
396
396
  // ── Debug mode ──
@@ -434,7 +434,7 @@ export async function bootstrapAutoSession(
434
434
  // was lost due to session ending between completion and teardown.
435
435
  // Must run after DB open and before worktree entry.
436
436
  try {
437
- const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode());
437
+ const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode(base));
438
438
  for (const msg of auditResult.recovered) {
439
439
  ctx.ui.notify(`Orphan audit: ${msg}`, "info");
440
440
  }
@@ -454,7 +454,7 @@ export async function bootstrapAutoSession(
454
454
  // Stale worktree state recovery (#654)
455
455
  if (
456
456
  state.activeMilestone &&
457
- shouldUseWorktreeIsolation() &&
457
+ shouldUseWorktreeIsolation(base) &&
458
458
  !detectWorktreeName(base)
459
459
  ) {
460
460
  const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
@@ -472,7 +472,7 @@ export async function bootstrapAutoSession(
472
472
  if (
473
473
  state.activeMilestone &&
474
474
  (state.phase === "pre-planning" || state.phase === "complete") &&
475
- getIsolationMode() !== "none" &&
475
+ getIsolationMode(base) !== "none" &&
476
476
  !detectWorktreeName(base) &&
477
477
  !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
478
478
  ) {
@@ -676,7 +676,7 @@ export async function bootstrapAutoSession(
676
676
 
677
677
  // Capture integration branch
678
678
  if (s.currentMilestoneId) {
679
- if (getIsolationMode() !== "none") {
679
+ if (getIsolationMode(base) !== "none") {
680
680
  captureIntegrationBranch(base, s.currentMilestoneId);
681
681
  }
682
682
  setActiveMilestoneId(base, s.currentMilestoneId);
@@ -685,7 +685,7 @@ export async function bootstrapAutoSession(
685
685
  // Guard against stale milestone branch when isolation:none (#3613).
686
686
  // A prior session with isolation:branch/worktree may have left HEAD on
687
687
  // milestone/<MID>. Auto-checkout back to the integration branch.
688
- if (getIsolationMode() === "none" && nativeIsRepo(base)) {
688
+ if (getIsolationMode(base) === "none" && nativeIsRepo(base)) {
689
689
  try {
690
690
  const currentBranch = nativeGetCurrentBranch(base);
691
691
  if (currentBranch.startsWith("milestone/")) {
@@ -716,7 +716,7 @@ export async function bootstrapAutoSession(
716
716
 
717
717
  if (
718
718
  s.currentMilestoneId &&
719
- getIsolationMode() !== "none" &&
719
+ getIsolationMode(base) !== "none" &&
720
720
  !detectWorktreeName(base) &&
721
721
  !isUnderGsdWorktrees(base)
722
722
  ) {
@@ -819,14 +819,11 @@ export async function bootstrapAutoSession(
819
819
  }
820
820
 
821
821
  // Snapshot installed skills
822
- if (resolveSkillDiscoveryMode() !== "off") {
822
+ if (resolveSkillDiscoveryMode(base) !== "off") {
823
823
  snapshotSkills();
824
824
  }
825
825
 
826
826
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
827
- ctx.ui.setFooter(hideFooter);
828
- // Hide gsd-health during AUTO — gsd-progress is the single source of truth
829
- // for last-commit / cost / health signal while auto is running.
830
827
  ctx.ui.setWidget("gsd-health", undefined);
831
828
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
832
829
  const pendingCount = (state.registry ?? []).filter(
@@ -853,7 +850,7 @@ export async function bootstrapAutoSession(
853
850
  // FlatRateContext used by selectAndApplyModel so user-declared
854
851
  // flat-rate providers and externalCli auto-detection are respected.
855
852
  const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
856
- const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
853
+ const bannerPrefs = loadEffectiveGSDPreferences(base)?.preferences;
857
854
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
858
855
  const effectivelyEnabled = routingConfig.enabled
859
856
  && (routingConfig.allow_flat_rate_providers
@@ -182,7 +182,6 @@ import {
182
182
  unitVerb,
183
183
  formatAutoElapsed as _formatAutoElapsed,
184
184
  formatWidgetTokens,
185
- hideFooter,
186
185
  type WidgetStateAccessors,
187
186
  } from "./auto-dashboard.js";
188
187
  import {
@@ -330,8 +329,8 @@ export function startAutoDetached(
330
329
  }
331
330
 
332
331
  /** Returns true if the project is configured for `isolation:worktree` mode. */
333
- export function shouldUseWorktreeIsolation(): boolean {
334
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
332
+ export function shouldUseWorktreeIsolation(basePath?: string): boolean {
333
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
335
334
  if (prefs?.isolation === "worktree") return true;
336
335
  // Default is false — worktree isolation requires explicit opt-in
337
336
  return false;
@@ -424,7 +423,7 @@ export function getAutoDashboardData(): AutoDashboardData {
424
423
  const rtkSavings = sessionId && s.basePath
425
424
  ? getRtkSessionSavings(s.basePath, sessionId)
426
425
  : null;
427
- const rtkEnabled = loadEffectiveGSDPreferences()?.preferences.experimental?.rtk === true;
426
+ const rtkEnabled = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences.experimental?.rtk === true;
428
427
  // Pending capture count — lazy check, non-fatal
429
428
  let pendingCaptureCount = 0;
430
429
  try {
@@ -648,7 +647,7 @@ function buildSnapshotOpts(
648
647
  gitStatus?: "ok" | "failed";
649
648
  gitError?: string;
650
649
  } & Record<string, unknown> {
651
- const prefs = loadEffectiveGSDPreferences()?.preferences;
650
+ const prefs = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
652
651
  const uokFlags = resolveUokFlags(prefs);
653
652
  return {
654
653
  ...(s.autoStartTime > 0 ? { autoSessionKey: String(s.autoStartTime) } : {}),
@@ -686,7 +685,7 @@ function handleLostSessionLock(
686
685
  restoreProjectRootEnv();
687
686
  restoreMilestoneLockEnv();
688
687
  deregisterSigtermHandler();
689
- clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
688
+ clearCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences);
690
689
  const base = lockBase();
691
690
  const lockFilePath = base ? join(gsdRoot(base), "auto.lock") : "unknown";
692
691
  const recoverySuggestion = "\nTo recover, run: gsd doctor --fix";
@@ -706,7 +705,6 @@ function handleLostSessionLock(
706
705
  );
707
706
  ctx?.ui.setStatus("gsd-auto", undefined);
708
707
  ctx?.ui.setWidget("gsd-progress", undefined);
709
- ctx?.ui.setFooter(undefined);
710
708
  if (ctx) initHealthWidget(ctx);
711
709
  }
712
710
 
@@ -742,7 +740,6 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
742
740
  if (!s.paused) {
743
741
  ctx.ui.setStatus("gsd-auto", undefined);
744
742
  ctx.ui.setWidget("gsd-progress", undefined);
745
- ctx.ui.setFooter(undefined);
746
743
  initHealthWidget(ctx);
747
744
  }
748
745
 
@@ -764,7 +761,7 @@ export async function stopAuto(
764
761
  reason?: string,
765
762
  ): Promise<void> {
766
763
  if (!s.active && !s.paused) return;
767
- const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
764
+ const loadedPreferences = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
768
765
  const reasonSuffix = reason ? ` — ${reason}` : "";
769
766
 
770
767
  try {
@@ -1018,7 +1015,6 @@ export async function stopAuto(
1018
1015
  // UI cleanup
1019
1016
  ctx?.ui.setStatus("gsd-auto", undefined);
1020
1017
  ctx?.ui.setWidget("gsd-progress", undefined);
1021
- ctx?.ui.setFooter(undefined);
1022
1018
  if (ctx) initHealthWidget(ctx);
1023
1019
  restoreProjectRootEnv();
1024
1020
  restoreMilestoneLockEnv();
@@ -1121,7 +1117,6 @@ export async function pauseAuto(
1121
1117
  s.verificationRetryCount.clear();
1122
1118
  ctx?.ui.setStatus("gsd-auto", "paused");
1123
1119
  ctx?.ui.setWidget("gsd-progress", undefined);
1124
- ctx?.ui.setFooter(undefined);
1125
1120
  if (ctx) initHealthWidget(ctx);
1126
1121
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
1127
1122
  ctx?.ui.notify(
@@ -1495,7 +1490,7 @@ export async function startAuto(
1495
1490
  // ── Auto-worktree / branch-mode: re-enter on resume ──
1496
1491
  if (
1497
1492
  s.currentMilestoneId &&
1498
- getIsolationMode() !== "none" &&
1493
+ getIsolationMode(s.originalBasePath || s.basePath) !== "none" &&
1499
1494
  s.originalBasePath &&
1500
1495
  !isInAutoWorktree(s.basePath) &&
1501
1496
  !detectWorktreeName(s.basePath) &&
@@ -1509,7 +1504,7 @@ export async function startAuto(
1509
1504
  registerSigtermHandler(lockBase());
1510
1505
 
1511
1506
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1512
- ctx.ui.setFooter(hideFooter);
1507
+ ctx.ui.setWidget("gsd-health", undefined);
1513
1508
  ctx.ui.notify(
1514
1509
  s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.",
1515
1510
  "info",
@@ -1534,7 +1529,7 @@ export async function startAuto(
1534
1529
  await openProjectDbIfPresent(s.basePath);
1535
1530
  try {
1536
1531
  await rebuildState(s.basePath);
1537
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1532
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1538
1533
  } catch (e) {
1539
1534
  debugLog("resume-rebuild-state-failed", {
1540
1535
  error: e instanceof Error ? e.message : String(e),
@@ -1584,7 +1579,7 @@ export async function startAuto(
1584
1579
  "resuming",
1585
1580
  s.currentMilestoneId ?? "unknown",
1586
1581
  );
1587
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1582
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1588
1583
 
1589
1584
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1590
1585
  startAutoCommandPolling(s.basePath);
@@ -1622,12 +1617,12 @@ export async function startAuto(
1622
1617
 
1623
1618
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1624
1619
  try {
1625
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1620
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1626
1621
  } catch (err) {
1627
1622
  // Best-effort only — sidebar sync must never block auto-mode startup
1628
1623
  logWarning("engine", `cmux sync failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1629
1624
  }
1630
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1625
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1631
1626
 
1632
1627
  startAutoCommandPolling(s.basePath);
1633
1628
 
@@ -1,7 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
2
2
 
3
3
  import { logWarning } from "../workflow-logger.js";
4
- import { checkAutoStartAfterDiscuss } from "../guided-flow.js";
4
+ import {
5
+ checkAutoStartAfterDiscuss,
6
+ maybeHandleReadyPhraseWithoutFiles,
7
+ maybeHandleEmptyIntentTurn,
8
+ resetEmptyTurnCounter,
9
+ } from "../guided-flow.js";
5
10
  import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, pauseAuto, setCurrentDispatchedModelId } from "../auto.js";
6
11
  import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js";
7
12
  import { pauseAutoForProviderError } from "../provider-error-pause.js";
@@ -75,6 +80,20 @@ export async function handleAgentEnd(
75
80
  clearDiscussionFlowState();
76
81
  return;
77
82
  }
83
+
84
+ // #4573 — When the LLM emits "Milestone X ready." but the required files
85
+ // are missing, `checkAutoStartAfterDiscuss` returns false silently. Surface
86
+ // that and nudge the LLM to complete the writes before the user hits the
87
+ // downstream "All milestones complete" warning loop.
88
+ if (maybeHandleReadyPhraseWithoutFiles(event)) return;
89
+
90
+ // #4573 — Empty-turn recovery: if the LLM announced intent in prose but
91
+ // emitted no tool calls, nudge it to execute. Fires only when auto-mode is
92
+ // active or a discussion autostart is pending (non-auto interactive discuss
93
+ // is user-driven). Runs before `isAutoActive` early return so pending
94
+ // discussions (where isAutoActive may be false) still get recovered.
95
+ if (maybeHandleEmptyIntentTurn(event, isAutoActive())) return;
96
+
78
97
  if (!isAutoActive()) return;
79
98
  if (isSessionSwitchInFlight()) return;
80
99
 
@@ -336,6 +355,9 @@ export async function handleAgentEnd(
336
355
  // ── Success path ─────────────────────────────────────────────────────────
337
356
  try {
338
357
  resetRetryState(retryState);
358
+ // #4573 — Reset the empty-turn counter on any successful agent turn so
359
+ // transient stalls don't accumulate across independent units.
360
+ resetEmptyTurnCounter();
339
361
  resolveAgentEnd(event);
340
362
  } catch (err) {
341
363
  const message = err instanceof Error ? err.message : String(err);