@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -15,6 +15,7 @@ const node_os_1 = __importDefault(require("node:os"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const clock_cjs_1 = require("./clock.cjs");
17
17
  const pattern_cjs_1 = require("./pattern.cjs");
18
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
18
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
19
20
  const io = require("./io.cjs");
20
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
@@ -40,6 +41,8 @@ const { SCOPE } = planningScopeMod;
40
41
  const secrets_cjs_1 = require("./secrets.cjs");
41
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
42
43
  const scanPhasePlans = require("./plan-scan.cjs");
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-document.cjs is an export= CommonJS module
45
+ const planDocument = require("./plan-document.cjs");
43
46
  const state_document_cjs_1 = require("./state-document.cjs");
44
47
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
45
48
  const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
@@ -86,9 +89,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
86
89
  const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
87
90
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
88
91
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
89
- const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
92
+ const { comparePhaseNum, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
90
93
  const { pruneOrphanedWorktrees } = worktreeSafety;
91
- const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
94
+ const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, resolvePhaseIdConvention, } = planningWorkspace;
92
95
  const { determinePhaseStatus } = commandsMod;
93
96
  const { extractFrontmatter } = frontmatterMod;
94
97
  const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
@@ -99,7 +102,6 @@ const { resolveCapabilityRuntimeState } = capabilityStateMod;
99
102
  // Unused but imported for structural parity
100
103
  void stripShippedMilestones;
101
104
  // Accept all bold/colon variants of the Requirements header (#2769)
102
- const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
103
105
  // #2056/#2104: isForeignPrefixedPhaseQuery is imported from phase-id.cts
104
106
  // (the canonical predicate). parsePhasePrefix is no longer needed locally.
105
107
  // phaseInfoMatchesExactPrefix and roadmapPhaseMatchesExactPrefix are local
@@ -109,9 +111,18 @@ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
109
111
  const numStr = typeof num === 'string' ? num : (typeof num === 'number' ? String(num) : '');
110
112
  return numStr.toUpperCase() === phase.toUpperCase();
111
113
  }
114
+ // #4906 Phase 5 (#4984): NOT migrated onto the heading-baseline selector —
115
+ // this site was never one of the five ADR-4910 §8 call sites this phase owns
116
+ // (init milestone/progress heading scans + milestone.cts's unstarted-phase
117
+ // guard, all via buildPhaseHeadingScanRegex), and adding a NEW direct
118
+ // any-bracket selector consumer here inflated init.cts's
119
+ // tests/adr-612-bracket-heading-selection.test.cjs census past its pinned
120
+ // count. Left bracket-blind deliberately, matching this call's pre-#4984
121
+ // behavior; folding it into the census is Phase 6 work.
112
122
  function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
113
123
  const sectionRaw = roadmapPhase?.['section'];
114
124
  const section = typeof sectionRaw === 'string' ? sectionRaw : '';
125
+ // phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above the function.
115
126
  return new RegExp(`^#{2,4}\\s*Phase\\s+${(0, pattern_cjs_1.escapeRegex)(phase)}(?:\\b|\\s|:)`, 'i').test(section);
116
127
  }
117
128
  // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
@@ -257,7 +268,15 @@ function withProjectRoot(cwd, result) {
257
268
  if (config.project_code) {
258
269
  result['project_code'] = config.project_code;
259
270
  }
260
- const projectMdPath = node_path_1.default.join(planningDir(cwd), 'PROJECT.md');
271
+ // #4455 follow-up (self-discovered): PROJECT.md is shared across a
272
+ // project's own workstreams (never cloned per workstream) but DOES
273
+ // respect the separate GSD_PROJECT multi-project namespace (#3749) — see
274
+ // cmdInitCompleteMilestone's projectPath comment for the full evidence.
275
+ // `ws` explicitly nulled, `project` left to default from GSD_PROJECT.
276
+ // Reading via the workstream-aware planningDir(cwd) meant every init.*
277
+ // call's project_title silently vanished whenever a workstream was
278
+ // active, since no PROJECT.md ever exists at the workstream path.
279
+ const projectMdPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
261
280
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(projectMdPath);
262
281
  if (content) {
263
282
  const h1Match = content.match(/^#\s+(.+)$/m);
@@ -477,6 +496,16 @@ function readConfigJsonBoolean(cwd, keyPath) {
477
496
  return false;
478
497
  }
479
498
  }
499
+ /** Reads `filePath`; returns its content, or `null` when missing/unreadable/empty. */
500
+ function readNonEmptyFileOrNull(filePath) {
501
+ try {
502
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
503
+ return content && content.length > 0 ? content : null;
504
+ }
505
+ catch {
506
+ return null;
507
+ }
508
+ }
480
509
  /**
481
510
  * Bounded, non-throwing read of a dotted key path from `.planning/config.json`,
482
511
  * returning the raw resolved value (any JSON type) or `undefined` on any
@@ -565,6 +594,13 @@ function detectPhaseMvpMode(cwd, phaseNumber) {
565
594
  const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
566
595
  const content = extractCurrentMilestone(rawContent, cwd);
567
596
  const escapedPhase = (0, pattern_cjs_1.escapeRegex)(phaseNumber);
597
+ // #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
598
+ // out-of-scope reasoning as roadmapPhaseMatchesExactPrefix above (only the
599
+ // five buildPhaseHeadingScanRegex sites in init.cts/milestone.cts are this
600
+ // phase's owned migration); a direct selector call here would have added
601
+ // a fourth uncounted ANY_BRACKET consumer to init.cts's pinned
602
+ // tests/adr-612-bracket-heading-selection.test.cjs census.
603
+ // phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above.
568
604
  const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
569
605
  const headerMatch = content.match(phaseHeader);
570
606
  if (!headerMatch || headerMatch.index === undefined)
@@ -732,6 +768,54 @@ function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides
732
768
  function milestoneRecord(cwd) {
733
769
  return (getMilestoneInfo(cwd).value ?? {});
734
770
  }
771
+ /**
772
+ * #4683 — threat IDs claimed by more than one of the phase's live PLAN files.
773
+ * `scanPhasePlans().planFiles` is the right input set twice over: it excludes
774
+ * derivative files (OUTLINE / PLAN-REVIEW / pre-bounce) AND `status: superseded`
775
+ * plans (#2349) — a superseded plan's IDs were deliberately reassigned to its
776
+ * replacement, so they must not hold against it. The per-document row parse is
777
+ * planDocument.extractThreatRegisterIds; only `<threat_model>` register rows
778
+ * count, and the reserved `T-{phase}-SC` shape is excluded there by grammar
779
+ * (every plan keeps that row by design). A missing/unreadable phase directory
780
+ * degrades to "no duplicates" — planning a brand-new phase has nothing to
781
+ * collide with.
782
+ */
783
+ function findDuplicateThreatIds(cwd, phaseDirRel) {
784
+ if (!phaseDirRel)
785
+ return [];
786
+ const phaseDir = node_path_1.default.join(cwd, phaseDirRel);
787
+ let planFiles;
788
+ try {
789
+ planFiles = scanPhasePlans(phaseDir).planFiles;
790
+ }
791
+ catch {
792
+ return [];
793
+ }
794
+ const owners = new Map();
795
+ for (const planFile of planFiles) {
796
+ let content;
797
+ try {
798
+ content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, planFile), 'utf-8');
799
+ }
800
+ catch {
801
+ continue;
802
+ }
803
+ for (const id of planDocument.extractThreatRegisterIds(content)) {
804
+ const claimed = owners.get(id);
805
+ if (claimed) {
806
+ if (!claimed.includes(planFile))
807
+ claimed.push(planFile);
808
+ }
809
+ else {
810
+ owners.set(id, [planFile]);
811
+ }
812
+ }
813
+ }
814
+ return [...owners.entries()]
815
+ .filter(([, claims]) => claims.length > 1)
816
+ .map(([id, plans]) => ({ id, plans: [...plans].sort() }))
817
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
818
+ }
735
819
  function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
