gsd-pi 2.82.0-dev.725028083 → 2.82.0-dev.9d5798940

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 (210) hide show
  1. package/README.md +3 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +3 -1
  4. package/dist/resources/extensions/claude-code-cli/partial-builder.js +2 -1
  5. package/dist/resources/extensions/cmux/index.js +5 -0
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +113 -6
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
  8. package/dist/resources/extensions/gsd/auto-post-unit.js +232 -126
  9. package/dist/resources/extensions/gsd/auto-prompts.js +2 -2
  10. package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
  11. package/dist/resources/extensions/gsd/auto-start.js +7 -3
  12. package/dist/resources/extensions/gsd/auto-verification.js +28 -22
  13. package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
  14. package/dist/resources/extensions/gsd/auto.js +128 -52
  15. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
  16. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +16 -7
  17. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -2
  18. package/dist/resources/extensions/gsd/clean-root-preflight.js +170 -8
  19. package/dist/resources/extensions/gsd/commands/catalog.js +4 -1
  20. package/dist/resources/extensions/gsd/commands/handlers/core.js +37 -0
  21. package/dist/resources/extensions/gsd/commands-bootstrap.js +5 -0
  22. package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
  23. package/dist/resources/extensions/gsd/export-html.js +27 -425
  24. package/dist/resources/extensions/gsd/md-importer.js +1 -1
  25. package/dist/resources/extensions/gsd/migrate/command.js +5 -0
  26. package/dist/resources/extensions/gsd/migrate/preview.js +9 -0
  27. package/dist/resources/extensions/gsd/migrate/transformer.js +51 -4
  28. package/dist/resources/extensions/gsd/migrate/writer.js +11 -1
  29. package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
  30. package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
  31. package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
  32. package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
  33. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +119 -0
  34. package/dist/resources/extensions/gsd/unit-context-manifest.js +32 -10
  35. package/dist/resources/extensions/gsd/verification-verdict.js +26 -0
  36. package/dist/resources/extensions/gsd/worktree-lifecycle.js +49 -9
  37. package/dist/resources/extensions/shared/html-shell.js +388 -0
  38. package/dist/resources/extensions/subagent/index.js +448 -78
  39. package/dist/resources/extensions/subagent/launch.js +77 -0
  40. package/dist/resources/extensions/subagent/run-store.js +148 -0
  41. package/dist/resources/extensions/visual-brief/artifact-policy.js +29 -0
  42. package/dist/resources/extensions/visual-brief/extension-manifest.json +8 -0
  43. package/dist/resources/extensions/visual-brief/index.js +5 -0
  44. package/dist/resources/extensions/visual-brief/page-contract.js +124 -0
  45. package/dist/resources/extensions/visual-brief/prompts.js +140 -0
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  49. package/dist/web/standalone/.next/build-manifest.json +3 -3
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  52. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  62. package/dist/web/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -7
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -5
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
  72. package/dist/web/standalone/.next/server/app/index.html +1 -1
  73. package/dist/web/standalone/.next/server/app/index.rsc +4 -7
  74. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -7
  76. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -5
  78. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
  79. package/dist/web/standalone/.next/server/app/page.js +2 -2
  80. package/dist/web/standalone/.next/server/app/page.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  83. package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
  84. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  88. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  89. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  90. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  91. package/dist/web/standalone/.next/static/chunks/2973.33f26573894b6153.js +2 -0
  92. package/dist/web/standalone/.next/static/chunks/{8359.e059d86b255fce1c.js → 8359.7eb3bb8f8ecf4c01.js} +2 -2
  93. package/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
  94. package/dist/web/standalone/.next/static/chunks/{webpack-de742b64187e13fe.js → webpack-9a4db269f9ed63ad.js} +1 -1
  95. package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
  96. package/package.json +3 -3
  97. package/packages/contracts/dist/rpc.test.js +7 -0
  98. package/packages/contracts/dist/rpc.test.js.map +1 -1
  99. package/packages/contracts/dist/workflow.d.ts +21 -0
  100. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  101. package/packages/contracts/dist/workflow.js +24 -0
  102. package/packages/contracts/dist/workflow.js.map +1 -1
  103. package/packages/contracts/src/rpc.test.ts +8 -0
  104. package/packages/contracts/src/workflow.ts +24 -0
  105. package/packages/mcp-server/README.md +13 -4
  106. package/packages/mcp-server/dist/workflow-tools.d.ts +0 -3
  107. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  108. package/packages/mcp-server/dist/workflow-tools.js +80 -0
  109. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  110. package/packages/mcp-server/src/workflow-tools.test.ts +22 -0
  111. package/packages/mcp-server/src/workflow-tools.ts +168 -0
  112. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  113. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  114. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  115. package/packages/pi-tui/dist/tui.js +5 -0
  116. package/packages/pi-tui/dist/tui.js.map +1 -1
  117. package/packages/pi-tui/src/tui.ts +6 -0
  118. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  119. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  120. package/src/resources/GSD-WORKFLOW.md +3 -1
  121. package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
  122. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
  123. package/src/resources/extensions/cmux/index.ts +6 -0
  124. package/src/resources/extensions/gsd/auto/contracts.ts +46 -11
  125. package/src/resources/extensions/gsd/auto/orchestrator.ts +118 -6
  126. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
  127. package/src/resources/extensions/gsd/auto-post-unit.ts +265 -138
  128. package/src/resources/extensions/gsd/auto-prompts.ts +2 -2
  129. package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
  130. package/src/resources/extensions/gsd/auto-start.ts +7 -3
  131. package/src/resources/extensions/gsd/auto-verification.ts +36 -34
  132. package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
  133. package/src/resources/extensions/gsd/auto.ts +136 -51
  134. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
  135. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +16 -6
  136. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +19 -3
  137. package/src/resources/extensions/gsd/clean-root-preflight.ts +174 -8
  138. package/src/resources/extensions/gsd/commands/catalog.ts +4 -1
  139. package/src/resources/extensions/gsd/commands/handlers/core.ts +40 -0
  140. package/src/resources/extensions/gsd/commands-bootstrap.ts +10 -0
  141. package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
  142. package/src/resources/extensions/gsd/export-html.ts +27 -427
  143. package/src/resources/extensions/gsd/md-importer.ts +1 -1
  144. package/src/resources/extensions/gsd/migrate/command.ts +5 -0
  145. package/src/resources/extensions/gsd/migrate/preview.ts +10 -0
  146. package/src/resources/extensions/gsd/migrate/transformer.ts +58 -4
  147. package/src/resources/extensions/gsd/migrate/writer.ts +14 -1
  148. package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
  149. package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
  150. package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
  151. package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
  152. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +408 -4
  153. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -5
  154. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
  155. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
  156. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +4 -4
  157. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
  158. package/src/resources/extensions/gsd/tests/brief-command.test.ts +89 -0
  159. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +107 -2
  160. package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +16 -0
  161. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
  162. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
  163. package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +38 -0
  164. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
  165. package/src/resources/extensions/gsd/tests/integration/migrate-command.test.ts +48 -3
  166. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +5 -1
  167. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +6 -1
  168. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
  169. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
  170. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +79 -1
  171. package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
  172. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
  173. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
  174. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +6 -0
  175. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
  176. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +86 -7
  177. package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +78 -0
  178. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
  179. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +25 -0
  180. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +47 -0
  181. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  182. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +135 -0
  183. package/src/resources/extensions/gsd/unit-context-manifest.ts +47 -11
  184. package/src/resources/extensions/gsd/verification-verdict.ts +47 -0
  185. package/src/resources/extensions/gsd/worktree-lifecycle.ts +54 -9
  186. package/src/resources/extensions/shared/html-shell.ts +412 -0
  187. package/src/resources/extensions/subagent/index.ts +567 -103
  188. package/src/resources/extensions/subagent/launch.ts +131 -0
  189. package/src/resources/extensions/subagent/run-store.ts +218 -0
  190. package/src/resources/extensions/subagent/tests/launch.test.ts +115 -0
  191. package/src/resources/extensions/subagent/tests/run-store.test.ts +111 -0
  192. package/src/resources/extensions/visual-brief/artifact-policy.ts +41 -0
  193. package/src/resources/extensions/visual-brief/extension-manifest.json +8 -0
  194. package/src/resources/extensions/visual-brief/index.ts +8 -0
  195. package/src/resources/extensions/visual-brief/page-contract.ts +136 -0
  196. package/src/resources/extensions/visual-brief/prompts.ts +183 -0
  197. package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +212 -0
  198. package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
  199. package/dist/web/standalone/.next/static/chunks/2556.0527fea66e123b7f.js +0 -1
  200. package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
  201. package/dist/web/standalone/.next/static/css/54ec2745c1da488b.css +0 -1
  202. package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
  203. package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  204. package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
  205. package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
  206. package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  207. package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
  208. package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  209. /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
  210. /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
