@opengsd/gsd-core 1.12.0 → 1.14.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 (455) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. 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
@@ -34,6 +35,9 @@ const phaseId = require("./phase-id.cjs");
34
35
  const worktreeSafety = require("./worktree-safety.cjs");
35
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
36
37
  const planningWorkspace = require("./planning-workspace.cjs");
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const planningScopeMod = require("./planning-scope.cjs");
40
+ const { SCOPE } = planningScopeMod;
37
41
  const secrets_cjs_1 = require("./secrets.cjs");
38
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
39
43
  const scanPhasePlans = require("./plan-scan.cjs");
@@ -83,9 +87,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
83
87
  const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
84
88
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
85
89
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
86
- const { normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
90
+ const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName } = phaseId;
87
91
  const { pruneOrphanedWorktrees } = worktreeSafety;
88
- const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
92
+ const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
89
93
  const { determinePhaseStatus } = commandsMod;
90
94
  const { extractFrontmatter } = frontmatterMod;
91
95
  const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
@@ -254,7 +258,15 @@ function withProjectRoot(cwd, result) {
254
258
  if (config.project_code) {
255
259
  result['project_code'] = config.project_code;
256
260
  }
257
- const projectMdPath = node_path_1.default.join(planningDir(cwd), 'PROJECT.md');
261
+ // #4455 follow-up (self-discovered): PROJECT.md is shared across a
262
+ // project's own workstreams (never cloned per workstream) but DOES
263
+ // respect the separate GSD_PROJECT multi-project namespace (#3749) — see
264
+ // cmdInitCompleteMilestone's projectPath comment for the full evidence.
265
+ // `ws` explicitly nulled, `project` left to default from GSD_PROJECT.
266
+ // Reading via the workstream-aware planningDir(cwd) meant every init.*
267
+ // call's project_title silently vanished whenever a workstream was
268
+ // active, since no PROJECT.md ever exists at the workstream path.
269
+ const projectMdPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
258
270
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(projectMdPath);
259
271
  if (content) {
260
272
  const h1Match = content.match(/^#\s+(.+)$/m);
@@ -474,6 +486,16 @@ function readConfigJsonBoolean(cwd, keyPath) {
474
486
  return false;
475
487
  }
476
488
  }
489
+ /** Reads `filePath`; returns its content, or `null` when missing/unreadable/empty. */
490
+ function readNonEmptyFileOrNull(filePath) {
491
+ try {
492
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
493
+ return content && content.length > 0 ? content : null;
494
+ }
495
+ catch {
496
+ return null;
497
+ }
498
+ }
477
499
  /**
478
500
  * Bounded, non-throwing read of a dotted key path from `.planning/config.json`,
479
501
  * returning the raw resolved value (any JSON type) or `undefined` on any
@@ -771,7 +793,6 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
771
793
  ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
772
794
  : null;
773
795
  const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
774
- const wf = (config.workflow ?? {});
775
796
  // #3188: these paths are null when the file is absent, matching the contract
776
797
  // the conditional sibling fields (context_path, patterns_path, ...) already
777
798
  // honour and that ultraplan-phase.md / execute-phase.md gate on. Hoisted so
@@ -782,7 +803,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
782
803
  const result = {
783
804
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
784
805
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
785
- tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
806
+ tdd_mode: options['tdd'] || Boolean(config.tdd_mode) || false,
786
807
  commit_docs: config.commit_docs,
787
808
  sub_repos: config.sub_repos,
788
809
  parallelization: config.parallelization,
@@ -826,10 +847,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
826
847
  runnable_plans: phaseInfo?.['runnable_plans'] || [],
827
848
  runnable_count: phaseInfo?.['runnable_plans']?.length || 0,
828
849
  branch_name: config.branching_strategy === 'phase' && phaseInfo
829
- ? config.phase_branch_template
830
- .replace('{project}', config.project_code || '')
831
- .replace('{phase}', normalizePhaseName(phaseInfo['phase_number']))
832
- .replace('{slug}', phaseInfo['phase_slug'] || 'phase')
850
+ ? renderPhaseBranchName(config.phase_branch_template.replace('{project}', config.project_code || ''), phaseInfo['phase_number'], phaseInfo['phase_slug'])
833
851
  : config.branching_strategy === 'milestone'
834
852
  ? config.milestone_branch_template
835
853
  .replace('{milestone}', milestone['version'] ?? '')
@@ -845,6 +863,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
845
863
  // #3188: null when the file is absent (parity with patterns_path/context_path).
846
864
  state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
847
865
  roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
866
+ // #4456 correction: an isolated review pass initially "fixed" this to
867
+ // planningDir(cwd, null) on the assumption that config.json is shared
868
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
869
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
870
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
871
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
872
+ // caught the regression immediately. The diagram is stale for
873
+ // config.json specifically (same class of staleness already found for
874
+ // `milestones/` during the #4455 follow-up) — reverted to the
875
+ // workstream-aware planningDir(cwd), matching the established,
876
+ // ADR-governed, tested contract.
848
877
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
849
878
  // #2376: execute-phase.md's verify_phase_goal step reads this instead of
850
879
  // hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
@@ -924,7 +953,6 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
924
953
  const granularityOverride = options['granularity'];
925
954
  assertValidGranularityOverride(granularityOverride, error);
926
955
  const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
927
- const wf = (config.workflow ?? {});
928
956
  // #3188: see cmdInitExecutePhase — null when absent, parity with the
929
957
  // conditional sibling fields in this same result object.
930
958
  const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
@@ -934,11 +962,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
934
962
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
935
963
  planner_model: resolveModelInternal(cwd, 'gsd-planner'),
936
964
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
937
- tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
965
+ tdd_mode: options['tdd'] || Boolean(config.tdd_mode) || false,
938
966
  granularity,
939
- research_enabled: wf['research'],
967
+ research_enabled: config.research,
940
968
  plan_checker_enabled: config.plan_checker,
941
- nyquist_validation_enabled: wf['nyquist_validation'],
969
+ nyquist_validation_enabled: config.nyquist_validation,
942
970
  commit_docs: config.commit_docs,
943
971
  text_mode: config.text_mode,
944
972
  auto_advance: !!(config.auto_advance),
@@ -960,6 +988,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
960
988
  : 'Pending',
961
989
  has_research: phaseInfo?.['has_research'] || false,
962
990
  has_context: phaseInfo?.['has_context'] || false,
991
+ // #4014 (epic #3473 B4-unreadable): additive scope signal adjacent to
992
+ // has_context — SCOPE.COMPLETE by default (no phase directory to read is
993
+ // a genuine, not-unreadable answer), overwritten below to whatever
994
+ // findContextMdIn(phaseDirFull) reports once a directory is known.
995
+ context_scope: SCOPE.COMPLETE,
963
996
  has_reviews: phaseInfo?.['has_reviews'] || false,
964
997
  has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
965
998
  plan_count: phaseInfo?.['plans']?.length || 0,
@@ -974,6 +1007,13 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
974
1007
  };
975
1008
  if (phaseInfo?.['directory']) {
976
1009
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
1010
+ // #4014 (epic #3473 B4-unreadable): findContextMdIn's directory-string
1011
+ // form never throws, so this can record the real scope BEFORE the
1012
+ // pre-existing `fs.readdirSync(phaseDirFull)` immediately below
1013
+ // (unchanged) throws on the same unreadable directory and is caught
1014
+ // exactly as before — additive only, the failure control-flow for
1015
+ // context_path/research_path/etc. is untouched.
1016
+ result['context_scope'] = findContextMdIn(phaseDirFull).scope;
977
1017
  try {
978
1018
  const files = node_fs_1.default.readdirSync(phaseDirFull);
979
1019
  const phaseDirName = node_path_1.default.basename(phaseDirFull);
@@ -1100,6 +1140,40 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
1100
1140
  result['prior_verify_commands'] = priorVerifyCommands;
1101
1141
  output(withProjectRoot(cwd, result), raw);
1102
1142
  }
1143
+ // #4040: shared partial-init discriminator for the init.progress / init.resume /
1144
+ // init.new-project payloads. A bootstrap interrupted before the core quartet
1145
+ // (PROJECT.md / REQUIREMENTS.md / ROADMAP.md / STATE.md) all landed is a
1146
+ // DISTINCT routing state from "new project" and from "between milestones";
1147
+ // pre-#4040 the payloads could not express it, so progress.md mis-routed it to
1148
+ // Route F and resume-project.md offered STATE.md reconstruction.
1149
+ //
1150
+ // Negative-space guard: `milestone.complete` archives ROADMAP.md (and
1151
+ // REQUIREMENTS.md) but always leaves MILESTONES.md behind — so MILESTONES.md
1152
+ // present proves missing core files are archival (between-milestones), never an
1153
+ // unfinished bootstrap. REQUIREMENTS.md is written by new-project BEFORE
1154
+ // ROADMAP/STATE, so its absence also proves init never finished.
1155
+ function buildInitCompletenessFields(cwd) {
1156
+ const dir = planningDir(cwd);
1157
+ const planningExists = node_fs_1.default.existsSync(dir);
1158
+ const requirementsExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'REQUIREMENTS.md'));
1159
+ const milestonesExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'MILESTONES.md'));
1160
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1161
+ // workstreams (see cmdInitCompleteMilestone's projectPath comment for the
1162
+ // full evidence) — checked at planningRoot(cwd), never the workstream-scoped
1163
+ // `dir`, so a workstream whose own REQUIREMENTS/ROADMAP/STATE are all
1164
+ // present isn't wrongly reported incomplete just because the shared
1165
+ // PROJECT.md isn't ALSO duplicated under its own directory.
1166
+ const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')) &&
1167
+ requirementsExists &&
1168
+ node_fs_1.default.existsSync(node_path_1.default.join(dir, 'ROADMAP.md')) &&
1169
+ node_fs_1.default.existsSync(node_path_1.default.join(dir, 'STATE.md'));
1170
+ return {
1171
+ planning_exists: planningExists,
1172
+ requirements_exists: requirementsExists,
1173
+ milestones_exists: milestonesExists,
1174
+ init_incomplete: planningExists && !coreComplete && !milestonesExists,
1175
+ };
1176
+ }
1103
1177
  function cmdInitNewProject(cwd, raw, options = {}) {
1104
1178
  const config = loadConfig(cwd);
1105
1179
  const homedir = node_os_1.default.homedir();
@@ -1119,23 +1193,51 @@ function cmdInitNewProject(cwd, raw, options = {}) {
1119
1193
  synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
1120
1194
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
1121
1195
  commit_docs: config.commit_docs,
1122
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1196
+ // #4040: partial-init discriminator (see buildInitCompletenessFields).
1197
+ // Spread BEFORE this literal's own planning_exists so the existing
1198
+ // root-scoped (`pathExistsInternal(cwd, '.planning')`) semantics for that
1199
+ // one key stay byte-identical for existing consumers.
1200
+ ...buildInitCompletenessFields(cwd),
1201
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1202
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1203
+ // the full evidence.
1204
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1123
1205
  has_codebase_map: hasCodebaseMap,
1124
1206
  planning_exists: pathExistsInternal(cwd, '.planning'),
1125
1207
  has_existing_code: hasCode,
1126
1208
  has_package_file: hasPackageFile,
1127
1209
  is_brownfield: isBrownfield,
1210
+ // #4458: new-project.md's Step 5.1 (Sub-Repo Detection) used to run its own
1211
+ // narrower `find ... -exec test -d "{}/.git"` predicate, which requires
1212
+ // .git to be a DIRECTORY and so silently excluded linked git worktree
1213
+ // children (.git is a FILE there). detectSubRepos already handled this
1214
+ // correctly (fs.existsSync, not isDirectory) but had zero callers anywhere
1215
+ // in the codebase — reused here instead of leaving the workflow to
1216
+ // maintain its own duplicate, narrower detection logic.
1217
+ sub_repos_detected: coreUtils.detectSubRepos(cwd),
1128
1218
  needs_codebase_map: isBrownfield && !hasCodebaseMap,
1129
1219
  ...getInitGitState(cwd),
1130
1220
  brave_search_available: hasBraveSearch,
1131
1221
  firecrawl_available: hasFirecrawl,
1132
1222
  exa_search_available: hasExaSearch,
1133
1223
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1134
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1224
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1225
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1135
1226
  // #2376: new-project.md's research-synthesizer/roadmapper spawn prompts
1136
1227
  // read these instead of hardcoding '.planning/...' literals.
1137
1228
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1138
1229
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1230
+ // #4456 correction: an isolated review pass initially "fixed" this to
1231
+ // planningDir(cwd, null) on the assumption that config.json is shared
1232
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
1233
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
1234
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
1235
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
1236
+ // caught the regression immediately. The diagram is stale for
1237
+ // config.json specifically (same class of staleness already found for
1238
+ // `milestones/` during the #4455 follow-up) — reverted to the
1239
+ // workstream-aware planningDir(cwd), matching the established,
1240
+ // ADR-governed, tested contract.
1139
1241
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
1140
1242
  research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
1141
1243
  };
@@ -1158,13 +1260,12 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1158
1260
  // owns — routed through it instead of a local readdirSync + hand-rolled
1159
1261
  // window filter (which also never excluded sentinels, unlike the owner).
1160
1262
  const phaseDirCount = listMilestonePhaseDirs(phasesDir, { cwd }).value.length;
1161
- const wf = (config.workflow ?? {});
1162
1263
  const result = {
1163
1264
  researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
1164
1265
  synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
1165
1266
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
1166
1267
  commit_docs: config.commit_docs,
1167
- research_enabled: wf['research'],
1268
+ research_enabled: config.research,
1168
1269
  // #3216 review Finding 2: `?? null` so an unresolved milestone still emits
1169
1270
  // the key with an explicit `null` rather than letting JSON.stringify drop
1170
1271
  // it — an omitted key reaches the prompt layer's `{current_milestone}`
@@ -1178,18 +1279,41 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1178
1279
  phase_archive_path: latestCompleted
1179
1280
  ? toPosixPath(node_path_1.default.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))
1180
1281
  : null,
1181
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1282
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1283
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1284
+ // the full evidence.
1285
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1182
1286
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1183
1287
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1184
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1288
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1289
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1185
1290
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1186
1291
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1187
1292
  // #2376: new-milestone.md's research-synthesizer/roadmapper spawn prompts
1188
1293
  // read these instead of hardcoding '.planning/...' literals.
1189
1294
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1295
+ // #4456 correction: an isolated review pass initially "fixed" this to
1296
+ // planningDir(cwd, null) on the assumption that config.json is shared
1297
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
1298
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
1299
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
1300
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
1301
+ // caught the regression immediately. The diagram is stale for
1302
+ // config.json specifically (same class of staleness already found for
1303
+ // `milestones/` during the #4455 follow-up) — reverted to the
1304
+ // workstream-aware planningDir(cwd), matching the established,
1305
+ // ADR-governed, tested contract.
1190
1306
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
1191
1307
  research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
1192
1308
  milestones_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'MILESTONES.md')),
1309
+ // #4456: new-milestone.md's Step 6 stages the phase-archive move
1310
+ // (`git add .planning/milestones/ .planning/phases/`) — both
1311
+ // workstream-scoped (phases_dir mirrors the phasesDir local above;
1312
+ // archive_dir mirrors cmdInitCompleteMilestone's own field of the same
1313
+ // name), so a literal root `git add` misses the actual files
1314
+ // phases.clear just moved under an active workstream.
1315
+ phases_dir: toPosixPath(phasesDir),
1316
+ archive_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'milestones')),
1193
1317
  };
1194
1318
  // `state:flat-mode` (#2994): whether NO workstream is active — the inverse
1195
1319
  // of `state:workstream-active` (introduced for `cmdInitTransition` below).
@@ -1246,6 +1370,10 @@ function cmdInitQuick(cwd, description, raw, options = {}) {
1246
1370
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
1247
1371
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
1248
1372
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
1373
+ // #3936: Step 4.75 dispatches gsd-phase-researcher; resolve its own tier
1374
+ // (parity with cmdInitPlanPhase) so the research spawn stops pinning
1375
+ // planner_model.
1376
+ researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
1249
1377
  // #2072: the quick review step spawns gsd-code-reviewer; resolve its own model
1250
1378
  // so model_overrides / models.verification apply (was reusing executor_model).
1251
1379
  reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
@@ -1281,10 +1409,52 @@ function cmdInitQuick(cwd, description, raw, options = {}) {
1281
1409
  result['section_manifest'] = buildSectionManifestField(cwd, null, sectionManifestOptions, 'quick');
1282
1410
  output(withProjectRoot(cwd, result), raw);
1283
1411
  }
1412
+ /**
1413
+ * `init.quick-batch` (#3676, Phase 4 of epic #3344, ADR-1239 "Quick-batch
1414
+ * binding"). Unlike `cmdInitQuick`, this init bundle does NOT allocate a
1415
+ * quick id / slug / task directory itself — batch-level id allocation and
1416
+ * `BATCH.json` creation is the job of the `quick-batch create` CLI verb
1417
+ * (`src/quick-batch-command-router.cts`, wrapping `createBatch` in
1418
+ * `src/quick-batch.cts`). This bundle supplies the per-role model profiles,
1419
+ * `commit_docs`, the roadmap/planning existence checks `quick-batch.md`'s
1420
+ * ROADMAP.md gate needs (same check `cmdInitQuick` runs), the `.planning/quick`
1421
+ * directory path, and the `section_manifest` field gating the optional
1422
+ * `--research`/`--validate` step fragments — the same `flag:--research`/
1423
+ * `flag:--validate` atoms `quick`'s own section manifest already uses
1424
+ * (`WHEN_VOCABULARY` is workflow-agnostic; no new atom is needed). `--discuss`/
1425
+ * `--full` are rejected by `quick-batch-dispatch.cts`'s `parseQuickBatchArgs`
1426
+ * before this init bundle is ever reached, so no `discuss`/`full` flag key is
1427
+ * accepted here (unlike `cmdInitQuick`, which still supports both).
1428
+ */
1429
+ function cmdInitQuickBatch(cwd, raw, options = {}) {
1430
+ const config = loadConfig(cwd);
1431
+ const result = {
1432
+ planner_model: resolveModelInternal(cwd, 'gsd-planner'),
1433
+ executor_model: resolveModelInternal(cwd, 'gsd-executor'),
1434
+ checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
1435
+ verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
1436
+ researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
1437
+ reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
1438
+ commit_docs: config.commit_docs,
1439
+ // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase; a
1440
+ // per-item task_dir is re-derived by the workflow itself (quick_id +
1441
+ // generate-slug over that item's description), never allocated here.
1442
+ quick_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'quick')),
1443
+ quick_batches_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'quick-batches')),
1444
+ roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1445
+ planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1446
+ };
1447
+ // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
1448
+ result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'quick-batch');
1449
+ output(withProjectRoot(cwd, result), raw);
1450
+ }
1284
1451
  function cmdInitIngestDocs(cwd, raw) {
1285
1452
  const config = loadConfig(cwd);
1286
1453
  const result = {
1287
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1454
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1455
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1456
+ // the full evidence.
1457
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1288
1458
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1289
1459
  ...getInitGitState(cwd),
1290
1460
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The
@@ -1293,7 +1463,8 @@ function cmdInitIngestDocs(cwd, raw) {
1293
1463
  // hardcoded bare '.planning/intel/...', '.planning/PROJECT.md', etc.
1294
1464
  // literals into their Agent(prompt=...) blocks; those now interpolate
1295
1465
  // these fields instead.
1296
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1466
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1467
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1297
1468
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1298
1469
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1299
1470
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
@@ -1305,12 +1476,11 @@ function cmdInitIngestDocs(cwd, raw) {
1305
1476
  }
1306
1477
  function cmdInitOnboard(cwd, raw, options = {}) {
1307
1478
  const config = loadConfig(cwd);
1308
- const workflowConfig = (config.workflow ?? {});
1309
1479
  const result = {
1310
1480
  ...buildOnboardProjection(cwd, {
1311
1481
  commitDocs: !!config.commit_docs,
1312
1482
  fast: options['fast'] === true,
1313
- textMode: options['text'] === true || !!config.text_mode || !!workflowConfig['text_mode'],
1483
+ textMode: options['text'] === true || !!config.text_mode,
1314
1484
  }),
1315
1485
  ...getInitGitState(cwd),
1316
1486
  };
@@ -1323,14 +1493,23 @@ function cmdInitResume(cwd, raw) {
1323
1493
  if (agentIdRaw !== null)
1324
1494
  interruptedAgentId = agentIdRaw.trim();
1325
1495
  const result = {
1496
+ // #4040: partial-init discriminator (see buildInitCompletenessFields).
1497
+ // Spread FIRST so this literal's own root-scoped planning_exists
1498
+ // (planningRoot) keeps its existing semantics; init_incomplete itself
1499
+ // keys off the artifact dir (planningDir) where the core docs live.
1500
+ ...buildInitCompletenessFields(cwd),
1326
1501
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1327
1502
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1328
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1503
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1504
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1505
+ // the full evidence.
1506
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1329
1507
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1330
1508
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1331
1509
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1332
1510
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1333
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1511
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1512
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1334
1513
  has_interrupted_agent: !!interruptedAgentId,
1335
1514
  interrupted_agent_id: interruptedAgentId,
1336
1515
  commit_docs: config.commit_docs,
@@ -1720,6 +1899,9 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1720
1899
  padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
1721
1900
  has_research: phaseInfo?.['has_research'] || false,
1722
1901
  has_context: phaseInfo?.['has_context'] || false,
1902
+ // #4014 (epic #3473 B4-unreadable): see the parallel field in
1903
+ // cmdInitPlanPhase — additive scope signal adjacent to has_context.
1904
+ context_scope: SCOPE.COMPLETE,
1723
1905
  has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
1724
1906
  has_verification: phaseInfo?.['has_verification'] || false,
1725
1907
  has_reviews: phaseInfo?.['has_reviews'] || false,
@@ -1734,6 +1916,9 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1734
1916
  };
1735
1917
  if (phaseInfo?.['directory']) {
1736
1918
  const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
1919
+ // #4014 (epic #3473 B4-unreadable): see the parallel site in
1920
+ // cmdInitPlanPhase — additive only, failure control-flow below unchanged.
1921
+ result['context_scope'] = findContextMdIn(phaseDirFull).scope;
1737
1922
  try {
1738
1923
  const files = node_fs_1.default.readdirSync(phaseDirFull);
1739
1924
  const phaseDirName = node_path_1.default.basename(phaseDirFull);
@@ -1800,13 +1985,156 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1800
1985
  }
1801
1986
  output(withProjectRoot(cwd, result), raw);
1802
1987
  }
1988
+ // #2618: bullet-cap and title-floor for renderPendingTodosMarkdown below.
1989
+ // 240 matches the bound already vetted by maintainer review on the prior
1990
+ // attempt at this issue (PR #2662) — re-deriving a different number would be
1991
+ // pure bikeshedding, not a correctness improvement. See
1992
+ // .gsd/phase/feat-2618-compact-todo-pointers/40-design.md.
1993
+ const PENDING_TODO_BULLET_MAX_CHARS = 240;
1994
+ const PENDING_TODO_TITLE_FLOOR = 15;
1995
+ const PENDING_TODO_AREA_FLOOR = 3;
1996
+ function sanitizePendingTodoInline(value) {
1997
+ // Defensive: the regex captures that populate title/area/needs can only
1998
+ // ever match a single line, so this is belt-and-suspenders against any
1999
+ // future non-regex-sourced input, not a reachable case today.
2000
+ return value.replace(/[\r\n]+/g, ' ').trim();
2001
+ }
2002
+ function truncatePendingTodoText(value, maxLen) {
2003
+ if (value.length <= maxLen)
2004
+ return value;
2005
+ if (maxLen <= 1)
2006
+ return value.slice(0, Math.max(0, maxLen));
2007
+ return `${value.slice(0, maxLen - 1)}…`;
2008
+ }
2009
+ /**
2010
+ * #2618: pure renderer for STATE.md's "### Pending Todos" section BODY (not
2011
+ * the heading). One bullet per todo, each capped at
2012
+ * PENDING_TODO_BULLET_MAX_CHARS. `gsd-core/workflows/add-todo.md` and
2013
+ * `check-todos.md` splice this string in verbatim instead of free-hand
2014
+ * editing STATE.md — see the design doc for why this is real, unit-tested
2015
+ * code rather than a prose algorithm (DEFECT.GENERATIVE-FIX: a prose
2016
+ * algorithm duplicated as a test oracle is exactly the divergence class
2017
+ * this avoids).
2018
+ *
2019
+ * #4384 regression fix: the optional `projectRoot` makes the bullet's
2020
+ * `[todo file](…)` link repo-relative (see pendingTodoLinkTarget) so the cap
2021
+ * is deterministic w.r.t. where the repo is checked out. Omitting it keeps
2022
+ * the legacy absolute-link behavior for existing direct callers.
2023
+ */
2024
+ function renderPendingTodosMarkdown(todos, projectRoot) {
2025
+ if (!Array.isArray(todos) || todos.length === 0) {
2026
+ return 'None yet.';
2027
+ }
2028
+ return todos.map((todo) => renderPendingTodoBullet(todo, projectRoot)).join('\n');
2029
+ }
2030
+ function pendingTodoFieldAsString(value, fallback) {
2031
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
2032
+ }
2033
+ /**
2034
+ * #4384 regression fix: the bullet's markdown link target, rendered
2035
+ * repo-relative when `projectRoot` is given and the todo's `path` is
2036
+ * absolute. The JSON `todos[].path` field stays absolute (#2376 contract);
2037
+ * only the rendered display link changes — embedding the machine-variable
2038
+ * absolute base let macOS's /private/var/folders/… temp paths consume the
2039
+ * 240-char budget and drop the "Needs <solution>" clause on long-path
2040
+ * machines only (next's own macos CI shard went red on exactly this, run
2041
+ * 34038716700). Repo-relative links also resolve correctly from STATE.md at
2042
+ * the repo root and survive repo moves.
2043
+ */
2044
+ function pendingTodoLinkTarget(todo, projectRoot) {
2045
+ const raw = pendingTodoFieldAsString(todo['path'], '');
2046
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0 || !node_path_1.default.isAbsolute(raw)) {
2047
+ return raw;
2048
+ }
2049
+ const rel = toPosixPath(node_path_1.default.relative(projectRoot, raw));
2050
+ if (rel.length === 0 || node_path_1.default.isAbsolute(rel)) {
2051
+ // Degenerate (path === projectRoot) or Windows cross-drive fallback:
2052
+ // keep the raw target rather than emitting an empty or incorrect link.
2053
+ return raw;
2054
+ }
2055
+ return rel;
2056
+ }
2057
+ /**
2058
+ * #4439: the stored `created:` frontmatter (and the JSON `todos[].created`
2059
+ * field it round-trips through) is always a full ISO-8601 timestamp, by
2060
+ * design — this is the display-only seam that reformats it to the
2061
+ * date-only `[date]` bullet documented in docs/reference/state-md.md and
2062
+ * docs/COMMANDS.md. A value that doesn't start with a well-formed
2063
+ * `YYYY-MM-DD` (the 'unknown' fallback, or any other non-conforming
2064
+ * string) passes through unchanged rather than being mangled.
2065
+ */
2066
+ function pendingTodoDateOnly(value) {
2067
+ const match = value.match(/^\d{4}-\d{2}-\d{2}/);
2068
+ return match ? match[0] : value;
2069
+ }
2070
+ function renderPendingTodoBullet(todo, projectRoot) {
2071
+ const date = pendingTodoDateOnly(sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown')));
2072
+ let area = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['area'], 'general'));
2073
+ let title = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['title'], 'Untitled'));
2074
+ // Strip trailing "." so the fixed "Needs ....` template below never
2075
+ // produces a doubled period when the source text already ended in one.
2076
+ let needs = typeof todo['needs'] === 'string'
2077
+ ? sanitizePendingTodoInline(todo['needs']).replace(/\.+$/, '')
2078
+ : '';
2079
+ const link = `[todo file](${pendingTodoLinkTarget(todo, projectRoot)})`;
2080
+ const assemble = () => {
2081
+ const needsClause = needs ? ` — Needs ${needs}.` : '';
2082
+ return `- [${date}] [${area}] ${title} — ${link}${needsClause}`;
2083
+ };
2084
+ let line = assemble();
2085
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2086
+ return line;
2087
+ // 1) Drop the needs clause entirely first — date/area/title/link untouched.
2088
+ needs = '';
2089
+ line = assemble();
2090
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2091
+ return line;
2092
+ // 2) Shorten the title next, down to a floor — date/area/link untouched.
2093
+ const titleOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
2094
+ const targetTitleLen = Math.max(PENDING_TODO_TITLE_FLOOR, title.length - titleOverage);
2095
+ if (targetTitleLen < title.length) {
2096
+ title = truncatePendingTodoText(title, targetTitleLen);
2097
+ line = assemble();
2098
+ }
2099
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2100
+ return line;
2101
+ // 3) Shorten area as a last resort — date and the markdown link are never
2102
+ // altered (link correctness > strict cap; see design doc "Known limits").
2103
+ const areaOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
2104
+ const targetAreaLen = Math.max(PENDING_TODO_AREA_FLOOR, area.length - areaOverage);
2105
+ if (targetAreaLen < area.length) {
2106
+ area = truncatePendingTodoText(area, targetAreaLen);
2107
+ line = assemble();
2108
+ }
2109
+ return line;
2110
+ }
1803
2111
  function cmdInitTodos(cwd, area, raw) {
1804
2112
  const config = loadConfig(cwd);
1805
- const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
2113
+ // #4256: todos are root-scoped shared state (migrateToWorkstreams keeps
2114
+ // them at .planning/todos/ and every workflow writer writes that literal
2115
+ // path), so this read resolves via todosDir(cwd) — NOT planningDir(cwd),
2116
+ // which would look in .planning/workstreams/<ws>/todos/ under a workstream
2117
+ // (a directory nothing creates) and report existing todos as absent.
2118
+ const todosRoot = todosDir(cwd);
2119
+ const pendingDir = node_path_1.default.join(todosRoot, 'pending');
1806
2120
  let count = 0;
1807
2121
  const todos = [];
2122
+ // #2618: distinct from "genuinely zero pending todos" — false only when
2123
+ // readdirSync itself failed for a reason OTHER than the directory simply
2124
+ // not existing yet (ENOENT), mirroring the ENOENT-vs-other-errno split
2125
+ // already used above in this file (#3885, ADR-3473 §8.5). Without this,
2126
+ // a real I/O/permission error on the pending dir would look identical to
2127
+ // "no pending todos" and could wipe an existing, non-empty Pending Todos
2128
+ // section in STATE.md on refresh — the fail-safe requirement for #2618.
2129
+ let pendingReadOk = true;
1808
2130
  try {
1809
- const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md'));
2131
+ // #2618: sorted so pending_todos_markdown's bullet order is stable across
2132
+ // runs — readdirSync's order is filesystem-dependent, not contractually
2133
+ // stable, and an unstable order would reorder every bullet on an
2134
+ // unrelated re-render, turning a one-line git diff into a full-section
2135
+ // rewrite (must-have #3). Filenames are `YYYY-MM-DD-slug.md`, so this
2136
+ // also yields a sensible chronological order as a side effect.
2137
+ const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md')).sort();
1810
2138
  for (const file of files) {
1811
2139
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file));
1812
2140
  if (content === null)
@@ -1818,6 +2146,21 @@ function cmdInitTodos(cwd, area, raw) {
1818
2146
  // #2337: kept in parity with cmdListTodos — surface severity when
1819
2147
  // present, omit the key entirely for todos with no severity line.
1820
2148
  const severityMatch = content.match(/^severity:\s*(.+)$/m);
2149
+ // #2618: first non-empty line of the `## Solution` body, used as the
2150
+ // bullet's "Needs ..." clause. "TBD" (the create_file template's own
2151
+ // placeholder for an unresolved solution) renders no clause at all
2152
+ // rather than the useless literal "Needs TBD.".
2153
+ const solutionSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => h.level === 2 && h.text.trim() === 'Solution');
2154
+ let needs;
2155
+ if (solutionSection) {
2156
+ const firstLine = solutionSection.body
2157
+ .split('\n')
2158
+ .map((l) => l.trim())
2159
+ .find((l) => l.length > 0);
2160
+ if (firstLine && firstLine.toUpperCase() !== 'TBD') {
2161
+ needs = firstLine;
2162
+ }
2163
+ }
1821
2164
  const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
1822
2165
  if (area && todoArea !== area)
1823
2166
  continue;
@@ -1828,8 +2171,9 @@ function cmdInitTodos(cwd, area, raw) {
1828
2171
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
1829
2172
  area: todoArea,
1830
2173
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1831
- path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'pending', file)),
2174
+ path: toPosixPath(node_path_1.default.join(pendingDir, file)),
1832
2175
  ...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
2176
+ ...(needs ? { needs } : {}),
1833
2177
  });
