@opengsd/gsd-core 1.11.0 → 1.12.0

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 (395) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +1 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-dom-verifier.md +169 -0
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-executor.md +17 -9
  9. package/agents/gsd-framework-selector.md +1 -3
  10. package/agents/gsd-intel-updater.md +1 -1
  11. package/agents/gsd-mempalace-curator.md +0 -1
  12. package/agents/gsd-pattern-mapper.md +11 -0
  13. package/agents/gsd-phase-researcher.md +3 -1
  14. package/agents/gsd-plan-checker.md +15 -55
  15. package/agents/gsd-planner.md +6 -4
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +2 -2
  18. package/agents/gsd-roadmapper.md +15 -11
  19. package/agents/gsd-ui-checker.md +63 -4
  20. package/agents/gsd-ui-researcher.md +41 -3
  21. package/agents/gsd-verifier.md +1 -1
  22. package/bin/install.js +609 -134
  23. package/commands/gsd/discuss-phase.md +1 -1
  24. package/commands/gsd/import.md +1 -1
  25. package/commands/gsd/quick.md +8 -4
  26. package/gsd-core/bin/gsd-tools.cjs +567 -51
  27. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  28. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  29. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  30. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  31. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  32. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  33. package/gsd-core/bin/lib/audit.cjs +163 -41
  34. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  35. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  36. package/gsd-core/bin/lib/capability-registry.cjs +336 -95
  37. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  38. package/gsd-core/bin/lib/capability-validator.cjs +205 -18
  39. package/gsd-core/bin/lib/check-command-router.cjs +145 -5
  40. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  41. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  43. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  44. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  45. package/gsd-core/bin/lib/commands.cjs +543 -44
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  47. package/gsd-core/bin/lib/config-loader.cjs +118 -29
  48. package/gsd-core/bin/lib/config.cjs +92 -2
  49. package/gsd-core/bin/lib/configuration.cjs +129 -37
  50. package/gsd-core/bin/lib/core-utils.cjs +84 -7
  51. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  52. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  53. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  54. package/gsd-core/bin/lib/frontmatter.cjs +840 -305
  55. package/gsd-core/bin/lib/gap-checker.cjs +27 -3
  56. package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
  57. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  58. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
  59. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  60. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  61. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  62. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  63. package/gsd-core/bin/lib/init.cjs +120 -41
  64. package/gsd-core/bin/lib/install-engine.cjs +68 -3
  65. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
  66. package/gsd-core/bin/lib/install-profiles.cjs +78 -4
  67. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  68. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  69. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  70. package/gsd-core/bin/lib/intel.cjs +101 -26
  71. package/gsd-core/bin/lib/io.cjs +160 -15
  72. package/gsd-core/bin/lib/learnings.cjs +85 -14
  73. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  74. package/gsd-core/bin/lib/markdown-table.cjs +52 -4
  75. package/gsd-core/bin/lib/milestone.cjs +90 -5
  76. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  77. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  78. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  79. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  80. package/gsd-core/bin/lib/phase-id.cjs +70 -4
  81. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  82. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  83. package/gsd-core/bin/lib/phase.cjs +405 -84
  84. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  85. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  86. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  87. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  88. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
  89. package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
  90. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  91. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  92. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  93. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  94. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  95. package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
  96. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  97. package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
  98. package/gsd-core/bin/lib/roadmap.cjs +74 -19
  99. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
  100. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
  101. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
  102. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  103. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  104. package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
  105. package/gsd-core/bin/lib/smart-entry.cjs +12 -22
  106. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  107. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  108. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  109. package/gsd-core/bin/lib/state-document.cjs +186 -0
  110. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  111. package/gsd-core/bin/lib/state-transition.cjs +517 -101
  112. package/gsd-core/bin/lib/state.cjs +946 -163
  113. package/gsd-core/bin/lib/surface.cjs +10 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  115. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  116. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  117. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  118. package/gsd-core/bin/lib/uat.cjs +1376 -125
  119. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  120. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  121. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  122. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  123. package/gsd-core/bin/lib/vendor/README.md +43 -5
  124. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  125. package/gsd-core/bin/lib/verification.cjs +14 -1
  126. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  127. package/gsd-core/bin/lib/verify.cjs +95 -40
  128. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  129. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  130. package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
  131. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  132. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  133. package/gsd-core/bin/shared/exit-codes.json +8 -0
  134. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  135. package/gsd-core/bin/shared/model-catalog.json +8 -1
  136. package/gsd-core/references/agent-contracts.md +3 -2
  137. package/gsd-core/references/api-coverage.md +24 -2
  138. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  139. package/gsd-core/references/checkpoints.md +37 -19
  140. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  141. package/gsd-core/references/edge-probe.md +8 -0
  142. package/gsd-core/references/execute-mvp-tdd.md +1 -3
  143. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  144. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  145. package/gsd-core/references/failing-direction.md +78 -0
  146. package/gsd-core/references/gate-prompts.md +1 -1
  147. package/gsd-core/references/git-integration.md +5 -5
  148. package/gsd-core/references/git-planning-commit.md +3 -3
  149. package/gsd-core/references/gsd-run-resolver.md +1 -1
  150. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  151. package/gsd-core/references/model-profiles.md +1 -1
  152. package/gsd-core/references/nyquist-compliance.md +74 -0
  153. package/gsd-core/references/offer-next.md +3 -5
  154. package/gsd-core/references/phase-argument-parsing.md +3 -3
  155. package/gsd-core/references/planner-failing-direction.md +53 -0
  156. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  157. package/gsd-core/references/planner-revision.md +1 -1
  158. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  159. package/gsd-core/references/planning-config.md +37 -8
  160. package/gsd-core/references/reviewer-instances.md +31 -0
  161. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  162. package/gsd-core/references/tdd.md +1 -3
  163. package/gsd-core/references/ui-brand.md +65 -21
  164. package/gsd-core/references/ui-consideration-probe.md +1 -1
  165. package/gsd-core/references/universal-anti-patterns.md +2 -2
  166. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  167. package/gsd-core/references/verify-mvp-mode.md +1 -1
  168. package/gsd-core/references/workstream-flag.md +11 -11
  169. package/gsd-core/templates/README.md +1 -1
  170. package/gsd-core/templates/SECURITY.md +3 -3
  171. package/gsd-core/templates/UI-SPEC.md +25 -3
  172. package/gsd-core/templates/VALIDATION.md +3 -3
  173. package/gsd-core/templates/phase-prompt.md +3 -0
  174. package/gsd-core/templates/state.md +7 -0
  175. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  176. package/gsd-core/workflows/add-backlog.md +1 -1
  177. package/gsd-core/workflows/add-phase.md +3 -3
  178. package/gsd-core/workflows/add-tests.md +3 -8
  179. package/gsd-core/workflows/add-todo.md +1 -1
  180. package/gsd-core/workflows/ai-integration-phase.md +4 -9
  181. package/gsd-core/workflows/audit-fix.md +12 -3
  182. package/gsd-core/workflows/audit-milestone.md +9 -9
  183. package/gsd-core/workflows/audit-uat.md +17 -2
  184. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  185. package/gsd-core/workflows/autonomous.md +10 -26
  186. package/gsd-core/workflows/check-todos.md +1 -1
  187. package/gsd-core/workflows/cleanup.md +2 -2
  188. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  189. package/gsd-core/workflows/code-review-fix.md +1 -1
  190. package/gsd-core/workflows/code-review.md +121 -40
  191. package/gsd-core/workflows/complete-milestone.md +15 -10
  192. package/gsd-core/workflows/debug.md +5 -3
  193. package/gsd-core/workflows/diagnose-issues.md +12 -6
  194. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  195. package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
  196. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  197. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  198. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
  199. package/gsd-core/workflows/discuss-phase.md +1 -1
  200. package/gsd-core/workflows/do.md +3 -6
  201. package/gsd-core/workflows/docs-update.md +5 -4
  202. package/gsd-core/workflows/edit-phase.md +1 -1
  203. package/gsd-core/workflows/eval-review.md +4 -9
  204. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  205. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
  206. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  207. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  208. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  209. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
  210. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  211. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  212. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  213. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  214. package/gsd-core/workflows/execute-phase.md +38 -54
  215. package/gsd-core/workflows/execute-plan.md +17 -12
  216. package/gsd-core/workflows/explore.md +1 -1
  217. package/gsd-core/workflows/extract-learnings.md +1 -1
  218. package/gsd-core/workflows/fast.md +2 -2
  219. package/gsd-core/workflows/forensics.md +1 -1
  220. package/gsd-core/workflows/graduation.md +5 -5
  221. package/gsd-core/workflows/health.md +3 -6
  222. package/gsd-core/workflows/import.md +14 -11
  223. package/gsd-core/workflows/inbox.md +4 -5
  224. package/gsd-core/workflows/ingest-docs.md +44 -11
  225. package/gsd-core/workflows/insert-phase.md +5 -5
  226. package/gsd-core/workflows/list-seeds.md +5 -3
  227. package/gsd-core/workflows/list-workspaces.md +1 -1
  228. package/gsd-core/workflows/manager.md +12 -23
  229. package/gsd-core/workflows/map-codebase.md +1 -1
  230. package/gsd-core/workflows/milestone-summary.md +1 -1
  231. package/gsd-core/workflows/mvp-phase.md +2 -2
  232. package/gsd-core/workflows/new-milestone.md +9 -21
  233. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  234. package/gsd-core/workflows/new-project.md +12 -26
  235. package/gsd-core/workflows/new-workspace.md +1 -1
  236. package/gsd-core/workflows/next.md +2 -2
  237. package/gsd-core/workflows/pause-work.md +1 -1
  238. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  239. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  240. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  241. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  242. package/gsd-core/workflows/plan-phase.md +121 -42
  243. package/gsd-core/workflows/plan-review-convergence.md +46 -9
  244. package/gsd-core/workflows/plant-seed.md +2 -2
  245. package/gsd-core/workflows/pr-branch.md +187 -51
  246. package/gsd-core/workflows/profile-user.md +16 -14
  247. package/gsd-core/workflows/progress.md +27 -12
  248. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  249. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
  250. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  251. package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
  252. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  253. package/gsd-core/workflows/quick.md +20 -29
  254. package/gsd-core/workflows/remove-phase.md +4 -4
  255. package/gsd-core/workflows/remove-workspace.md +2 -2
  256. package/gsd-core/workflows/resume-project.md +8 -12
  257. package/gsd-core/workflows/review.md +193 -15
  258. package/gsd-core/workflows/scan.md +1 -1
  259. package/gsd-core/workflows/secure-phase.md +2 -2
  260. package/gsd-core/workflows/settings-advanced.md +7 -9
  261. package/gsd-core/workflows/settings-integrations.md +64 -31
  262. package/gsd-core/workflows/settings.md +3 -5
  263. package/gsd-core/workflows/ship.md +12 -6
  264. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  265. package/gsd-core/workflows/sketch.md +12 -18
  266. package/gsd-core/workflows/smart-entry.md +3 -5
  267. package/gsd-core/workflows/spec-phase.md +23 -1
  268. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  269. package/gsd-core/workflows/spike.md +20 -31
  270. package/gsd-core/workflows/stats.md +2 -2
  271. package/gsd-core/workflows/sync-skills.md +1 -1
  272. package/gsd-core/workflows/thread.md +11 -7
  273. package/gsd-core/workflows/transition.md +5 -5
  274. package/gsd-core/workflows/ui-phase.md +10 -16
  275. package/gsd-core/workflows/ui-review.md +6 -10
  276. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  277. package/gsd-core/workflows/undo.md +8 -16
  278. package/gsd-core/workflows/update.md +6 -10
  279. package/gsd-core/workflows/validate-phase.md +2 -2
  280. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  281. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  282. package/gsd-core/workflows/verify-work.md +57 -18
  283. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  284. package/hooks/dist/gsd-config-reload.js +18 -12
  285. package/hooks/dist/gsd-context-monitor.js +19 -10
  286. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  287. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  288. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  289. package/hooks/dist/gsd-cursor-stop.js +2 -1
  290. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  291. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  292. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  293. package/hooks/dist/gsd-graphify-update.sh +22 -18
  294. package/hooks/dist/gsd-node-runner.sh +76 -0
  295. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  296. package/hooks/dist/gsd-prompt-guard.js +16 -7
  297. package/hooks/dist/gsd-read-guard.js +16 -7
  298. package/hooks/dist/gsd-read-injection-scanner.js +17 -8
  299. package/hooks/dist/gsd-session-state.sh +1 -0
  300. package/hooks/dist/gsd-statusline.js +215 -26
  301. package/hooks/dist/gsd-validate-commit.sh +80 -6
  302. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  303. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  304. package/hooks/dist/gsd-workflow-guard.js +34 -16
  305. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  306. package/hooks/dist/gsd-write-guard.js +35 -25
  307. package/hooks/dist/lib/cli-exit.js +560 -0
  308. package/hooks/dist/lib/exit-code-registry.js +98 -0
  309. package/hooks/dist/lib/git-probe.js +84 -0
  310. package/hooks/dist/lib/hook-exit.js +81 -0
  311. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  312. package/hooks/gsd-agent-isolation-guard.js +77 -38
  313. package/hooks/gsd-config-reload.js +18 -12
  314. package/hooks/gsd-context-monitor.js +19 -10
  315. package/hooks/gsd-cursor-post-tool.js +3 -1
  316. package/hooks/gsd-cursor-pre-tool.js +3 -1
  317. package/hooks/gsd-cursor-session-start.js +2 -1
  318. package/hooks/gsd-cursor-stop.js +2 -1
  319. package/hooks/gsd-cursor-subagent-start.js +28 -23
  320. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  321. package/hooks/gsd-ensure-canonical-path.js +2 -1
  322. package/hooks/gsd-graphify-update.sh +22 -18
  323. package/hooks/gsd-node-runner.sh +76 -0
  324. package/hooks/gsd-phase-boundary.sh +1 -0
  325. package/hooks/gsd-prompt-guard.js +16 -7
  326. package/hooks/gsd-read-guard.js +16 -7
  327. package/hooks/gsd-read-injection-scanner.js +17 -8
  328. package/hooks/gsd-session-state.sh +1 -0
  329. package/hooks/gsd-statusline.js +215 -26
  330. package/hooks/gsd-validate-commit.sh +80 -6
  331. package/hooks/gsd-windsurf-pre-command.js +16 -11
  332. package/hooks/gsd-windsurf-pre-write.js +22 -13
  333. package/hooks/gsd-workflow-guard.js +34 -16
  334. package/hooks/gsd-worktree-path-guard.js +36 -21
  335. package/hooks/gsd-write-guard.js +35 -25
  336. package/hooks/lib/cli-exit.js +560 -0
  337. package/hooks/lib/exit-code-registry.js +98 -0
  338. package/hooks/lib/git-probe.js +84 -0
  339. package/hooks/lib/hook-exit.js +81 -0
  340. package/hooks/managed-hooks-registry.cjs +3 -0
  341. package/package.json +12 -7
  342. package/scripts/base64-scan.sh +74 -12
  343. package/scripts/build-hooks.js +5 -0
  344. package/scripts/check-glossary-refs.cjs +77 -15
  345. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  346. package/scripts/ci-check-job-near-cap.cjs +49 -0
  347. package/scripts/ci-pr-mergeability.cjs +262 -0
  348. package/scripts/ci-test-scope.cjs +45 -12
  349. package/scripts/ci-timeout-report.cjs +230 -0
  350. package/scripts/docs-guard-registry.cjs +396 -0
  351. package/scripts/gen-capability-registry.cjs +8 -6
  352. package/scripts/gen-exit-code-docs.cjs +318 -0
  353. package/scripts/gen-exit-code-registry.cjs +891 -0
  354. package/scripts/gen-features.cjs +836 -0
  355. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  356. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  357. package/scripts/gen-loop-host-contract.cjs +134 -1
  358. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  359. package/scripts/gen-state-md-docs.cjs +727 -0
  360. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  361. package/scripts/lib/ci-job-timing.cjs +72 -0
  362. package/scripts/lib/cli-exit.cjs +546 -44
  363. package/scripts/lib/drift-scan.cjs +32 -2
  364. package/scripts/lib/exit-code-registry.cjs +98 -0
  365. package/scripts/lib/ndjson-reporter.cjs +119 -0
  366. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  367. package/scripts/lint-docs-guard-registration.cjs +495 -0
  368. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  369. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  370. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  371. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  372. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  373. package/scripts/lint-phase-enumeration-drift.cjs +21 -8
  374. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  375. package/scripts/lint-removed-but-needed.cjs +184 -16
  376. package/scripts/lint-seam-enforcement.cjs +182 -0
  377. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  378. package/scripts/lint-source-test-name-collision.cjs +241 -0
  379. package/scripts/lint-state-write-path-drift.cjs +337 -432
  380. package/scripts/lint-test-file-count.allowlist.json +122 -4
  381. package/scripts/lint-test-file-count.cjs +25 -3
  382. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  383. package/scripts/lint-vendored-deps.cjs +208 -35
  384. package/scripts/mutation-matrix.cjs +599 -50
  385. package/scripts/prompt-injection-scan.sh +75 -14
  386. package/scripts/secret-scan.sh +75 -13
  387. package/scripts/select-docs-guards.cjs +56 -0
  388. package/scripts/sync-runtime-launcher.cjs +22 -3
  389. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  390. package/skills/gsd-import/SKILL.md +1 -1
  391. package/skills/gsd-quick/SKILL.md +8 -4
  392. package/vscode/package.json +1 -1
  393. package/bin/lib/ui-safety-gate.cjs +0 -109
  394. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  395. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -13,7 +13,7 @@