@@ -40,6 +40,7 @@ import { join } from "node:path";
40
40
  import { resolveUokFlags } from "./uok/flags.js";
41
41
  import { UokGateRunner } from "./uok/gate-runner.js";
42
42
  import { verificationRetryKey } from "./auto/verification-retry-policy.js";
43
+ import { decideVerificationVerdict } from "./verification-verdict.js";
43
44
 
44
45
  export interface VerificationContext {
45
46
  s: AutoSession;
@@ -49,12 +50,6 @@ export interface VerificationContext {
49
50
 
50
51
  export type VerificationResult = "continue" | "retry" | "pause";
51
52
 
52
- function isInfraVerificationFailure(stderr: string): boolean {
53
- return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(
54
- stderr,
55
- );
56
- }
57
-
58
53
  /**
59
54
  * Post-unit guard for `validate-milestone` units (#4094).
60
55
  *
@@ -196,7 +191,7 @@ async function countIncompleteSlices(basePath: string, milestoneId: string): Pro
196
191
  /**
197
192
  * Run the verification gate for the current execute-task unit.
198
193
  * Returns:
199
- * - "continue" — gate passed (or no checks configured), proceed normally
194
+ * - "continue" — host-owned verification passed, proceed normally
200
195
  * - "retry" — gate failed with retries remaining, s.pendingVerificationRetry set for loop re-iteration
201
196
  * - "pause" — gate failed with retries exhausted, pauseAuto already called
202
197
  */
@@ -260,6 +255,11 @@ export async function runPostUnitVerification(
260
255
  }
261
256
  }
262
257
 
258
+ const verdict = decideVerificationVerdict(s.currentUnit.type, result);
259
+ if (!verdict.passed) {
260
+ result.passed = false;
261
+ }
262
+
263
263
  if (uokFlags.gates) {
264
264
  const gateRunner = new UokGateRunner();
265
265
  gateRunner.register({
@@ -272,10 +272,12 @@ export async function runPostUnitVerification(
272
272
  : "verification",
273
273
  rationale: result.passed
274
274
  ? "verification checks passed"
275
- : "verification checks failed",
275
+ : verdict.reason === "no-host-checks"
276
+ ? "no runnable host-owned verification checks discovered"
277
+ : "verification checks failed",
276
278
  findings: result.passed
277
279
  ? ""
278
- : formatFailureContext(result),
280
+ : verdict.failureContext || formatFailureContext(result),
279
281
  }),
280
282
  });
281
283
 
@@ -343,13 +345,18 @@ export async function runPostUnitVerification(
343
345
  writeVerificationJSON(result, tasksDir, tid, s.currentUnit.id);
344
346
  } else {
345
347
  const nextAttempt = attempt + 1;
348
+ const includeRetryMetadata =
349
+ !result.passed &&
350
+ verdict.retryable &&
351
+ autoFixEnabled &&
352
+ nextAttempt <= maxRetries;
346
353
  writeVerificationJSON(
347
354
  result,
348
355
  tasksDir,
349
356
  tid,
350
357
  s.currentUnit.id,
351
- nextAttempt,
352
- maxRetries,
358
+ includeRetryMetadata ? nextAttempt : undefined,
359
+ includeRetryMetadata ? maxRetries : undefined,
353
360
  );
354
361
  }
355
362
  }