736
820
  if (!phase) {
737
821
  error('phase required for init execute-phase');
@@ -769,9 +853,14 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
769
853
  has_reviews: false,
770
854
  };
771
855
  });
772
- const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
773
- const reqExtracted = reqMatch
774
- ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
856
+ // #4731: multiline-aware — the Requirements field may hard-wrap, so the
857
+ // value is extracted past the line break before the ID scan.
858
+ const phaseSection = roadmapPhase?.['section'];
859
+ const reqLine = phaseSection
860
+ ? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
861
+ : null;
862
+ const reqExtracted = reqLine
863
+ ? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
775
864
  : null;
776
865
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
777
866
  // #3188: these paths are null when the file is absent, matching the contract
@@ -781,6 +870,8 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
781
870
  const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
782
871
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
783
872
  const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
873
+ // #4683: computed once — see the threat_id_duplicates fields in the payload.
874
+ const threatIdDuplicates = findDuplicateThreatIds(cwd, phaseInfo?.['directory']);
784
875
  const result = {
785
876
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
786
877
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
@@ -800,6 +891,13 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
800
891
  ? toPosixPath(node_path_1.default.join(cwd, phaseInfo['directory']))
801
892
  : null,
802
893
  phase_number: phaseInfo?.['phase_number'] || null,
894
+ // #4748: the disk path hands back the directory's padded number (`03A`)
895
+ // but the ROADMAP fallback above hands back the heading's bare one (`3A`),
896
+ // and execute-phase.md's review lookup needs the padded form for
897
+ // `{PADDED}-REVIEW.md`. It used to re-pad in shell with `printf "%02d"`,
898
+ // which cannot pad a letter id and reads an already-padded `08` as octal.
899
+ // Emit the canonical normalization, as the plan-phase/code-review inits do.
900
+ padded_phase: phaseInfo?.['phase_number'] ? normalizePhaseName(phaseInfo['phase_number']) : null,
803
901
  // #3171: prefer the ROADMAP's curated display name for `phase_name`. When
804
902
  // the phase directory already exists on disk, the disk-lookup path
805
903
  // (searchPhaseInDir) derives phase_name from the directory-name remainder
@@ -818,6 +916,12 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
818
916
  incomplete_plans: phaseInfo?.['incomplete_plans'] || [],
819
917
  plan_count: phaseInfo?.['plans']?.length || 0,
820
918
  incomplete_count: phaseInfo?.['incomplete_plans']?.length || 0,
919
+ // #4683: cross-plan threat-ID collisions (gap-closure plans renumbering
920
+ // from T-{phase}-01 again). execute-phase.md hard-stops on a non-empty
921
+ // list BEFORE any dispatch — SECURITY.md rows and VALIDATION.md's Threat
922
+ // Ref column key on this ID, so a reused ID is ambiguous downstream.
923
+ threat_id_duplicates: threatIdDuplicates,
924
+ threat_id_duplicate_count: threatIdDuplicates.length,
821
925
  // #2830: the halt-aware view, forwarded from the shared computation in
822
926
  // phase-locator. Additive — `incomplete_plans`/`incomplete_count` above keep
823
927
  // their exact name, type and semantics. Without this passthrough the shared
@@ -828,10 +932,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
828
932
  runnable_plans: phaseInfo?.['runnable_plans'] || [],
829
933
  runnable_count: phaseInfo?.['runnable_plans']?.length || 0,
830
934
  branch_name: config.branching_strategy === 'phase' && phaseInfo
831
- ? config.phase_branch_template
832
- .replace('{project}', config.project_code || '')
833
- .replace('{phase}', normalizePhaseName(phaseInfo['phase_number']))
834
- .replace('{slug}', phaseInfo['phase_slug'] || 'phase')
935
+ ? renderPhaseBranchName(config.phase_branch_template.replace('{project}', config.project_code || ''), phaseInfo['phase_number'], phaseInfo['phase_slug'])
835
936
  : config.branching_strategy === 'milestone'
836
937
  ? config.milestone_branch_template
837
938
  .replace('{milestone}', milestone['version'] ?? '')
@@ -847,6 +948,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
847
948
  // #3188: null when the file is absent (parity with patterns_path/context_path).
848
949
  state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
849
950
  roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
951
+ // #4456 correction: an isolated review pass initially "fixed" this to
952
+ // planningDir(cwd, null) on the assumption that config.json is shared
953
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
954
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
955
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
956
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
957
+ // caught the regression immediately. The diagram is stale for
958
+ // config.json specifically (same class of staleness already found for
959
+ // `milestones/` during the #4455 follow-up) — reverted to the
960
+ // workstream-aware planningDir(cwd), matching the established,
961
+ // ADR-governed, tested contract.
850
962
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
851
963
  // #2376: execute-phase.md's verify_phase_goal step reads this instead of
852
964
  // hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
@@ -903,9 +1015,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
903
1015
  has_reviews: false,
904
1016
  };
905
1017
  });
906
- const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
907
- const reqExtracted = reqMatch
908
- ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
1018
+ // #4731: multiline-aware — the Requirements field may hard-wrap, so the
1019
+ // value is extracted past the line break before the ID scan.
1020
+ const phaseSection = roadmapPhase?.['section'];
1021
+ const reqLine = phaseSection
1022
+ ? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
1023
+ : null;
1024
+ const reqExtracted = reqLine
1025
+ ? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
909
1026
  : null;
910
1027
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
911
1028
  const phaseDirPlan = phaseInfo?.['directory'] || null;
@@ -925,6 +1042,8 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
925
1042
  }
926
1043
  const granularityOverride = options['granularity'];
927
1044
  assertValidGranularityOverride(granularityOverride, error);
1045
+ // #4683: computed once — see the threat_id_duplicates fields in the payload.
1046
+ const threatIdDuplicatesPlan = findDuplicateThreatIds(cwd, phaseDirPlan);
928
1047
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
929
1048
  // #3188: see cmdInitExecutePhase — null when absent, parity with the
930
1049
  // conditional sibling fields in this same result object.
@@ -969,6 +1088,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
969
1088
  has_reviews: phaseInfo?.['has_reviews'] || false,
970
1089
  has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
971
1090
  plan_count: phaseInfo?.['plans']?.length || 0,
1091
+ // #4683: the same cross-plan threat-ID duplicate list execute-phase gates
1092
+ // on, surfaced at PLAN time so the checker/reviewer catches the collision
1093
+ // before the plans are approved — not just before execution.
1094
+ threat_id_duplicates: threatIdDuplicatesPlan,
1095
+ threat_id_duplicate_count: threatIdDuplicatesPlan.length,
972
1096
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
973
1097
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
974
1098
  // #2376: absolute — see comment on phase_dir above.
@@ -1130,7 +1254,13 @@ function buildInitCompletenessFields(cwd) {
1130
1254
  const planningExists = node_fs_1.default.existsSync(dir);
1131
1255
  const requirementsExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'REQUIREMENTS.md'));
1132
1256
  const milestonesExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'MILESTONES.md'));