1834
2178
  }
1835
2179
  catch {
@@ -1837,8 +2181,11 @@ function cmdInitTodos(cwd, area, raw) {
1837
2181
  }
1838
2182
  }
1839
2183
  }
1840
- catch {
1841
- /* intentionally empty */
2184
+ catch (err) {
2185
+ const code = err?.code;
2186
+ if (code !== 'ENOENT') {
2187
+ pendingReadOk = false;
2188
+ }
1842
2189
  }
1843
2190
  const result = {
1844
2191
  commit_docs: config.commit_docs,
@@ -1848,11 +2195,22 @@ function cmdInitTodos(cwd, area, raw) {
1848
2195
  todos,
1849
2196
  area_filter: area || null,
1850
2197
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1851
- pending_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')),
1852
- completed_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'completed')),
2198
+ // #4256: both dir fields probe the ROOT todos tree via todosDir(cwd).
2199
+ pending_dir: toPosixPath(pendingDir),
2200
+ completed_dir: toPosixPath(node_path_1.default.join(todosRoot, 'completed')),
2201
+ // planning_exists intentionally stays workstream/project-scoped — it
2202
+ // answers "does the ACTIVE planning dir exist", not a todos question.
1853
2203
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
1854
- todos_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos')),
1855
- pending_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')),
2204
+ todos_dir_exists: node_fs_1.default.existsSync(todosRoot),
2205
+ pending_dir_exists: node_fs_1.default.existsSync(pendingDir),
2206
+ // #2618: see PENDING_TODO_BULLET_MAX_CHARS comment / design doc. Consumed
2207
+ // by add-todo.md / check-todos.md's update_state step; omitted entirely
2208
+ // (rather than emitted with possibly-wrong data) when pendingReadOk is
2209
+ // false, so the workflow's fail-safe check can key off field presence.
2210
+ pending_read_ok: pendingReadOk,
2211
+ // #4384 fix: pass cwd as projectRoot so the bullet link renders
2212
+ // repo-relative — see pendingTodoLinkTarget.
2213
+ ...(pendingReadOk ? { pending_todos_markdown: renderPendingTodosMarkdown(todos, cwd) } : {}),
1856
2214
  };
