gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.fe143342a

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 (139) hide show
  1. package/dist/mcp-server.d.ts +7 -0
  2. package/dist/mcp-server.js +35 -1
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +2 -8
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
  6. package/dist/resources/extensions/gsd/auto-start.js +27 -14
  7. package/dist/resources/extensions/gsd/auto.js +11 -11
  8. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  9. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  10. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  12. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  13. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  14. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  15. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  16. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  17. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  18. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  19. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  20. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  21. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  22. package/dist/resources/extensions/gsd/preferences.js +17 -17
  23. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  24. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  25. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  26. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  27. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  28. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  29. package/dist/web/standalone/.next/BUILD_ID +1 -1
  30. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  31. package/dist/web/standalone/.next/build-manifest.json +2 -2
  32. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  33. package/dist/web/standalone/.next/required-server-files.json +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.html +1 -1
  51. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  58. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  67. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  68. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  69. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  70. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  71. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  72. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  74. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  76. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  78. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  81. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  83. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  92. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  93. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  94. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  95. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  96. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  97. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  98. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  99. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  100. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  101. package/src/resources/extensions/gsd/auto-start.ts +28 -15
  102. package/src/resources/extensions/gsd/auto.ts +11 -11
  103. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  104. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  105. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  106. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  107. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  108. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  109. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  110. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  111. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  112. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  113. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  114. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  115. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  116. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  117. package/src/resources/extensions/gsd/preferences.ts +17 -17
  118. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  119. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  120. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  121. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  122. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  123. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  124. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  125. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  126. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  127. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  128. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  129. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  130. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  131. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  132. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  133. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  134. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  135. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  136. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  137. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  138. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  139. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
@@ -867,6 +867,69 @@ function normalizeToolResultContent(content: unknown): ExternalToolResultContent
867
867
  return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
868
868
  }
869
869
 
870
+ /**
871
+ * Extract a `details` payload from an MCP tool-result block.
872
+ *
873
+ * MCP's `CallToolResult` carries structured data in `structuredContent` — the
874
+ * protocol's supported channel for non-text payloads. Claude Code's synthetic
875
+ * user message may surface that field in one of two shapes depending on SDK
876
+ * version: as a sibling on the `mcp_tool_result` block itself, or as a
877
+ * dedicated content sub-block with `type: "structuredContent"`. Snake-case
878
+ * (`structured_content`) is accepted defensively in case a transport hop
879
+ * rewrites casing. All other shapes fall back to an empty object so callers
880
+ * can rely on `details` being present.
881
+ */
882
+ function extractStructuredDetailsFromBlock(block: Record<string, unknown>): Record<string, unknown> | undefined {
883
+ const sibling = block.structuredContent ?? (block as Record<string, unknown>).structured_content;
884
+ if (sibling && typeof sibling === "object" && !Array.isArray(sibling)) {
885
+ return sibling as Record<string, unknown>;
886
+ }
887
+
888
+ if (Array.isArray(block.content)) {
889
+ for (const item of block.content) {
890
+ if (!item || typeof item !== "object") continue;
891
+ const sub = item as Record<string, unknown>;
892
+ if (sub.type !== "structuredContent" && sub.type !== "structured_content") continue;
893
+ const payload = sub.structuredContent ?? sub.structured_content ?? sub.data ?? sub.value;
894
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
895
+ return payload as Record<string, unknown>;
896
+ }
897
+ }
898
+ }
899
+
900
+ // Return undefined (not {}) when no structured payload is present, matching
901
+ // the pre-#4477 contract where `details` was nullable. An empty-object
902
+ // sentinel is truthy and breaks downstream consumers that gate on
903
+ // `if (details)`. `undefined` matches the type of the field these results
904
+ // flow into (`Record<string, unknown> | undefined`).
905
+ return undefined;
906
+ }
907
+
908
+ /**
909
+ * True for items that are MCP `structuredContent` pseudo-blocks living inside
910
+ * a tool-result `content[]` array. These blocks carry the structured payload
911
+ * (extracted separately by `extractStructuredDetailsFromBlock`) and must NOT
912
+ * leak into the visible content rendered to the user — otherwise the renderer
913
+ * stringifies the JSON pseudo-block and shows it next to the actual tool
914
+ * output. See PR #4477 review (CodeRabbit, post-fix-round).
915
+ */
916
+ function isStructuredContentPseudoBlock(item: unknown): boolean {
917
+ if (!item || typeof item !== "object") return false;
918
+ const type = (item as Record<string, unknown>).type;
919
+ return type === "structuredContent" || type === "structured_content";
920
+ }
921
+
922
+ /**
923
+ * Strip `structuredContent` pseudo-blocks from a tool-result content array
924
+ * before normalization. The structured payload is extracted via the sibling
925
+ * `structuredContent` field (or a dedicated extractor pass on the raw block);
926
+ * the visible content path must not include the pseudo-block itself.
927
+ */
928
+ function stripStructuredContentPseudoBlocks(content: unknown): unknown {
929
+ if (!Array.isArray(content)) return content;
930
+ return content.filter((item) => !isStructuredContentPseudoBlock(item));
931
+ }
932
+
870
933
  /** Extract tool result payloads from an SDK synthetic user message, keyed by tool-use ID. */
