gsd-pi 2.81.0-dev.72a81bdf3 → 2.82.0-dev.2841a1e44

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 (243) hide show
  1. package/README.md +49 -30
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +3 -1
  4. package/dist/resources/extensions/browser-tools/tools/screenshot.js +1 -0
  5. package/dist/resources/extensions/browser-tools/tools/zoom.js +1 -0
  6. package/dist/resources/extensions/cmux/index.js +5 -0
  7. package/dist/resources/extensions/gsd/auto/orchestrator.js +113 -6
  8. package/dist/resources/extensions/gsd/auto/phases.js +9 -0
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +169 -124
  10. package/dist/resources/extensions/gsd/auto-prompts.js +13 -5
  11. package/dist/resources/extensions/gsd/auto-verification.js +28 -22
  12. package/dist/resources/extensions/gsd/auto.js +128 -52
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +5 -0
  14. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +16 -7
  15. package/dist/resources/extensions/gsd/bootstrap/system-context.js +55 -12
  16. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +3 -1
  17. package/dist/resources/extensions/gsd/clean-root-preflight.js +170 -8
  18. package/dist/resources/extensions/gsd/commands/catalog.js +4 -1
  19. package/dist/resources/extensions/gsd/commands/handlers/core.js +22 -1
  20. package/dist/resources/extensions/gsd/commands-bootstrap.js +5 -0
  21. package/dist/resources/extensions/gsd/commands-handlers.js +15 -2
  22. package/dist/resources/extensions/gsd/context-store.js +112 -0
  23. package/dist/resources/extensions/gsd/db-writer.js +150 -84
  24. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  25. package/dist/resources/extensions/gsd/doctor-git-checks.js +41 -6
  26. package/dist/resources/extensions/gsd/knowledge-backfill.js +144 -0
  27. package/dist/resources/extensions/gsd/knowledge-capture.js +136 -0
  28. package/dist/resources/extensions/gsd/knowledge-parser.js +154 -0
  29. package/dist/resources/extensions/gsd/knowledge-projection.js +210 -0
  30. package/dist/resources/extensions/gsd/markdown-renderer.js +6 -1
  31. package/dist/resources/extensions/gsd/md-importer.js +1 -1
  32. package/dist/resources/extensions/gsd/memory-backfill.js +73 -17
  33. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +222 -0
  34. package/dist/resources/extensions/gsd/migrate/command.js +5 -0
  35. package/dist/resources/extensions/gsd/migrate/preview.js +9 -0
  36. package/dist/resources/extensions/gsd/migrate/transformer.js +51 -4
  37. package/dist/resources/extensions/gsd/migrate/writer.js +11 -1
  38. package/dist/resources/extensions/gsd/prompts/system.md +2 -2
  39. package/dist/resources/extensions/gsd/provider-switch-observer.js +146 -0
  40. package/dist/resources/extensions/gsd/templates/knowledge.md +2 -2
  41. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +119 -0
  42. package/dist/resources/extensions/gsd/unit-context-manifest.js +25 -2
  43. package/dist/resources/extensions/gsd/verification-verdict.js +26 -0
  44. package/dist/resources/extensions/gsd/worktree-lifecycle.js +21 -2
  45. package/dist/resources/extensions/subagent/index.js +448 -78
  46. package/dist/resources/extensions/subagent/launch.js +77 -0
  47. package/dist/resources/extensions/subagent/run-store.js +148 -0
  48. package/dist/resources/extensions/visual-brief/artifact-policy.js +29 -0
  49. package/dist/resources/extensions/visual-brief/extension-manifest.json +8 -0
  50. package/dist/resources/extensions/visual-brief/index.js +5 -0
  51. package/dist/resources/extensions/visual-brief/page-contract.js +122 -0
  52. package/dist/resources/extensions/visual-brief/prompts.js +111 -0
  53. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  54. package/dist/web/standalone/.next/BUILD_ID +1 -1
  55. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  56. package/dist/web/standalone/.next/build-manifest.json +3 -3
  57. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  58. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  59. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  77. package/dist/web/standalone/.next/server/app/index.html +1 -1
  78. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  79. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  81. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  83. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  84. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  86. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +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/{webpack-de742b64187e13fe.js → webpack-6a95bc41e0f7ec89.js} +1 -1
  94. package/dist/web/standalone/.next/static/css/0262768ec1b89d34.css +1 -0
  95. package/package.json +5 -4
  96. package/packages/contracts/dist/rpc.test.js +7 -0
  97. package/packages/contracts/dist/rpc.test.js.map +1 -1
  98. package/packages/contracts/dist/workflow.d.ts +21 -0
  99. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  100. package/packages/contracts/dist/workflow.js +24 -0
  101. package/packages/contracts/dist/workflow.js.map +1 -1
  102. package/packages/contracts/src/rpc.test.ts +8 -0
  103. package/packages/contracts/src/workflow.ts +24 -0
  104. package/packages/daemon/package.json +2 -2
  105. package/packages/mcp-server/README.md +14 -3
  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/package.json +2 -2
  111. package/packages/mcp-server/src/workflow-tools-parity.test.ts +244 -0
  112. package/packages/mcp-server/src/workflow-tools.test.ts +22 -0
  113. package/packages/mcp-server/src/workflow-tools.ts +168 -0
  114. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  115. package/packages/native/package.json +1 -1
  116. package/packages/pi-agent-core/package.json +1 -1
  117. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  118. package/packages/pi-ai/dist/index.d.ts +2 -2
  119. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  120. package/packages/pi-ai/dist/index.js +1 -1
  121. package/packages/pi-ai/dist/index.js.map +1 -1
  122. package/packages/pi-ai/dist/providers/transform-messages.d.ts +11 -0
  123. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  124. package/packages/pi-ai/dist/providers/transform-messages.js +20 -0
  125. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  126. package/packages/pi-ai/package.json +1 -1
  127. package/packages/pi-ai/src/index.ts +7 -2
  128. package/packages/pi-ai/src/providers/transform-messages.ts +24 -0
  129. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  130. package/packages/pi-coding-agent/dist/core/system-prompt.js +4 -4
  131. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  132. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.d.ts +2 -0
  133. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.d.ts.map +1 -0
  134. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.js +17 -0
  135. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.js.map +1 -0
  136. package/packages/pi-coding-agent/package.json +1 -1
  137. package/packages/pi-coding-agent/src/core/system-prompt.ts +4 -4
  138. package/packages/pi-coding-agent/src/tests/system-prompt-file-safety.test.ts +22 -0
  139. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  140. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  141. package/packages/pi-tui/dist/tui.js +5 -0
  142. package/packages/pi-tui/dist/tui.js.map +1 -1
  143. package/packages/pi-tui/package.json +1 -1
  144. package/packages/pi-tui/src/tui.ts +6 -0
  145. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  146. package/packages/rpc-client/package.json +1 -1
  147. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  148. package/pkg/package.json +1 -1
  149. package/src/resources/GSD-WORKFLOW.md +3 -1
  150. package/src/resources/extensions/browser-tools/tools/screenshot.ts +1 -0
  151. package/src/resources/extensions/browser-tools/tools/zoom.ts +1 -0
  152. package/src/resources/extensions/cmux/index.ts +6 -0
  153. package/src/resources/extensions/gsd/auto/contracts.ts +46 -11
  154. package/src/resources/extensions/gsd/auto/orchestrator.ts +118 -6
  155. package/src/resources/extensions/gsd/auto/phases.ts +14 -0
  156. package/src/resources/extensions/gsd/auto-post-unit.ts +194 -137
  157. package/src/resources/extensions/gsd/auto-prompts.ts +13 -5
  158. package/src/resources/extensions/gsd/auto-verification.ts +36 -34
  159. package/src/resources/extensions/gsd/auto.ts +136 -51
  160. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
  161. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +16 -6
  162. package/src/resources/extensions/gsd/bootstrap/system-context.ts +58 -15
  163. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +3 -2
  164. package/src/resources/extensions/gsd/clean-root-preflight.ts +174 -8
  165. package/src/resources/extensions/gsd/commands/catalog.ts +4 -1
  166. package/src/resources/extensions/gsd/commands/handlers/core.ts +25 -1
  167. package/src/resources/extensions/gsd/commands-bootstrap.ts +10 -0
  168. package/src/resources/extensions/gsd/commands-handlers.ts +19 -2
  169. package/src/resources/extensions/gsd/context-store.ts +120 -1
  170. package/src/resources/extensions/gsd/db-writer.ts +167 -84
  171. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  172. package/src/resources/extensions/gsd/doctor-git-checks.ts +44 -6
  173. package/src/resources/extensions/gsd/doctor-types.ts +2 -0
  174. package/src/resources/extensions/gsd/knowledge-backfill.ts +164 -0
  175. package/src/resources/extensions/gsd/knowledge-capture.ts +160 -0
  176. package/src/resources/extensions/gsd/knowledge-parser.ts +174 -0
  177. package/src/resources/extensions/gsd/knowledge-projection.ts +241 -0
  178. package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
  179. package/src/resources/extensions/gsd/md-importer.ts +1 -1
  180. package/src/resources/extensions/gsd/memory-backfill.ts +89 -17
  181. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +277 -0
  182. package/src/resources/extensions/gsd/migrate/command.ts +5 -0
  183. package/src/resources/extensions/gsd/migrate/preview.ts +10 -0
  184. package/src/resources/extensions/gsd/migrate/transformer.ts +58 -4
  185. package/src/resources/extensions/gsd/migrate/writer.ts +14 -1
  186. package/src/resources/extensions/gsd/prompts/system.md +2 -2
  187. package/src/resources/extensions/gsd/provider-switch-observer.ts +185 -0
  188. package/src/resources/extensions/gsd/templates/knowledge.md +2 -2
  189. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +75 -0
  190. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +408 -4
  191. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -5
  192. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +4 -4
  193. package/src/resources/extensions/gsd/tests/brief-command.test.ts +89 -0
  194. package/src/resources/extensions/gsd/tests/browser-tools-compatibility-declarations.test.ts +62 -0
  195. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +107 -2
  196. package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +16 -0
  197. package/src/resources/extensions/gsd/tests/context-store-decisions-from-memories.test.ts +312 -0
  198. package/src/resources/extensions/gsd/tests/db-writer.test.ts +13 -8
  199. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +453 -0
  200. package/src/resources/extensions/gsd/tests/decisions-stop-table-writes.test.ts +348 -0
  201. package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +38 -0
  202. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +8 -4
  203. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +11 -7
  204. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +44 -0
  205. package/src/resources/extensions/gsd/tests/integration/integration-lifecycle.test.ts +13 -5
  206. package/src/resources/extensions/gsd/tests/integration/migrate-command.test.ts +48 -3
  207. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +323 -0
  208. package/src/resources/extensions/gsd/tests/knowledge-capture.test.ts +242 -0
  209. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -2
  210. package/src/resources/extensions/gsd/tests/load-knowledge-block-rules-only.test.ts +209 -0
  211. package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +316 -0
  212. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +5 -1
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +6 -1
  214. package/src/resources/extensions/gsd/tests/plan-milestone-sketch-render.test.ts +157 -0
  215. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +79 -1
  216. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -0
  217. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +252 -0
  218. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +16 -4
  219. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +6 -0
  220. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +21 -0
  221. package/src/resources/extensions/gsd/tests/verification-verdict.test.ts +78 -0
  222. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +25 -0
  223. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +16 -0
  224. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +135 -0
  225. package/src/resources/extensions/gsd/unit-context-manifest.ts +35 -2
  226. package/src/resources/extensions/gsd/verification-verdict.ts +47 -0
  227. package/src/resources/extensions/gsd/workflow-logger.ts +4 -0
  228. package/src/resources/extensions/gsd/worktree-lifecycle.ts +20 -2
  229. package/src/resources/extensions/subagent/index.ts +567 -103
  230. package/src/resources/extensions/subagent/launch.ts +131 -0
  231. package/src/resources/extensions/subagent/run-store.ts +218 -0
  232. package/src/resources/extensions/subagent/tests/launch.test.ts +115 -0
  233. package/src/resources/extensions/subagent/tests/run-store.test.ts +111 -0
  234. package/src/resources/extensions/visual-brief/artifact-policy.ts +41 -0
  235. package/src/resources/extensions/visual-brief/extension-manifest.json +8 -0
  236. package/src/resources/extensions/visual-brief/index.ts +8 -0
  237. package/src/resources/extensions/visual-brief/page-contract.ts +134 -0
  238. package/src/resources/extensions/visual-brief/prompts.ts +147 -0
  239. package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +172 -0
  240. package/dist/web/standalone/.next/static/chunks/2556.0527fea66e123b7f.js +0 -1
  241. package/dist/web/standalone/.next/static/css/54ec2745c1da488b.css +0 -1
  242. /package/dist/web/standalone/.next/static/{rIkMv4YSNlfSeqmGqWVns → Qgr2B_MRhPxC0z8fwv4vT}/_buildManifest.js +0 -0
  243. /package/dist/web/standalone/.next/static/{rIkMv4YSNlfSeqmGqWVns → Qgr2B_MRhPxC0z8fwv4vT}/_ssgManifest.js +0 -0