@@ -358,26 +365,6 @@ export async function runPostUnitVerification(
358
365
  }
359
366
  }
360
367
 
361
- const advisoryFailure =
362
- !result.passed &&
363
- (result.discoverySource === "package-json" ||
364
- result.checks.some((check) =>
365
- isInfraVerificationFailure(check.stderr),
366
- ));
367
-
368
- if (advisoryFailure) {
369
- s.verificationRetryCount.delete(retryKey);
370
- s.verificationRetryFailureHashes.delete(retryKey);
371
- s.pendingVerificationRetry = null;
372
- ctx.ui.notify(
373
- result.discoverySource === "package-json"
374
- ? "Verification failed in auto-discovered package.json checks — treating as advisory."
375
- : "Verification failed due to infrastructure/runtime environment issues — treating as advisory.",
376
- "warning",
377
- );
378
- return "continue";
379
- }
380
-
381
368
  // ── Post-execution checks (run after main verification passes for execute-task units) ──
382
369
  let postExecChecks: PostExecutionCheckJSON[] | undefined;
383
370
  let postExecBlockingFailure = false;
@@ -572,6 +559,17 @@ export async function runPostUnitVerification(
572
559
  s.verificationRetryFailureHashes.delete(retryKey);
573
560
  s.pendingVerificationRetry = null;
574
561
  return "continue";
562
+ } else if (verdict.reason === "no-host-checks") {
563
+ s.verificationRetryCount.delete(retryKey);
564
+ s.verificationRetryFailureHashes.delete(retryKey);
565
+ s.pendingVerificationRetry = null;
566
+ ctx.ui.notify(
567
+ "Verification gate FAILED — no runnable host-owned verification checks were discovered. Pausing for human review.",
568
+ "error",
569
+ );
570
+ process.stderr.write(`verification-gate: ${verdict.failureContext}\n`);
571
+ await pauseAuto(ctx, pi);
572
+ return "pause";
575
573
  } else if (postExecBlockingFailure) {
576
574
  // Post-execution failures are cross-task consistency issues — retrying the same task won't fix them.
577
575
  // Skip retry and pause immediately for human review.
@@ -589,7 +587,7 @@ export async function runPostUnitVerification(
589
587
  s.verificationRetryCount.set(retryKey, nextAttempt);
590
588
  s.pendingVerificationRetry = {
591
589
  unitId: s.currentUnit.id,
592
- failureContext: formatFailureContext(result),
590
+ failureContext: verdict.failureContext || formatFailureContext(result),
593
591
  attempt: nextAttempt,
594
592
  };
595
593
  const failedCmds = result.checks
@@ -623,9 +621,13 @@ export async function runPostUnitVerification(
623
621
  return "pause";
624
622
  }
625
623
  } catch (err) {
626
- // Gate errors are non-fatal
627
624
  logWarning("engine", `verification-gate error: ${(err as Error).message}`);
628
- return "continue";
625
+ ctx.ui.notify(
626
+ `Verification gate errored before producing an authoritative verdict: ${(err as Error).message}`,
627
+ "error",
628
+ );
629
+ await pauseAuto(ctx, pi);
630
+ return "pause";
629
631
  }
630
632
  }