871
934
  export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): Array<{
872
935
  toolUseId: string;
@@ -890,8 +953,8 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
890
953
  extracted.push({
891
954
  toolUseId,
892
955
  result: {
893
- content: normalizeToolResultContent(block.content),
894
- details: {},
956
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(block.content)),
957
+ details: extractStructuredDetailsFromBlock(block),
895
958
  isError: block.is_error === true,
896
959
  },
897
960
  });
@@ -906,8 +969,8 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
906
969
  extracted.push({
907
970
  toolUseId,
908
971
  result: {
909
- content: normalizeToolResultContent(toolResult.content),
910
- details: {},
972
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(toolResult.content)),
973
+ details: extractStructuredDetailsFromBlock(toolResult),
911
974
  isError: toolResult.is_error === true,
912
975
  },
913
976
  });
@@ -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
  },
@@ -60,7 +60,7 @@ import { initRoutingHistory } from "./routing-history.js";
60
60
  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
- import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
63
+ import { isDbAvailable, getMilestone, openDatabase, getDbStatus } from "./gsd-db.js";
64
64
  import { hideFooter } from "./auto-dashboard.js";
65
65
  import {
66
66
  debugLog,
@@ -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
  ) {
@@ -762,9 +762,22 @@ export async function bootstrapAutoSession(
762
762
  // call returns "db_unavailable", triggering artifact-retry which
763
763
  // re-dispatches the same task — producing an infinite loop (#2419).
764
764
  if (existsSync(gsdDbPath) && !isDbAvailable()) {
765
+ const dbStatus = getDbStatus();
766
+ const phaseHint = dbStatus.lastPhase === "open"
767
+ ? "The database file could not be opened"
768
+ : dbStatus.lastPhase === "initSchema"
769
+ ? "The database schema could not be initialized"
770
+ : dbStatus.lastPhase === "vacuum-recovery"
771
+ ? "Corruption recovery (VACUUM) failed"
772
+ : dbStatus.attempted
773
+ ? "The database could not be opened (phase unknown)"
774
+ : "The database provider could not be loaded";
775
+ const errorDetail = dbStatus.lastError ? ` (${dbStatus.lastError.message})` : "";
776
+ const providerHint = dbStatus.provider
777
+ ? ` Provider: ${dbStatus.provider}.`
778
+ : " No SQLite provider available — check Node >= 22 or install better-sqlite3.";
765
779
  ctx.ui.notify(
766
- "SQLite database exists but failed to open. Auto-mode cannot proceed without a working database provider. " +
767
- "Check for corrupt gsd.db or missing native SQLite bindings.",
780
+ `SQLite database exists but failed to open: ${gsdDbPath}. ${phaseHint}${errorDetail}.${providerHint}`,
768
781
  "error",
769
782
  );
770
783
  return releaseLockAndReturn();
@@ -806,7 +819,7 @@ export async function bootstrapAutoSession(
806
819
  }
807
820
 
808
821
  // Snapshot installed skills
809
- if (resolveSkillDiscoveryMode() !== "off") {
822
+ if (resolveSkillDiscoveryMode(base) !== "off") {
810
823
  snapshotSkills();
811
824
  }
812
825
 
@@ -840,7 +853,7 @@ export async function bootstrapAutoSession(
840
853
  // FlatRateContext used by selectAndApplyModel so user-declared
841
854
  // flat-rate providers and externalCli auto-detection are respected.
842
855
  const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
843
- const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
856
+ const bannerPrefs = loadEffectiveGSDPreferences(base)?.preferences;
844
857
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
845
858
  const effectivelyEnabled = routingConfig.enabled
846
859
  && (routingConfig.allow_flat_rate_providers
@@ -330,8 +330,8 @@ export function startAutoDetached(
330
330
  }
331
331
 
332
332
  /** Returns true if the project is configured for `isolation:worktree` mode. */
333
- export function shouldUseWorktreeIsolation(): boolean {
334
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
333
+ export function shouldUseWorktreeIsolation(basePath?: string): boolean {
334
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
335
335
  if (prefs?.isolation === "worktree") return true;
336
336
  // Default is false — worktree isolation requires explicit opt-in
337
337
  return false;
@@ -424,7 +424,7 @@ export function getAutoDashboardData(): AutoDashboardData {
424
424
  const rtkSavings = sessionId && s.basePath
425
425
  ? getRtkSessionSavings(s.basePath, sessionId)
426
426
  : null;
427
- const rtkEnabled = loadEffectiveGSDPreferences()?.preferences.experimental?.rtk === true;
427
+ const rtkEnabled = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences.experimental?.rtk === true;
428
428
  // Pending capture count — lazy check, non-fatal
429
429
  let pendingCaptureCount = 0;
430
430
  try {
@@ -648,7 +648,7 @@ function buildSnapshotOpts(
648
648
  gitStatus?: "ok" | "failed";
649
649
  gitError?: string;
650
650
  } & Record<string, unknown> {
651
- const prefs = loadEffectiveGSDPreferences()?.preferences;
651
+ const prefs = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
652
652
  const uokFlags = resolveUokFlags(prefs);
653
653
  return {
654
654
  ...(s.autoStartTime > 0 ? { autoSessionKey: String(s.autoStartTime) } : {}),
@@ -686,7 +686,7 @@ function handleLostSessionLock(
686
686
  restoreProjectRootEnv();
687
687
  restoreMilestoneLockEnv();
688
688
  deregisterSigtermHandler();
689
- clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
689
+ clearCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences);
690
690
  const base = lockBase();
691
691
  const lockFilePath = base ? join(gsdRoot(base), "auto.lock") : "unknown";
692
692
  const recoverySuggestion = "\nTo recover, run: gsd doctor --fix";
@@ -764,7 +764,7 @@ export async function stopAuto(
764
764
  reason?: string,
765
765
  ): Promise<void> {
766
766
  if (!s.active && !s.paused) return;
767
- const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
767
+ const loadedPreferences = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
768
768
  const reasonSuffix = reason ? ` — ${reason}` : "";
769
769
 
770
770
  try {
@@ -1495,7 +1495,7 @@ export async function startAuto(
1495
1495
  // ── Auto-worktree / branch-mode: re-enter on resume ──
1496
1496
  if (
1497
1497
  s.currentMilestoneId &&
1498
- getIsolationMode() !== "none" &&
1498
+ getIsolationMode(s.originalBasePath || s.basePath) !== "none" &&
1499
1499
  s.originalBasePath &&
1500
1500
  !isInAutoWorktree(s.basePath) &&
1501
1501
  !detectWorktreeName(s.basePath) &&
@@ -1534,7 +1534,7 @@ export async function startAuto(
1534
1534
  await openProjectDbIfPresent(s.basePath);
1535
1535
  try {
1536
1536
  await rebuildState(s.basePath);
1537
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1537
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1538
1538
  } catch (e) {
1539
1539
  debugLog("resume-rebuild-state-failed", {
1540
1540
  error: e instanceof Error ? e.message : String(e),
@@ -1584,7 +1584,7 @@ export async function startAuto(
1584
1584
  "resuming",
1585
1585
  s.currentMilestoneId ?? "unknown",
1586
1586
  );
1587
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1587
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1588
1588
 
1589
1589
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1590
1590
  startAutoCommandPolling(s.basePath);
@@ -1622,12 +1622,12 @@ export async function startAuto(
1622
1622
 
1623
1623
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1624
1624
  try {
1625
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1625
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1626
1626
  } catch (err) {
1627
1627
  // Best-effort only — sidebar sync must never block auto-mode startup
1628
1628
  logWarning("engine", `cmux sync failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1629
1629
  }
1630
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1630
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1631
1631
 
1632
1632
  startAutoCommandPolling(s.basePath);
1633
1633
 
@@ -35,6 +35,19 @@ function registerAlias(pi: ExtensionAPI, toolDef: any, aliasName: string, canoni
35
35
  });
36
36
  }
37
37
 
38
+ /**
39
+ * Read a tool result's structured payload, accommodating MCP's `details` →
40
+ * `structuredContent` rename (#4472, #4477). In-process executions still
41
+ * deliver the payload on `result.details`; MCP-routed executions deliver it
42
+ * on `result.structuredContent` (post `adaptExecutorResult` transform). All
43
+ * `renderResult` callbacks in this file route through this helper so a future
44
+ * field rename only needs to be applied in one place.
45
+ */
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- result shape varies by tool
47
+ function readDetails(result: any): any {
48
+ return result?.details ?? result?.structuredContent;
49
+ }
50
+
38
51
  export function registerDbTools(pi: ExtensionAPI): void {
39
52
  // ─── gsd_decision_save (formerly gsd_save_decision) ─────────────────────
40
53
 
@@ -110,7 +123,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
110
123
  return new Text(text, 0, 0);
111
124
  },
112
125
  renderResult(result: any, _options: any, theme: any) {
113
- const d = result.details;
126
+ const d = readDetails(result);
114
127
  if (result.isError || d?.error) {
115
128
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
116
129
  }
@@ -188,7 +201,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
188
201
  return new Text(text, 0, 0);
189
202
  },
190
203
  renderResult(result: any, _options: any, theme: any) {
191
- const d = result.details;
204
+ const d = readDetails(result);
192
205
  if (result.isError || d?.error) {
193
206
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
194
207
  }
@@ -273,7 +286,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
273
286
  return new Text(text, 0, 0);
274
287
  },
275
288
  renderResult(result: any, _options: any, theme: any) {
276
- const d = result.details;
289
+ const d = readDetails(result);
277
290
  if (result.isError || d?.error) {
278
291
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
279
292
  }
@@ -322,7 +335,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
322
335
  return new Text(text, 0, 0);
323
336
  },
324
337
  renderResult(result: any, _options: any, theme: any) {
325
- const d = result.details;
338
+ const d = readDetails(result);
326
339
  if (result.isError || d?.error) {
327
340
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
328
341
  }
@@ -406,7 +419,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
406
419
  return new Text(theme.fg("toolTitle", theme.bold("milestone_generate_id")), 0, 0);
407
420
  },
408
421
  renderResult(result: any, _options: any, theme: any) {
409
- const d = result.details;
422
+ const d = readDetails(result);
410
423
  if (result.isError || d?.error) {
411
424
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
412
425
  }
@@ -1074,13 +1087,31 @@ export function registerDbTools(pi: ExtensionAPI): void {
1074
1087
  text += theme.fg("dim", ` → ${args.verdict ?? ""}`);
1075
1088
  return new Text(text, 0, 0);
1076
1089
  },
1090
+ /**
1091
+ * Render the save_gate_result tool output for the TUI.
1092
+ *
1093
+ * Prefers structured fields, but falls back to `content[0].text` when the
1094
+ * structured payload is empty. Defensive: the structural fix on this
1095
+ * branch plumbs `details` through MCP via `structuredContent`, but older
1096
+ * hosts, a future handler that forgets `structuredContent`, or any drop
1097
+ * of non-standard return fields would otherwise render as
1098
+ * "undefined: undefined". Same fallback applies to error rendering, and
1099
+ * we strip a leading `Error:` from the fallback text to avoid producing
1100
+ * `Error: Error: ...`.
1101
+ */
1077
1102
  renderResult(result: any, _options: any, theme: any) {
1078
- const d = result.details;
1103
+ const d = readDetails(result);
1079
1104
  if (result.isError || d?.error) {
1080
- return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
1105
+ const rawMsg = d?.error ?? result.content?.[0]?.text ?? "unknown";
1106
+ const msg = rawMsg.replace(/^\s*Error:\s*/i, "");
1107
+ return new Text(theme.fg("error", `Error: ${msg}`), 0, 0);
1108
+ }
1109
+ if (!d?.gateId || !d?.verdict) {
1110
+ const text = result.content?.[0]?.text ?? "Gate result saved";
1111
+ return new Text(theme.fg("success", text), 0, 0);
1081
1112
  }
1082
- const color = d?.verdict === "flag" ? "warning" : "success";
1083
- return new Text(theme.fg(color, `${d?.gateId}: ${d?.verdict}`), 0, 0);
1113
+ const color = d.verdict === "flag" ? "warning" : "success";
1114
+ return new Text(theme.fg(color, `${d.gateId}: ${d.verdict}`), 0, 0);
1084
1115
  },
1085
1116
  };
1086
1117