1133
- const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'PROJECT.md')) &&
1257
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1258
+ // workstreams (see cmdInitCompleteMilestone's projectPath comment for the
1259
+ // full evidence) — checked at planningRoot(cwd), never the workstream-scoped
1260
+ // `dir`, so a workstream whose own REQUIREMENTS/ROADMAP/STATE are all
1261
+ // present isn't wrongly reported incomplete just because the shared
1262
+ // PROJECT.md isn't ALSO duplicated under its own directory.
1263
+ const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')) &&
1134
1264
  requirementsExists &&
1135
1265
  node_fs_1.default.existsSync(node_path_1.default.join(dir, 'ROADMAP.md')) &&
1136
1266
  node_fs_1.default.existsSync(node_path_1.default.join(dir, 'STATE.md'));
@@ -1165,23 +1295,46 @@ function cmdInitNewProject(cwd, raw, options = {}) {
1165
1295
  // root-scoped (`pathExistsInternal(cwd, '.planning')`) semantics for that
1166
1296
  // one key stay byte-identical for existing consumers.
1167
1297
  ...buildInitCompletenessFields(cwd),
1168
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1298
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1299
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1300
+ // the full evidence.
1301
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1169
1302
  has_codebase_map: hasCodebaseMap,
1170
1303
  planning_exists: pathExistsInternal(cwd, '.planning'),
1171
1304
  has_existing_code: hasCode,
1172
1305
  has_package_file: hasPackageFile,
1173
1306
  is_brownfield: isBrownfield,
1307
+ // #4458: new-project.md's Step 5.1 (Sub-Repo Detection) used to run its own
1308
+ // narrower `find ... -exec test -d "{}/.git"` predicate, which requires
1309
+ // .git to be a DIRECTORY and so silently excluded linked git worktree
1310
+ // children (.git is a FILE there). detectSubRepos already handled this
1311
+ // correctly (fs.existsSync, not isDirectory) but had zero callers anywhere
1312
+ // in the codebase — reused here instead of leaving the workflow to
1313
+ // maintain its own duplicate, narrower detection logic.
1314
+ sub_repos_detected: coreUtils.detectSubRepos(cwd),
1174
1315
  needs_codebase_map: isBrownfield && !hasCodebaseMap,
1175
1316
  ...getInitGitState(cwd),
1176
1317
  brave_search_available: hasBraveSearch,
1177
1318
  firecrawl_available: hasFirecrawl,
1178
1319
  exa_search_available: hasExaSearch,
1179
1320
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1180
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1321
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1322
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1181
1323
  // #2376: new-project.md's research-synthesizer/roadmapper spawn prompts
1182
1324
  // read these instead of hardcoding '.planning/...' literals.
1183
1325
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1184
1326
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1327
+ // #4456 correction: an isolated review pass initially "fixed" this to
1328
+ // planningDir(cwd, null) on the assumption that config.json is shared
1329
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
1330
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
1331
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
1332
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
1333
+ // caught the regression immediately. The diagram is stale for
1334
+ // config.json specifically (same class of staleness already found for
1335
+ // `milestones/` during the #4455 follow-up) — reverted to the
1336
+ // workstream-aware planningDir(cwd), matching the established,
1337
+ // ADR-governed, tested contract.
1185
1338
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
1186
1339
  research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
1187
1340
  };
@@ -1223,18 +1376,41 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1223
1376
  phase_archive_path: latestCompleted
1224
1377
  ? toPosixPath(node_path_1.default.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))
1225
1378
  : null,
1226
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1379
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1380
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1381
+ // the full evidence.
1382
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1227
1383
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1228
1384
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1229
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1385
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1386
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1230
1387
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1231
1388
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1232
1389
  // #2376: new-milestone.md's research-synthesizer/roadmapper spawn prompts
1233
1390
  // read these instead of hardcoding '.planning/...' literals.
1234
1391
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1392
+ // #4456 correction: an isolated review pass initially "fixed" this to
1393
+ // planningDir(cwd, null) on the assumption that config.json is shared
1394
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
1395
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
1396
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
1397
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
1398
+ // caught the regression immediately. The diagram is stale for
1399
+ // config.json specifically (same class of staleness already found for
1400
+ // `milestones/` during the #4455 follow-up) — reverted to the
1401
+ // workstream-aware planningDir(cwd), matching the established,
1402
+ // ADR-governed, tested contract.
1235
1403
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
1236
1404
  research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
1237
1405
  milestones_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'MILESTONES.md')),
1406
+ // #4456: new-milestone.md's Step 6 stages the phase-archive move
1407
+ // (`git add .planning/milestones/ .planning/phases/`) — both
1408
+ // workstream-scoped (phases_dir mirrors the phasesDir local above;
1409
+ // archive_dir mirrors cmdInitCompleteMilestone's own field of the same
1410
+ // name), so a literal root `git add` misses the actual files
1411
+ // phases.clear just moved under an active workstream.
1412
+ phases_dir: toPosixPath(phasesDir),
1413
+ archive_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'milestones')),
1238
1414
  };
1239
1415
  // `state:flat-mode` (#2994): whether NO workstream is active — the inverse
1240
1416
  // of `state:workstream-active` (introduced for `cmdInitTransition` below).
@@ -1257,7 +1433,7 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1257
1433
  // would otherwise silently delete a stale/invalid pointer as a side effect
1258
1434
  // of building a JSON report field, and (per #3579) could change what a
1259
1435
  // LATER resolution in the same process observes.
1260
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
1436
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
1261
1437
  const workstreamActive = !!resolvedWorkstream;
1262
1438
  const flatMode = !workstreamActive;