631
633
 
@@ -282,6 +282,50 @@ function gitRemoteExists(basePath: string, remote: string): boolean {
282
282
  }
283
283
  }
284
284
 
285
+ function findRegularMergeChangedPaths(basePath: string, milestoneBranch: string, mainBranch: string): Set<string> {
286
+ const changedPaths = new Set<string>();
287
+ let mergeLog = "";
288
+ try {
289
+ mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
290
+ cwd: basePath,
291
+ stdio: ["ignore", "pipe", "pipe"],
292
+ encoding: "utf-8",
293
+ }).trim();
294
+ } catch (err) {
295
+ logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
296
+ return changedPaths;
297
+ }
298
+
299
+ for (const line of mergeLog.split("\n").filter(Boolean)) {
300
+ const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
301
+ if (!mergeCommit || !firstParent || otherParents.length === 0) continue;
302
+ const mergedMilestone = otherParents.some((parent) => {
303
+ try {
304
+ return nativeIsAncestor(basePath, milestoneBranch, parent);
305
+ } catch {
306
+ return false;
307
+ }
308
+ });
309
+ if (!mergedMilestone) continue;
310
+
311
+ try {
312
+ const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
313
+ cwd: basePath,
314
+ stdio: ["ignore", "pipe", "pipe"],
315
+ encoding: "utf-8",
316
+ }).trim();
317
+ for (const path of output.split("\n").filter(Boolean)) {
318
+ if (!path.startsWith(".gsd/")) changedPaths.add(path);
319
+ }
320
+ } catch (err) {
321
+ logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
322
+ }
323
+ return changedPaths;
324
+ }
325
+
326
+ return changedPaths;
327
+ }
328
+
285
329
  function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
286
330
  const gsdDir = gsdRoot(basePath);
287
331
  // Phase C pt 2: auto.lock removed from this list — the file is gone
@@ -1739,6 +1783,66 @@ export function mergeMilestoneToMain(
1739
1783
  }
1740
1784
  }
1741
1785
 
