gsd-pi 2.76.0-dev.b072ebb73 → 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 (200) 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/phases.js +4 -1
  7. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +39 -21
  10. package/dist/resources/extensions/gsd/auto.js +15 -12
  11. package/dist/resources/extensions/gsd/blocked-models.js +68 -0
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
  13. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  14. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  17. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  18. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  19. package/dist/resources/extensions/gsd/error-classifier.js +31 -3
  20. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  23. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  24. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  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/prompt-loader.js +22 -7
  30. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  31. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  32. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  33. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  34. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  35. package/dist/resources/extensions/search-the-web/command-search-provider.js +5 -4
  36. package/dist/resources/extensions/search-the-web/native-search.js +45 -13
  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 +8 -8
  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 +8 -8
  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/workflow-tools.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  76. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  77. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  78. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  79. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  82. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  84. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  86. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  88. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  89. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  90. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  91. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  92. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  93. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  94. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  95. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  96. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  98. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  100. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  104. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  106. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  114. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  117. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  119. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  132. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  133. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  134. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  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/provider-manager.ts +16 -7
  141. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  142. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  143. package/scripts/link-workspace-packages.cjs +1 -0
  144. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  145. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  146. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  147. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  148. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  149. package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
  150. package/src/resources/extensions/gsd/auto-start.ts +40 -22
  151. package/src/resources/extensions/gsd/auto.ts +15 -12
  152. package/src/resources/extensions/gsd/blocked-models.ts +98 -0
  153. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
  154. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  155. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  156. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  157. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  158. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  159. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  160. package/src/resources/extensions/gsd/error-classifier.ts +36 -3
  161. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  162. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  163. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  164. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  165. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  166. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  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/prompt-loader.ts +30 -7
  171. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  172. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  173. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  174. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  175. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
  176. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  177. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  178. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  179. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  180. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  181. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  182. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  183. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  184. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  185. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  186. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  187. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  188. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  189. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -0
  190. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  191. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  192. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  193. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  194. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  195. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  196. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  197. package/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
  198. package/src/resources/extensions/search-the-web/native-search.ts +48 -12
  199. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  200. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
3
+ import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
@@ -288,6 +288,136 @@ describe("workflow MCP tools", () => {
288
288
  }
289
289
  });
290
290
 