1263
1439
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -1372,7 +1548,10 @@ function cmdInitQuickBatch(cwd, raw, options = {}) {
1372
1548
  function cmdInitIngestDocs(cwd, raw) {
1373
1549
  const config = loadConfig(cwd);
1374
1550
  const result = {
1375
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1551
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1552
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1553
+ // the full evidence.
1554
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1376
1555
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1377
1556
  ...getInitGitState(cwd),
1378
1557
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The
@@ -1381,7 +1560,8 @@ function cmdInitIngestDocs(cwd, raw) {
1381
1560
  // hardcoded bare '.planning/intel/...', '.planning/PROJECT.md', etc.
1382
1561
  // literals into their Agent(prompt=...) blocks; those now interpolate
1383
1562
  // these fields instead.
1384
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1563
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1564
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1385
1565
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1386
1566
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1387
1567
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
@@ -1417,12 +1597,16 @@ function cmdInitResume(cwd, raw) {
1417
1597
  ...buildInitCompletenessFields(cwd),
1418
1598
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1419
1599
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1420
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1600
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1601
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1602
+ // the full evidence.
1603
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1421
1604
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1422
1605
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1423
1606
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1424
1607
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1425
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1608
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1609
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1426
1610
  has_interrupted_agent: !!interruptedAgentId,
1427
1611
  interrupted_agent_id: interruptedAgentId,
1428
1612
  commit_docs: config.commit_docs,
@@ -1898,13 +2082,156 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1898
2082
  }
1899
2083
  output(withProjectRoot(cwd, result), raw);
1900
2084
  }
2085
+ // #2618: bullet-cap and title-floor for renderPendingTodosMarkdown below.
2086
+ // 240 matches the bound already vetted by maintainer review on the prior
2087
+ // attempt at this issue (PR #2662) — re-deriving a different number would be
2088
+ // pure bikeshedding, not a correctness improvement. See
2089
+ // .gsd/phase/feat-2618-compact-todo-pointers/40-design.md.
2090
+ const PENDING_TODO_BULLET_MAX_CHARS = 240;
2091
+ const PENDING_TODO_TITLE_FLOOR = 15;
2092
+ const PENDING_TODO_AREA_FLOOR = 3;
2093
+ function sanitizePendingTodoInline(value) {
2094
+ // Defensive: the regex captures that populate title/area/needs can only
2095
+ // ever match a single line, so this is belt-and-suspenders against any
2096
+ // future non-regex-sourced input, not a reachable case today.
2097
+ return value.replace(/[\r\n]+/g, ' ').trim();
2098
+ }
2099
+ function truncatePendingTodoText(value, maxLen) {
2100
+ if (value.length <= maxLen)
2101
+ return value;
2102
+ if (maxLen <= 1)
2103
+ return value.slice(0, Math.max(0, maxLen));
2104
+ return `${value.slice(0, maxLen - 1)}…`;
2105
+ }
2106
+ /**
2107
+ * #2618: pure renderer for STATE.md's "### Pending Todos" section BODY (not
2108
+ * the heading). One bullet per todo, each capped at
2109
+ * PENDING_TODO_BULLET_MAX_CHARS. `gsd-core/workflows/add-todo.md` and
2110
+ * `check-todos.md` splice this string in verbatim instead of free-hand
2111
+ * editing STATE.md — see the design doc for why this is real, unit-tested
2112
+ * code rather than a prose algorithm (DEFECT.GENERATIVE-FIX: a prose
2113
+ * algorithm duplicated as a test oracle is exactly the divergence class
2114
+ * this avoids).
2115
+ *
2116
+ * #4384 regression fix: the optional `projectRoot` makes the bullet's
2117
+ * `[todo file](…)` link repo-relative (see pendingTodoLinkTarget) so the cap
2118
+ * is deterministic w.r.t. where the repo is checked out. Omitting it keeps
2119
+ * the legacy absolute-link behavior for existing direct callers.
2120
+ */
2121
+ function renderPendingTodosMarkdown(todos, projectRoot) {
2122
+ if (!Array.isArray(todos) || todos.length === 0) {
2123
+ return 'None yet.';
2124
+ }
2125
+ return todos.map((todo) => renderPendingTodoBullet(todo, projectRoot)).join('\n');
2126
+ }
2127
+ function pendingTodoFieldAsString(value, fallback) {
2128
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
2129
+ }
2130
+ /**
2131
+ * #4384 regression fix: the bullet's markdown link target, rendered
2132
+ * repo-relative when `projectRoot` is given and the todo's `path` is
2133
+ * absolute. The JSON `todos[].path` field stays absolute (#2376 contract);
2134
+ * only the rendered display link changes — embedding the machine-variable
2135
+ * absolute base let macOS's /private/var/folders/… temp paths consume the
2136
+ * 240-char budget and drop the "Needs <solution>" clause on long-path
2137
+ * machines only (next's own macos CI shard went red on exactly this, run
2138
+ * 34038716700). Repo-relative links also resolve correctly from STATE.md at
2139
+ * the repo root and survive repo moves.
2140
+ */
2141
+ function pendingTodoLinkTarget(todo, projectRoot) {
2142
+ const raw = pendingTodoFieldAsString(todo['path'], '');
2143
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0 || !node_path_1.default.isAbsolute(raw)) {
2144
+ return raw;
2145
+ }
2146
+ const rel = toPosixPath(node_path_1.default.relative(projectRoot, raw));
2147
+ if (rel.length === 0 || node_path_1.default.isAbsolute(rel)) {
2148
+ // Degenerate (path === projectRoot) or Windows cross-drive fallback:
2149
+ // keep the raw target rather than emitting an empty or incorrect link.
2150
+ return raw;
2151
+ }
2152
+ return rel;
2153
+ }
2154
+ /**
2155
+ * #4439: the stored `created:` frontmatter (and the JSON `todos[].created`
2156
+ * field it round-trips through) is always a full ISO-8601 timestamp, by
2157
+ * design — this is the display-only seam that reformats it to the
2158
+ * date-only `[date]` bullet documented in docs/reference/state-md.md and
2159
+ * docs/COMMANDS.md. A value that doesn't start with a well-formed
2160
+ * `YYYY-MM-DD` (the 'unknown' fallback, or any other non-conforming
2161
+ * string) passes through unchanged rather than being mangled.
2162
+ */
2163
+ function pendingTodoDateOnly(value) {
2164
+ const match = value.match(/^\d{4}-\d{2}-\d{2}/);
2165
+ return match ? match[0] : value;
2166
+ }
2167
+ function renderPendingTodoBullet(todo, projectRoot) {
2168
+ const date = pendingTodoDateOnly(sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown')));
2169
+ let area = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['area'], 'general'));
2170
+ let title = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['title'], 'Untitled'));
2171
+ // Strip trailing "." so the fixed "Needs ....` template below never
2172
+ // produces a doubled period when the source text already ended in one.
2173
+ let needs = typeof todo['needs'] === 'string'
2174
+ ? sanitizePendingTodoInline(todo['needs']).replace(/\.+$/, '')
2175
+ : '';
2176
+ const link = `[todo file](${pendingTodoLinkTarget(todo, projectRoot)})`;
2177
+ const assemble = () => {
2178
+ const needsClause = needs ? ` — Needs ${needs}.` : '';
2179
+ return `- [${date}] [${area}] ${title} — ${link}${needsClause}`;
2180
+ };
2181
+ let line = assemble();
2182
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2183
+ return line;
2184
+ // 1) Drop the needs clause entirely first — date/area/title/link untouched.
2185
+ needs = '';
2186
+ line = assemble();
2187
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2188
+ return line;
2189
+ // 2) Shorten the title next, down to a floor — date/area/link untouched.
2190
+ const titleOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
2191
+ const targetTitleLen = Math.max(PENDING_TODO_TITLE_FLOOR, title.length - titleOverage);
2192
+ if (targetTitleLen < title.length) {
2193
+ title = truncatePendingTodoText(title, targetTitleLen);
2194
+ line = assemble();
2195
+ }
2196
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2197
+ return line;
2198
+ // 3) Shorten area as a last resort — date and the markdown link are never
2199
+ // altered (link correctness > strict cap; see design doc "Known limits").
2200
+ const areaOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
2201
+ const targetAreaLen = Math.max(PENDING_TODO_AREA_FLOOR, area.length - areaOverage);
2202
+ if (targetAreaLen < area.length) {
2203
+ area = truncatePendingTodoText(area, targetAreaLen);
2204
+ line = assemble();
2205
+ }
2206
+ return line;
2207
+ }
1901
2208
  function cmdInitTodos(cwd, area, raw) {
1902
2209
  const config = loadConfig(cwd);
1903
- const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
2210
+ // #4256: todos are root-scoped shared state (migrateToWorkstreams keeps
2211
+ // them at .planning/todos/ and every workflow writer writes that literal
2212
+ // path), so this read resolves via todosDir(cwd) — NOT planningDir(cwd),
2213
+ // which would look in .planning/workstreams/<ws>/todos/ under a workstream
2214
+ // (a directory nothing creates) and report existing todos as absent.
2215
+ const todosRoot = todosDir(cwd);
2216
+ const pendingDir = node_path_1.default.join(todosRoot, 'pending');
1904
2217
  let count = 0;
1905
2218
  const todos = [];
2219
+ // #2618: distinct from "genuinely zero pending todos" — false only when
2220
+ // readdirSync itself failed for a reason OTHER than the directory simply
2221
+ // not existing yet (ENOENT), mirroring the ENOENT-vs-other-errno split
2222
+ // already used above in this file (#3885, ADR-3473 §8.5). Without this,
2223
+ // a real I/O/permission error on the pending dir would look identical to
2224
+ // "no pending todos" and could wipe an existing, non-empty Pending Todos
2225
+ // section in STATE.md on refresh — the fail-safe requirement for #2618.
2226
+ let pendingReadOk = true;
1906
2227
  try {
1907
- const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md'));
2228
+ // #2618: sorted so pending_todos_markdown's bullet order is stable across
2229
+ // runs — readdirSync's order is filesystem-dependent, not contractually
2230
+ // stable, and an unstable order would reorder every bullet on an
2231
+ // unrelated re-render, turning a one-line git diff into a full-section
2232
+ // rewrite (must-have #3). Filenames are `YYYY-MM-DD-slug.md`, so this
2233
+ // also yields a sensible chronological order as a side effect.
2234
+ const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md')).sort();
1908
2235
  for (const file of files) {
1909
2236
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file));
1910
2237
  if (content === null)
@@ -1916,6 +2243,21 @@ function cmdInitTodos(cwd, area, raw) {
1916
2243
  // #2337: kept in parity with cmdListTodos — surface severity when
1917
2244
  // present, omit the key entirely for todos with no severity line.
1918
2245
  const severityMatch = content.match(/^severity:\s*(.+)$/m);
2246
+ // #2618: first non-empty line of the `## Solution` body, used as the
2247
+ // bullet's "Needs ..." clause. "TBD" (the create_file template's own
2248
+ // placeholder for an unresolved solution) renders no clause at all
2249
+ // rather than the useless literal "Needs TBD.".
2250
+ const solutionSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => h.level === 2 && h.text.trim() === 'Solution');
2251
+ let needs;
2252
+ if (solutionSection) {
2253
+ const firstLine = solutionSection.body
2254
+ .split('\n')
2255
+ .map((l) => l.trim())
2256
+ .find((l) => l.length > 0);
2257
+ if (firstLine && firstLine.toUpperCase() !== 'TBD') {
2258
+ needs = firstLine;
2259
+ }
2260
+ }
1919
2261
  const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
1920
2262
  if (area && todoArea !== area)
1921
2263
  continue;
@@ -1926,8 +2268,9 @@ function cmdInitTodos(cwd, area, raw) {
1926
2268
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
1927
2269
  area: todoArea,
1928
2270
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1929
- path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'pending', file)),
2271
+ path: toPosixPath(node_path_1.default.join(pendingDir, file)),
1930
2272
  ...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