@@ -0,0 +1,157 @@
1
+ // ADR-011 #5750: ROADMAP.md renders sketch slices with a `[sketch]` badge.
2
+ //
3
+ // Locks in the visual distinction so an auditor scanning the rendered roadmap
4
+ // can tell which slices are sketches awaiting refine-slice expansion vs which
5
+ // already carry a full plan. Sits alongside `plan-milestone.test.ts` which
6
+ // covers the full-plan render path.
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
15
+ import { handlePlanMilestone, type PlanMilestoneParams } from "../tools/plan-milestone.ts";
16
+
17
+ function makeTmpBase(): string {
18
+ const base = mkdtempSync(join(tmpdir(), "gsd-plan-sketch-render-"));
19
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
20
+ openDatabase(join(base, ".gsd", "gsd.db"));
21
+ return base;
22
+ }
23
+
24
+ function cleanup(base: string): void {
25
+ try {
26
+ closeDatabase();
27
+ } catch {
28
+ /* noop */
29
+ }
30
+ try {
31
+ rmSync(base, { recursive: true, force: true });
32
+ } catch {
33
+ /* noop */
34
+ }
35
+ }
36
+
37
+ function planMilestoneWithSketches(): PlanMilestoneParams {
38
+ return {
39
+ milestoneId: "M001",
40
+ title: "Progressive Planning Demo",
41
+ vision: "Demonstrate sketch slices in ROADMAP rendering.",
42
+ successCriteria: ["S01 full, S02 sketch", "ROADMAP distinguishes them"],
43
+ keyRisks: [{ risk: "Visual collision", whyItMatters: "Auditors need to spot sketches." }],
44
+ proofStrategy: [{ riskOrUnknown: "Render correctness", retireIn: "S01", whatWillBeProven: "Roadmap shows the badge." }],
45
+ verificationContract: "Contract verification text",
46
+ verificationIntegration: "Integration verification text",
47
+ verificationOperational: "Operational verification text",
48
+ verificationUat: "UAT verification text",
49
+ definitionOfDone: ["Renderer emits badge", "Test asserts it"],
50
+ requirementCoverage: "Covers ADR-011 #5750.",
51
+ boundaryMapMarkdown: "| From | To | Produces | Consumes |\n|------|----|----------|----------|\n| S01 | S02 | scaffold | nothing |",
52
+ slices: [
53
+ {
54
+ sliceId: "S01",
55
+ title: "Fully planned scaffold",
56
+ risk: "medium" as const,
57
+ depends: [],
58
+ demo: "Scaffold is in place.",
59
+ goal: "Lay down the structural foundation.",
60
+ successCriteria: "Scaffold tests pass.",
61
+ proofLevel: "integration" as const,
62
+ integrationClosure: "Downstream slices depend on this scaffold.",
63
+ observabilityImpact: "No new telemetry.",
64
+ // No isSketch flag — defaults to full plan.
65
+ },
66
+ {
67
+ sliceId: "S02",
68
+ title: "Refinement candidate",
69
+ risk: "low" as const,
70
+ depends: ["S01"],
71
+ demo: "Sketched until S01 ships.",
72
+ goal: "Refine into a full plan after S01 lands.",
73
+ successCriteria: "",
74
+ proofLevel: "",
75
+ integrationClosure: "",
76
+ observabilityImpact: "",
77
+ isSketch: true,
78
+ sketchScope: "Pick up the scaffold from S01 and add the demo behavior. Stay inside the existing module boundaries.",
79
+ },
80
+ ],
81
+ };
82
+ }
83
+
84
+ test("ROADMAP renders sketch slices with [sketch] badge and full slices without", async () => {
85
+ const base = makeTmpBase();
86
+ try {
87
+ const params = planMilestoneWithSketches();
88
+ const result = await handlePlanMilestone(params, base);
89
+ if ("error" in result) {
90
+ assert.fail(`handlePlanMilestone failed: ${result.error}`);
91
+ }
92
+
93
+ const roadmapPath = join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
94
+ const roadmap = readFileSync(roadmapPath, "utf-8");
95
+
96
+ // S01 is a full slice — no sketch badge.
97
+ const s01Line = roadmap.split("\n").find((line) => line.includes("**S01:"));
98
+ assert.ok(s01Line, "S01 slice line must exist in roadmap");
99
+ assert.equal(
100
+ s01Line.includes("`[sketch]`"),
101
+ false,
102
+ "fully-planned S01 must NOT carry the sketch badge",
103
+ );
104
+ assert.match(s01Line, /`risk:medium`/);
105
+
106
+ // S02 is a sketch — badge required, positioned before risk.
107
+ const s02Line = roadmap.split("\n").find((line) => line.includes("**S02:"));
108
+ assert.ok(s02Line, "S02 slice line must exist in roadmap");
109
+ assert.ok(
110
+ s02Line.includes("`[sketch]`"),
111
+ `sketch slice S02 must carry the sketch badge, got: ${s02Line}`,
112
+ );
113
+ // Badge sits before risk so it stays visible if the line truncates.
114
+ const sketchIdx = s02Line.indexOf("`[sketch]`");
115
+ const riskIdx = s02Line.indexOf("`risk:");
116
+ assert.ok(
117
+ sketchIdx >= 0 && riskIdx >= 0 && sketchIdx < riskIdx,
118
+ "sketch badge must appear before the risk tag",
119
+ );
120
+ } finally {
121
+ cleanup(base);
122
+ }
123
+ });
124
+
125
+ test("ROADMAP omits sketch badge when no slices are sketches", async () => {
126
+ const base = makeTmpBase();
127
+ try {
128
+ const params = planMilestoneWithSketches();
129
+ // Strip the sketch designation from S02 so both slices are fully planned.
130
+ params.slices[1] = {
131
+ ...params.slices[1],
132
+ isSketch: false,
133
+ successCriteria: "Demo behavior works.",
134
+ proofLevel: "unit" as const,
135
+ integrationClosure: "S02 closes the demo behavior.",
136
+ observabilityImpact: "No new telemetry.",
137
+ };
138
+
139
+ const result = await handlePlanMilestone(params, base);
140
+ if ("error" in result) {
141
+ assert.fail(`handlePlanMilestone failed: ${result.error}`);
142
+ }
143
+
144
+ const roadmap = readFileSync(
145
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
146
+ "utf-8",
147
+ );
148
+
149
+ assert.equal(
150
+ roadmap.includes("`[sketch]`"),
151
+ false,
152
+ "roadmap must not carry the sketch badge when no slice is a sketch",
153
+ );
154
+ } finally {
155
+ cleanup(base);
156
+ }
157
+ });
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Tests for post-execution retry bypass and verification gate failure handling.
1
3
  /**
2
4
  * post-exec-retry-bypass.test.ts — Tests for post-execution blocking failure retry bypass.
3
5
  *
@@ -9,7 +11,7 @@
9
11
  import { describe, test, mock, beforeEach, afterEach } from "node:test";
10
12
  import assert from "node:assert/strict";
11
13
  import { tmpdir } from "node:os";
12
- import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
14
+ import { mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
13
15
  import { join } from "node:path";
14
16
 
15
17
  import { runPostUnitVerification, type VerificationContext } from "../auto-verification.ts";
@@ -71,6 +73,7 @@ function setupTestEnvironment(): void {
71
73
  mkdirSync(milestonesDir, { recursive: true });
72
74
 
73
75
  process.chdir(tempDir);
76
+ invalidateAllCaches();
74
77
  _clearGsdRootCache();
75
78
 
76
79
  dbPath = join(gsdDir, "gsd.db");
@@ -140,6 +143,34 @@ function createBasicTask(): void {
140
143
  });
141
144
  }
142
145
 
146
+ function createTaskWithoutVerify(): void {
147
+ insertMilestone({ id: "M001" });
148
+ insertSlice({
149
+ id: "S01",
150
+ milestoneId: "M001",
151
+ title: "Test Slice",
152
+ risk: "low",
153
+ });
154
+
155
+ insertTask({
156
+ id: "T01",
157
+ sliceId: "S01",
158
+ milestoneId: "M001",
159
+ title: "Task without host verification",
160
+ status: "pending",
161
+ planning: {
162
+ description: "Task intentionally missing runnable verification",
163
+ estimate: "1h",
164
+ files: [],
165
+ verify: "",
166
+ inputs: [],
167
+ expectedOutput: [],
168
+ observabilityImpact: "",
169
+ },
170
+ sequence: 0,
171
+ });
172
+ }
173
+
143
174
  function createPostExecFailureTask(): void {
144
175
  insertMilestone({ id: "M001" });
145
176
  insertSlice({
@@ -327,6 +358,53 @@ describe("Post-execution blocking failure retry bypass", () => {
327
358
  assert.equal(row?.outcome, "fail");
328
359
  assert.equal(row?.failure_class, "artifact");
329
360
  });
361
+
362
+ test("execute-task with no host-owned verification pauses fail-closed", async () => {
363
+ createTaskWithoutVerify();
364
+
365
+ const ctx = makeMockCtx();
366
+ const pi = makeMockPi();
367
+ const pauseAutoMock = mock.fn(async () => {});
368
+ const s = makeMockSession(tempDir, { type: "execute-task", id: "M001/S01/T01" });
369
+
370
+ const result = await runPostUnitVerification({ s, ctx, pi }, pauseAutoMock);
371
+
372
+ assert.equal(result, "pause");
373
+ assert.equal(pauseAutoMock.mock.callCount(), 1);
374
+ assert.equal(s.pendingVerificationRetry, null);
375
+
376
+ const evidencePath = join(tempDir, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-VERIFY.json");
377
+ const evidence = JSON.parse(readFileSync(evidencePath, "utf-8"));
378
+ assert.equal(evidence.passed, false);
379
+ assert.equal(evidence.discoverySource, "none");
380
+ assert.ok(!("retryAttempt" in evidence), "no-host-checks evidence must not include retryAttempt");
381
+ assert.ok(!("maxRetries" in evidence), "no-host-checks evidence must not include maxRetries");
382
+ });
383
+
384
+ test("auto-discovered package.json verification failure retries instead of continuing", async () => {
385
+ createTaskWithoutVerify();
386
+ writeFileSync(
387
+ join(tempDir, "package.json"),
388
+ JSON.stringify({ scripts: { test: "exit 1" } }),
389
+ "utf-8",
390
+ );
391
+ writePreferences({
392
+ verification_auto_fix: true,
393
+ verification_max_retries: 2,
394
+ });
395
+
396
+ const ctx = makeMockCtx();
397
+ const pi = makeMockPi();
398
+ const pauseAutoMock = mock.fn(async () => {});
399
+ const s = makeMockSession(tempDir, { type: "execute-task", id: "M001/S01/T01" });
400
+
401
+ const result = await runPostUnitVerification({ s, ctx, pi }, pauseAutoMock);
402
+
403
+ assert.equal(result, "retry");
404
+ assert.equal(pauseAutoMock.mock.callCount(), 0);
405
+ assert.equal(s.pendingVerificationRetry?.unitId, "M001/S01/T01");
406
+ assert.match(s.pendingVerificationRetry?.failureContext ?? "", /npm run test/);
407
+ });
330
408
  });
331
409
 
332
410
  describe("Post-execution retry behavior", () => {
@@ -55,6 +55,14 @@ test("system prompt hard rules forbid fabricating user responses", () => {
55
55
  assert.match(prompt, /ask_user_questions.*only valid structured user input/i);
56
56
  });
57
57
 
58
+ test("system prompt requires reading before edit or overwrite", () => {
59
+ const prompt = readPrompt("system");
60
+ assert.match(prompt, /Read before edit or overwrite/i);
61
+ assert.match(prompt, /Before any write that creates or replaces a file/i);
62
+ assert.match(prompt, /confirm whether the path exists; if it does, `read` it first/i);
63
+ assert.match(prompt, /For truly new files, confirm the path does not already exist/i);
64
+ });
65
+
58
66
  test("discuss prompt allows implementation questions when they materially matter", () => {
59
67
  const prompt = readPrompt("discuss");
60
68
  assert.match(prompt, /Lead with experience, but ask implementation when it materially matters/i);
@@ -0,0 +1,252 @@
1
+ // GSD-2 — ADR-005 Phase 3b: ProviderSwitchObserver Tests
2
+ import test from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ import { setProviderSwitchObserver, type ProviderSwitchReport } from "@gsd/pi-ai";
9
+
10
+ import { autoSession } from "../auto-runtime-state.ts";
11
+ import {
12
+ initNotificationStore,
13
+ readNotifications,
14
+ _resetNotificationStore,
15
+ } from "../notification-store.ts";
16
+ import {
17
+ _resetProviderSwitchStats,
18
+ getProviderSwitchStats,
19
+ installProviderSwitchObserver,
20
+ uninstallProviderSwitchObserver,
21
+ } from "../provider-switch-observer.ts";
22
+
23
+ function makeReport(overrides: Partial<ProviderSwitchReport> = {}): ProviderSwitchReport {
24
+ return {
25
+ fromApi: "anthropic-messages",
26
+ toApi: "openai-responses",
27
+ thinkingBlocksDropped: 0,
28
+ thinkingBlocksDowngraded: 0,
29
+ toolCallIdsRemapped: 0,
30
+ syntheticToolResultsInserted: 0,
31
+ thoughtSignaturesDropped: 0,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function withTempBasePath(): { basePath: string; cleanup: () => void } {
37
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-provider-switch-"));
38
+ return {
39
+ basePath,
40
+ cleanup: () => rmSync(basePath, { recursive: true, force: true }),
41
+ };
42
+ }
43
+
44
+ test.beforeEach(() => {
45
+ _resetProviderSwitchStats();
46
+ _resetNotificationStore();
47
+ autoSession.currentTraceId = null;
48
+ autoSession.basePath = "";
49
+ });
50
+
51
+ test.afterEach(() => {
52
+ uninstallProviderSwitchObserver();
53
+ _resetNotificationStore();
54
+ autoSession.currentTraceId = null;
55
+ autoSession.basePath = "";
56
+ });
57
+
58
+ test("installProviderSwitchObserver wires the pi-ai observer hook", () => {
59
+ installProviderSwitchObserver();
60
+ // Indirect: a second install is a no-op. We verify by checking the counter
61
+ // increments exactly once when we fire a single report directly into pi-ai's
62
+ // observer slot.
63
+ installProviderSwitchObserver();
64
+
65
+ // Drive the pi-ai observer directly. The install above pointed it at our
66
+ // handler; firing here exercises the same code path as a real transform.
67
+ // We can't reach the installed handler reference from here, so we re-install
68
+ // a sentinel and confirm setProviderSwitchObserver accepts undefined.
69
+ setProviderSwitchObserver(undefined);
70
+ assert.ok(true); // install/uninstall did not throw
71
+ });
72
+
73
+ test("non-empty report increments the in-memory counter", () => {
74
+ installProviderSwitchObserver();
75
+
76
+ // Reach into the installed handler via setProviderSwitchObserver re-binding.
77
+ // We snapshot the handler by re-installing, capturing nothing — instead we
78
+ // use the public API: drive a report through the observer hook directly by
79
+ // calling setProviderSwitchObserver with a wrapper that proxies into the
80
+ // installed handler. This is awkward without a separate seam, so we test
81
+ // the recordReport path end-to-end by firing through pi-ai's transform helper
82
+ // pattern in a sibling test below. Here we exercise install idempotency only.
83
+
84
+ // Fire by setting our own observer that forwards to the module API isn't
85
+ // possible without exporting handleReport. Instead, verify install state
86
+ // doesn't crash; the real fire path is covered by the integration test
87
+ // below that exercises transformMessagesWithReport.
88
+ assert.deepEqual(getProviderSwitchStats().totalSwitches, 0);
89
+ });
90
+
91
+ test("end-to-end: transformMessagesWithReport fires the observer and updates stats + notifications", async () => {
92
+ const { transformMessagesWithReport } = await import("@gsd/pi-ai");
93
+
94
+ const { basePath, cleanup } = withTempBasePath();
95
+ try {
96
+ initNotificationStore(basePath);
97
+ installProviderSwitchObserver();
98
+
99
+ // Construct a cross-API transform that will drop a redacted thinking block.
100
+ const targetModel = {
101
+ id: "gpt-5",
102
+ name: "GPT-5",
103
+ api: "openai-responses",
104
+ provider: "openai",
105
+ baseUrl: "",
106
+ reasoning: false,
107
+ input: ["text"],
108
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
109
+ contextWindow: 128000,
110
+ maxTokens: 8192,
111
+ } as Parameters<typeof transformMessagesWithReport>[1];
112
+
113
+ const messages = [
114
+ {
115
+ role: "assistant" as const,
116
+ content: [
117
+ { type: "thinking" as const, thinking: "", redacted: true },
118
+ { type: "text" as const, text: "hi" },
119
+ ],
120
+ api: "anthropic-messages",
121
+ provider: "anthropic",
122
+ model: "claude-sonnet-4-6",
123
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
124
+ stopReason: "stop" as const,
125
+ timestamp: Date.now(),
126
+ },
127
+ ];
128
+
129
+ transformMessagesWithReport(messages as Parameters<typeof transformMessagesWithReport>[0], targetModel, undefined, "anthropic-messages");
130
+
131
+ const stats = getProviderSwitchStats();
132
+ assert.equal(stats.totalSwitches, 1, "non-empty report should bump the counter");
133
+ assert.ok(stats.totals.thinkingBlocksDropped >= 1, "thinking block drop should be tallied");
134
+ assert.ok(stats.lastReport, "last report should be retained");
135
+ assert.equal(stats.lastReport?.fromApi, "anthropic-messages");
136
+ assert.equal(stats.lastReport?.toApi, "openai-responses");
137
+
138
+ // Notification persistence — the observer is interactive (no traceId), so
139
+ // the byTrace key falls back to "interactive".
140
+ assert.ok("interactive" in stats.byTrace, "interactive trace bucket should exist");
141
+ assert.equal(stats.byTrace.interactive?.switches, 1);
142
+
143
+ const notifications = readNotifications(basePath);
144
+ const switchNotifs = notifications.filter((n) => n.message.includes("Provider switch"));
145
+ assert.ok(switchNotifs.length >= 1, "a provider-switch notification should be persisted");
146
+ assert.equal(switchNotifs[0]?.severity, "warning");
147
+ } finally {
148
+ cleanup();
149
+ }
150
+ });
151
+
152
+ test("end-to-end: audit event is emitted when an auto trace is active", async () => {
153
+ const { transformMessagesWithReport } = await import("@gsd/pi-ai");
154
+
155
+ const { basePath, cleanup } = withTempBasePath();
156
+ try {
157
+ initNotificationStore(basePath);
158
+ installProviderSwitchObserver();
159
+ autoSession.basePath = basePath;
160
+ autoSession.currentTraceId = "trace-provider-switch-1";
161
+
162
+ const targetModel = {
163
+ id: "gpt-5",
164
+ name: "GPT-5",
165
+ api: "openai-responses",
166
+ provider: "openai",
167
+ baseUrl: "",
168
+ reasoning: false,
169
+ input: ["text"],
170
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
171
+ contextWindow: 128000,
172
+ maxTokens: 8192,
173
+ } as Parameters<typeof transformMessagesWithReport>[1];
174
+
175
+ const messages = [
176
+ {
177
+ role: "assistant" as const,
178
+ content: [
179
+ { type: "thinking" as const, thinking: "", redacted: true },
180
+ { type: "text" as const, text: "hi" },
181
+ ],
182
+ api: "anthropic-messages",
183
+ provider: "anthropic",
184
+ model: "claude-sonnet-4-6",
185
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
186
+ stopReason: "stop" as const,
187
+ timestamp: Date.now(),
188
+ },
189
+ ];
190
+
191
+ transformMessagesWithReport(messages as Parameters<typeof transformMessagesWithReport>[0], targetModel, undefined, "anthropic-messages");
192
+
193
+ const auditLogPath = join(basePath, ".gsd", "audit", "events.jsonl");
194
+ assert.ok(existsSync(auditLogPath), "audit events file should be created");
195
+ const auditLines = readFileSync(auditLogPath, "utf-8")
196
+ .trim()
197
+ .split("\n")
198
+ .map((line) => JSON.parse(line) as { category: string; type: string; traceId: string; payload: Record<string, unknown> });
199
+ const switchEvent = auditLines.find((e) => e.category === "model-policy" && e.type === "provider-switch");
200
+ assert.ok(switchEvent, "a model-policy/provider-switch audit event should be present");
201
+ assert.equal(switchEvent?.traceId, "trace-provider-switch-1");
202
+ assert.equal(switchEvent?.payload.fromApi, "anthropic-messages");
203
+ assert.equal(switchEvent?.payload.toApi, "openai-responses");
204
+
205
+ const stats = getProviderSwitchStats();
206
+ assert.ok("trace-provider-switch-1" in stats.byTrace, "trace-keyed bucket should be populated");
207
+ assert.equal(stats.byTrace["trace-provider-switch-1"]?.switches, 1);
208
+ } finally {
209
+ cleanup();
210
+ }
211
+ });
212
+
213
+ test("empty report does not bump counter or emit a notification", async () => {
214
+ const { transformMessagesWithReport } = await import("@gsd/pi-ai");
215
+
216
+ const { basePath, cleanup } = withTempBasePath();
217
+ try {
218
+ initNotificationStore(basePath);
219
+ installProviderSwitchObserver();
220
+
221
+ // Same-API transform → no transformations → empty report.
222
+ const sameApiModel = {
223
+ id: "claude-sonnet-4-6",
224
+ name: "Claude Sonnet 4.6",
225
+ api: "anthropic-messages",
226
+ provider: "anthropic",
227
+ baseUrl: "",
228
+ reasoning: false,
229
+ input: ["text"],
230
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
231
+ contextWindow: 200000,
232
+ maxTokens: 8192,
233
+ } as Parameters<typeof transformMessagesWithReport>[1];
234
+
235
+ transformMessagesWithReport(
236
+ [
237
+ {
238
+ role: "user" as const,
239
+ content: "plain text — no transforms triggered",
240
+ },
241
+ ] as Parameters<typeof transformMessagesWithReport>[0],
242
+ sameApiModel,
243
+ undefined,
244
+ "anthropic-messages",
245
+ );
246
+
247
+ assert.equal(getProviderSwitchStats().totalSwitches, 0);
248
+ assert.equal(readNotifications(basePath).length, 0);
249
+ } finally {
250
+ cleanup();
251
+ }
252
+ });
@@ -1,9 +1,11 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Verifies GSD session hook widget and footer lifecycle behavior.
3
+
1
4
  /**
2
- * session-start-footer.test.ts
3
- *
4
5
  * Verifies that register-hooks.ts suppresses the gsd-health widget (not the
5
- * built-in footer) when isAutoActive() is true, and that setFooter is never
6
- * called by the extension in either session_start or session_switch.
6
+ * built-in footer) when isAutoActive() is true, clears stale completion widgets
7
+ * on inactive session switches, and that setFooter is never called by the
8
+ * extension in either session_start or session_switch.
7
9
  *
8
10
  * Testing strategy:
9
11
  * 1. Source-code regression guards: structural checks on register-hooks.ts.
@@ -139,6 +141,16 @@ test("session_switch toggles gsd-health from runtime auto state without touching
139
141
  widgetCalls.length = 0;
140
142
  autoSession.active = false;
141
143
  await sessionSwitch!({ reason: "resume" }, ctx);
144
+ assert.deepEqual(
145
+ widgetCalls
146
+ .filter((call) => call.key === "gsd-progress" || call.key === "gsd-outcome")
147
+ .map((call) => [call.key, call.value]),
148
+ [
149
+ ["gsd-progress", undefined],
150
+ ["gsd-outcome", undefined],
151
+ ],
152
+ "session_switch should clear stale GSD completion widgets when auto is inactive",
153
+ );
142
154
  const healthWidgetValues = widgetCalls
143
155
  .filter((call) => call.key === "gsd-health")
144
156
  .map((call) => call.value);
@@ -271,6 +271,11 @@ describe("#2945 Bug 3: mergeAndExit must teardown worktree after successful merg
271
271
  // a real git fixture and verify the worktree directory is removed
272
272
  // from disk after the merge.
273
273
  const tmpBase = realpathSync(mkdtempSync(join(tmpdir(), "gsd-2945-bug3-")));
274
+ // ADR-016 phase 3 (#5693): Lifecycle.restoreToProjectRoot now chdirs to
275
+ // s.originalBasePath. Save cwd before the test so we can restore it
276
+ // before rmSync removes tmpBase — otherwise the next test in this file
277
+ // inherits a deleted cwd and process.cwd() throws ENOENT (uv_cwd).
278
+ const prevCwd = process.cwd();
274
279
  try {
275
280
  const git = (args: string[]): void => {
276
281
  execFileSync("git", args, { cwd: tmpBase, stdio: "pipe" });
@@ -325,6 +330,7 @@ describe("#2945 Bug 3: mergeAndExit must teardown worktree after successful merg
325
330
  `teardownAutoWorktree must be called after successful merge — worktree directory at ${wt} should be removed`,
326
331
  );
327
332
  } finally {
333
+ try { process.chdir(prevCwd); } catch { /* noop */ }
328
334
  try { rmSync(tmpBase, { recursive: true, force: true }); } catch { /* noop */ }