13
13
  * readSurface(runtimeConfigDir)
14
14
  * writeSurface(runtimeConfigDir, surfaceState)
15
15
  * resolveSurface(runtimeConfigDir, manifest, clusterMap?, registry?)
16
- * applySurface(runtimeConfigDir, layout, manifest, clusterMap?, registry?)
16
+ * applySurface(runtimeConfigDir, layout, manifest, clusterMap?, registry?, opts?, deps?)
17
17
  * listSurface(runtimeConfigDir, manifest, clusterMap?, registry?)
18
18
  * pruneSkillDirs(skillsDir, retainedNames, prefix, manifest)
19
19
  *
@@ -37,6 +37,8 @@ const node_path_1 = __importDefault(require("node:path"));
37
37
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
38
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
39
  const installProfiles = require("./install-profiles.cjs");
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const testHomeGuard = require("./real-home-guard.cjs");
40
42
  const { readActiveProfile, resolveProfile, loadSkillsManifest,
41
43
  // #2322 HIGH-3: shared marker name — single source of truth with the writer
42
44
  // (install-profiles.cts stageSkillsForRuntimeAsSkills) so the prune reader
@@ -303,10 +305,16 @@ function resolveSurface(runtimeConfigDir, manifest, clusterMap, registry) {
303
305
  * Re-stage the active surface using the resolved layout.
304
306
  * Iterates layout.kinds and syncs each artifact kind to its destination.
305
307
  */
306
- function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry, opts) {
308
+ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry, opts, deps = {}) {
307
309
  if (node_path_1.default.resolve(runtimeConfigDir) !== node_path_1.default.resolve(layout.configDir)) {
308
310
  throw new TypeError('applySurface runtimeConfigDir must match layout.configDir');
309
311
  }
312
+ // #3712: the dest selection below prefers `kind.home` over layout.configDir and
313
+ // then hands it to the destructive _syncGsdDir, so surface apply is a third
314
+ // escape route into the developer's real home alongside install/uninstall.
315
+ testHomeGuard.assertTestHomeSandboxed('applySurface', layout.runtime, layout.kinds, {
316
+ os: deps.os, env: deps.env,
317
+ });
310
318
  const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
311
319
  const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
312
320
  // Profile toggles must converge retired surfaces too. Once a kind disappears
@@ -14,6 +14,14 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  // eslint-disable-next-line @typescript-eslint/no-require-imports
15
15
  const ioMod = require("./io.cjs");
16
16
  const { output, error, ERROR_REASON } = ioMod;
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const planDocumentMod = require("./plan-document.cjs");
19
+ const { parsePlanDocument } = planDocumentMod;
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const capabilityLoaderMod = require("./capability-loader.cjs");
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ const taskContentResolutionMod = require("./task-content-resolution.cjs");
24
+ const { resolveTaskContent, ResolverAmbiguousError, ResolverFailedError, ResolverTimeoutError, ResolverMalformedOutputError, } = taskContentResolutionMod;
17
25
  // ─── Implementation ───────────────────────────────────────────────────────────
18
26
  function isBehaviorAddingTaskContent(content) {
19
27
  const tddTrue = /\btdd\s*=\s*["']true["']/i.test(content);
@@ -52,10 +60,111 @@ function isBehaviorAddingTaskContent(content) {
52
60
  reason: isBehaviorAdding ? null : `Not behavior-adding: ${missing.join('; ')}`,
53
61
  };
54
62
  }
63
+ /**
64
+ * Default (production) capability loader for `resolve-content`: the merged
65
+ * first-party + validated-installed-overlay registry (ADR-1244 D2), the
66
+ * established runtime read path for "installed capabilities including
67
+ * third-party" — as opposed to `capability-loader.cts`'s heavier build-time
68
+ * validation entry points or the static `capability-registry.cjs` alone
69
+ * (first-party only, would miss a third-party capability's
70
+ * `taskContentResolver` declaration entirely).
71
+ */
72
+ function defaultLoadCapabilities(cwd) {
73
+ const registry = capabilityLoaderMod.loadRegistry({ includeInstalled: true, cwd });
74
+ return Object.values(registry.capabilities ?? {});
75
+ }
76
+ function parseResolveContentArgs(args) {
77
+ let plan = null;
78
+ let taskId = null;
79
+ for (let i = 2; i < args.length; i++) {
80
+ if (args[i] === '--plan') {
81
+ plan = args[i + 1] ?? null;
82
+ i++;
83
+ }
84
+ else if (args[i] === '--task-id') {
85
+ taskId = args[i + 1] ?? null;
86
+ i++;
87
+ }
88
+ }
89
+ return { plan, taskId };
90
+ }
91
+ /**
92
+ * `task resolve-content --plan <PLAN.md path> --task-id <tracker-id value> --raw`
93
+ * (ADR-3646 Decision 2). Resolves one task's content from the external
94
+ * tracker its `tracker-id` attribute names, via `task-content-resolution.cts`.
95
+ *
96
+ * HARD-HALT CONTRACT: a thrown `ResolverAmbiguousError` / `ResolverFailedError`
97
+ * / `ResolverTimeoutError` / `ResolverMalformedOutputError` from
98
+ * `resolveTaskContent` is turned into this CLI's own non-zero exit via
99
+ * `error()` — never swallowed into a `{resolved: false}` JSON answer. Any
100
+ * other thrown error is not one of the four documented resolver-error
101
+ * classes and is allowed to propagate uncaught.
102
+ */
103
+ function routeResolveContent({ args, cwd, raw }, deps = {}) {
104
+ const usage = 'Usage: task resolve-content --plan <path> --task-id <tracker-id> --raw';
105
+ const { plan, taskId } = parseResolveContentArgs(args);
106
+ if (!plan || !taskId) {
107
+ error(usage, ERROR_REASON.USAGE);
108
+ return;
109
+ }
110
+ const projectRoot = node_path_1.default.resolve(cwd || process.cwd());
111
+ const resolvedPlanPath = node_path_1.default.resolve(projectRoot, plan);
112
+ const rel = node_path_1.default.relative(projectRoot, resolvedPlanPath);
113
+ if (rel === '..' || rel.startsWith(`..${node_path_1.default.sep}`)) {
114
+ error(`Plan file is outside project scope: ${plan}`, ERROR_REASON.USAGE);
115
+ return;
116
+ }
117
+ if (!node_fs_1.default.existsSync(resolvedPlanPath)) {
118
+ error(`Plan file not found: ${plan}`, ERROR_REASON.USAGE);
119
+ return;
120
+ }
121
+ const planContent = node_fs_1.default.readFileSync(resolvedPlanPath, 'utf-8');
122
+ const parsedPlan = parsePlanDocument(planContent, resolvedPlanPath);
123
+ const task = (parsedPlan.tasks ?? []).find((t) => t.trackerId === taskId);
124
+ if (!task) {
125
+ error(`No task with tracker-id '${taskId}' found in plan: ${plan}`, ERROR_REASON.USAGE);
126
+ return;
127
+ }
128
+ const loadCapabilities = deps.loadCapabilities ?? defaultLoadCapabilities;
129
+ const capabilities = loadCapabilities(projectRoot);
130
+ const resolveFn = deps.resolveTaskContentFn ?? resolveTaskContent;
131
+ let result;
132
+ try {
133
+ result = resolveFn({ trackerId: task.trackerId, capabilities });
134
+ }
135
+ catch (err) {
136
+ if (err instanceof ResolverAmbiguousError ||
137
+ err instanceof ResolverFailedError ||
138
+ err instanceof ResolverTimeoutError ||
139
+ err instanceof ResolverMalformedOutputError) {
140
+ error(err.message, ERROR_REASON.UNKNOWN);
141
+ return;
142
+ }
143
+ throw err;
144
+ }
145
+ switch (result.kind) {
146
+ case 'not-applicable':
147
+ output({ resolved: false }, raw, undefined);
148
+ return;
149
+ case 'no-resolver':
150
+ output({ resolved: false, reason: 'no-resolver' }, raw, undefined);
151
+ return;
152
+ case 'empty':
153
+ output({ resolved: false, reason: 'empty' }, raw, undefined);
154
+ return;
155
+ case 'resolved':
156
+ output({ resolved: true, content: result.content }, raw, undefined);
157
+ return;
158
+ }
159
+ }
55
160
  function routeTaskCommand({ args, cwd, raw }) {
56
161
  const subcommand = args[1];
162
+ if (subcommand === 'resolve-content') {
163
+ routeResolveContent({ args, cwd, raw });
164
+ return;
165
+ }
57
166
  if (subcommand !== 'is-behavior-adding') {
58
- error('Unknown task subcommand. Available: is-behavior-adding', ERROR_REASON.SDK_UNKNOWN_COMMAND);
167
+ error('Unknown task subcommand. Available: is-behavior-adding, resolve-content', ERROR_REASON.SDK_UNKNOWN_COMMAND);
59
168
  }
60
169
  let content = null;
61
170
  if (args[2] === '--task-content') {
@@ -82,4 +191,5 @@ function routeTaskCommand({ args, cwd, raw }) {
82
191
  module.exports = {
83
192
  isBehaviorAddingTaskContent,
84
193
  routeTaskCommand,
194
+ routeResolveContent,
85
195
  };
@@ -0,0 +1,368 @@
1
+ "use strict";
2
+ /**
3
+ * Task Content Resolution Module (ADR-3646 Phase 1, #3970).
4
+ *
5
+ * Given a task's `tracker-id` attribute value (parsed verbatim by
6
+ * `plan-document.cts`, never split there) and the set of installed
7
+ * capabilities' `taskContentResolver` declarations, resolves the task's
8
+ * content from the matching external tracker via a bounded subprocess call —
9
+ * or reports that no resolution applies.
10
+ *
11
+ * PURE / IMPURE SPLIT, loosely mirroring `review-lane-invocation.cts`'s
12
+ * resolve-then-run shape, but deliberately NOT copying its full machinery
13
+ * (Gall's Law — see `40-design.md`'s "Laws that apply" section): this problem
14
+ * has no probe/model/effort/prompt-channel axes, just one deterministic
15
+ * id-lookup. `splitTrackerId`, `findResolver`, and `buildInvocation` are pure
16
+ * and total (never throw, even on hostile third-party-shaped input — a
17
+ * capability manifest is third-party-authored, and while `capability-
18
+ * validator.cjs` validates it at install time, this module re-validates
19
+ * defensively rather than trusting that boundary). `resolveTaskContent` is
20
+ * the one impure boundary: it spawns exactly one bounded subprocess, through
21
+ * an injectable `execFn` so tests never spawn a real process or wait a real
22
+ * timeout (CLAUDE.md's clock-seam rule).
23
+ *
24
+ * HARD-HALT CONTRACT (ADR-3646 Decision 4): an ambiguous resolver match, a
25
+ * non-zero resolver exit, a timeout, or malformed resolver stdout all THROW.
26
+ * None of these degrade to a silently-empty or silently-picked result — a
27
+ * task-content resolution failure must halt the caller (`task-command-
28
+ * router.cts`'s `resolve-content` subcommand turns each throw into the CLI's
29
+ * own non-zero exit), never fall back to inline PLAN.md content pretending
30
+ * nothing happened.
31
+ *
32
+ * ADR-457 build-at-publish: source in src/task-content-resolution.cts,
33
+ * compiled to gsd-core/bin/lib/task-content-resolution.cjs (gitignored).
34
+ */
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ // Use non-destructured namespace import so test-time mock.method(childProcess,
39
+ // 'spawnSync') can intercept calls from this seam — destructured imports
40
+ // capture references at load time and become un-mockable (matches the
41
+ // convention documented in shell-command-projection.cts).
42
+ const node_child_process_1 = __importDefault(require("node:child_process"));
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const ioMod = require("./io.cjs");
45
+ const { formatDiagnosticToken } = ioMod;
46
+ // ─── Result taxonomy ──────────────────────────────────────────────────────────
47
+ /**
48
+ * The four non-throwing outcomes of `resolveTaskContent`. Frozen because the
49
+ * `kind` discriminant is the product — callers (the CLI seam) switch on it
50
+ * directly rather than string-matching prose.
51
+ */
52
+ const TASK_CONTENT_RESULT = Object.freeze({
53
+ NOT_APPLICABLE: 'not-applicable',
54
+ NO_RESOLVER: 'no-resolver',
55
+ RESOLVED: 'resolved',
56
+ EMPTY: 'empty',
57
+ });
58
+ // ─── Throwable error taxonomy ─────────────────────────────────────────────────
59
+ // These four ALWAYS throw — they are configuration/execution defects, never a
60
+ // value `resolveTaskContent` returns. See the module docstring's hard-halt
61
+ // contract.
62
+ /**
63
+ * Two or more installed capabilities declare a `taskContentResolver` for the
64
+ * same `trackerPrefix`. Structurally impossible in a correctly-validated
65
+ * install (`capability-validator.cjs` enforces cross-capability prefix
66
+ * uniqueness), but `findResolver` must still refuse to silently pick one if a
67
+ * test harness or a validator bug ever produces this shape.
68
+ */
69
+ class ResolverAmbiguousError extends Error {
70
+ prefix;
71
+ capabilityIds;
72
+ constructor(prefix, capabilityIds) {
73
+ super(`tracker prefix '${prefix}' matches ${capabilityIds.length} installed capability resolvers ` +
74
+ `(${capabilityIds.join(', ')}) — ambiguous resolver registration must never silently pick one`);
75
+ this.name = 'ResolverAmbiguousError';
76
+ this.prefix = prefix;
77
+ this.capabilityIds = capabilityIds;
78
+ }
79
+ }
80
+ /**
81
+ * The resolver subprocess exited non-zero (or failed to spawn at all).
82
+ *
83
+ * `stderrTail` is UNTRUSTED subprocess-sourced text (the resolver binary is
84
+ * declared by a capability manifest and invoked with an argv token derived
85
+ * from a PLAN.md `tracker-id` attribute, which is often LLM/agent-authored —
86
+ * a hostile or buggy resolver could echo attacker-influenced text back on
87
+ * its own stderr). `.message` embeds it through `io.cjs`'s
88
+ * `formatDiagnosticToken()` so every caller of `resolveTaskContent` gets a
89
+ * `.message` that is already safe to write verbatim to a plain-text
90
+ * diagnostic — see that function's docstring for why this must happen here,
91
+ * at the point the untrusted substring is embedded, rather than at each
92
+ * call site.
93
+ */
94
+ class ResolverFailedError extends Error {
95
+ exitCode;
96
+ stderrTail;
97
+ constructor(binary, exitCode, stderrTail) {
98
+ super(`resolver command '${binary}' exited ${exitCode === null ? 'with no exit code (spawn failure)' : exitCode}` +
99
+ (stderrTail ? `: ${formatDiagnosticToken(stderrTail)}` : ''));
100
+ this.name = 'ResolverFailedError';
101
+ this.exitCode = exitCode;
102
+ this.stderrTail = stderrTail;
103
+ }
104
+ }
105
+ /** The resolver subprocess exceeded its declared `invoke.timeoutMs` bound. */
106
+ class ResolverTimeoutError extends Error {
107
+ timeoutMs;
108
+ constructor(binary, timeoutMs) {
109
+ super(`resolver command '${binary}' timed out after ${timeoutMs}ms`);
110
+ this.name = 'ResolverTimeoutError';
111
+ this.timeoutMs = timeoutMs;
112
+ }
113
+ }
114
+ /**
115
+ * The resolver's stdout was not valid JSON, or was valid JSON that is not a
116
+ * plain object (a `null`, array, string, number, or boolean top-level value
117
+ * is rejected — only a plain object can carry the `description`/`verify`/
118
+ * `acceptance_criteria`/`read_first`/`done` fields this seam reads).
119
+ */
120
+ class ResolverMalformedOutputError extends Error {
121
+ stdoutSample;
122
+ constructor(binary, reason, stdoutSample) {
123
+ super(`resolver command '${binary}' produced malformed output: ${reason}` +
124
+ (stdoutSample ? ` (stdout sample: ${formatDiagnosticToken(stdoutSample)})` : ''));
125
+ this.name = 'ResolverMalformedOutputError';
126
+ this.stdoutSample = stdoutSample;
127
+ }
128
+ }
129
+ // ─── Pure functions ────────────────────────────────────────────────────────────
130
+ /**
131
+ * The SAME kebab-case grammar `capability-validator.cjs`'s `KEBAB_RE`
132
+ * enforces on `taskContentResolver.trackerPrefix` at install time
133
+ * (`validateTaskContentResolverFields`). Duplicated as a literal rather than
134
+ * imported — `.cts` (build-at-publish, ADR-457) and the hand-written
135
+ * `capability-validator.cjs` are genuinely two different build targets with
136
+ * no shared-constants module between them today — but `tests/task-content-
137
+ * resolver-grammar-parity.test.cjs` asserts both regexes agree on a shared
138
+ * table of inputs, so a future edit to either one that silently diverges from
139
+ * the other fails a test instead of drifting quietly (CLAUDE.md's Generative
140
+ * Fix Divergence rule).
141
+ */
142
+ const TRACKER_PREFIX_RE = /^[a-z][a-z0-9-]*$/;
143
+ /**
144
+ * Split a `tracker-id` attribute value into its prefix and id, on the FIRST
145
+ * `:` only — colons after the first stay in the id verbatim (a tracker whose
146
+ * native ids contain colons, e.g. `beads:issue:GSD-1` → `{prefix: "beads",
147
+ * id: "issue:GSD-1"}`).
148
+ *
149
+ * PURE. Returns `null` for `null`/empty input, and for a string with no `:`
150
+ * at all (nothing to split — there is no prefix to match a resolver against).
151
+ */
152
+ function splitTrackerId(trackerId) {
153
+ if (typeof trackerId !== 'string' || trackerId.length === 0)
154
+ return null;
155
+ const colonIdx = trackerId.indexOf(':');
156
+ if (colonIdx === -1)
157
+ return null;
158
+ const prefix = trackerId.slice(0, colonIdx);
159
+ const id = trackerId.slice(colonIdx + 1);
160
+ if (!prefix || !id)
161
+ return null;
162
+ return { prefix, id };
163
+ }
164
+ /**
165
+ * Defensively re-validate a raw `taskContentResolver` declaration's shape.
166
+ * `capability-validator.cjs` already enforces this at install time, but this
167
+ * module treats every capability manifest as third-party-authored input and
168
+ * never trusts a shape it has not itself checked — a malformed declaration is
169
+ * treated as though it does not exist for matching purposes, never thrown on.
170
+ */
171
+ function parseResolverDeclaration(capabilityId, raw) {
172
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
173
+ return null;
174
+ const body = raw;
175
+ const trackerPrefix = typeof body.trackerPrefix === 'string' ? body.trackerPrefix.trim() : '';
176
+ if (!trackerPrefix || !TRACKER_PREFIX_RE.test(trackerPrefix))
177
+ return null;
178
+ const inv = body.invoke;
179
+ if (inv === null || typeof inv !== 'object' || Array.isArray(inv))
180
+ return null;
181
+ const invBody = inv;
182
+ const binary = typeof invBody.binary === 'string' ? invBody.binary.trim() : '';
183
+ if (!binary)
184
+ return null;
185
+ const args = Array.isArray(invBody.args)
186
+ ? invBody.args.filter((a) => typeof a === 'string')
187
+ : null;
188
+ if (args === null || args.length !== invBody.args.length)
189
+ return null;
190
+ if (!args.includes('{{id}}'))
191
+ return null;
192
+ const timeoutMs = invBody.timeoutMs;
193
+ if (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0)
194
+ return null;
195
+ return {
196
+ capabilityId,
197
+ trackerPrefix,
198
+ invoke: { binary, args, timeoutMs },
199
+ };
200
+ }
201
+ /**
202
+ * Find the resolver declared for `prefix` among `capabilities`.
203
+ *
204
+ * PURE, total. Returns:
205
+ * - the single matching declaration when exactly one well-formed resolver
206
+ * declares `trackerPrefix === prefix`;
207
+ * - `null` when zero capabilities declare a well-formed resolver for it
208
+ * (an unrecognized prefix is a data case, not a defect — see design row 11);
209
+ * - the literal string `'ambiguous'` when two or more do — this must be
210
+ * structurally impossible in a correctly-validated install, but this
211
+ * function refuses to silently pick one regardless.
212
+ */
213
+ function findResolver(prefix, capabilities) {
214
+ const matches = [];
215
+ for (const cap of capabilities ?? []) {
216
+ if (!cap || typeof cap !== 'object')
217
+ continue;
218
+ const decl = parseResolverDeclaration(cap.id, cap.taskContentResolver);
219
+ if (decl && decl.trackerPrefix === prefix)
220
+ matches.push(decl);
221
+ }
222
+ if (matches.length === 0)
223
+ return null;
224
+ if (matches.length > 1)
225
+ return 'ambiguous';
226
+ return matches[0];
227
+ }
228
+ /**
229
+ * Expand a resolver's `invoke.args` template, replacing every `"{{id}}"`
230
+ * entry with the literal `id` string. Exact-match token replacement, not
231
+ * template-string interpolation — mirrors `review-lane-invocation.cts`'s
232
+ * argv-expansion discipline (a placeholder is a whole array element, not a
233
+ * substring).
234
+ *
235
+ * PURE.
236
+ */
237
+ function buildInvocation(resolver, id) {
238
+ return {
239
+ binary: resolver.invoke.binary,
240
+ args: resolver.invoke.args.map((a) => (a === '{{id}}' ? id : a)),
241
+ timeoutMs: resolver.invoke.timeoutMs,
242
+ };
243
+ }
244
+ /**
245
+ * Real subprocess execution — the default `execFn`. Uses Node's `spawnSync`
246
+ * with the `timeout` option so a hung resolver is killed at the bound rather
247
+ * than hanging the caller (CLAUDE.md's Unbounded Subprocesses gauntlet line).
248
+ */
249
+ function realExec(binary, args, opts) {
250
+ const result = node_child_process_1.default.spawnSync(binary, args, {
251
+ encoding: 'utf-8',
252
+ stdio: 'pipe',
253
+ timeout: opts.timeout,
254
+ windowsHide: true,
255
+ });
256
+ return {
257
+ status: result.status ?? null,
258
+ stdout: (result.stdout ?? '').toString(),
259
+ stderr: (result.stderr ?? '').toString(),
260
+ error: result.error ?? undefined,
261
+ };
262
+ }
263
+ /**
264
+ * True when an `ExecResult` indicates the subprocess was killed by the
265
+ * `timeout` option, i.e. it never completed and reported a real answer. Only
266
+ * `error.code === 'ETIMEDOUT'` is checked — Node.js guarantees this
267
+ * cross-platform when `spawnSync`'s `timeout` option fires; pairing it with a
268
+ * `signal === 'SIGTERM'` check is platform-fragile (Windows does not
269
+ * necessarily report SIGTERM the same way) and risks a false negative. Same
270
+ * predicate discipline as `shell-command-projection.cts`'s `isSpawnTimeout`.
271
+ */
272
+ function isExecTimeout(result) {
273
+ const err = result.error;
274
+ return err?.code === 'ETIMEDOUT';
275
+ }
276
+ // ─── Resolver JSON → content mapping ──────────────────────────────────────────
277
+ function coerceStringOrNull(value) {
278
+ return typeof value === 'string' ? value : null;
279
+ }
280
+ function coerceStringArray(value) {
281
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
282
+ }
283
+ /**
284
+ * Map a resolver's validated JSON object onto `ResolvedTaskContent`. Every
285
+ * field is coerced defensively — the resolver's JSON is a third-party CLI's
286
+ * output, validated for exit code and JSON-object-shape upstream, but never
287
+ * trusted field-by-field. A missing or wrong-typed field degrades sanely
288
+ * (string/null fields fall back to `null`, array fields fall back to `[]`);
289
+ * only the caller's `description`-emptiness check throws no further errors
290
+ * here — this function is never the one that decides `resolved` vs `empty`.
291
+ */
292
+ function mapResolverOutput(body) {
293
+ return {
294
+ action: coerceStringOrNull(body['description']),
295
+ verify: coerceStringOrNull(body['verify']),
296
+ acceptanceCriteria: coerceStringArray(body['acceptance_criteria']),
297
+ readFirst: coerceStringArray(body['read_first']),
298
+ done: coerceStringOrNull(body['done']),
299
+ };
300
+ }
301
+ /**
302
+ * Orchestrate one task's content resolution: split the `tracker-id`, find the
303
+ * matching capability's resolver, invoke it through the bounded subprocess
304
+ * boundary, and map its JSON output onto the four documented outcomes.
305
+ *
306
+ * The only impure boundary is `execFn` (defaults to a real `spawnSync` call).
307
+ * Injecting a fake `execFn` lets tests assert every outcome — including a
308
+ * timeout — deterministically, without spawning a real process or waiting a
309
+ * real `timeoutMs`.
310
+ */
311
+ function resolveTaskContent(input) {
312
+ const split = splitTrackerId(input.trackerId);
313
+ if (split === null)
314
+ return { kind: TASK_CONTENT_RESULT.NOT_APPLICABLE };
315
+ const resolver = findResolver(split.prefix, input.capabilities ?? []);
316
+ if (resolver === null)
317
+ return { kind: TASK_CONTENT_RESULT.NO_RESOLVER };
318
+ if (resolver === 'ambiguous') {
319
+ // findResolver never returns the capability ids for the ambiguous case
320
+ // (it discards the losing matches) — re-derive them here for the error.
321
+ const ids = (input.capabilities ?? [])
322
+ .filter((cap) => {
323
+ const decl = parseResolverDeclaration(cap?.id, cap?.taskContentResolver);
324
+ return decl !== null && decl.trackerPrefix === split.prefix;
325
+ })
326
+ .map((cap) => cap.id);
327
+ throw new ResolverAmbiguousError(split.prefix, ids);
328
+ }
329
+ const invocation = buildInvocation(resolver, split.id);
330
+ const timeoutMs = input.timeoutOverrideMs ?? invocation.timeoutMs;
331
+ const execFn = input.execFn ?? realExec;
332
+ const result = execFn(invocation.binary, invocation.args, { timeout: timeoutMs });
333
+ if (isExecTimeout(result)) {
334
+ throw new ResolverTimeoutError(invocation.binary, timeoutMs);
335
+ }
336
+ if (result.error || result.status !== 0) {
337
+ const stderrTail = (result.stderr ?? '').trim().slice(-2000);
338
+ throw new ResolverFailedError(invocation.binary, result.status, stderrTail);
339
+ }
340
+ let parsed;
341
+ try {
342
+ parsed = JSON.parse(result.stdout);
343
+ }
344
+ catch {
345
+ throw new ResolverMalformedOutputError(invocation.binary, 'stdout is not valid JSON', result.stdout.slice(0, 200));
346
+ }
347
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
348
+ throw new ResolverMalformedOutputError(invocation.binary, `stdout parsed as valid JSON but is not a plain object (got ${Array.isArray(parsed) ? 'array' : typeof parsed})`, result.stdout.slice(0, 200));
349
+ }
350
+ const content = mapResolverOutput(parsed);
351
+ const description = typeof content.action === 'string' ? content.action.trim() : '';
352
+ if (description.length === 0) {
353
+ return { kind: TASK_CONTENT_RESULT.EMPTY };
354
+ }
355
+ return { kind: TASK_CONTENT_RESULT.RESOLVED, content };
356
+ }
357
+ const taskContentResolution = {
358
+ TASK_CONTENT_RESULT,
359
+ splitTrackerId,
360
+ findResolver,
361
+ buildInvocation,
362
+ resolveTaskContent,
363
+ ResolverAmbiguousError,
364
+ ResolverFailedError,
365
+ ResolverTimeoutError,
366
+ ResolverMalformedOutputError,
367
+ };
368
+ module.exports = taskContentResolution;
@@ -31,6 +31,9 @@ exports.cmdTeamsStatus = cmdTeamsStatus;
31
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
32
  const ioMod = require("./io.cjs");
33
33
  const { output: coreOutput } = ioMod;
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const cliExitModule = require("./cli-exit.cjs");
36
+ const { ExitError } = cliExitModule;
34
37
  // ─── Pure core ────────────────────────────────────────────────────────────────
35
38
  /**
36
39
  * Resolve the agent-teams status from injected runtime and env.
@@ -67,7 +70,7 @@ function cmdTeamsStatus(cwd, opts) {
67
70
  const status = resolveTeamsStatus({ runtime, env: process.env });
68
71
  if (opts.active) {
69
72
  // --active mode: no output, exit code encodes the boolean
70
- process.exit(status.active ? 0 : 1);
73
+ throw new ExitError(status.active ? 0 : 1);
71
74
  }
72
75
  // Default: emit JSON to stdout via io.output, exit 0
73
76
  coreOutput(status, false);
@@ -28,6 +28,9 @@ const { readVerificationStatus } = verification;
28
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
29
  const phaseIdMod = require("./phase-id.cjs");
30
30
  const { scopeToPhase } = phaseIdMod;
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const coreUtils = require("./core-utils.cjs");
33
+ const { normalizeLineEndings } = coreUtils;
31
34
  // ─── Blocking state sets (documented for maintainability) ─────────────────────
32
35
  // UAT file frontmatter `status` values that indicate the file is not fully done
33
36
  const BLOCKING_UAT_FM_STATUSES = new Set([
@@ -112,32 +115,62 @@ function analyzeMarkdown(raw) {
112
115
  */
113
116
  function parseUatResultItems(cleanContent) {
114
117
  const items = [];
115
- // Find all ### N. Name headings (line-anchored)
116
- const headingPattern = /^###\s*(\d+)\.\s*(.+)$/gm;
118
+ // Find all ### N. Name headings.
119
+ // #3078-CR MEDIUM (security review follow-up): STRUCTURE and ATTRIBUTION
120
+ // need different split frames. This is a STRUCTURE scan — finding where a
121
+ // heading block begins — and there is no attribution distinction to
122
+ // preserve, so split on any of `\n`, U+2028, U+2029: a heading delimited by
123
+ // an exotic line separator (origin/next's `/m`-anchored scan found these;
124
+ // a naive `split('\n')`-only port silently stopped finding them, making the
125
+ // gate MORE permissive than origin/next) is found exactly like a
126
+ // `\n`-delimited one. Contrast the `result:` scan below, which is an
127
+ // ATTRIBUTION scan and must NOT do this.
128
+ const HEADING_LINE_RE = /^###\s*(\d+)\.\s*(.+)$/;
117
129
  const headings = [];
118
- let hMatch;
119
- while ((hMatch = headingPattern.exec(cleanContent)) !== null) {
120
- headings.push({
121
- index: hMatch.index + hMatch[0].length,
122
- test: parseInt(hMatch[1], 10),
123
- name: hMatch[2].trim(),
124
- });
130
+ {
131
+ // All three separators are exactly one UTF-16 code unit, so the
132
+ // `line.length + 1` offset arithmetic below stays valid regardless of
133
+ // which separator terminated a given line.
134
+ const lines = cleanContent.split(/[\n\u2028\u2029]/);
135
+ let offset = 0;
136
+ for (const line of lines) {
137
+ const hMatch = line.match(HEADING_LINE_RE);
138
+ if (hMatch) {
139
+ headings.push({
140
+ index: offset + hMatch[0].length,
141
+ lineStart: offset,
142
+ test: parseInt(hMatch[1], 10),
143
+ name: hMatch[2].trim(),
144
+ });
145
+ }
146
+ offset += line.length + 1; // +1 for the separator consumed by split
147
+ }
125
148
  }
126
149
  for (let i = 0; i < headings.length; i++) {
127
150
  const h = headings[i];
128
151
  const blockStart = h.index;
129
- // More precise: find next heading's position in the original string
130
- // We'll slice from current heading end to the position just before next heading's "###"
131
- const nextHeadingMatch = i + 1 < headings.length
132
- ? cleanContent.lastIndexOf('\n###', headings[i + 1].index)
133
- : -1;
134
- const blockContent = nextHeadingMatch >= blockStart
135
- ? cleanContent.slice(blockStart, nextHeadingMatch)
152
+ // A block spans until the START of the next heading's line (tracked
153
+ // directly from the same split-frame scan above), not a re-search for a
154
+ // literal '\n###' over unsplit text -- the latter would silently miss a
155
+ // next heading delimited by U+2028/U+2029 instead of '\n' and swallow
156
+ // every subsequent block into this one.
157
+ const blockContent = i + 1 < headings.length
158
+ ? cleanContent.slice(blockStart, headings[i + 1].lineStart)
136
159
  : cleanContent.slice(blockStart);
137
- // Column-0 anchored result line: /^result:[ \t]*\[?([\w-]+)\]?/mi
160
+ // Column-0 result line, split-then-match (#3078-CR MEDIUM — same fix as
161
+ // the heading scan above): test each already-split line individually
162
+ // against a single-line (no `/m` anchor) pattern instead of anchoring
163
+ // over unsplit `blockContent`, so a `result:`-shaped line reachable only
164
+ // via a U+2028/U+2029 separator inside an `expected: |` scalar body can
165
+ // never register as a fake column-0 match. FIRST MATCH WINS — no
166
+ // ambiguity counting, matching src/uat.cts's contract.
138
167
  // Uses [ \t]* (not \s*) so the captured value must sit on the SAME line as result:.
139
168
  // A result: key with the value on a subsequent line yields no match → 'missing' (blocker).
140
- const resultMatch = /^result:[ \t]*\[?([\w-]+)\]?/mi.exec(blockContent);
169
+ const RESULT_LINE_RE = /^result:[ \t]*\[?([\w-]+)\]?/i;
170
+ const resultMatch = blockContent
171
+ .split('\n')
172
+ .map((line) => line.match(RESULT_LINE_RE))
173
+ .find((m) => m !== null) ?? null;
141
174
  if (resultMatch) {
142
175
  items.push({
143
176
  test: h.test,
@@ -205,7 +238,11 @@ function evaluateUatPassed(phaseFullDir, opts) {
205
238
  const uatFilePath = node_path_1.default.join(phaseFullDir, file);
206
239
  let raw = '';
207
240
  try {
208
- raw = node_fs_1.default.readFileSync(uatFilePath, 'utf-8');
241
+ // #3078-CR MEDIUM: normalize line endings at the read boundary — the
242
+ // same seam src/uat.cts and src/verification.cts route through — so a
243
+ // lone-CR *-UAT.md is not read as one unbroken line by the column-0
244
+ // scans below.
245
+ raw = normalizeLineEndings(node_fs_1.default.readFileSync(uatFilePath, 'utf-8'));
209
246
  }
210
247
  catch {
211
248
  blockers.push(`${file}: could not read file`);
@@ -252,7 +289,8 @@ function evaluateUatPassed(phaseFullDir, opts) {
252
289
  const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
253
290
  let raw = '';
254
291
  try {
255
- raw = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
292
+ // #3078-CR MEDIUM: same read-boundary normalization as the UAT loop above.
293
+ raw = normalizeLineEndings(node_fs_1.default.readFileSync(verificationFilePath, 'utf-8'));
256
294
  }
257
295
  catch {
258
296
  blockers.push(`${file}: could not read verification file`);