2273
+ ...(needs ? { needs } : {}),
1931
2274
  });
1932
2275
  }
1933
2276
  catch {
@@ -1935,8 +2278,11 @@ function cmdInitTodos(cwd, area, raw) {
1935
2278
  }
1936
2279
  }
1937
2280
  }
1938
- catch {
1939
- /* intentionally empty */
2281
+ catch (err) {
2282
+ const code = err?.code;
2283
+ if (code !== 'ENOENT') {
2284
+ pendingReadOk = false;
2285
+ }
1940
2286
  }
1941
2287
  const result = {
1942
2288
  commit_docs: config.commit_docs,
@@ -1946,11 +2292,22 @@ function cmdInitTodos(cwd, area, raw) {
1946
2292
  todos,
1947
2293
  area_filter: area || null,
1948
2294
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1949
- pending_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')),
1950
- completed_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'completed')),
2295
+ // #4256: both dir fields probe the ROOT todos tree via todosDir(cwd).
2296
+ pending_dir: toPosixPath(pendingDir),
2297
+ completed_dir: toPosixPath(node_path_1.default.join(todosRoot, 'completed')),
2298
+ // planning_exists intentionally stays workstream/project-scoped — it
2299
+ // answers "does the ACTIVE planning dir exist", not a todos question.
1951
2300
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
1952
- todos_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos')),
1953
- pending_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')),
2301
+ todos_dir_exists: node_fs_1.default.existsSync(todosRoot),
2302
+ pending_dir_exists: node_fs_1.default.existsSync(pendingDir),
2303
+ // #2618: see PENDING_TODO_BULLET_MAX_CHARS comment / design doc. Consumed
2304
+ // by add-todo.md / check-todos.md's update_state step; omitted entirely
2305
+ // (rather than emitted with possibly-wrong data) when pendingReadOk is
2306
+ // false, so the workflow's fail-safe check can key off field presence.
2307
+ pending_read_ok: pendingReadOk,
2308
+ // #4384 fix: pass cwd as projectRoot so the bullet link renders
2309
+ // repo-relative — see pendingTodoLinkTarget.
2310
+ ...(pendingReadOk ? { pending_todos_markdown: renderPendingTodosMarkdown(todos, cwd) } : {}),
1954
2311
  };
1955
2312
  output(withProjectRoot(cwd, result), raw);
1956
2313
  }
@@ -1965,14 +2322,16 @@ function cmdInitMilestoneOp(cwd, raw) {
1965
2322
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
1966
2323
  const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
1967
2324
  const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
1968
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1969
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
2325
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
2326
+ // buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
2327
+ // bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
2328
+ const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, resolvePhaseIdConvention(cwd));
1970
2329
  let m;
1971
2330
  while ((m = phasePattern.exec(currentSection)) !== null) {
1972
2331
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
1973
- if (isSentinelPhaseId(m[1]))
2332
+ if (isSentinelPhaseId(m[phaseNumGroup]))
1974
2333
  continue;
1975
- roadmapPhaseNumbers.push(m[1]);
2334
+ roadmapPhaseNumbers.push(m[phaseNumGroup]);
1976
2335
  }
1977
2336
  }
1978
2337
  catch {
@@ -2065,7 +2424,10 @@ function cmdInitMilestoneOp(cwd, raw) {
2065
2424
  all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
2066
2425
  archived_milestones: archivedMilestones,
2067
2426
  archive_count: archivedMilestones.length,
2068
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2427
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
2428
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
2429
+ // the full evidence.
2430
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2069
2431
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2070
2432
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2071
2433
  archive_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'archive')),
@@ -2112,6 +2474,10 @@ function cmdInitManager(cwd, raw) {
2112
2474
  const config = loadConfig(cwd);
2113
2475
  const milestone = milestoneRecord(cwd);
2114
2476
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
2477
+ const phaseIdConvention = resolvePhaseIdConvention(cwd);
2478
+ const capturesBracketId = phaseIdConvention === 'bracket';
2479
+ const phaseHeadingPrefix = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, phaseIdConvention, capturesBracketId);
2480
+ const phaseHeadingPrefixNoCapture = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, phaseIdConvention);
2115
2481
  const paths = planningPaths(cwd);
2116
2482
  if (!node_fs_1.default.existsSync(paths.roadmap)) {
2117
2483
  error(`No ROADMAP.md found. Run ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', _slashRuntime)} first.`);
@@ -2121,38 +2487,48 @@ function cmdInitManager(cwd, raw) {
2121
2487
  }
2122
2488
  const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
2123
2489
  const content = extractCurrentMilestone(rawContent, cwd);
2124
- const phasesDir = paths.phases;
2125
2490
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2126
2491
  // CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
2127
2492
  // routed through it instead of a hand-rolled readdirSync + a separate
2128
2493
  // getMilestonePhaseFilter window check (which also never excluded
2129
2494
  // sentinels, unlike the owner).
2130
- const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, { cwd }).value;
2131
2495
  const _checkboxStates = new Map();
2132
- const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2496
+ const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2133
2497
  let _cbMatch;