291
+ it("#4477 gsd_task_complete forwards every schema field to the executor (regression for destructure-rebuild bug class)", async () => {
292
+ // Locks in the class-fix from PR #4477 review: handleTaskComplete previously
293
+ // destructured args into a hand-listed set of fields and rebuilt the call
294
+ // payload, which silently dropped ADR-011's `escalation` field (and any
295
+ // future schema field added without updating the rebuild). The fix passes
296
+ // `args` through directly, matching the spread pattern of sibling
297
+ // handlers. This test verifies the contract by injecting a mock executor
298
+ // module that captures the args, calling gsd_task_complete with an
299
+ // `escalation` payload, and asserting the field reached the executor.
300
+ const base = makeTmpBase();
301
+ const capturePath = join(base, "captured-args.json");
302
+ const mockModulePath = join(base, "mock-executors.mjs");
303
+ const prevModule = process.env.GSD_WORKFLOW_EXECUTORS_MODULE;
304
+ const prevCapture = process.env.GSD_TEST_TASK_COMPLETE_CAPTURE_PATH;
305
+ try {
306
+ // Mock module: implements the WorkflowToolExecutors shape.
307
+ // executeTaskComplete writes its received args to disk for assertion.
308
+ // Other executors are no-op stubs to satisfy isWorkflowToolExecutors.
309
+ const mockSource = `
310
+ import { writeFileSync } from "node:fs";
311
+
312
+ const noop = async () => ({ content: [{ type: "text", text: "noop" }] });
313
+
314
+ export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = ["SUMMARY", "UAT", "CONTEXT", "PLAN"];
315
+ export const executeMilestoneStatus = noop;
316
+ export const executePlanMilestone = noop;
317
+ export const executePlanSlice = noop;
318
+ export const executeReplanSlice = noop;
319
+ export const executeSliceComplete = noop;
320
+ export const executeCompleteMilestone = noop;
321
+ export const executeValidateMilestone = noop;
322
+ export const executeReassessRoadmap = noop;
323
+ export const executeSaveGateResult = noop;
324
+ export const executeSummarySave = noop;
325
+
326
+ export const executeTaskComplete = async (params, projectDir) => {
327
+ const capturePath = process.env.GSD_TEST_TASK_COMPLETE_CAPTURE_PATH;
328
+ if (capturePath) {
329
+ writeFileSync(capturePath, JSON.stringify({ params, projectDir }, null, 2));
330
+ }
331
+ return {
332
+ content: [{ type: "text", text: "mock task complete" }],
333
+ details: { taskId: params.taskId },
334
+ };
335
+ };
336
+ `;
337
+ writeFileSync(mockModulePath, mockSource, "utf-8");
338
+ process.env.GSD_WORKFLOW_EXECUTORS_MODULE = mockModulePath;
339
+ process.env.GSD_TEST_TASK_COMPLETE_CAPTURE_PATH = capturePath;
340
+
341
+ // Fresh import bypasses the cached workflowToolExecutorsPromise so the
342
+ // mock module is actually loaded for this test.
343
+ const { registerWorkflowTools: freshRegisterWorkflowTools } = await import(
344
+ `./workflow-tools.ts?escalation-test=${randomUUID()}`
345
+ );
346
+ const server = makeMockServer();
347
+ freshRegisterWorkflowTools(server as any);
348
+ const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
349
+ assert.ok(taskTool, "task tool should be registered");
350
+
351
+ // Mirrors the ADR-011 escalation schema: question + 2-4 options
352
+ // (each with id/label/tradeoffs) + recommendation + rationale +
353
+ // continueWithDefault flag.
354
+ const escalationPayload = {
355
+ question: "Should the auth flow use OAuth or PAT?",
356
+ options: [
357
+ { id: "A", label: "OAuth", tradeoffs: "Best UX; requires more setup." },
358
+ { id: "B", label: "PAT", tradeoffs: "Simpler; weaker rotation story." },
359
+ ],
360
+ recommendation: "A",
361
+ recommendationRationale: "Initial requirement implied multi-user; OAuth fits better.",
362
+ continueWithDefault: true,
363
+ };
364
+
365
+ await taskTool!.handler({
366
+ projectDir: base,
367
+ taskId: "T01",
368
+ sliceId: "S01",
369
+ milestoneId: "M001",
370
+ oneLiner: "Completed task with escalation",
371
+ narrative: "Did the work but flagged an ambiguity",
372
+ verification: "npm test",
373
+ escalation: escalationPayload,
374
+ verificationEvidence: [
375
+ { command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
376
+ ],
377
+ });
378
+
379
+ assert.ok(existsSync(capturePath), "mock executor should have written captured args to disk");
380
+ const captured = JSON.parse(readFileSync(capturePath, "utf-8"));
381
+
382
+ // The handler resolves projectDir via realpathSync (security/symlink check),
383
+ // so on macOS where /var symlinks to /private/var, the captured path will
384
+ // be the realpath form. Normalize both sides.
385
+ assert.equal(captured.projectDir, realpathSync(base), "projectDir should be passed as second arg");
386
+ assert.deepEqual(
387
+ captured.params.escalation,
388
+ escalationPayload,
389
+ "escalation payload must reach the executor verbatim — regression guard for the destructure-rebuild bug class (#4477 review)",
390
+ );
391
+ // Spot-check a couple of other fields to ensure the spread pattern
392
+ // doesn't accidentally exclude the rest while including escalation.
393
+ assert.equal(captured.params.taskId, "T01", "taskId must be forwarded");
394
+ assert.equal(captured.params.milestoneId, "M001", "milestoneId must be forwarded");
395
+ assert.deepEqual(
396
+ captured.params.verificationEvidence,
397
+ [{ command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 }],
398
+ "verificationEvidence must be forwarded (existing field)",
399
+ );
400
+ // Ensure no projectDir leak into params (it should be the second arg only).
401
+ assert.equal(
402
+ captured.params.projectDir,
403
+ undefined,
404
+ "projectDir must NOT appear in params — it's stripped via the spread destructure",
405
+ );
406
+ } finally {
407
+ if (prevModule === undefined) {
408
+ delete process.env.GSD_WORKFLOW_EXECUTORS_MODULE;
409
+ } else {
410
+ process.env.GSD_WORKFLOW_EXECUTORS_MODULE = prevModule;
411
+ }
412
+ if (prevCapture === undefined) {
413
+ delete process.env.GSD_TEST_TASK_COMPLETE_CAPTURE_PATH;
414
+ } else {
415
+ process.env.GSD_TEST_TASK_COMPLETE_CAPTURE_PATH = prevCapture;
416
+ }
417
+ cleanup(base);
418
+ }
419
+ });
420
+
291
421
  it("gsd_complete_task alias delegates to gsd_task_complete behavior", async () => {
292
422
  const base = makeTmpBase();
293
423
  try {
@@ -1237,6 +1367,21 @@ describe("workflow MCP tools", () => {
1237
1367
  findings: "No new attack surface was introduced.",
1238
1368
  });
1239
1369
  assert.match((gateResult as any).content[0].text as string, /Gate Q3 result saved/);
1370
+ // #4472: executor `details` must be adapted to MCP `structuredContent`
1371
+ // so it survives the protocol transport intact. Asserting property
1372
+ // *absence* rather than `=== undefined` so a future regression that
1373
+ // explicitly sets `details: undefined` (rather than removing it) still
1374
+ // fails this contract test.
1375
+ assert.equal(
1376
+ Object.prototype.hasOwnProperty.call(gateResult, "details"),
1377
+ false,
1378
+ "executor `details` field must be stripped from MCP tool result",
1379
+ );
1380
+ assert.deepEqual(
1381
+ (gateResult as any).structuredContent,
1382
+ { operation: "save_gate_result", gateId: "Q3", verdict: "pass" },
1383
+ "executor details must be forwarded on the MCP `structuredContent` channel",
1384
+ );
1240
1385
  const gateRows = _getAdapter()!.prepare(
1241
1386
  "SELECT status, verdict, rationale FROM quality_gates WHERE milestone_id = ? AND slice_id = ? AND gate_id = ?",
1242
1387
  ).all("M006", "S06", "Q3") as Array<Record<string, unknown>>;
@@ -617,6 +617,51 @@ function getWorkflowOpTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
617
617
  return parsed; // 0 disables the timeout
618
618
  }
619
619
 
620
+ /**
621
+ * Adapt an executor `ToolExecutionResult` ({ content, details?, isError? }) to
622
+ * the MCP `CallToolResult` shape ({ content, structuredContent?, isError? }).
623
+ *
624
+ * MCP transports (including stdio) only serialize fields declared in the
625
+ * protocol, so a non-standard `details` field is silently dropped over the
626
+ * wire. Mirroring it into `structuredContent` — the protocol's supported
627
+ * channel for structured tool payloads — preserves the data for clients that
628
+ * render from it (e.g. the save_gate_result renderer that reads gateId /
629
+ * verdict). See #4472.
630
+ *
631
+ * Discard policy for non-plain-object `details`: the `isPlainObject` guard
632
+ * accepts the canonical case (a record literal) and intentionally drops bare
633
+ * primitives (string, number, boolean), bare arrays, and class instances /
634
+ * Date objects. This is deliberate — MCP `structuredContent` is specified as
635
+ * a JSON object; non-object payloads can't round-trip cleanly. No current
636
+ * executor returns a non-object `details`, so this never fires in practice.
637
+ * Future executors needing to return a primitive should wrap it
638
+ * (`details: { value: 42 }`) rather than relying on the discard.
639
+ */
640
+ function adaptExecutorResult(result: unknown): unknown {
641
+ if (!result || typeof result !== "object") return result;
642
+ const r = result as Record<string, unknown>;
643
+ if (!("details" in r)) return result;
644
+ const { details, ...rest } = r;
645
+ return isPlainObject(details) ? { ...rest, structuredContent: details } : rest;
646
+ }
647
+
648
+ /**
649
+ * Strict plain-object guard. True only for object literals and
650
+ * `Object.create(null)` — not for `Date`, `URL`, `Map`, `Set`, class instances,
651
+ * or arrays. Used to gate `structuredContent` forwarding so the MCP transport
652
+ * receives only true JSON objects (the protocol contract).
653
+ *
654
+ * Mirrored in `src/mcp-server.ts` for the agent-tool registry path's
655
+ * structured-content gate. Keep both copies in sync if the contract definition
656
+ * needs to evolve. See #4477 review.
657
+ */
658
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
659
+ if (value === null || typeof value !== "object") return false;
660
+ if (Array.isArray(value)) return false;
661
+ const proto = Object.getPrototypeOf(value);
662
+ return proto === null || proto === Object.prototype;
663
+ }
664
+
620
665
  async function runSerializedWorkflowOperation<T>(fn: () => Promise<T>): Promise<T> {
621
666
  // The shared DB adapter and workflow log base path are process-global, so
622
667
  // workflow MCP mutations must not overlap within a single server process.
@@ -709,39 +754,15 @@ async function handleTaskComplete(
709
754
  args: Omit<z.infer<typeof taskCompleteSchema>, "projectDir">,
710
755
  ): Promise<unknown> {
711
756
  await enforceWorkflowWriteGate("gsd_task_complete", projectDir, args.milestoneId);
712
- const {
713
- taskId,
714
- sliceId,
715
- milestoneId,
716
- oneLiner,
717
- narrative,
718
- verification,
719
- deviations,
720
- knownIssues,
721
- keyFiles,
722
- keyDecisions,
723
- blockerDiscovered,
724
- verificationEvidence,
725
- } = args;
726
757
  const { executeTaskComplete } = await getWorkflowToolExecutors();
727
- return runSerializedWorkflowOperation(() =>
728
- executeTaskComplete(
729
- {
730
- taskId,
731
- sliceId,
732
- milestoneId,
733
- oneLiner,
734
- narrative,
735
- verification,
736
- deviations,
737
- knownIssues,
738
- keyFiles,
739
- keyDecisions,
740
- blockerDiscovered,
741
- verificationEvidence,
742
- },
743
- projectDir,
744
- ),
758
+ // Pass `args` through directly rather than destructure-then-rebuild. The
759
+ // previous implementation re-listed each field, which silently dropped
760
+ // schema fields that weren't in the rebuild list (e.g., ADR-011's
761
+ // `escalation` payload). The destructure-then-rebuild pattern is the bug
762
+ // class; matching the spread shape used by sibling handlers (handleSliceComplete,
763
+ // handleReplanSlice) eliminates the recurrence risk by construction.
764
+ return adaptExecutorResult(
765
+ await runSerializedWorkflowOperation(() => executeTaskComplete(args, projectDir)),
745
766
  );
746
767
  }
747
768
 
@@ -752,7 +773,9 @@ async function handleSliceComplete(
752
773
  await enforceWorkflowWriteGate("gsd_slice_complete", projectDir, args.milestoneId);
753
774
  const { executeSliceComplete } = await getWorkflowToolExecutors();
754
775
  const { projectDir: _projectDir, ...params } = args;
755
- return runSerializedWorkflowOperation(() => executeSliceComplete(params, projectDir));
776
+ return adaptExecutorResult(
777
+ await runSerializedWorkflowOperation(() => executeSliceComplete(params, projectDir)),
778
+ );
756
779
  }
757
780
 
758
781
  async function handleReplanSlice(
@@ -762,7 +785,9 @@ async function handleReplanSlice(
762
785
  await enforceWorkflowWriteGate("gsd_replan_slice", projectDir, args.milestoneId);
763
786
  const { executeReplanSlice } = await getWorkflowToolExecutors();
764
787
  const { projectDir: _projectDir, ...params } = args;
765
- return runSerializedWorkflowOperation(() => executeReplanSlice(params, projectDir));
788
+ return adaptExecutorResult(
789
+ await runSerializedWorkflowOperation(() => executeReplanSlice(params, projectDir)),
790
+ );
766
791
  }
767
792
 
768
793
  async function handleCompleteMilestone(
@@ -772,7 +797,9 @@ async function handleCompleteMilestone(
772
797
  await enforceWorkflowWriteGate("gsd_complete_milestone", projectDir, args.milestoneId);
773
798
  const { executeCompleteMilestone } = await getWorkflowToolExecutors();
774
799
  const { projectDir: _projectDir, ...params } = args;
775
- return runSerializedWorkflowOperation(() => executeCompleteMilestone(params, projectDir));
800
+ return adaptExecutorResult(
801
+ await runSerializedWorkflowOperation(() => executeCompleteMilestone(params, projectDir)),
802
+ );
776
803
  }
777
804
 
778
805
  async function handleValidateMilestone(
@@ -782,7 +809,9 @@ async function handleValidateMilestone(
782
809
  await enforceWorkflowWriteGate("gsd_validate_milestone", projectDir, args.milestoneId);
783
810
  const { executeValidateMilestone } = await getWorkflowToolExecutors();
784
811
  const { projectDir: _projectDir, ...params } = args;
785
- return runSerializedWorkflowOperation(() => executeValidateMilestone(params, projectDir));
812
+ return adaptExecutorResult(
813
+ await runSerializedWorkflowOperation(() => executeValidateMilestone(params, projectDir)),
814
+ );
786
815
  }
787
816
 
788
817
  async function handleReassessRoadmap(
@@ -792,7 +821,9 @@ async function handleReassessRoadmap(
792
821
  await enforceWorkflowWriteGate("gsd_reassess_roadmap", projectDir, args.milestoneId);
793
822
  const { executeReassessRoadmap } = await getWorkflowToolExecutors();
794
823
  const { projectDir: _projectDir, ...params } = args;
795
- return runSerializedWorkflowOperation(() => executeReassessRoadmap(params, projectDir));
824
+ return adaptExecutorResult(
825
+ await runSerializedWorkflowOperation(() => executeReassessRoadmap(params, projectDir)),
826
+ );
796
827
  }
797
828
 
798
829
  async function handleSaveGateResult(
@@ -802,7 +833,9 @@ async function handleSaveGateResult(
802
833
  await enforceWorkflowWriteGate("gsd_save_gate_result", projectDir, args.milestoneId);
803
834
  const { executeSaveGateResult } = await getWorkflowToolExecutors();
804
835
  const { projectDir: _projectDir, ...params } = args;
805
- return runSerializedWorkflowOperation(() => executeSaveGateResult(params, projectDir));
836
+ return adaptExecutorResult(
837
+ await runSerializedWorkflowOperation(() => executeSaveGateResult(params, projectDir)),
838
+ );
806
839
  }
807
840
 
808
841
  async function ensureMilestoneDbRow(milestoneId: string): Promise<void> {
@@ -1384,7 +1417,9 @@ export function registerWorkflowTools(server: McpToolServer): void {
1384
1417
  const { projectDir, ...params } = parsed;
1385
1418
  await enforceWorkflowWriteGate("gsd_plan_milestone", projectDir, params.milestoneId);
1386
1419
  const { executePlanMilestone } = await getWorkflowToolExecutors();
1387
- return runSerializedWorkflowOperation(() => executePlanMilestone(params, projectDir));
1420
+ return adaptExecutorResult(
1421
+ await runSerializedWorkflowOperation(() => executePlanMilestone(params, projectDir)),
1422
+ );
1388
1423
  },
1389
1424
  );
1390
1425
 
@@ -1397,7 +1432,9 @@ export function registerWorkflowTools(server: McpToolServer): void {
1397
1432
  const { projectDir, ...params } = parsed;
1398
1433
  await enforceWorkflowWriteGate("gsd_plan_slice", projectDir, params.milestoneId);
1399
1434
  const { executePlanSlice } = await getWorkflowToolExecutors();
1400
- return runSerializedWorkflowOperation(() => executePlanSlice(params, projectDir));
1435
+ return adaptExecutorResult(
1436
+ await runSerializedWorkflowOperation(() => executePlanSlice(params, projectDir)),
1437
+ );
1401
1438
  },
1402
1439
  );
1403
1440
 
@@ -1598,8 +1635,10 @@ export function registerWorkflowTools(server: McpToolServer): void {
1598
1635
  `artifact_type must be one of: ${supportedArtifactTypes.join(", ")}`,
1599
1636
  );
1600
1637
  }
1601
- return runSerializedWorkflowOperation(() =>
1602
- executors.executeSummarySave({ milestone_id, slice_id, task_id, artifact_type, content }, projectDir),
1638
+ return adaptExecutorResult(
1639
+ await runSerializedWorkflowOperation(() =>
1640
+ executors.executeSummarySave({ milestone_id, slice_id, task_id, artifact_type, content }, projectDir),
1641
+ ),
1603
1642
  );
1604
1643
  },
1605
1644
  );
@@ -1636,7 +1675,9 @@ export function registerWorkflowTools(server: McpToolServer): void {
1636
1675
  // during pending-gate or queue-mode states.
1637
1676
  const { projectDir, milestoneId } = parseWorkflowArgs(milestoneStatusSchema, args);
1638
1677
  const { executeMilestoneStatus } = await getWorkflowToolExecutors();
1639
- return runSerializedWorkflowOperation(() => executeMilestoneStatus({ milestoneId }, projectDir));
1678
+ return adaptExecutorResult(
1679
+ await runSerializedWorkflowOperation(() => executeMilestoneStatus({ milestoneId }, projectDir)),
1680
+ );
1640
1681
  },
1641
1682
  );
1642
1683