1786
+ // Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
1787
+ if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
1788
+ const codeChanges = nativeDiffNumstat(
1789
+ originalBasePath_,
1790
+ mainBranch,
1791
+ milestoneBranch,
1792
+ ).filter((entry) => !entry.path.startsWith(".gsd/"));
1793
+ if (codeChanges.length > 0) {
1794
+ const regularMergeChangedPaths = findRegularMergeChangedPaths(
1795
+ originalBasePath_,
1796
+ milestoneBranch,
1797
+ mainBranch,
1798
+ );
1799
+ const unanchoredCodeChanges = codeChanges.filter((entry) =>
1800
+ regularMergeChangedPaths.has(entry.path)
1801
+ );
1802
+ if (unanchoredCodeChanges.length > 0) {
1803
+ process.chdir(previousCwd);
1804
+ throw new GSDError(
1805
+ GSD_GIT_ERROR,
1806
+ `Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
1807
+ `but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
1808
+ `Aborting worktree teardown to prevent data loss.`,
1809
+ );
1810
+ }
1811
+ }
1812
+ debugLog("mergeMilestoneToMain", {
1813
+ action: "skip-squash-already-merged",
1814
+ milestoneId,
1815
+ milestoneBranch,
1816
+ mainBranch,
1817
+ });
1818
+ try {
1819
+ clearProjectRootStateFiles(originalBasePath_, milestoneId);
1820
+ } catch (err) {
1821
+ logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
1822
+ }
1823
+ try {
1824
+ removeWorktree(originalBasePath_, milestoneId, {
1825
+ branch: milestoneBranch,
1826
+ deleteBranch: false,
1827
+ });
1828
+ } catch (err) {
1829
+ logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
1830
+ }
1831
+ try {
1832
+ nativeBranchDelete(originalBasePath_, milestoneBranch);
1833
+ } catch (err) {
1834
+ logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
1835
+ }
1836
+ setActiveWorkspace(null);
1837
+ nudgeGitBranchCache(previousCwd);
1838
+ try {
1839
+ process.chdir(originalBasePath_);
1840
+ } catch (err) {
1841
+ logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1842
+ }
1843
+ return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
1844
+ }
1845
+
1742
1846
  // 7. Shelter queued milestone directories before the squash merge (#2505).
1743
1847
  // The milestone branch may contain copies of queued milestone dirs (via
1744
1848
  // copyPlanningArtifacts), so `git merge --squash` rejects when those same
@@ -240,12 +240,14 @@ import { runAutoLoopWithUok } from "./uok/kernel.js";
240
240
  import { resolveUokFlags } from "./uok/flags.js";
241
241
  import { validateDirectory } from "./validate-directory.js";
242
242
  import { createAutoOrchestrator } from "./auto/orchestrator.js";
243
- import type { AutoOrchestrationModule, AutoOrchestratorDeps } from "./auto/contracts.js";
243
+ import type { AutoOrchestrationModule, AutoOrchestratorDeps, DispatchAdapter } from "./auto/contracts.js";
244
244
  import { reconcileBeforeDispatch } from "./state-reconciliation.js";
245
245
  import { compileUnitToolContract } from "./tool-contract.js";
246
246
  import { createWorktreeSafetyModule } from "./worktree-safety.js";
247
247
  import { resolveManifest } from "./unit-context-manifest.js";
248
248
  import { classifyFailure } from "./recovery-classification.js";
249
+ import { supportsStructuredQuestions } from "./workflow-mcp.js";
250
+ import type { MinimalModelRegistry } from "./context-budget.js";
249
251
  // Slice-level parallelism (#2340)
250
252
  import { getEligibleSlices } from "./slice-parallel-eligibility.js";
251
253
  import { startSliceParallel } from "./slice-parallel-orchestrator.js";
@@ -1048,13 +1050,11 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
1048
1050
  initHealthWidget(ctx);
1049
1051
  }
1050
1052
 
1051
- // ADR-016 phase 3 (#5693): the stop-path basePath restore routes through
1052
- // `Lifecycle.restoreToProjectRoot()`, the sole owner of `s.basePath`
1053
- // mutation. The verb assigns `s.basePath` before any throwable work
1054
- // (rebuildGitService, cache invalidation), so a thrown error still leaves
1055
- // basePath restored no fallback assignment needed at the call site.
1056
- // The chdir stays here because `restoreToProjectRoot` is a pure
1057
- // session-state mutation.
1053
+ // ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
1054
+ // through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
1055
+ // `s.basePath` mutation and the paired `process.chdir` for auto-loop
1056
+ // transitions. The verb assigns `s.basePath` before any throwable work, so
1057
+ // a thrown error still leaves basePath restored.
1058
1058
  if (s.originalBasePath) {
1059
1059
  try {
1060
1060
  buildLifecycle().restoreToProjectRoot();
@@ -1065,11 +1065,6 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
1065
1065
  { file: "auto.ts" },
1066
1066
  );
1067
1067
  }
1068
- try {
1069
- process.chdir(s.originalBasePath);
1070
- } catch (err) {
1071
- logWarning("engine", `basePath restore/chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1072
- }
1073
1068
  }
1074
1069
 
1075
1070
  if (s.originalBasePath && s.cmdCtx) {
@@ -1384,20 +1379,14 @@ export async function stopAuto(
1384
1379
  }
1385
1380
 
1386
1381
  // ── Step 7: Restore basePath and chdir (ADR-016 phase 3, #5693) ──
1387
- // `restoreToProjectRoot` assigns s.basePath before any throwable work;
1388
- // no fallback assignment is needed at the call site.
1382
+ // `restoreToProjectRoot` owns both s.basePath restore and process.chdir;
1383
+ // no paired chdir is needed at the call site.
1389
1384
  if (s.originalBasePath) {
1390
1385
  try {
1391
1386
  buildLifecycle().restoreToProjectRoot();
1392
1387
  } catch (e) {
1393
1388
  debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
1394
1389
  }
1395
- try {
1396
- process.chdir(s.basePath);
1397
- } catch (err) {
1398
- /* best-effort */
1399
- logWarning("engine", `chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1400
- }
1401
1390
  }
