@opengsd/gsd-core 1.11.0 → 1.13.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 (498) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-debug-session-manager.md +1 -1
  6. package/agents/gsd-debugger.md +1 -1
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +78 -42
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +0 -1
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +3 -1
  15. package/agents/gsd-plan-checker.md +91 -112
  16. package/agents/gsd-planner.md +20 -4
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +82 -7
  21. package/agents/gsd-ui-researcher.md +70 -3
  22. package/agents/gsd-verifier.md +24 -2
  23. package/bin/install.js +847 -200
  24. package/commands/gsd/discuss-phase.md +1 -1
  25. package/commands/gsd/execute-phase.md +1 -1
  26. package/commands/gsd/import.md +1 -1
  27. package/commands/gsd/ns-workflow.md +2 -1
  28. package/commands/gsd/phase.md +1 -1
  29. package/commands/gsd/quick-batch.md +105 -0
  30. package/commands/gsd/quick.md +8 -4
  31. package/commands/gsd/surface.md +18 -8
  32. package/gsd-core/bin/gsd-tools.cjs +761 -100
  33. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  34. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  35. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  36. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  37. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  38. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  39. package/gsd-core/bin/lib/audit.cjs +163 -41
  40. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  41. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  42. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  43. package/gsd-core/bin/lib/capability-registry.cjs +785 -144
  44. package/gsd-core/bin/lib/capability-state.cjs +25 -4
  45. package/gsd-core/bin/lib/capability-validator.cjs +321 -18
  46. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  47. package/gsd-core/bin/lib/check-command-router.cjs +229 -6
  48. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  49. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  50. package/gsd-core/bin/lib/clusters.cjs +1 -0
  51. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  52. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  53. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  54. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  55. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  56. package/gsd-core/bin/lib/commands.cjs +877 -54
  57. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  58. package/gsd-core/bin/lib/config-loader.cjs +121 -29
  59. package/gsd-core/bin/lib/config.cjs +92 -2
  60. package/gsd-core/bin/lib/configuration.cjs +129 -37
  61. package/gsd-core/bin/lib/core-utils.cjs +118 -14
  62. package/gsd-core/bin/lib/decisions.cjs +213 -1
  63. package/gsd-core/bin/lib/edge-probe.cjs +23 -2
  64. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  65. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  66. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  67. package/gsd-core/bin/lib/frontmatter.cjs +975 -326
  68. package/gsd-core/bin/lib/gap-checker.cjs +41 -8
  69. package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  72. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
  73. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  74. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  75. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  76. package/gsd-core/bin/lib/host-integration.cjs +96 -11
  77. package/gsd-core/bin/lib/init-command-router.cjs +132 -21
  78. package/gsd-core/bin/lib/init.cjs +252 -56
  79. package/gsd-core/bin/lib/install-engine.cjs +252 -15
  80. package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
  81. package/gsd-core/bin/lib/install-profiles.cjs +100 -18
  82. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  83. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  84. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  85. package/gsd-core/bin/lib/intel.cjs +101 -26
  86. package/gsd-core/bin/lib/io.cjs +195 -15
  87. package/gsd-core/bin/lib/learnings.cjs +85 -14
  88. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  89. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  90. package/gsd-core/bin/lib/markdown-table.cjs +175 -4
  91. package/gsd-core/bin/lib/milestone.cjs +112 -7
  92. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  93. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  94. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  95. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  96. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  97. package/gsd-core/bin/lib/phase-id.cjs +321 -13
  98. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  99. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  100. package/gsd-core/bin/lib/phase.cjs +1175 -115
  101. package/gsd-core/bin/lib/plan-document.cjs +273 -0
  102. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  103. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  104. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  105. package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
  106. package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
  107. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  108. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  109. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  110. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  111. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  112. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  113. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  114. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  115. package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
  116. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  117. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  118. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  119. package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
  120. package/gsd-core/bin/lib/roadmap.cjs +248 -64
  121. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
  122. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  123. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
  124. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
  125. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  126. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  127. package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
  128. package/gsd-core/bin/lib/smart-entry.cjs +19 -31
  129. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  130. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  131. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  132. package/gsd-core/bin/lib/state-document.cjs +216 -5
  133. package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
  134. package/gsd-core/bin/lib/state-transition.cjs +850 -145
  135. package/gsd-core/bin/lib/state.cjs +1629 -287
  136. package/gsd-core/bin/lib/surface.cjs +33 -10
  137. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  138. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  139. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  140. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  141. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  142. package/gsd-core/bin/lib/uat.cjs +2542 -387
  143. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  144. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  145. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  146. package/gsd-core/bin/lib/update-context.cjs +6 -2
  147. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  148. package/gsd-core/bin/lib/validate.cjs +230 -12
  149. package/gsd-core/bin/lib/vendor/README.md +43 -5
  150. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  151. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  152. package/gsd-core/bin/lib/verification.cjs +287 -13
  153. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  154. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  155. package/gsd-core/bin/lib/verify.cjs +441 -56
  156. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  157. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  158. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  159. package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
  160. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  161. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  162. package/gsd-core/bin/shared/exit-codes.json +8 -0
  163. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  164. package/gsd-core/bin/shared/model-catalog.json +8 -1
  165. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  166. package/gsd-core/references/agent-contracts.md +6 -5
  167. package/gsd-core/references/api-coverage.md +24 -2
  168. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  169. package/gsd-core/references/checkpoints.md +37 -19
  170. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  171. package/gsd-core/references/edge-probe.md +17 -5
  172. package/gsd-core/references/execute-mvp-tdd.md +18 -18
  173. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  174. package/gsd-core/references/execute-phase-response-language.md +6 -0
  175. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  176. package/gsd-core/references/executor-examples.md +42 -0
  177. package/gsd-core/references/failing-direction.md +78 -0
  178. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  179. package/gsd-core/references/gate-prompts.md +1 -1
  180. package/gsd-core/references/git-integration.md +5 -5
  181. package/gsd-core/references/git-planning-commit.md +3 -3
  182. package/gsd-core/references/gsd-run-resolver.md +1 -1
  183. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  184. package/gsd-core/references/model-profiles.md +1 -1
  185. package/gsd-core/references/mvp-concepts.md +2 -2
  186. package/gsd-core/references/nyquist-compliance.md +74 -0
  187. package/gsd-core/references/offer-next.md +3 -5
  188. package/gsd-core/references/phase-argument-parsing.md +3 -3
  189. package/gsd-core/references/plan-checker-examples.md +41 -0
  190. package/gsd-core/references/planner-antipatterns.md +25 -0
  191. package/gsd-core/references/planner-chunked.md +5 -1
  192. package/gsd-core/references/planner-coupling.md +42 -0
  193. package/gsd-core/references/planner-failing-direction.md +53 -0
  194. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  195. package/gsd-core/references/planner-quick-batch.md +71 -0
  196. package/gsd-core/references/planner-reviews.md +47 -0
  197. package/gsd-core/references/planner-revision.md +76 -3
  198. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  199. package/gsd-core/references/planning-config.md +39 -9
  200. package/gsd-core/references/response-language-directive.md +9 -0
  201. package/gsd-core/references/reviewer-instances.md +31 -0
  202. package/gsd-core/references/revision-loop.md +118 -11
  203. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  204. package/gsd-core/references/tdd.md +15 -12
  205. package/gsd-core/references/ui-brand.md +65 -21
  206. package/gsd-core/references/ui-consideration-probe.md +1 -1
  207. package/gsd-core/references/universal-anti-patterns.md +2 -2
  208. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  209. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  210. package/gsd-core/references/verify-mvp-mode.md +1 -1
  211. package/gsd-core/references/workstream-flag.md +11 -11
  212. package/gsd-core/templates/README.md +1 -1
  213. package/gsd-core/templates/SECURITY.md +3 -3
  214. package/gsd-core/templates/UI-SPEC.md +25 -3
  215. package/gsd-core/templates/VALIDATION.md +3 -3
  216. package/gsd-core/templates/phase-prompt.md +7 -0
  217. package/gsd-core/templates/state.md +7 -0
  218. package/gsd-core/templates/verification-report.md +5 -0
  219. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  220. package/gsd-core/workflows/add-backlog.md +3 -1
  221. package/gsd-core/workflows/add-phase.md +5 -3
  222. package/gsd-core/workflows/add-tests.md +4 -9
  223. package/gsd-core/workflows/add-todo.md +2 -2
  224. package/gsd-core/workflows/ai-integration-phase.md +5 -10
  225. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  226. package/gsd-core/workflows/audit-fix.md +14 -3
  227. package/gsd-core/workflows/audit-milestone.md +11 -9
  228. package/gsd-core/workflows/audit-uat.md +19 -2
  229. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  230. package/gsd-core/workflows/autonomous.md +12 -26
  231. package/gsd-core/workflows/check-todos.md +2 -2
  232. package/gsd-core/workflows/cleanup.md +3 -3
  233. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
  234. package/gsd-core/workflows/code-review-fix.md +3 -1
  235. package/gsd-core/workflows/code-review.md +192 -69
  236. package/gsd-core/workflows/complete-milestone.md +28 -14
  237. package/gsd-core/workflows/debug.md +6 -4
  238. package/gsd-core/workflows/diagnose-issues.md +17 -7
  239. package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
  240. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  241. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  242. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  243. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  244. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
  245. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  246. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  247. package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
  248. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  249. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  250. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
  252. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  253. package/gsd-core/workflows/discuss-phase.md +2 -2
  254. package/gsd-core/workflows/do.md +46 -19
  255. package/gsd-core/workflows/docs-update.md +6 -5
  256. package/gsd-core/workflows/edit-phase.md +3 -1
  257. package/gsd-core/workflows/eval-review.md +5 -10
  258. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
  259. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
  260. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  261. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  263. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
  264. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  265. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  266. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
  267. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  268. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  269. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  270. package/gsd-core/workflows/execute-phase.md +68 -66
  271. package/gsd-core/workflows/execute-plan.md +25 -20
  272. package/gsd-core/workflows/explore.md +3 -1
  273. package/gsd-core/workflows/extract-learnings.md +3 -1
  274. package/gsd-core/workflows/fast.md +8 -2
  275. package/gsd-core/workflows/forensics.md +3 -1
  276. package/gsd-core/workflows/graduation.md +6 -6
  277. package/gsd-core/workflows/health.md +4 -7
  278. package/gsd-core/workflows/help/modes/brief.md +2 -0
  279. package/gsd-core/workflows/help/modes/default.md +2 -0
  280. package/gsd-core/workflows/help/modes/full.md +12 -0
  281. package/gsd-core/workflows/help/modes/topic.md +2 -0
  282. package/gsd-core/workflows/help.md +2 -0
  283. package/gsd-core/workflows/import.md +17 -14
  284. package/gsd-core/workflows/inbox.md +5 -6
  285. package/gsd-core/workflows/ingest-docs.md +45 -12
  286. package/gsd-core/workflows/insert-phase.md +7 -5
  287. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  288. package/gsd-core/workflows/list-seeds.md +7 -3
  289. package/gsd-core/workflows/list-workspaces.md +3 -1
  290. package/gsd-core/workflows/manager.md +15 -26
  291. package/gsd-core/workflows/map-codebase.md +3 -1
  292. package/gsd-core/workflows/milestone-summary.md +3 -1
  293. package/gsd-core/workflows/mvp-phase.md +3 -3
  294. package/gsd-core/workflows/new-milestone.md +10 -22
  295. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  296. package/gsd-core/workflows/new-project.md +17 -29
  297. package/gsd-core/workflows/new-workspace.md +2 -2
  298. package/gsd-core/workflows/next.md +4 -2
  299. package/gsd-core/workflows/node-repair.md +2 -0
  300. package/gsd-core/workflows/note.md +2 -0
  301. package/gsd-core/workflows/onboard.md +1 -1
  302. package/gsd-core/workflows/pause-work.md +20 -5
  303. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  304. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  305. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
  306. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
  307. package/gsd-core/workflows/plan-phase.md +251 -54
  308. package/gsd-core/workflows/plan-review-convergence.md +148 -19
  309. package/gsd-core/workflows/plant-seed.md +3 -3
  310. package/gsd-core/workflows/pr-branch.md +195 -51
  311. package/gsd-core/workflows/profile-user.md +17 -15
  312. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  313. package/gsd-core/workflows/progress.md +52 -15
  314. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  315. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
  316. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  317. package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
  318. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  319. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  320. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  321. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  322. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  323. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  324. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  325. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  326. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  327. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  328. package/gsd-core/workflows/quick-batch.md +203 -0
  329. package/gsd-core/workflows/quick.md +33 -32
  330. package/gsd-core/workflows/reapply-patches.md +2 -0
  331. package/gsd-core/workflows/remove-phase.md +6 -4
  332. package/gsd-core/workflows/remove-workspace.md +3 -3
  333. package/gsd-core/workflows/resume-project.md +14 -14
  334. package/gsd-core/workflows/review.md +404 -21
  335. package/gsd-core/workflows/scan.md +3 -1
  336. package/gsd-core/workflows/section-manifest.json +12 -0
  337. package/gsd-core/workflows/secure-phase.md +3 -3
  338. package/gsd-core/workflows/session-report.md +2 -0
  339. package/gsd-core/workflows/settings-advanced.md +9 -9
  340. package/gsd-core/workflows/settings-integrations.md +66 -32
  341. package/gsd-core/workflows/settings.md +4 -6
  342. package/gsd-core/workflows/ship.md +22 -16
  343. package/gsd-core/workflows/sketch-wrap-up.md +13 -17
  344. package/gsd-core/workflows/sketch.md +13 -19
  345. package/gsd-core/workflows/smart-entry.md +4 -6
  346. package/gsd-core/workflows/spec-phase.md +31 -4
  347. package/gsd-core/workflows/spike-wrap-up.md +9 -11
  348. package/gsd-core/workflows/spike.md +21 -32
  349. package/gsd-core/workflows/stats.md +4 -2
  350. package/gsd-core/workflows/sync-skills.md +13 -5
  351. package/gsd-core/workflows/thread.md +13 -7
  352. package/gsd-core/workflows/transition.md +7 -5
  353. package/gsd-core/workflows/ui-phase.md +36 -21
  354. package/gsd-core/workflows/ui-review.md +7 -11
  355. package/gsd-core/workflows/ultraplan-phase.md +7 -13
  356. package/gsd-core/workflows/undo.md +9 -17
  357. package/gsd-core/workflows/update.md +47 -48
  358. package/gsd-core/workflows/validate-phase.md +3 -3
  359. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  360. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  361. package/gsd-core/workflows/verify-work.md +106 -21
  362. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  363. package/hooks/dist/gsd-check-update-worker.js +19 -2
  364. package/hooks/dist/gsd-config-reload.js +18 -12
  365. package/hooks/dist/gsd-context-monitor.js +302 -22
  366. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  367. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  368. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  369. package/hooks/dist/gsd-cursor-stop.js +2 -1
  370. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  371. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  372. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  373. package/hooks/dist/gsd-graphify-update.sh +22 -18
  374. package/hooks/dist/gsd-node-runner.sh +77 -0
  375. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  376. package/hooks/dist/gsd-prompt-guard.js +46 -12
  377. package/hooks/dist/gsd-read-guard.js +18 -7
  378. package/hooks/dist/gsd-read-injection-scanner.js +22 -13
  379. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  380. package/hooks/dist/gsd-session-state.sh +1 -0
  381. package/hooks/dist/gsd-statusline.js +222 -29
  382. package/hooks/dist/gsd-validate-commit.sh +523 -12
  383. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  384. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  385. package/hooks/dist/gsd-workflow-guard.js +36 -17
  386. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  387. package/hooks/dist/gsd-write-guard.js +35 -25
  388. package/hooks/dist/lib/cli-exit.js +560 -0
  389. package/hooks/dist/lib/exit-code-registry.js +98 -0
  390. package/hooks/dist/lib/git-cmd.js +210 -1
  391. package/hooks/dist/lib/git-probe.js +84 -0
  392. package/hooks/dist/lib/hook-exit.js +81 -0
  393. package/hooks/dist/lib/injection-patterns.js +36 -6
  394. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  395. package/hooks/gsd-agent-isolation-guard.js +77 -38
  396. package/hooks/gsd-check-update-worker.js +19 -2
  397. package/hooks/gsd-config-reload.js +18 -12
  398. package/hooks/gsd-context-monitor.js +302 -22
  399. package/hooks/gsd-cursor-post-tool.js +3 -1
  400. package/hooks/gsd-cursor-pre-tool.js +3 -1
  401. package/hooks/gsd-cursor-session-start.js +2 -1
  402. package/hooks/gsd-cursor-stop.js +2 -1
  403. package/hooks/gsd-cursor-subagent-start.js +28 -23
  404. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  405. package/hooks/gsd-ensure-canonical-path.js +2 -1
  406. package/hooks/gsd-graphify-update.sh +22 -18
  407. package/hooks/gsd-node-runner.sh +77 -0
  408. package/hooks/gsd-phase-boundary.sh +1 -0
  409. package/hooks/gsd-prompt-guard.js +46 -12
  410. package/hooks/gsd-read-guard.js +18 -7
  411. package/hooks/gsd-read-injection-scanner.js +22 -13
  412. package/hooks/gsd-secret-read-guard.js +1079 -0
  413. package/hooks/gsd-session-state.sh +1 -0
  414. package/hooks/gsd-statusline.js +222 -29
  415. package/hooks/gsd-validate-commit.sh +523 -12
  416. package/hooks/gsd-windsurf-pre-command.js +16 -11
  417. package/hooks/gsd-windsurf-pre-write.js +22 -13
  418. package/hooks/gsd-workflow-guard.js +36 -17
  419. package/hooks/gsd-worktree-path-guard.js +36 -21
  420. package/hooks/gsd-write-guard.js +35 -25
  421. package/hooks/hooks.json +6 -0
  422. package/hooks/lib/cli-exit.js +560 -0
  423. package/hooks/lib/exit-code-registry.js +98 -0
  424. package/hooks/lib/git-cmd.js +210 -1
  425. package/hooks/lib/git-probe.js +84 -0
  426. package/hooks/lib/hook-exit.js +81 -0
  427. package/hooks/lib/injection-patterns.js +36 -6
  428. package/hooks/managed-hooks-registry.cjs +4 -0
  429. package/package.json +14 -9
  430. package/scripts/base64-scan.sh +74 -12
  431. package/scripts/build-hooks.js +12 -0
  432. package/scripts/check-glossary-refs.cjs +77 -15
  433. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  434. package/scripts/ci-check-job-near-cap.cjs +49 -0
  435. package/scripts/ci-pr-mergeability.cjs +262 -0
  436. package/scripts/ci-test-scope.cjs +52 -12
  437. package/scripts/ci-timeout-report.cjs +230 -0
  438. package/scripts/docs-guard-registry.cjs +406 -0
  439. package/scripts/gen-capability-registry.cjs +8 -6
  440. package/scripts/gen-exit-code-docs.cjs +318 -0
  441. package/scripts/gen-exit-code-registry.cjs +891 -0
  442. package/scripts/gen-features.cjs +836 -0
  443. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  444. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  445. package/scripts/gen-loop-host-contract.cjs +189 -4
  446. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  447. package/scripts/gen-state-md-docs.cjs +727 -0
  448. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  449. package/scripts/lib/ci-job-timing.cjs +72 -0
  450. package/scripts/lib/cli-exit.cjs +546 -44
  451. package/scripts/lib/drift-scan.cjs +32 -2
  452. package/scripts/lib/exit-code-registry.cjs +98 -0
  453. package/scripts/lib/ndjson-reporter.cjs +119 -0
  454. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  455. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  456. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  457. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  458. package/scripts/lint-docs-guard-registration.cjs +495 -0
  459. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
  460. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  461. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  462. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  463. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  464. package/scripts/lint-phase-enumeration-drift.cjs +45 -14
  465. package/scripts/lint-phase-id-drift.cjs +133 -8
  466. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  467. package/scripts/lint-portable-grep.cjs +176 -0
  468. package/scripts/lint-removed-but-needed.cjs +184 -16
  469. package/scripts/lint-response-language-coverage.cjs +524 -0
  470. package/scripts/lint-seam-enforcement.cjs +182 -0
  471. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  472. package/scripts/lint-source-test-name-collision.cjs +241 -0
  473. package/scripts/lint-state-write-path-drift.cjs +337 -432
  474. package/scripts/lint-test-file-count.allowlist.json +124 -4
  475. package/scripts/lint-test-file-count.cjs +25 -3
  476. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  477. package/scripts/lint-vendored-deps.cjs +208 -35
  478. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  479. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  480. package/scripts/mutation-matrix.cjs +599 -50
  481. package/scripts/npm-audit-baseline.cjs +376 -0
  482. package/scripts/prompt-injection-scan.sh +83 -14
  483. package/scripts/require-issue-link-policy.cjs +16 -1
  484. package/scripts/secret-scan.sh +75 -13
  485. package/scripts/select-docs-guards.cjs +56 -0
  486. package/scripts/sync-runtime-launcher.cjs +22 -3
  487. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  488. package/skills/gsd-execute-phase/SKILL.md +1 -1
  489. package/skills/gsd-import/SKILL.md +1 -1
  490. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  491. package/skills/gsd-phase/SKILL.md +1 -1
  492. package/skills/gsd-quick/SKILL.md +8 -4
  493. package/skills/gsd-quick-batch/SKILL.md +105 -0
  494. package/skills/gsd-surface/SKILL.md +18 -8
  495. package/vscode/package.json +1 -1
  496. package/bin/lib/ui-safety-gate.cjs +0 -109
  497. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  498. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -29,7 +29,7 @@ const roadmapParserMod = require("./roadmap-parser.cjs");