2134
2498
  while ((_cbMatch = _cbPattern.exec(content)) !== null) {
2135
- _checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
2499
+ const phaseGroup = capturesBracketId ? 3 : 2;
2500
+ _checkboxStates.set(_cbMatch[phaseGroup], _cbMatch[1].toLowerCase() === 'x');
2136
2501
  }
2137
2502
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
2138
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2503
+ const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2139
2504
  const phases = [];
2140
2505
  let match;
2141
2506
  while ((match = phasePattern.exec(content)) !== null) {
2142
- const phaseNum = match[1];
2143
- const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
2507
+ const bracketId = capturesBracketId ? match[1] : undefined;
2508
+ const phaseNum = capturesBracketId ? match[2] : match[1];
2509
+ const phaseName = (capturesBracketId ? match[3] : match[2])
2510
+ .replace(/\(INSERTED\)/i, '')
2511
+ .trim();
2512
+ let displayId;
2513
+ if (bracketId) {
2514
+ try {
2515
+ displayId = renderPhaseId(parsePhaseId(`${bracketId}-${phaseNum}`));
2516
+ }
2517
+ catch {
2518
+ // The bracket selector is deliberately read-tolerant. If a heading is
2519
+ // non-canonical, retain the manager row without fabricating display_id.
2520
+ }
2521
+ }
2144
2522
  const sectionStart = match.index;
2145
2523
  const restOfContent = content.slice(sectionStart);
2146
- const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d[\d.]*/i);
2524
+ const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}\\d[\\d.]*`, 'i'));
2147
2525
  const sectionEnd = nextHeader
2148
2526
  ? sectionStart + nextHeader.index
2149
2527
  : content.length;
2150
2528
  const section = content.slice(sectionStart, sectionEnd);
2151
- const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
2152
- const goal = goalMatch ? goalMatch[1].trim() : null;
2529
+ const goal = roadmapParser.extractPhaseFieldMultiline(section, 'Goal');
2153
2530
  const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
2154
2531
  const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
2155
- const normalized = normalizePhaseName(phaseNum);
2156
2532
  let diskStatus = 'no_directory';
2157
2533
  let planCount = 0;
2158
2534
  let summaryCount = 0;
@@ -2165,14 +2541,23 @@ function cmdInitManager(cwd, raw) {
2165
2541
  let contextScope = SCOPE.COMPLETE;
2166
2542
  let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
2167
2543
  try {
2168
- // #3185 (ADR-3180 Decision 2) moved this lookup off the
2169
- // milestone-scoped set and onto the physical one; that scope choice is
2170
- // kept. Only the matcher is this PR's: matchPhaseDirs resolves
2171
- // digit-leading directory names the token predicate cannot (#2528).
2172
- const dirMatch = matchPhaseDirs(_phaseDirEntries, normalized).matches[0];
2544
+ // #4801: resolve through the canonical locator instead of a private
2545
+ // current-milestone-only scan. findPhaseInternal searches the live
2546
+ // .planning/phases directory FIRST (same set the retired
2547
+ // matchPhaseDirs/_phaseDirEntries pair scanned — the #3185 physical-dir
2548
+ // scope choice is inherited by the locator's live arm) and then falls
2549
+ // back through listArchiveVersionDirs (workstream-scoped, #2855), so an
2550
+ // ARCHIVED phase directory with a passing verification resolves instead
2551
+ // of reporting no_directory/phase_complete:false. Five other init
2552
+ // commands already route through this same primitive; this was the
2553
+ // holdout private copy (#4793-family declared-owner drift).
2554
+ const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention);
2555
+ const dirMatch = located && located['found']
2556
+ ? node_path_1.default.posix.basename(String(located['directory']))
2557
+ : null;
2173
2558
  if (dirMatch) {
2174
- const fullDir = node_path_1.default.join(phasesDir, dirMatch);
2175
- const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
2559
+ const fullDir = node_path_1.default.join(cwd, String(located['directory']));
2560
+ const phaseDirRel = String(located['directory']);
2176
2561
  // #4014 (epic #3473 B4-unreadable): this whole block used to swallow
2177
2562
  // ANY readdirSync failure below into the bare `catch { /* empty */ }`
2178
2563
  // at the bottom — an unreadable phase directory reported the exact
@@ -2246,6 +2631,7 @@ function cmdInitManager(cwd, raw) {
2246
2631
  const roadmapComplete = _checkboxStates.get(phaseNum) || false;
2247
2632
  phases.push({
2248
2633
  number: phaseNum,
2634
+ ...(displayId ? { display_id: displayId } : {}),
2249
2635
  name: phaseName,
2250
2636
  goal,
2251
2637
  depends_on,
@@ -2286,7 +2672,7 @@ function cmdInitManager(cwd, raw) {
2286
2672
  .filter((p) => p['phase_complete'] === true)
2287
2673
  .map((p) => normalizePhaseNumber(p['number'])));
2288
2674
  const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
2289
- const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2675
+ const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2290
2676
  let _allMatch;
2291
2677
  while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
2292
2678
  const phaseNum = normalizePhaseNumber(_allMatch[1]);
@@ -2312,13 +2698,47 @@ function cmdInitManager(cwd, raw) {
2312
2698
  function hasDepRelationship(numA, numB) {
2313
2699
  return reaches(numA, numB) || reaches(numB, numA);
2314
2700
  }
2701
+ // #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
2702
+ // context — directly following "Phase"/"Phases" — never a bare digit run.
2703
+ // The previous whole-field scrape matched the token grammar against every
2704
+ // digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
2705
+ // ("8bf403100d" → 8b, 403100d, …), bracketed ledger ids (WINDOWS #1843) and
2706
+ // the row's OWN number all became "dependencies", and deps_satisfied came
2707
+ // back false for phases whose prose declares none (50 of 92 phases in the
2708
+ // reporter's milestone). The anchored grammar (owned by phase-id.cts as
2709
+ // PHASE_DEP_REF_SOURCE, shared with planning-inspect's dependencies) keeps
2710
+ // lists fully extracted ("Phases 601 and 602", "Phase 601, 602, and 603",
2711
+ // "Phase 1-3") — silently dropping a REAL dependency would clear
2712
+ // deps_satisfied prematurely, the dangerous direction. Negation prose
2713
+ // ("dropped the dependency on Phase 654") is NOT detected: the issue's own
2714
+ // minimum keeps such tokens.
2715
+ const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
2716
+ const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
2315
2717
  for (const phase of phases) {
2316
2718
  if (!phase['depends_on'] ||
2317
2719
  /^none$/i.test(phase['depends_on'].trim())) {
2318
2720
  phase['deps_satisfied'] = true;
2319
2721
  }
2320
2722
  else {
2321
- const depNums = phase['depends_on'].match(new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi')) || [];
2723
+ const prose = phase['depends_on'];
2724
+ const ownNumber = normalizePhaseNumber(phase['number']);
2725
+ const depNums = [];
2726
+ const seen = new Set();
2727
+ let refMatch;
2728
+ depPhaseRefRe.lastIndex = 0;
2729
+ while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
2730
+ let tok;
2731
+ depTokenRe.lastIndex = 0;
2732
+ while ((tok = depTokenRe.exec(refMatch[1])) !== null) {
2733
+ const normalized = normalizePhaseNumber(tok[0]);
2734
+ if (normalized === ownNumber)
2735
+ continue; // #4764: never the row's own phase
2736
+ if (seen.has(normalized))
2737
+ continue;
2738
+ seen.add(normalized);
2739
+ depNums.push(tok[0]);
2740
+ }
2741
+ }
2322
2742
  phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
2323
2743
  phase['dep_phases'] = depNums;
2324
2744
  }
@@ -2447,10 +2867,23 @@ function cmdInitManager(cwd, raw) {
2447
2867
  recommended_actions: filteredActions,
2448
2868
  waiting_signal: waitingSignal,
2449
2869
  all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
2450
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2870
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
2871
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
2872
+ // the full evidence.
2873
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2451
2874
  roadmap_exists: true,
2452
2875
  state_exists: true,
2453
2876
  manager_flags: managerFlags,
2877
+ // #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths — same
2878
+ // pattern cmdInitPlanPhase already uses (existence-checked, toPosixPath'd,
2879
+ // null when absent) plus the archive dir composition milestone.cts's
2880
+ // `cmdMilestoneComplete` uses (#1911: planningPaths(cwd).planning +
2881
+ // 'milestones', workstream-aware). autonomous.md's discover_phases/
2882
+ // iterate/lifecycle steps consume these instead of hardcoding
2883
+ // `.planning/STATE.md` / `.planning/milestones/...`.
2884
+ state_path: node_fs_1.default.existsSync(paths.state) ? toPosixPath(paths.state) : null,
2885
+ roadmap_path: node_fs_1.default.existsSync(paths.roadmap) ? toPosixPath(paths.roadmap) : null,
2886
+ archive_dir: toPosixPath(node_path_1.default.join(paths.planning, 'milestones')),
2454
2887
  };
2455
2888
  output(withProjectRoot(cwd, result), raw);
2456
2889
  }
@@ -2469,10 +2902,57 @@ function cmdInitManager(cwd, raw) {
2469
2902
  */
2470
2903
  function cmdInitCompleteMilestone(cwd, raw, options = {}) {
2471
2904
  const gitCreateTag = detectGitCreateTag(cwd);
2905
+ // #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths for
2906
+ // complete-milestone.md's reorganize_roadmap_and_delete_originals step —
2907
+ // same pattern cmdInitPlanPhase already uses, mirrored here since this is
2908
+ // that workflow's own dedicated init entry point.
2909
+ const planningBase = planningDir(cwd);
2910
+ const statePath = node_path_1.default.join(planningBase, 'STATE.md');
2911
+ const roadmapPath = node_path_1.default.join(planningBase, 'ROADMAP.md');
2912
+ const archiveDir = node_path_1.default.join(planningBase, 'milestones');
2913
+ // #4455 follow-up (code-review finding): MILESTONES.md is workstream-scoped
2914
+ // too — cmdMilestoneComplete (src/milestone.cts) writes it via
2915
+ // planningPaths(cwd).planning (the workstream base, not root; #1911). It is
2916
+ // not the deliberately-root-scoped exception `todos` is (#4256) — an
2917
+ // earlier version of this fix wrongly treated it as a shared root file,
2918
+ // which would have made the safety commit below silently miss the actual
2919
+ // file milestone.complete just wrote under an active workstream.
2920
+ const milestonesPath = node_path_1.default.join(planningBase, 'MILESTONES.md');
2921
+ // #4455 follow-up round 2 (self-discovered regression): PROJECT.md, unlike
2922
+ // MILESTONES.md, is genuinely SHARED across a project's own workstreams —
2923
+ // never cloned per workstream. gsd-core/references/workstream-flag.md's
2924
+ // directory diagram marks it `# Shared`; new-milestone.md states it
2925
+ // outright ("PROJECT.md is shared across workstreams") and explicitly
2926
+ // SKIPS writing its `## Current Milestone` heading under an active
2927
+ // workstream specifically to avoid clobbering the one shared file (#2308);
2928
+ // cmdWorkstreamCreate (src/workstream.cts) never creates a PROJECT.md
2929
+ // under a workstream directory. The first version of this #4455 follow-up
2930
+ // wrongly generalized from planningPaths()'s structural shape (which
2931
+ // composes `project` under the workstream base) without checking an
2932
+ // actual PROJECT.md write path — resolved against planningRoot(cwd)
2933
+ // (round 2), but that ALSO ignores the separate GSD_PROJECT dimension
2934
+ // (multi-project namespacing, #3749: PROJECT.md legitimately lives at
2935
+ // `.planning/<project>/PROJECT.md` when GSD_PROJECT is set — a real,
2936
+ // tested, pre-existing feature planningRoot's blanket root-only read
2937
+ // broke), caught by gsd-test on this fix's own first push. `planningDir`
2938
+ // with `ws` explicitly nulled (never read from GSD_WORKSTREAM) but
2939
+ // `project` left to default from GSD_PROJECT is the correct middle
2940
+ // ground: respects project-namespacing, ignores workstream-namespacing.
2941
+ const projectPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
2942
+ // REQUIREMENTS.md is workstream-scoped the same way (planningPaths(cwd).requirements,
2943
+ // src/planning-workspace.cts) — the git-rm-after-archive step needs the
2944
+ // resolved path too, not the literal root file.
2945
+ const requirementsPath = node_path_1.default.join(planningBase, 'REQUIREMENTS.md');
2472
2946
  const result = {
2473
2947
  // #2994: hoisted from complete-milestone.md's git_tag step
2474
2948
  // <config-check> resolver (git.create_tag, fail-open default true).
2475
2949
  git_create_tag: gitCreateTag,
2950
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
2951
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
2952
+ archive_dir: toPosixPath(archiveDir),
2953
+ milestones_path: node_fs_1.default.existsSync(milestonesPath) ? toPosixPath(milestonesPath) : null,
2954
+ project_path: node_fs_1.default.existsSync(projectPath) ? toPosixPath(projectPath) : null,
2955
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
2476
2956
  };