1402
1391
 
1403
1392
  // Re-root the active command session/tool runtime after worktree teardown.
@@ -1792,6 +1781,75 @@ function buildLifecycle(): WorktreeLifecycle {
1792
1781
  return new WorktreeLifecycle(s, buildWorktreeLifecycleDeps());
1793
1782
  }
1794
1783
 
1784
+ /**
1785
+ * Build the production `DispatchAdapter` used by `createWiredAutoOrchestrationModule`.
1786
+ *
1787
+ * Exported so tests can verify parity with `runDispatch`'s `resolveDispatch` call —
1788
+ * the wired adapter must derive `structuredQuestionsAvailable`, `sessionContextWindow`,
1789
+ * `sessionProvider`, and `modelRegistry` the same way phases.ts:runDispatch does.
1790
+ */
1791
+ export function createWiredDispatchAdapter(
1792
+ ctx: ExtensionContext,
1793
+ pi: ExtensionAPI,
1794
+ dispatchBasePath: string,
1795
+ ): DispatchAdapter {
1796
+ return {
1797
+ async decideNextUnit(input) {
1798
+ const state = input.stateSnapshot;
1799
+ const active = state.activeMilestone;
1800
+ if (!active) return null;
1801
+
1802
+ const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
1803
+
1804
+ // Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
1805
+ // (#5789). Prefer caller-supplied values when present so test harnesses and
1806
+ // alternative wirings can inject deterministic snapshots; otherwise pull from
1807
+ // the captured pi/ctx references.
1808
+ const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
1809
+ const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
1810
+ const modelRegistry = input.modelRegistry ?? (ctx.modelRegistry as MinimalModelRegistry | undefined);
1811
+ const authMode =
1812
+ sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
1813
+ ? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
1814
+ : undefined;
1815
+ const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
1816
+ // Mirrors runDispatch: deep-planning keeps approval gates in plain chat
1817
+ // because structured questions can be cancelled outside the chat turn on
1818
+ // some transports.
1819
+ const structuredQuestionsAvailable =
1820
+ input.structuredQuestionsAvailable ??
1821
+ (prefs?.planning_depth === "deep"
1822
+ ? "false"
1823
+ : supportsStructuredQuestions(activeTools, {
1824
+ authMode,
1825
+ baseUrl: ctx.model?.baseUrl,
1826
+ })
1827
+ ? "true"
1828
+ : "false");
1829
+
1830
+ const action = await resolveDispatch({
1831
+ basePath: dispatchBasePath,
1832
+ mid: active.id,
1833
+ midTitle: active.title,
1834
+ state,
1835
+ prefs,
1836
+ structuredQuestionsAvailable,
1837
+ sessionContextWindow,
1838
+ sessionProvider,
1839
+ modelRegistry,
1840
+ });
1841
+
1842
+ if (action.action !== "dispatch") return null;
1843
+ return {
1844
+ unitType: action.unitType,
1845
+ unitId: action.unitId,
1846
+ reason: action.matchedRule ?? "dispatch",
1847
+ preconditions: [],
1848
+ };
1849
+ },
1850
+ };
1851
+ }
1852
+
1795
1853
  /**
1796
1854
  * Thin entry glue for the new Auto Orchestration module.
1797
1855
  *
@@ -1801,7 +1859,7 @@ function buildLifecycle(): WorktreeLifecycle {
1801
1859
  */
1802
1860
  export function createWiredAutoOrchestrationModule(
1803
1861
  ctx: ExtensionContext,
1804
- _pi: ExtensionAPI,
1862
+ pi: ExtensionAPI,
1805
1863
  dispatchBasePath: string,
1806
1864
  runtimeBasePath = resolveProjectRoot(dispatchBasePath),
1807
1865
  ): AutoOrchestrationModule {
@@ -1830,30 +1888,7 @@ export function createWiredAutoOrchestrationModule(
1830
1888
  };
1831
1889
  },
1832
1890
  },