29
29
  const { getMilestoneInfo, extractCurrentMilestone } = roadmapParserMod;
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const phaseLocatorMod = require("./phase-locator.cjs");
32
- const { listMilestonePhaseDirs } = phaseLocatorMod;
32
+ const { listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const verificationMod = require("./verification.cjs");
35
35
  const { isPhaseComplete } = verificationMod;
@@ -37,7 +37,11 @@ const { isPhaseComplete } = verificationMod;
37
37
  const scanPhasePlans = require("./plan-scan.cjs");
38
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
39
  const planningWorkspace = require("./planning-workspace.cjs");
40
- const { planningPaths, planningRoot } = planningWorkspace;
40
+ // #612: `resolvePhaseIdConvention` is the federated (workstream -> root)
41
+ // `phase_id_convention` reader, from the same §7 owner module `planningPaths`
42
+ // comes from. Resolved once in `buildPlanningSnapshot` — see the
43
+ // `phaseIdConvention` field's comment for why one resolution point matters.
44
+ const { planningPaths, planningRoot, resolvePhaseIdConvention } = planningWorkspace;
41
45
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
42
46
  // eslint-disable-next-line @typescript-eslint/no-require-imports
43
47
  const frontmatterMod = require("./frontmatter.cjs");
@@ -64,7 +68,15 @@ const configLoaderMod = require("./config-loader.cjs");
64
68
  const { isGitIgnored } = configLoaderMod;
65
69
  // eslint-disable-next-line @typescript-eslint/no-require-imports
66
70
  const phaseIdMod = require("./phase-id.cjs");
67
- const { PHASE_NUMBER_TOKEN_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, stripProjectCodePrefix, scopeToPhase } = phaseIdMod;
71
+ // #612: `phaseHeadingPrefixSrcFor`/`PHASE_HEADING_BASELINE` SELECT a heading
72
+ // intro by convention (a convention-less call compiles the byte-identical base
73
+ // source the literal it replaced spelled); `isSentinelPhaseId` gets its bracket
74
+ // reading only when handed the convention explicitly.
75
+ const { PHASE_NUMBER_TOKEN_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, stripProjectCodePrefix, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, isSentinelPhaseId, scopeToPhase, } = phaseIdMod;
76
+ // #612: `phaseTokenFromDir` is the convention-SELECTED counterpart of
77
+ // `PHASE_TOKEN_FROM_DIR_RE` — handed no convention it delegates to that very
78
+ // regex, so a legacy repo's tokenization is unchanged. `checkBracketCoherence`
79
+ // re-homed into `validate.cts` when #3309 deleted its `verify.cts` neighbours.
68
80
  const validate_cjs_1 = require("./validate.cjs");
69
81
  // ─── worstScope — the one new piece of coordination logic ───────────────────
70
82
  /**
@@ -462,18 +474,31 @@ function buildProjectSectionsField(cwd) {
462
474
  * only the mismatches" to "record every attribution" — this field exposes
463
475
  * the parsed fact; the future W021/W026 rules make the mismatch judgment.
464
476
  */
465
- function buildRoadmapDeclaredPhasesField(roadmapPath) {
477
+ function buildRoadmapDeclaredPhasesField(roadmapPath, convention) {
466
478
  if (!node_fs_1.default.existsSync(roadmapPath)) {
467
- return { value: [], scope: SCOPE.UNREADABLE };
479
+ return {
480
+ declared: { value: [], scope: SCOPE.UNREADABLE },
481
+ sentinelTokens: { value: [], scope: SCOPE.UNREADABLE },
482
+ };
468
483
  }
469
484
  let content;
470
485
  try {
471
486
  content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
472
487
  }
473
488
  catch {
474
- return { value: [], scope: SCOPE.UNREADABLE };
489
+ return {
490
+ declared: { value: [], scope: SCOPE.UNREADABLE },
491
+ sentinelTokens: { value: [], scope: SCOPE.UNREADABLE },
492
+ };
475
493
  }
476
- const { roadmapPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(content);
494
+ // #612: the declared-phase scan is SELECTED by the resolved convention — a
495
+ // non-bracket repo compiles the byte-identical pattern sources this call
496
+ // compiled before, so its declared set is unchanged. `sentinelPhases` is the
497
+ // same call's third output (empty off the bracket convention) and is surfaced
498
+ // rather than filtered in place: `roadmapPhases` feeds both a membership check
499
+ // (W002's valid-phase set) and a missing-directory warning (W006), and only
500
+ // the latter should ignore an icebox item.
501
+ const { roadmapPhases, sentinelPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(content, convention);
477
502
  const milestoneByPhase = new Map();
478
503
  const sectionRx = /^#{1,3}\s+(?:\[[^\]]{1,200}\]\s*)?.*v(\d+\.\d+)/gim;
479
504
  const sections = [];
@@ -497,7 +522,10 @@ function buildRoadmapDeclaredPhasesField(roadmapPath) {
497
522
  phaseId,
498
523
  milestone: milestoneByPhase.get(phaseId) ?? null,
499
524
  }));
500
- return { value, scope: SCOPE.COMPLETE };
525
+ return {
526
+ declared: { value, scope: SCOPE.COMPLETE },
527
+ sentinelTokens: { value: [...sentinelPhases], scope: SCOPE.COMPLETE },
528
+ };
501
529
  }
502
530
  /**
503
531
  * Resolve `roadmapPhaseCheckboxes` — parsed `[x]`/`[ ]` checkbox state per
@@ -518,7 +546,7 @@ function buildRoadmapDeclaredPhasesField(roadmapPath) {
518
546
  * diagnostic (W011) whose entire purpose is flagging when the two DISAGREE —
519
547
  * reading the data is not re-litigating who is authoritative.
520
548
  */
521
- function buildRoadmapPhaseCheckboxesField(roadmapPath) {
549
+ function buildRoadmapPhaseCheckboxesField(roadmapPath, convention) {
522
550
  if (!node_fs_1.default.existsSync(roadmapPath)) {
523
551
  return { value: {}, scope: SCOPE.UNREADABLE };
524
552
  }
@@ -529,7 +557,15 @@ function buildRoadmapPhaseCheckboxesField(roadmapPath) {
529
557
  catch {
530
558
  return { value: {}, scope: SCOPE.UNREADABLE };
531
559
  }
532
- const checkboxRe = new RegExp(`-\\s*\\[([xX ])\\].*?Phase\\s+0*(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
560
+ // #612: the `Phase\s+` label intro is SELECTED, exactly as
561
+ // `buildNotStartedPhaseVariants` (`validate.cts`) selects it for the same
562
+ // ROADMAP checklist shape — this field is what W006's not-started exclusion
563
+ // now reads instead of that helper, so the two must recognize the same
564
+ // checklist lines or a bracket repo's `- [ ] **[GSD.02] 05: Name**` entries
565
+ // vanish from the exclusion set and every unstarted bracket phase gains a
566
+ // W006. NON-capturing (`capturing` defaults false), so the phase token stays
567
+ // group 2 and the legacy repo compiles a byte-identical source.
568
+ const checkboxRe = new RegExp(`-\\s*\\[([xX ])\\].*?${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention)}0*(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
533
569
  const value = {};
534
570
  let m;
535
571
  while ((m = checkboxRe.exec(content)) !== null) {
@@ -537,6 +573,42 @@ function buildRoadmapPhaseCheckboxesField(roadmapPath) {
537
573
  }
538
574
  return { value, scope: SCOPE.COMPLETE };
539
575
  }
576
+ /**
577
+ * Resolve `roadmapBracketIncoherences` — W021's bracket half (#612). Delegates
578
+ * wholly to `checkBracketCoherence` (`validate.cjs`), which is pure and owns
579
+ * both sub-checks; this builder only supplies the ROADMAP text and the
580
+ * convention gate.
581
+ *
582
+ * GATED, not merely filtered downstream: off the bracket convention the ROADMAP
583
+ * is never parsed for this at all and the field is a `COMPLETE`-scoped empty
584
+ * list. Inferring 'bracket' from the SHAPE of a matched heading would run a
585
+ * repo-failing check against a repo that never opted in — a legacy ROADMAP
586
+ * containing `### [RFC.2119] 5:` is legal legacy content, and that is the exact
587
+ * regression PR-2's round 1 killed the original ungated design over.
588
+ */
589
+ function buildRoadmapBracketIncoherencesField(roadmapPath, convention) {
590
+ // File-readability is decided FIRST, so `scope` means the same thing on every
591
+ // repo: UNREADABLE iff ROADMAP.md could not be read, never "this convention
592
+ // was skipped." Ordering the convention gate first would have made an absent
593
+ // ROADMAP.md report COMPLETE on a legacy repo and UNREADABLE on a bracket one
594
+ // — the same "empty, nothing to say" state wearing two different scopes, which
595
+ // is precisely the non-answer/answer distinction ADR-3180 §8.1 gives `scope`
596
+ // to carry.
597
+ if (!node_fs_1.default.existsSync(roadmapPath))
598
+ return { value: [], scope: SCOPE.UNREADABLE };
599
+ // A non-bracket repo has no bracket incoherences BY DEFINITION — a real,
600
+ // COMPLETE answer, not a skipped read.
601
+ if (convention !== 'bracket')
602
+ return { value: [], scope: SCOPE.COMPLETE };
603
+ let content;
604
+ try {
605
+ content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
606
+ }
607
+ catch {
608
+ return { value: [], scope: SCOPE.UNREADABLE };
609
+ }
610
+ return { value: (0, validate_cjs_1.checkBracketCoherence)(content), scope: SCOPE.COMPLETE };
611
+ }
540
612
  /**
541
613
  * Resolve `researchValidationStatus` — per phase directory, whether its
542
614
  * `*-RESEARCH.md` contains the literal heading `## Validation Architecture`,
@@ -666,21 +738,25 @@ function buildPlanningRootFilesField(cwd) {
666
738
  * `phaseDirs` cannot). An absent `phases/` root is a real empty, not a
667
739
  * failure (mirrors `listMilestonePhaseDirs`'s own treatment); a present but
668
740
  * unreadable root degrades to `UNREADABLE` with an empty list.
741
+ *
742
+ * #3882 (ADR-3473 §8.3): delegates the actual disk scan to
743
+ * `listAllPhaseDirs(phasesDir, {includeSentinels: true})` — that function is
744
+ * the sole owner of "readdirSync the phases/ root, map to dir names, handle
745
+ * absent-vs-unreadable"; this field is one more consumer of that scan, not a
746
+ * second implementation of it. The two functions previously duplicated the
747
+ * same readdirSync + filter + map + absent/unreadable handling, which is
748
+ * exactly the defect class ADR-3473 §8.3 forbids.
749
+ *
750
+ * The RE-SORT below is deliberate, not leftover duplication:
751
+ * `listAllPhaseDirs` orders its `value` by `comparePhaseNum` (numeric phase
752
+ * order — its own documented contract), but `allPhaseDirNames`'s existing,
753
+ * externally-observable order is plain lexicographic `.sort()`, and W007's
754
+ * consumers depend on that order today. Re-sorting here preserves that
755
+ * contract without forking the underlying scan.
669
756
  */
670
757
  function buildAllPhaseDirNamesField(phasesDir) {
671
- if (!node_fs_1.default.existsSync(phasesDir))
672
- return { value: [], scope: SCOPE.COMPLETE };
673
- try {
674
- const value = node_fs_1.default
675
- .readdirSync(phasesDir, { withFileTypes: true })
676
- .filter((e) => e.isDirectory())
677
- .map((e) => e.name)
678
- .sort();
679
- return { value, scope: SCOPE.COMPLETE };
680
- }
681
- catch {
682
- return { value: [], scope: SCOPE.UNREADABLE };
683
- }
758
+ const { value, scope } = listAllPhaseDirs(phasesDir, { includeSentinels: true });
759
+ return { value: value.slice().sort(), scope };
684
760
  }
685
761
  /**
686
762
  * Resolve `archivedPhaseTokens` — every phase-number token belonging to a
@@ -695,7 +771,7 @@ function buildAllPhaseDirNamesField(phasesDir) {
695
771
  * present-but-unreadable per-archive-dir entry is silently skipped, mirroring
696
772
  * `forEachArchivedPhaseToken`'s own per-directory `catch { /* absent/unreadable *\/ }`.
697
773
  */
698
- function buildArchivedPhaseTokensField(planBase) {
774
+ function buildArchivedPhaseTokensField(planBase, convention) {
699
775
  const milestonesDir = node_path_1.default.join(planBase, 'milestones');
700
776
  let archiveDirs;
701
777
  try {
@@ -716,9 +792,19 @@ function buildArchivedPhaseTokensField(planBase) {
716
792
  for (const e of entries) {
717
793
  if (!e.isDirectory())
718
794
  continue;
719
- const m = e.name.match(validate_cjs_1.PHASE_TOKEN_FROM_DIR_RE);
720
- if (m)
721
- value.push(stripProjectCodePrefix(m[1]));
795
+ // #612: composed, not chosen. The convention-aware extractor decides
796
+ // WHICH directory shapes are recognized (so an archived
797
+ // `{CODE}.{MM}-{PP}-slug` is seen at all — `PHASE_TOKEN_FROM_DIR_RE`
798
+ // rejects it outright, which is why every archived bracket phase used
799
+ // to still draw a W006/W002); `stripProjectCodePrefix` then normalizes
800
+ // the token it returns. The strip is a no-op on every bracket token
801
+ // (`01`, `01.02` — the `{CODE}.{MM}` prefix is not part of the token)
802
+ // and does the #2528 work on legacy ones (`MEM-05` -> `05`), so neither
803
+ // side loses its case. Handed no convention, `phaseTokenFromDir`
804
+ // delegates to `PHASE_TOKEN_FROM_DIR_RE` itself — legacy is unchanged.
805
+ const token = (0, validate_cjs_1.phaseTokenFromDir)(e.name, convention);
806
+ if (token)
807
+ value.push(stripProjectCodePrefix(token));
722
808
  }
723
809
  }
724
810
  catch {
@@ -736,7 +822,7 @@ function buildArchivedPhaseTokensField(planBase) {
736
822
  * ROADMAP.md degrades to an empty list, mirroring every other
737
823
  * ROADMAP-sourced field's absent-file handling.
738
824
  */
739
- function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath) {
825
+ function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath, convention) {
740
826
  if (!node_fs_1.default.existsSync(roadmapPath))
741
827
  return { value: [], scope: SCOPE.UNREADABLE };
742
828
  let content;
@@ -749,8 +835,32 @@ function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath) {
749
835
  const scoped = extractCurrentMilestone(content, cwd);
750
836
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal
751
837
  // mirror of OPTIONAL_PHASE_TAG_SOURCE) — verbatim from `verify.cts:2366`.
752
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
753
- const value = [...scoped.matchAll(phasePattern)].map((m) => m[1]);
838
+ //
839
+ // #612: this scan is convention-AGNOSTIC in POSTURE — W026 is ungated, and
840
+ // bug-557 pins it with an empty config so it fires on every repo — but its
841
+ // heading grammar is still SELECTED, never widened. Under bracket the intro
842
+ // CAPTURES, so the phase token moves to group 2 and `bracketGroup` carries
843
+ // that offset; off bracket the source is byte-identical to the literal above
844
+ // and `bracketGroup` is 0. Inferring the convention from a matched bracket's
845
+ // shape would run a repo-failing check against a repo that never opted in.
846
+ const bracketGroup = convention === 'bracket' ? 1 : 0;
847
+ const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention, Boolean(bracketGroup))}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
848
+ const value = [];
849
+ for (const m of scoped.matchAll(phasePattern)) {
850
+ const bracketId = bracketGroup ? m[1] : undefined;
851
+ const phaseNum = m[1 + bracketGroup];
852
+ // A bracket sentinel is an ICEBOX item, not an unstarted phase — it
853
+ // legitimately has no directory, so leaving it in this list makes W026
854
+ // ("STATE says milestone complete but ROADMAP lists an unstarted phase")
855
+ // fire on every bracket repo that keeps an icebox. Filtered here rather
856
+ // than in `RULE_W026` because the bracket id is only visible at the match:
857
+ // the emitted token is `07`, and sentinel-ness lives in the `[GSD.999]`
858
+ // milestone this scan just discarded. This field's only consumer is W026
859
+ // (see its own doc comment above).
860
+ if (bracketId && isSentinelPhaseId(`${bracketId}-${phaseNum}`, 'bracket'))
861
+ continue;
862
+ value.push(phaseNum);
863
+ }
754
864
  return { value, scope: SCOPE.COMPLETE };
755
865
  }
756
866
  /**
@@ -849,11 +959,29 @@ function buildPerPhasePlanScanFields(phasesDir, phaseDirNames, enumerationScope)
849
959
  */
850
960
  function buildPlanningSnapshot(cwd) {
851
961
  const paths = planningPaths(cwd);
962
+ // #612: ONE federated (workstream -> root) resolution for the whole snapshot.
963
+ // See the `phaseIdConvention` field's comment for why one resolution point is
964
+ // load-bearing rather than a micro-optimisation.
965
+ const phaseIdConvention = resolvePhaseIdConvention(cwd) ?? null;
852
966
  const milestone = getMilestoneInfo(cwd);
967
+ // #612: deliberately LEFT to `listMilestonePhaseDirs`'s own lazy resolve —
968
+ // this call is byte-identical to upstream's.
969
+ //
970
+ // Passing `phaseIdConvention` here would NOT be a no-op, which is exactly why
971
+ // it is not passed. The lazy path resolves `resolvePhaseIdConvention(cwd, ws)`
972
+ // with this call's `ws`, which defaults to `null` — the PROJECT-only reading,
973
+ // with no root fallback. The field above is resolved with `ws` undefined,
974
+ // i.e. the FEDERATED workstream -> root reading. On a workstream repo whose
975
+ // root opts into bracket while the workstream config does not, the two answers
976
+ // genuinely differ, and substituting one for the other would silently re-scope
977
+ // `phaseDirs` — a change this PR does not need and no test covers. The
978
+ // federation guarantee PR-2 exists to deliver is delivered where it is
979
+ // observable: in the rules that read `snapshot.phaseIdConvention`.
853
980
  const phaseDirs = listMilestonePhaseDirs(paths.phases, { cwd });
854
981
  const phasesValue = phaseDirs.value.map((dir) => buildPhaseSnapshot(paths.phases, dir));
855
982
  const stateFields = buildStateFields(paths.state);
856
983
  const allPhaseDirNames = buildAllPhaseDirNamesField(paths.phases);
984
+ const roadmapDeclared = buildRoadmapDeclaredPhasesField(paths.roadmap, phaseIdConvention);
857
985
  const perPhasePlanScanFields = buildPerPhasePlanScanFields(paths.phases, allPhaseDirNames.value, allPhaseDirNames.scope);
858
986
  return {
859
987
  cwd: node_path_1.default.resolve(cwd),
@@ -871,17 +999,20 @@ function buildPlanningSnapshot(cwd) {
871
999
  projectSections: buildProjectSectionsField(cwd),
872
1000
  statePhaseTokens: stateFields.statePhaseTokens,
873
1001
  stateStatus: stateFields.stateStatus,
874
- roadmapDeclaredPhases: buildRoadmapDeclaredPhasesField(paths.roadmap),
875
- roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap),
1002
+ roadmapDeclaredPhases: roadmapDeclared.declared,
1003
+ roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap, phaseIdConvention),
876
1004
  researchValidationStatus: buildResearchValidationStatusField(paths.phases, phaseDirs.value, phaseDirs.scope),
877
1005
  milestoneArchiveStatus: buildMilestoneArchiveStatusField(cwd),
878
1006
  planningRootFiles: buildPlanningRootFilesField(cwd),
879
1007
  allPhaseDirNames,
880
- archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning),
881
- currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap),
1008
+ archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning, phaseIdConvention),
1009
+ currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap, phaseIdConvention),
882
1010
  perPhasePlanNumbering: perPhasePlanScanFields.perPhasePlanNumbering,
883
1011
  perPhaseOrphanSummaries: perPhasePlanScanFields.perPhaseOrphanSummaries,
884
1012
  perPhaseWaveMissingPlans: perPhasePlanScanFields.perPhaseWaveMissingPlans,
1013
+ phaseIdConvention,
1014
+ roadmapSentinelPhaseTokens: roadmapDeclared.sentinelTokens,
1015
+ roadmapBracketIncoherences: buildRoadmapBracketIncoherencesField(paths.roadmap, phaseIdConvention),
885
1016
  };
886
1017
  }
887
1018
  module.exports = {
@@ -21,6 +21,9 @@ const node_path_1 = __importDefault(require("node:path"));
21
21
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
22
22
  const clock_cjs_1 = require("./clock.cjs");
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const planningScopeMod = require("./planning-scope.cjs");
25
+ const { SCOPE } = planningScopeMod;
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
27
  const activeWorkstreamStore = require("./active-workstream-store.cjs");
25
28
  const { createSharedPointerAdapter, createSessionScopedPointerAdapter, createMemoryPointerAdapter, getActiveWorkstream: getStoredActiveWorkstream, peekActiveWorkstream: peekStoredActiveWorkstream, setActiveWorkstream: setStoredActiveWorkstream, clearActiveWorkstream: clearStoredActiveWorkstream, diagnoseUnresolvedActiveWorkstream: diagnoseUnresolvedStoredActiveWorkstream, } = activeWorkstreamStore;
26
29
  // Track .planning/.lock files held by this process so they can be removed on exit.
@@ -120,6 +123,142 @@ function planningDir(cwd, ws, project) {
120
123
  function planningRoot(cwd) {
121
124
  return node_path_1.default.join(cwd, '.planning');
122
125
  }
126
+ /**
127
+ * #3972: the ONE owner of "is this planning scope opted out of worktrees?" —
128
+ * the effective `workflow.use_worktrees === false` read every
129
+ * isolation-deciding surface must share (config-get's merged view is the
130
+ * contract). Ladder: the scoped config's OWN key wins (planningDir is
131
+ * project- and workstream-aware); otherwise the flat root's key, but only
132
+ * under the GSD_WORKSTREAM env gate — config-get deliberately does NOT
133
+ * inherit root under GSD_PROJECT alone, and this read must not diverge
134
+ * (#3963). Strict `=== false` (never coerced); any read failure degrades to
135
+ * "not opted out" (worktrees on — the fail-safe direction: the guard keeps
136
+ * enforcing). Direct file reads only — never loadConfig, which normalizes
137
+ * and rewrites config on paths that back sentinel writes.
138
+ */
139
+ function worktreesOptedOut(cwd) {
140
+ // #3972 review: the WHOLE body is guarded — planningDir/planningRoot
141
+ // themselves throw on a GSD_PROJECT/GSD_WORKSTREAM value containing path
142
+ // separators or `..`, and this contract ("any failure degrades to not
143
+ // opted out — worktrees on, keep enforcing") must hold for that shape too.
144
+ try {
145
+ return worktreesOptedOutUnguarded(cwd);
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
151
+ function worktreesOptedOutUnguarded(cwd) {
152
+ const readCfg = (p) => {
153
+ try {
154
+ return JSON.parse(String(node_fs_1.default.readFileSync(p, 'utf8')));
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ };
160
+ const ownKey = (cfg) => {
161
+ if (cfg === null || typeof cfg !== 'object')
162
+ return { present: false, value: undefined };
163
+ const wf = cfg.workflow;
164
+ if (wf === null || typeof wf !== 'object' || Array.isArray(wf))
165
+ return { present: false, value: undefined };
166
+ const wfRec = wf;
167
+ return Object.prototype.hasOwnProperty.call(wfRec, 'use_worktrees')
168
+ ? { present: true, value: wfRec['use_worktrees'] }
169
+ : { present: false, value: undefined };
170
+ };
171
+ const scoped = ownKey(readCfg(node_path_1.default.join(planningDir(cwd), 'config.json')));
172
+ if (scoped.present)
173
+ return scoped.value === false;
174
+ if (process.env['GSD_WORKSTREAM']) {
175
+ const root = ownKey(readCfg(node_path_1.default.join(planningRoot(cwd), 'config.json')));
176
+ if (root.present)
177
+ return root.value === false;
178
+ }
179
+ return false;
180
+ }
181
+ /**
182
+ * #612: resolve `phase_id_convention` with the SAME workstream->root federation
183
+ * config-loader uses (config-loader.cts:618/:649) — the workstream config wins,
184
+ * the root config is the fallback.
185
+ *
186
+ * Why this exists rather than `loadConfig(cwd)['phase_id_convention']`: as of
187
+ * #2997 (aa7697fe, in `next`), loadConfig surfaces `phase_id_convention` in
188
+ * its resolved `_baseConfig` — the "loadConfig drops keys it does not know"
189
+ * rationale this comment used to give is stale. The surviving reasons for the
190
+ * direct read are (1) the workstream->root federation below, a standalone
191
+ * resolution this function needs to run against a GIVEN cwd rather than
192
+ * whatever base a `loadConfig(cwd)` call elsewhere would federate from, and
193
+ * (2) convention-ENUM validation, which is still #612 PR-4 work — this
194
+ * function returns the raw string unvalidated, same as the now-surfaced
195
+ * resolved key would. #2997 surfacing the key makes consuming it from
196
+ * resolved config (instead of re-reading config.json here) a natural PR-4
197
+ * consolidation, not this PR's scope. Cycles were never the obstacle.
198
+ *
199
+ * Why federation matters here specifically: the phase-id readers were splitting
200
+ * on this value from two different bases — one resolving from the workstream
201
+ * directory, one from the root — so a workstream repo got the widened ROADMAP
202
+ * read with the narrow directory read, or the reverse, and reported every phase
203
+ * either missing from disk or malformed on disk. One resolver, one answer.
204
+ *
205
+ * The workstream is `planningDir`'s own `ws` parameter, forwarded, so this
206
+ * shares the canonical resolution (and its GSD_PROJECT/GSD_WORKSTREAM
207
+ * handling). Root is consulted as a fallback only when a workstream is active,
208
+ * matching config-loader; a project-scoped directory stands alone. Returns null
209
+ * when unset, absent, or unreadable — every caller treats null as "not the
210
+ * bracket convention".
211
+ *
212
+ * #2761 B1 (trek-e review): `ws` is a PARAMETER, not read from the environment
213
+ * here. It was omitted at first on the reasoning that "the active workstream is
214
+ * whatever planningDir resolves" — true only for the env-driven caller. A
215
+ * caller that iterates workstreams passes the name as an ARGUMENT (it cannot
216
+ * set `GSD_WORKSTREAM` per iteration), and `planningDir` falls back to the env
217
+ * only when `ws` is `undefined`, so an argument-driven call resolved this
218
+ * convention from the ROOT config while reading that workstream's ROADMAP. Two
219
+ * consequences, both reproduced: a workstream that explicitly declares its OWN
220
+ * convention had it ignored — the root's value decided how the workstream's
221
+ * roadmap was parsed, so flipping ONLY the root config changed which milestone
222
+ * a workstream extracted; and `--workstream foo` disagreed with
223
+ * `GSD_WORKSTREAM=foo` on the same repo.
224
+ *
225
+ * `undefined` (the default) keeps `planningDir`'s env fallback, so every
226
+ * pre-#2761 call site is byte-identical; `null` means "explicitly no
227
+ * workstream". Same discriminator `planningDir` and `getMilestonePhaseFilter`
228
+ * already carry.
229
+ *
230
+ * SCOPE: this governs the #612 bracket-selection reads ONLY. The shipped
231
+ * milestone-prefixed W021 gate keeps its own root-only read — re-basing a
232
+ * legacy convention's gate onto a different config is a behaviour change to a
233
+ * shipped check, in both directions, and is not part of read tolerance.
234
+ */
235
+ function resolvePhaseIdConvention(cwd, ws) {
236
+ const readFrom = (dir) => {
237
+ const configPath = node_path_1.default.join(dir, 'config.json');
238
+ if (!node_fs_1.default.existsSync(configPath))
239
+ return null;
240
+ try {
241
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
242
+ const value = parsed['phase_id_convention'];
243
+ return typeof value === 'string' && value !== '' ? value : null;
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ };
249
+ const scoped = planningDir(cwd, ws);
250
+ const root = planningRoot(cwd);
251
+ if (scoped === root)
252
+ return readFrom(root);
253
+ // Root is a fallback only when a WORKSTREAM is active — config-loader falls
254
+ // back to the root config under `if (ws)` and not otherwise, so a
255
+ // project-scoped directory stands alone. Detected by suppressing the
256
+ // workstream segment rather than re-reading the environment.
257
+ const projectOnly = planningDir(cwd, null);
258
+ if (scoped === projectOnly)
259
+ return readFrom(scoped);
260
+ return readFrom(scoped) ?? readFrom(root);
261
+ }
123
262
  // Sorted list of workstream directory names under `<root>/.planning/workstreams`,
124
263
  // or `[]` when the project is flat (no workstreams dir). Single source of truth
125
264
  // for the "workstream mode" detection shared by the #1912/#2028 fail-safe guards
@@ -394,49 +533,41 @@ function describeUnresolvedWorkstreamReason(reason) {
394
533
  return 'the name is not a valid workstream name';
395
534
  return "its workstream directory doesn't exist (it may have been renamed or removed)";
396
535
  }
397
- /**
398
- * Locate the CONTEXT.md file in a phase directory, handling both the bare
399
- * form (`CONTEXT.md`) and the padded-prefix convention (`NN-CONTEXT.md`,
400
- * `NN.N-CONTEXT.md`, etc.) used by gsd-discuss-phase output.
401
- *
402
- * Returns the filename (not the full path) of the first match, or null if
403
- * no CONTEXT.md exists in the directory.
404
- *
405
- * Canonical dual-form predicate extracted here to eliminate the 5-site
406
- * duplication that previously existed across init.cjs, roadmap.cjs,
407
- * core.cjs, gap-checker.cjs (#3739).
408
- *
409
- * @param absDirOrFiles - Absolute path to the phase directory,
410
- * OR an already-read files array (avoids a redundant readdirSync at call sites
411
- * that already hold a directory listing).
412
- */
413
536
  function findContextMdIn(absDirOrFiles) {
414
- try {
415
- const files = Array.isArray(absDirOrFiles)
416
- ? absDirOrFiles
417
- : node_fs_1.default.readdirSync(absDirOrFiles);
537
+ const matchIn = (files) => {
418
538
  if (files.includes('CONTEXT.md'))
419
539
  return 'CONTEXT.md';
420
540
  return files.find((f) => f.endsWith('-CONTEXT.md')) ?? null;
541
+ };
542
+ if (Array.isArray(absDirOrFiles)) {
543
+ return matchIn(absDirOrFiles);
544
+ }
545
+ try {
546
+ const files = node_fs_1.default.readdirSync(absDirOrFiles);
547
+ return { file: matchIn(files), files, scope: SCOPE.COMPLETE };
421
548
  }
422
549
  catch (err) {
423
- // #1883: distinguish genuine absence from a permission/I-O failure. ENOENT
424
- // ("nothing there") keeps the long-standing null contract the callers rely
425
- // on; every other error (EACCES, EIO, …) is a real read failure that must
426
- // propagate — otherwise an unreadable phase dir is silently reported as
427
- // "no CONTEXT.md" and the discuss/plan gates wrongly skip context.
428
- if (err.code === 'ENOENT')
429
- return null;
430
- throw err;
550
+ // #1883 / #4014: distinguish genuine absence from a permission/I-O
551
+ // failure. ENOENT ("nothing there") keeps the long-standing "real empty"
552
+ // contract callers rely on; every other error (EACCES, EIO, …) is a real
553
+ // read failure — reported as SCOPE.UNREADABLE rather than thrown, so a
554
+ // caller no longer needs its own try/catch to keep an unreadable phase
555
+ // dir from being silently reported the same as "no CONTEXT.md".
556
+ if (err.code === 'ENOENT') {
557
+ return { file: null, files: [], scope: SCOPE.COMPLETE };
558
+ }
559
+ return { file: null, files: [], scope: SCOPE.UNREADABLE };
431
560
  }
432
561
  }
433
562
  module.exports = {
563
+ worktreesOptedOut,
434
564
  createPlanningWorkspace,
435
565
  createSharedPointerAdapter,
436
566
  createSessionScopedPointerAdapter,
437
567
  createMemoryPointerAdapter,
438
568
  planningDir,
439
569
  planningRoot,
570
+ resolvePhaseIdConvention,
440
571
  listAvailableWorkstreams,
441
572
  planningPaths,
442
573
  quickDirFrom,
@@ -43,6 +43,9 @@ exports.projectTruths = projectTruths;
43
43
  exports.dispositionForUnverifiableTruth = dispositionForUnverifiableTruth;
44
44
  exports.runProbeCli = runProbeCli;
45
45
  const node_fs_1 = __importDefault(require("node:fs"));
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const cliExitModule = require("./cli-exit.cjs");
48
+ const { ExitError } = cliExitModule;
46
49
  /** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
47
50
  exports.VALID_STATUS = ['resolved', 'dismissed', 'unresolved'];
48
51
  function errMessage(e) {
@@ -497,7 +500,7 @@ function runProbeCli(analyze, options) {
497
500
  const readFile = options.readFile ?? ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
498
501
  const write = options.write ?? ((s) => { process.stdout.write(s); });
499
502
  const writeErr = options.writeErr ?? ((s) => { process.stderr.write(s); });
500
- const exit = options.exit ?? ((code) => { process.exit(code); });
503
+ const exit = options.exit ?? ((code) => { throw new ExitError(code); });
501
504
  const reqPath = argv[2];
502
505
  const resPath = argv[3];
503
506
  if (!reqPath) {
@@ -13,12 +13,55 @@
13
13
  * Async note: cmdExtractMessages and cmdProfileSample are async functions.
14
14
  * dispatchCapabilityCommand (gsd-tools.cjs:366-371) explicitly errors if a
15
15
  * router returns a Promise. Therefore these router functions call the async
16
- * function WITHOUT await and WITHOUT returning the Promise. The async functions
17
- * end with output() or process.exit() so the process terminates correctly once
18
- * the event loop drains. Unhandled rejections are caught by the .catch() wrapper
19
- * to surface errors via the error() callback.
16
+ * function WITHOUT await and WITHOUT returning the Promise; the event loop
17
+ * drains once the returned promise settles. Since ADR-3889 (#3910), the
18
+ * pipeline functions terminate by THROWING ExitError (never process.exit()
19
+ * directly), so a rejection surfacing here can carry either a genuine error
20
+ * OR a declared ExitError termination — the `.catch()` below must
21
+ * distinguish them: an ExitError sets process.exitCode directly (mirroring
22
+ * cli-exit.cjs's own runMain, the only other place ExitError is caught),
23
+ * while any other rejection is surfaced via the `error()` callback exactly
24
+ * as before. Calling `error(exitErr.message)` for an ExitError would be
25
+ * wrong on two counts: it discards the real exit code (error() always
26
+ * terminates at 1) and it re-derives a message from ExitError's generic
27
+ * "process exit N" constructor default rather than the (already emitted, or
28
+ * intentionally absent) stderr output the throwing call site controls.
20
29
  */
21
- const { ERROR_REASON } = require('./io.cjs');
30
+ const { ERROR_REASON, getJsonErrorMode } = require('./io.cjs');
31
+ const { ExitError } = require('./cli-exit.cjs');
32
+
33
+ /**
34
+ * Interpret a rejection from a fire-and-forget async pipeline call: an
35
+ * ExitError sets process.exitCode (and, for a non-zero code carrying a user
36
+ * message, writes it to stderr) exactly like runMain does; anything else
37
+ * reproduces io.cjs's error() stderr output byte-for-byte and sets
38
+ * process.exitCode directly instead of calling error() itself.
39
+ *
40
+ * error() (src/io.cts) is `never`-typed: it always throws ExitError(1) after
41
+ * writing to stderr. Calling it from inside this `.catch()` callback would
42
+ * throw from a detached promise chain that nothing awaits or re-catches —
43
+ * an unhandled promise rejection that Node (>=15, this repo's
44
+ * engines.node >= 24 default is --unhandled-rejections=throw) dumps as a
45
+ * raw stack trace on top of the clean line error() already wrote. Writing
46
+ * the same bytes directly and setting process.exitCode = 1 in place gets
47
+ * the identical observable stderr + exit code without ever throwing here.
48
+ */
49
+ function _handlePipelineRejection(e, error) {
50
+ void error;
51
+ if (e instanceof ExitError) {
52
+ if (e.hasUserMessage && e.code !== 0) process.stderr.write(`${e.message}\n`);
53
+ process.exitCode = e.code;
54
+ return;
55
+ }
56
+ const message = e && e.message ? e.message : String(e);
57
+ if (getJsonErrorMode()) {
58
+ const payload = JSON.stringify({ ok: false, reason: ERROR_REASON.UNKNOWN, message }) + '\n';
59
+ process.stderr.write(payload);
60
+ } else {
61
+ process.stderr.write('Error: ' + message + '\n');
62
+ }
63
+ process.exitCode = 1;
64
+ }
22
65
 
23
66
  // ─── Pipeline phase commands ───────────────────────────────────────────────────
24
67
 
@@ -51,7 +94,7 @@ function routeExtractMessages({ args, cwd, raw, error, _pipeline }) {
51
94
  // The function ends with output() or process.exit(); the event loop will drain.
52
95
  void cwd;
53
96
  p.cmdExtractMessages(projectArg, { sessionId, limit }, raw, sessionsPath)
54
- .catch(e => { error(e && e.message ? e.message : String(e)); });
97
+ .catch(e => { _handlePipelineRejection(e, error); });
55
98
  }
56
99
 
57
100
  function routeProfileSample({ args, cwd, raw, error, _pipeline }) {
@@ -67,7 +110,7 @@ function routeProfileSample({ args, cwd, raw, error, _pipeline }) {
67
110
  const maxChars = maxCharsIdx !== -1 ? parseInt(args[maxCharsIdx + 1], 10) : 500;
68
111
  // cmdProfileSample is async — do NOT return the Promise.
69
112
  p.cmdProfileSample(sessionsPath, { limit, maxPerProject, maxChars }, raw)
70
- .catch(e => { error(e && e.message ? e.message : String(e)); });
113
+ .catch(e => { _handlePipelineRejection(e, error); });
71
114
  }
72
115
 
73
116
  // ─── Output phase commands ─────────────────────────────────────────────────────