1857
2215
  output(withProjectRoot(cwd, result), raw);
1858
2216
  }
@@ -1967,7 +2325,10 @@ function cmdInitMilestoneOp(cwd, raw) {
1967
2325
  all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
1968
2326
  archived_milestones: archivedMilestones,
1969
2327
  archive_count: archivedMilestones.length,
1970
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2328
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
2329
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
2330
+ // the full evidence.
2331
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1971
2332
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1972
2333
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1973
2334
  archive_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'archive')),
@@ -2062,6 +2423,9 @@ function cmdInitManager(cwd, raw) {
2062
2423
  let hasResearch = false;
2063
2424
  let lastActivity = null;
2064
2425
  let isActive = false;
2426
+ // #4014 (epic #3473 B4-unreadable): default COMPLETE — no directory at
2427
+ // all (dirMatch not found) is a genuine, not-unreadable answer.
2428
+ let contextScope = SCOPE.COMPLETE;
2065
2429
  let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
2066
2430
  try {
2067
2431
  // #3185 (ADR-3180 Decision 2) moved this lookup off the
@@ -2072,6 +2436,17 @@ function cmdInitManager(cwd, raw) {
2072
2436
  if (dirMatch) {
2073
2437
  const fullDir = node_path_1.default.join(phasesDir, dirMatch);
2074
2438
  const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
2439
+ // #4014 (epic #3473 B4-unreadable): this whole block used to swallow
2440
+ // ANY readdirSync failure below into the bare `catch { /* empty */ }`
2441
+ // at the bottom — an unreadable phase directory reported the exact
2442
+ // same `has_context: false` / `disk_status: 'no_directory'` as a
2443
+ // directory that never existed. findContextMdIn's directory-string
2444
+ // form never throws, so it can record the real scope (COMPLETE vs
2445
+ // UNREADABLE) here, BEFORE the pre-existing `fs.readdirSync(fullDir)`
2446
+ // immediately below (unchanged) throws on the exact same unreadable
2447
+ // directory and is caught exactly as before — this line is additive
2448
+ // only, the failure control-flow is untouched.
2449
+ contextScope = findContextMdIn(fullDir).scope;
2075
2450
  const phaseFiles = node_fs_1.default.readdirSync(fullDir);
2076
2451
  planCount = listPhasePlanFiles(fullDir).length;
2077
2452
  summaryCount = listPhaseSummaryFiles(fullDir).length;
@@ -2146,6 +2521,7 @@ function cmdInitManager(cwd, raw) {
2146
2521
  ...completion,
2147
2522
  last_activity: lastActivity,
2148
2523
  is_active: isActive,
2524
+ context_scope: contextScope,
2149
2525
  });
2150
2526
  }
2151
2527
  const MAX_NAME_WIDTH = 20;
@@ -2334,10 +2710,23 @@ function cmdInitManager(cwd, raw) {
2334
2710
  recommended_actions: filteredActions,
2335
2711
  waiting_signal: waitingSignal,
2336
2712
  all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
2337
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2713
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
2714
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
2715
+ // the full evidence.
2716
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2338
2717
  roadmap_exists: true,
2339
2718
  state_exists: true,
2340
2719
  manager_flags: managerFlags,
2720
+ // #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths — same
2721
+ // pattern cmdInitPlanPhase already uses (existence-checked, toPosixPath'd,
2722
+ // null when absent) plus the archive dir composition milestone.cts's
2723
+ // `cmdMilestoneComplete` uses (#1911: planningPaths(cwd).planning +
2724
+ // 'milestones', workstream-aware). autonomous.md's discover_phases/
2725
+ // iterate/lifecycle steps consume these instead of hardcoding
2726
+ // `.planning/STATE.md` / `.planning/milestones/...`.
2727
+ state_path: node_fs_1.default.existsSync(paths.state) ? toPosixPath(paths.state) : null,
2728
+ roadmap_path: node_fs_1.default.existsSync(paths.roadmap) ? toPosixPath(paths.roadmap) : null,
2729
+ archive_dir: toPosixPath(node_path_1.default.join(paths.planning, 'milestones')),
2341
2730
  };
2342
2731
  output(withProjectRoot(cwd, result), raw);
2343
2732
  }
@@ -2356,10 +2745,57 @@ function cmdInitManager(cwd, raw) {
2356
2745
  */
2357
2746
  function cmdInitCompleteMilestone(cwd, raw, options = {}) {
2358
2747
  const gitCreateTag = detectGitCreateTag(cwd);
2748
+ // #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths for
2749
+ // complete-milestone.md's reorganize_roadmap_and_delete_originals step —
2750
+ // same pattern cmdInitPlanPhase already uses, mirrored here since this is
2751
+ // that workflow's own dedicated init entry point.
2752
+ const planningBase = planningDir(cwd);
2753
+ const statePath = node_path_1.default.join(planningBase, 'STATE.md');
2754
+ const roadmapPath = node_path_1.default.join(planningBase, 'ROADMAP.md');
2755
+ const archiveDir = node_path_1.default.join(planningBase, 'milestones');
2756
+ // #4455 follow-up (code-review finding): MILESTONES.md is workstream-scoped
2757
+ // too — cmdMilestoneComplete (src/milestone.cts) writes it via
2758
+ // planningPaths(cwd).planning (the workstream base, not root; #1911). It is
2759
+ // not the deliberately-root-scoped exception `todos` is (#4256) — an
2760
+ // earlier version of this fix wrongly treated it as a shared root file,
2761
+ // which would have made the safety commit below silently miss the actual
2762
+ // file milestone.complete just wrote under an active workstream.
2763
+ const milestonesPath = node_path_1.default.join(planningBase, 'MILESTONES.md');
2764
+ // #4455 follow-up round 2 (self-discovered regression): PROJECT.md, unlike
2765
+ // MILESTONES.md, is genuinely SHARED across a project's own workstreams —
2766
+ // never cloned per workstream. gsd-core/references/workstream-flag.md's
2767
+ // directory diagram marks it `# Shared`; new-milestone.md states it
2768
+ // outright ("PROJECT.md is shared across workstreams") and explicitly
2769
+ // SKIPS writing its `## Current Milestone` heading under an active
2770
+ // workstream specifically to avoid clobbering the one shared file (#2308);
2771
+ // cmdWorkstreamCreate (src/workstream.cts) never creates a PROJECT.md
2772
+ // under a workstream directory. The first version of this #4455 follow-up
2773
+ // wrongly generalized from planningPaths()'s structural shape (which
2774
+ // composes `project` under the workstream base) without checking an
2775
+ // actual PROJECT.md write path — resolved against planningRoot(cwd)
2776
+ // (round 2), but that ALSO ignores the separate GSD_PROJECT dimension
2777
+ // (multi-project namespacing, #3749: PROJECT.md legitimately lives at
2778
+ // `.planning/<project>/PROJECT.md` when GSD_PROJECT is set — a real,
2779
+ // tested, pre-existing feature planningRoot's blanket root-only read
2780
+ // broke), caught by gsd-test on this fix's own first push. `planningDir`
2781
+ // with `ws` explicitly nulled (never read from GSD_WORKSTREAM) but
2782
+ // `project` left to default from GSD_PROJECT is the correct middle
2783
+ // ground: respects project-namespacing, ignores workstream-namespacing.
2784
+ const projectPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
2785
+ // REQUIREMENTS.md is workstream-scoped the same way (planningPaths(cwd).requirements,
2786
+ // src/planning-workspace.cts) — the git-rm-after-archive step needs the
2787
+ // resolved path too, not the literal root file.
2788
+ const requirementsPath = node_path_1.default.join(planningBase, 'REQUIREMENTS.md');
2359
2789
  const result = {
2360
2790
  // #2994: hoisted from complete-milestone.md's git_tag step
2361
2791
  // <config-check> resolver (git.create_tag, fail-open default true).
2362
2792
  git_create_tag: gitCreateTag,
2793
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
2794
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
2795
+ archive_dir: toPosixPath(archiveDir),
2796
+ milestones_path: node_fs_1.default.existsSync(milestonesPath) ? toPosixPath(milestonesPath) : null,
2797
+ project_path: node_fs_1.default.existsSync(projectPath) ? toPosixPath(projectPath) : null,
2798
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
2363
2799
  };
2364
2800
  result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'complete-milestone', {
2365
2801
  gitCreateTag,
@@ -2522,7 +2958,7 @@ function cmdInitTransition(cwd, raw, options = {}) {
2522
2958
  * location has one source instead of two kept in sync by hand.
2523
2959
  * - `debugger_model` — `resolveModelInternal`, which IS what `query
2524
2960
  * resolve-model --pick model` returns (`cmdResolveModel`, src/commands.cts).
2525
- * - `tdd_mode` — the `Boolean(wf['tdd_mode'])` idiom `cmdInitExecutePhase` and
2961
+ * - `tdd_mode` — the `Boolean(config.tdd_mode)` idiom `cmdInitExecutePhase` and
2526
2962
  * `cmdInitPlanPhase` already use. `/gsd:debug` has no `--tdd` flag, so the
2527
2963
  * sibling handlers' `options['tdd'] ||` disjunct is deliberately omitted
2528
2964
  * rather than carried as a phantom.
@@ -2540,7 +2976,6 @@ function cmdInitTransition(cwd, raw, options = {}) {
2540
2976
  */
2541
2977
  function cmdInitDebug(cwd, raw, options = {}) {
2542
2978
  const config = loadConfig(cwd);
2543
- const wf = (config.workflow ?? {});
2544
2979
  const result = {
2545
2980
  commit_docs: config.commit_docs,
2546
2981
  // #2376: absolute — debug.md builds `debug_file_path` as
@@ -2548,7 +2983,7 @@ function cmdInitDebug(cwd, raw, options = {}) {
2548
2983
  // own cwd may differ from the orchestrator's.
2549
2984
  debug_dir: toPosixPath(planningPaths(cwd).debug),
2550
2985
  debugger_model: resolveModelInternal(cwd, 'gsd-debugger'),
2551
- tdd_mode: Boolean(wf['tdd_mode']),
2986
+ tdd_mode: Boolean(config.tdd_mode),
2552
2987
  diagnose: options['diagnose'] === true,
2553
2988
  };
2554
2989
  // Additive, optional field — degrades to null while `debug` has no key in
@@ -2709,7 +3144,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
2709
3144
  }
2710
3145
  }
2711
3146
  }
2712
- phases.sort((a, b) => parseInt(a['number'], 10) - parseInt(b['number'], 10));
3147
+ phases.sort((a, b) => comparePhaseNum(a['number'], b['number']));
2713
3148
  // #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
2714
3149
  // above could claim nextPhase from a stray out-of-order artifact directory
2715
3150
  // (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
@@ -2768,13 +3203,32 @@ function cmdInitProgress(cwd, raw, options = {}) {
2768
3203
  paused_at: pausedAt,
2769
3204
  has_work_in_progress: !!currentPhase,
2770
3205
  phase_mvp_mode: phaseMvpMode,
2771
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
3206
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
3207
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
3208
+ // the full evidence.
3209
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2772
3210
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2773
3211
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
3212
+ // #4040: partial-init discriminator (see buildInitCompletenessFields) —
3213
+ // also adds planning_exists / requirements_exists / milestones_exists so
3214
+ // progress.md's init_context routing never has to fall back to Glob.
3215
+ ...buildInitCompletenessFields(cwd),
2774
3216
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
2775
3217
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2776
3218
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2777
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
3219
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
3220
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
3221
+ // #4456 correction: an isolated review pass initially "fixed" this to
3222
+ // planningDir(cwd, null) on the assumption that config.json is shared
3223
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
3224
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
3225
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
3226
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
3227
+ // caught the regression immediately. The diagram is stale for
3228
+ // config.json specifically (same class of staleness already found for
3229
+ // `milestones/` during the #4455 follow-up) — reverted to the
3230
+ // workstream-aware planningDir(cwd), matching the established,
3231
+ // ADR-governed, tested contract.
2778
3232
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
2779
3233
  };
2780
3234
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -3009,12 +3463,9 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3009
3463
  warn(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`);
3010
3464
  continue;
3011
3465
  }
3012
- const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
3013
- if (!pathCheck['safe']) {
3014
- const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => {
3015
- const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true });
3016
- return Boolean(rootCheck['safe']);
3017
- });
3466
+ const globalSkillMdContained = (0, security_cjs_1.tryWithinRoot)(globalSkillMd, globalSkillsBase, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
3467
+ if (globalSkillMdContained === null) {
3468
+ const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => (0, security_cjs_1.tryWithinRoot)(globalSkillMd, root, security_cjs_1.PathAcceptance.AbsoluteInsideRoot) !== null);
3018
3469
  if (!acceptedViaTrustedRoot) {
3019
3470
  warn(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
3020
3471
  continue;
@@ -3023,15 +3474,23 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3023
3474
  // trace, not a skip, so it must not land in the diagnostics warnings[].
3024
3475
  process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
3025
3476
  }
3477
+ // `ref` is an emitted display token, not a path anything reads or writes
3478
+ // through — the containment check above is a gate, not a path producer.
3479
+ // Emitting the validated (realpath-resolved, platform-separator) value
3480
+ // instead of this literal broke symlinked skill dirs and Windows output.
3481
+ // The only filesystem read here (existsSync above) already ran on the
3482
+ // lexical path before containment was checked, so ADR-4650's "use the
3483
+ // validated value" rule doesn't apply to this emission.
3026
3484
  validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
3027
3485
  continue;
3028
3486
  }
3029
- const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot);
3030
- if (!pathCheck['safe']) {
3031
- warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck['error']}\n`);
3487
+ const skillPathContained = (0, security_cjs_1.tryWithinRoot)(skillPath, projectRoot);
3488
+ if (skillPathContained === null) {
3489
+ warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": not confined to the project directory\n`);
3032
3490
  continue;
3033
3491
  }
3034
- const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md');
3492
+ // ADR-4650: the validated value is the value used — never re-derive from raw input.
3493
+ const skillMdPath = node_path_1.default.join(skillPathContained, 'SKILL.md');
3035
3494
  if (!node_fs_1.default.existsSync(skillMdPath)) {
3036
3495
  // #2941: if the bare name matches a global skill, hint at the global: prefix.
3037
3496
  // The bare name resolves as project-relative (which doesn't exist), but the
@@ -3087,20 +3546,37 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3087
3546
  // persona fallback. Triggering the fallback for claude would change the
3088
3547
  // documented "unconfigured → empty block" contract that agent-skills tests
3089
3548
  // pin.
3549
+ //
3550
+ // #4407 (ADR-4139 stream 2): this is the one place GSD's own agent-persona
3551
+ // content is served through a real code seam rather than an eagerly
3552
+ // @-included file, so the compact/canonical choice is made here in code
3553
+ // (a real exit code) instead of a prose config-get gate. Compact is tried
3554
+ // first when requested; a missing compact sibling falls back to canonical
3555
+ // with the fallback disclosed in the payload itself, never a silent switch.
3556
+ let agentPayloadVariant = null;
3090
3557
  if (!block) {
3091
3558
  const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
3092
3559
  if (runtime !== 'claude') {
3093
3560
  const agentCheck = checkAgentsInstalled(runtime, projectRoot);
3094
3561
  const agentsDir = agentCheck?.agents_dir;
3095
3562
  if (typeof agentsDir === 'string' && agentsDir.length > 0) {
3096
- const agentFile = node_path_1.default.join(agentsDir, `${agentType}.md`);
3097
- try {
3098
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(agentFile);
3099
- if (content && content.length > 0) {
3100
- block = content;
3563
+ const compactRequested = readConfigJsonBoolean(projectRoot, ['workflow', 'compact_content']);
3564
+ const compactContent = compactRequested
3565
+ ? readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.compact.md`))
3566
+ : null;
3567
+ if (compactContent !== null) {
3568
+ block = compactContent;
3569
+ agentPayloadVariant = 'compact';
3570
+ }
3571
+ else {
3572
+ const canonicalContent = readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.md`));
3573
+ if (canonicalContent !== null) {
3574
+ block = compactRequested
3575
+ ? `<!-- gsd: no compact payload registered for ${agentType}; serving canonical -->\n\n${canonicalContent}`
3576
+ : canonicalContent;
3577
+ agentPayloadVariant = 'canonical';
3101
3578
  }
3102
3579
  }
3103
- catch { /* agent file not found — fall through to empty block */ }
3104
3580
  }
3105
3581
  }
3106
3582
  }
@@ -3147,8 +3623,8 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3147
3623
  if (jsonMode) {
3148
3624
  // Build the Resolution<AgentSkillsValue> envelope and embed .value additively.
3149
3625
  // Flat fields are retained unchanged for back-compat; value formalises the
3150
- // Resolution convention (ADR-1411 P3, #1416). source/degraded remain
3151
- // config-provenance extras, outside the Resolution<T> envelope.
3626
+ // Resolution convention (ADR-1411 P3, #1416). source/degraded/agent_payload_variant
3627
+ // remain config-provenance extras, outside the Resolution<T> envelope.
3152
3628
  const resolution = (0, resolution_cjs_1.makeResolution)({ block: block || '', skills_count: normalizedPaths.length }, { configured, reason, warnings: diagnostics.warnings });
3153
3629
  output({
3154
3630
  agent_type: agentType,
@@ -3159,6 +3635,7 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3159
3635
  reason,
3160
3636
  source,
3161
3637
  degraded,
3638
+ agent_payload_variant: agentPayloadVariant,
3162
3639
  value: resolution.value,
3163
3640
  }, raw);
3164
3641
  return;
@@ -3419,6 +3896,7 @@ module.exports = {
3419
3896
  cmdInitNewProject,
3420
3897
  cmdInitNewMilestone,
3421
3898
  cmdInitQuick,
3899
+ cmdInitQuickBatch,
3422
3900
  cmdInitIngestDocs,
3423
3901
  cmdInitOnboard,
3424
3902
  cmdInitResume,
@@ -3446,4 +3924,5 @@ module.exports = {
3446
3924
  cmdAgentSkills,
3447
3925
  buildSkillManifest,
3448
3926
  cmdSkillManifest,
3927
+ renderPendingTodosMarkdown,
3449
3928
  };