1833
- dispatch: {
1834
- async decideNextUnit(input) {
1835
- const state = input.stateSnapshot;
1836
- const active = state.activeMilestone;
1837
- if (!active) return null;
1838
-
1839
- const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
1840
- const action = await resolveDispatch({
1841
- basePath: dispatchBasePath,
1842
- mid: active.id,
1843
- midTitle: active.title,
1844
- state,
1845
- prefs,
1846
- });
1847
-
1848
- if (action.action !== "dispatch") return null;
1849
- return {
1850
- unitType: action.unitType,
1851
- unitId: action.unitId,
1852
- reason: action.matchedRule ?? "dispatch",
1853
- preconditions: [],
1854
- };
1855
- },
1856
- },
1891
+ dispatch: createWiredDispatchAdapter(ctx, pi, dispatchBasePath),
1857
1892
  recovery: {
1858
1893
  async classifyAndRecover(input) {
1859
1894
  const recovery = classifyFailure(input);
@@ -1902,12 +1937,25 @@ export function createWiredAutoOrchestrationModule(
1902
1937
  async cleanupOnStop() {},
1903
1938
  },
1904
1939
  health: {
1940
+ checkResourcesStale() {
1941
+ return checkResourcesStale(s.resourceVersionOnStart);
1942
+ },
1905
1943
  async preAdvanceGate() {
1906
- const gate = await preDispatchHealthGate(dispatchBasePath);
1907
- return {
1908
- allow: gate.proceed,
1909
- reason: gate.reason,
1910
- };
1944
+ try {
1945
+ const gate = await preDispatchHealthGate(dispatchBasePath);
1946
+ if (gate.proceed) {
1947
+ return {
1948
+ kind: "pass",
1949
+ fixesApplied: gate.fixesApplied,
1950
+ };
1951
+ }
1952
+ return {
1953
+ kind: "fail",
1954
+ reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
1955
+ };
1956
+ } catch (error) {
1957
+ return { kind: "threw", error };
1958
+ }
1911
1959
  },
1912
1960
  async postAdvanceRecord(result) {
1913
1961
  if (result.kind === "error") {
@@ -1975,6 +2023,43 @@ export function createWiredAutoOrchestrationModule(
1975
2023
  }
1976
2024
  },
1977
2025
  },
2026
+ uokGate: {
2027
+ async emit(input) {
2028
+ const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
2029
+ const uokFlags = resolveUokFlags(prefs);
2030
+ if (!uokFlags.gates) return;
2031
+ const milestoneId = input.milestoneId ?? s.currentMilestoneId ?? undefined;
2032
+ try {
2033
+ const { UokGateRunner } = await import("./uok/gate-runner.js");
2034
+ const runner = new UokGateRunner();
2035
+ runner.register({
2036
+ id: input.gateId,
2037
+ type: input.gateType,
2038
+ execute: async () => ({
2039
+ outcome: input.outcome,
2040
+ failureClass: input.failureClass,
2041
+ rationale: input.rationale,
2042
+ findings: input.findings ?? "",
2043
+ }),
2044
+ });
2045
+ await runner.run(input.gateId, {
2046
+ basePath: dispatchBasePath,
2047
+ traceId: `pre-dispatch:${flowId}`,
2048
+ turnId: `orch-${seq}`,
2049
+ milestoneId,
2050
+ unitType: "pre-dispatch",
2051
+ unitId: `orch-${seq}`,
2052
+ });
2053
+ } catch (err) {
2054
+ logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
2055
+ file: "auto.ts",
2056
+ gateId: input.gateId,
2057
+ gateType: input.gateType,
2058
+ ...(milestoneId ? { milestoneId } : {}),
2059
+ });
2060
+ }
2061
+ },
2062
+ },
1978
2063
  };
1979
2064
 
1980
2065
  return createAutoOrchestrator(deps);
@@ -48,6 +48,11 @@ const MAX_NETWORK_RETRIES = 2;
48
48
  function isObjectRecord(value: unknown): value is Record<string, unknown> {
49
49
  return !!value && typeof value === "object";
50
50
  }