329
335
  }
330
336
  });
@@ -7,6 +7,7 @@ import {
7
7
  ARTIFACT_KEYS,
8
8
  KNOWN_UNIT_TYPES,
9
9
  UNIT_MANIFESTS,
10
+ resolveSubagentPermissionContract,
10
11
  resolveManifest,
11
12
  type ArtifactKey,
12
13
  type ContextModePolicy,
@@ -244,9 +245,11 @@ test('#4934: only execute-task and reactive-execute may use tools.mode "all" (fu
244
245
  test('planning-dispatch mode is reserved for slice-level decomposition and completion units', () => {
245
246
  const allowedDispatchUnits = new Set([
246
247
  "plan-slice",
248
+ "research-slice",
247
249
  "refine-slice",
248
250
  "complete-slice",
249
251
  "complete-milestone",
252
+ "gate-evaluate",
250
253
  // Deep planning mode: research-project orchestrates 4 parallel research
251
254
  // subagents (stack/features/architecture/pitfalls). Subagent dispatch is
252
255
  // the unit's core mechanism — without it, the unit cannot do its job.
@@ -265,6 +268,24 @@ test('planning-dispatch mode is reserved for slice-level decomposition and compl
265
268
  }
266
269
  });
267
270
 
271
+ test('Unit Tool Contract exposes subagent dispatch permissions', () => {
272
+ assert.deepEqual(resolveSubagentPermissionContract("plan-slice"), {
273
+ allowed: true,
274
+ allowedSubagents: ["scout", "planner"],
275
+ toolsMode: "planning-dispatch",
276
+ });
277
+ assert.deepEqual(resolveSubagentPermissionContract("gate-evaluate"), {
278
+ allowed: true,
279
+ allowedSubagents: ["reviewer", "security", "tester"],
280
+ toolsMode: "planning-dispatch",
281
+ });
282
+ assert.deepEqual(resolveSubagentPermissionContract("discuss-milestone"), {
283
+ allowed: false,
284
+ allowedSubagents: [],
285
+ toolsMode: "planning",
286
+ });
287
+ });
288
+
268
289
  test('planning-dispatch manifests declare non-empty allowedSubagents lists', () => {
269
290
  for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
270
291
  if (manifest.tools.mode !== "planning-dispatch") continue;