2477
2957
  result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'complete-milestone', {
2478
2958
  gitCreateTag,
@@ -2598,7 +3078,7 @@ function cmdInitUpdate(cwd, raw, options = {}) {
2598
3078
  function cmdInitTransition(cwd, raw, options = {}) {
2599
3079
  // #3579 root-cause fix: read-only informational field — peek, don't
2600
3080
  // self-heal (see cmdInitNewMilestone's identical rationale above).
2601
- const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
3081
+ const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
2602
3082
  const workstreamActive = !!resolvedWorkstream;
2603
3083
  const result = {
2604
3084
  other_active_workstreams: workstreamActive
@@ -2690,7 +3170,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
2690
3170
  // non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
2691
3171
  // here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
2692
3172
  // which would misreport a present-but-bad marker as no marker at all.
2693
- const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
3173
+ const _resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
2694
3174
  if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
2695
3175
  // #3579: getActiveWorkstream now inherits a pointer-less session's read
2696
3176
  // from the shared .planning/active-workstream marker, so reaching this
@@ -2717,13 +3197,22 @@ function cmdInitProgress(cwd, raw, options = {}) {
2717
3197
  const roadmapCheckboxStates = new Map();
2718
3198
  try {
2719
3199
  const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
2720
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
2721
- const headingPattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
3200
+ const progressConvention = resolvePhaseIdConvention(cwd);
3201
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
3202
+ // buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
3203
+ // bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
3204
+ const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention);
2722
3205
  let hm;
2723
3206
  while ((hm = headingPattern.exec(roadmapContent)) !== null) {
2724
- roadmapPhaseNums.add(hm[1]);
2725
- roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
2726
- }
3207
+ roadmapPhaseNums.add(hm[hNumGroup]);
3208
+ roadmapPhaseNames.set(hm[hNumGroup], hm[hNameGroup].replace(/\(INSERTED\)/i, '').trim());
3209
+ }
3210
+ // #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
3211
+ // out-of-scope reasoning as roadmapPhaseMatchesExactPrefix/detectPhaseMvpMode
3212
+ // above; a direct LABEL_ONLY selector call here would have added a third
3213
+ // uncounted LABEL_ONLY consumer to init.cts's pinned
3214
+ // tests/adr-612-bracket-heading-selection.test.cjs census.
3215
+ // phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above.
2727
3216
  const cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2728
3217
  let cbm;
2729
3218
  while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
@@ -2880,7 +3369,10 @@ function cmdInitProgress(cwd, raw, options = {}) {
2880
3369
  paused_at: pausedAt,
2881
3370
  has_work_in_progress: !!currentPhase,
2882
3371
  phase_mvp_mode: phaseMvpMode,
2883
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
3372
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
3373
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
3374
+ // the full evidence.
3375
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2884
3376
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2885
3377
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2886
3378
  // #4040: partial-init discriminator (see buildInitCompletenessFields) —
@@ -2890,7 +3382,19 @@ function cmdInitProgress(cwd, raw, options = {}) {
2890
3382
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
2891
3383
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2892
3384
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2893
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
3385
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
3386
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
3387
+ // #4456 correction: an isolated review pass initially "fixed" this to
3388
+ // planningDir(cwd, null) on the assumption that config.json is shared
3389
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
3390
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
3391
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
3392
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
3393
+ // caught the regression immediately. The diagram is stale for
3394
+ // config.json specifically (same class of staleness already found for
3395
+ // `milestones/` during the #4455 follow-up) — reverted to the
3396
+ // workstream-aware planningDir(cwd), matching the established,
3397
+ // ADR-governed, tested contract.
2894
3398
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
2895
3399
  };
2896
3400
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -3125,12 +3629,9 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3125
3629
  warn(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`);
3126
3630
  continue;
3127
3631
  }
3128
- const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
3129
- if (!pathCheck['safe']) {
3130
- const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => {
3131
- const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true });
3132
- return Boolean(rootCheck['safe']);
3133
- });
3632
+ const globalSkillMdContained = (0, security_cjs_1.tryWithinRoot)(globalSkillMd, globalSkillsBase, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
3633
+ if (globalSkillMdContained === null) {
3634
+ const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => (0, security_cjs_1.tryWithinRoot)(globalSkillMd, root, security_cjs_1.PathAcceptance.AbsoluteInsideRoot) !== null);
3134
3635
  if (!acceptedViaTrustedRoot) {
3135
3636
  warn(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
3136
3637
  continue;
@@ -3139,15 +3640,23 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3139
3640
  // trace, not a skip, so it must not land in the diagnostics warnings[].
3140
3641
  process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
3141
3642
  }
3643
+ // `ref` is an emitted display token, not a path anything reads or writes
3644
+ // through — the containment check above is a gate, not a path producer.
3645
+ // Emitting the validated (realpath-resolved, platform-separator) value
3646
+ // instead of this literal broke symlinked skill dirs and Windows output.
3647
+ // The only filesystem read here (existsSync above) already ran on the
3648
+ // lexical path before containment was checked, so ADR-4650's "use the
3649
+ // validated value" rule doesn't apply to this emission.
3142
3650
  validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
3143
3651
  continue;
3144
3652
  }
3145
- const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot);
3146
- if (!pathCheck['safe']) {
3147
- warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck['error']}\n`);
3653
+ const skillPathContained = (0, security_cjs_1.tryWithinRoot)(skillPath, projectRoot);
3654
+ if (skillPathContained === null) {
3655
+ warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": not confined to the project directory\n`);
3148
3656
  continue;
3149
3657
  }
3150
- const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md');
3658
+ // ADR-4650: the validated value is the value used — never re-derive from raw input.
3659
+ const skillMdPath = node_path_1.default.join(skillPathContained, 'SKILL.md');
3151
3660
  if (!node_fs_1.default.existsSync(skillMdPath)) {
3152
3661
  // #2941: if the bare name matches a global skill, hint at the global: prefix.
3153
3662
  // The bare name resolves as project-relative (which doesn't exist), but the
@@ -3203,20 +3712,37 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3203
3712
  // persona fallback. Triggering the fallback for claude would change the
3204
3713
  // documented "unconfigured → empty block" contract that agent-skills tests
3205
3714
  // pin.
3715
+ //
3716
+ // #4407 (ADR-4139 stream 2): this is the one place GSD's own agent-persona
3717
+ // content is served through a real code seam rather than an eagerly
3718
+ // @-included file, so the compact/canonical choice is made here in code
3719
+ // (a real exit code) instead of a prose config-get gate. Compact is tried
3720
+ // first when requested; a missing compact sibling falls back to canonical
3721
+ // with the fallback disclosed in the payload itself, never a silent switch.
3722
+ let agentPayloadVariant = null;
3206
3723
  if (!block) {
3207
3724
  const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
3208
3725
  if (runtime !== 'claude') {
3209
3726
  const agentCheck = checkAgentsInstalled(runtime, projectRoot);
3210
3727
  const agentsDir = agentCheck?.agents_dir;
3211
3728
  if (typeof agentsDir === 'string' && agentsDir.length > 0) {
3212
- const agentFile = node_path_1.default.join(agentsDir, `${agentType}.md`);
3213
- try {
3214
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(agentFile);
3215
- if (content && content.length > 0) {
3216
- block = content;
3729
+ const compactRequested = readConfigJsonBoolean(projectRoot, ['workflow', 'compact_content']);
3730
+ const compactContent = compactRequested
3731
+ ? readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.compact.md`))
3732
+ : null;
3733
+ if (compactContent !== null) {
3734
+ block = compactContent;
3735
+ agentPayloadVariant = 'compact';
3736
+ }
3737
+ else {
3738
+ const canonicalContent = readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.md`));
3739
+ if (canonicalContent !== null) {
3740
+ block = compactRequested
3741
+ ? `<!-- gsd: no compact payload registered for ${agentType}; serving canonical -->\n\n${canonicalContent}`
3742
+ : canonicalContent;
3743
+ agentPayloadVariant = 'canonical';
3217
3744
  }
3218
3745
  }
3219
- catch { /* agent file not found — fall through to empty block */ }
3220
3746
  }
3221
3747
  }
3222
3748
  }
@@ -3263,8 +3789,8 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3263
3789
  if (jsonMode) {
3264
3790
  // Build the Resolution<AgentSkillsValue> envelope and embed .value additively.
3265
3791
  // Flat fields are retained unchanged for back-compat; value formalises the
3266
- // Resolution convention (ADR-1411 P3, #1416). source/degraded remain
3267
- // config-provenance extras, outside the Resolution<T> envelope.
3792
+ // Resolution convention (ADR-1411 P3, #1416). source/degraded/agent_payload_variant
3793
+ // remain config-provenance extras, outside the Resolution<T> envelope.
3268
3794
  const resolution = (0, resolution_cjs_1.makeResolution)({ block: block || '', skills_count: normalizedPaths.length }, { configured, reason, warnings: diagnostics.warnings });
3269
3795
  output({
3270
3796
  agent_type: agentType,
@@ -3275,6 +3801,7 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3275
3801
  reason,
3276
3802
  source,
3277
3803
  degraded,
3804
+ agent_payload_variant: agentPayloadVariant,
3278
3805
  value: resolution.value,
3279
3806
  }, raw);
3280
3807
  return;
@@ -3563,4 +4090,5 @@ module.exports = {
3563
4090
  cmdAgentSkills,
3564
4091
  buildSkillManifest,
3565
4092
  cmdSkillManifest,
4093
+ renderPendingTodosMarkdown,
3566
4094
  };