51
+
52
+ export function _hasEmptyAgentEndContent(content: unknown): boolean {
53
+ return content == null || (Array.isArray(content) && content.length === 0);
54
+ }
55
+
51
56
  /**
52
57
  * Cap on auto-resume attempts for sustained transient-provider errors.
53
58
  *
@@ -310,7 +315,7 @@ export async function handleAgentEnd(
310
315
  // that carry error context — e.g. errorMessage field or non-empty content
311
316
  // indicating a mid-stream failure. (#2695)
312
317
  const content = "content" in lastMsg ? lastMsg.content : undefined;
313
- const hasEmptyContent = Array.isArray(content) && content.length === 0;
318
+ const hasEmptyContent = _hasEmptyAgentEndContent(content);
314
319
  const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
315
320
 
316
321
  if (hasEmptyContent && !hasErrorMessage) {
@@ -1,20 +1,30 @@
1
1
  export function extractSubagentAgentClasses(input: unknown): string[] {
2
2
  if (!input || typeof input !== "object") return [];
3
3
 
4
- const record = input as Record<string, unknown>;
5
4
  const agentClasses: string[] = [];
5
+ const visited = new WeakSet<object>();
6
6
  const addAgentClass = (value: unknown): void => {
7
7
  if (typeof value === "string" && value.trim().length > 0) agentClasses.push(value.trim());
8
8
  };
9
- const addFromItems = (value: unknown): void => {
9
+
10
+ const visitItems = (value: unknown): void => {
10
11
  if (!Array.isArray(value)) return;
11
12
  for (const item of value) {
12
- if (item && typeof item === "object") addAgentClass((item as Record<string, unknown>).agent);
13
+ visit(item);
13
14
  }
14
15
  };
15
16
 
16
- addAgentClass(record.agent);
17
- addFromItems(record.tasks);
18
- addFromItems(record.chain);
17
+ const visit = (value: unknown): void => {
18
+ if (!value || typeof value !== "object") return;
19
+ if (visited.has(value)) return;
20
+ visited.add(value);
21
+ const record = value as Record<string, unknown>;
22
+ addAgentClass(record.agent);
23
+ visitItems(record.tasks);
24
+ visitItems(record.chain);
25
+ visitItems(record.parallel);
26
+ };
27
+
28
+ visit(input);
19
29
  return agentClasses;
20
30
  }
@@ -5,7 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { minimatch } from "minimatch";
6
6
 
7
7
  import { getIsolationMode } from "../preferences.js";
8
- import type { ToolsPolicy } from "../unit-context-manifest.js";
8
+ import { compileSubagentPermissionContract, type ToolsPolicy } from "../unit-context-manifest.js";
9
9
  import { logWarning } from "../workflow-logger.js";
10
10
  import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "../worktree-root.js";
11
11
 
@@ -65,6 +65,7 @@ const QUEUE_SAFE_TOOLS = new Set([
65
65
  * true / false — shell no-ops / test exit codes
66
66
  */
67
67
  const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
68
+ const BASH_VERIFICATION_RE = /^\s*(npm\s+(run\s+(build|test|test:\w+|lint|lint:\w+|typecheck|type-check|verify|ci|validate)\b|test\b)|pnpm\s+(build|test|lint|typecheck|verify)\b|yarn\s+(build|test|lint|typecheck|verify)\b|vitest\b|jest\b|go\s+test\b)/;
68
69
 
69
70
  interface InMemoryWriteGateState {
70
71
  verifiedDepthMilestones: Set<string>;
@@ -767,6 +768,9 @@ function blockReason(unitType: string, mode: string, what: string): string {
767
768
  * and listed in the policy's allowedSubagents.
768
769
  * - "docs" → like "planning" but also allows writes to paths
769
770
  * matching `allowedPathGlobs` relative to basePath.
771
+ * - "verification"
772
+ * → allows Bash for project verification commands, but keeps
773
+ * writes restricted to .gsd/ and blocks subagent dispatch.
770
774
  *
771
775
  * `pathOrCommand` is the file path for write/edit-shaped tools and the
772
776
  * shell command for bash. Other tools ignore this argument.
@@ -804,14 +808,15 @@ export function shouldBlockPlanningUnit(
804
808
  return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
805
809
  }
806
810
 
807
- // planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
811
+ // planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
808
812
  if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
809
813
  if (tool.startsWith("gsd_")) return { block: false };
810
814
 
811
815
  if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
812
816
  if (policy.mode === "planning-dispatch") {
813
817
  const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
814
- const allowedSubagents = Array.isArray(policy.allowedSubagents) ? policy.allowedSubagents : [];
818
+ const dispatchContract = compileSubagentPermissionContract(policy);
819
+ const allowedSubagents = dispatchContract.allowedSubagents;
815
820
  const allowed = new Set(allowedSubagents);
816
821
  // When agentClasses is undefined, the caller has not been updated to extract
817
822
  // agent identities yet. Block and warn so stale callers surface in telemetry
@@ -861,6 +866,17 @@ export function shouldBlockPlanningUnit(
861
866
  }
862
867
 
863
868
  if (tool === "bash") {
869
+ if (policy.mode === "verification") {
870
+ if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
871
+ return {
872
+ block: true,
873
+ reason: blockReason(
874
+ unitType,
875
+ policy.mode,
876
+ `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
877
+ ),
878
+ };
879
+ }
864
880
  if (BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
865
881
  return {
866
882
  block: true,