@opengsd/gsd-core 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -269,5 +269,53 @@ function parsePlanDocument(content, planPath = '') {
269
269
  taskCount: tasks.length,
270
270
  };
271
271
  }
272
- const planDocument = { TASK_KIND, parsePlanDocument, planIdFromFile };
272
+ const planDocument = { TASK_KIND, parsePlanDocument, planIdFromFile, extractThreatRegisterIds };
273
+ /**
274
+ * #4683 — first-cell IDs of the STRIDE register rows inside every
275
+ * `<threat_model>` block. The register is a markdown table (the
276
+ * `<threat_model>` template in agents/gsd-planner.md): one row per threat,
277
+ * first cell `T-{phase}-NN` — decimal phases included — or the reserved
278
+ * `T-{phase}-SC` supply-chain row. Only digit-suffixed IDs match: `-SC` is
279
+ * deliberately shared by EVERY plan in a phase (planner rule "Keep
280
+ * `T-{phase}-SC` in `<threat_model>`"), so it can never be a uniqueness
281
+ * violation. IDs in prose or non-threat tables never count; only register
282
+ * rows inside a threat_model block do. One entry per matched row, in document
283
+ * order — deciding that the same ID in two plans is a collision is the
284
+ * aggregator's question (init.cts), not the per-document parser's.
285
+ *
286
+ * Knowingly unmatched residual classes (#4683 review, accepted): lowercase
287
+ * `t-47-01`, letter suffixes (`T-47-05A`), annotated first cells
288
+ * (`| T-47-06 (revised) |`), IDs in non-first cells, and an unterminated
289
+ * `<threat_model>` block all yield no claim. All are off-template shapes — the
290
+ * planner template fixes the row grammar — so the residual risk is silent
291
+ * under-detection, never a false hard-stop.
292
+ */
293
+ const THREAT_MODEL_BLOCK_RE = /<threat_model>([\s\S]*?)<\/threat_model>/gi;
294
+ const THREAT_REGISTER_ROW_RE = /^[^\S\n]*\|[^\S\n]*(T-\d+(?:\.\d+)?-\d+)[^\S\n]*\|/;
295
+ function extractThreatRegisterIds(content) {
296
+ // Fenced code blocks are prose, not registers (#4683 review MAJOR): a plan
297
+ // that QUOTES an existing register — exactly what the gap-closure flow tells
298
+ // the planner to read — must not have its quoted IDs counted as claims, or
299
+ // the execute-phase gate would hard-stop a correct phase. Same line-toggling
300
+ // idiom as the deferred-scope scan in phase.cts.
301
+ const lines = [];
302
+ let inFence = false;
303
+ for (const line of content.split(/\r?\n/)) {
304
+ if (/^\s*(?:```|~~~)/.test(line)) {
305
+ inFence = !inFence;
306
+ lines.push('');
307
+ continue;
308
+ }
309
+ lines.push(inFence ? '' : line);
310
+ }
311
+ const ids = [];
312
+ for (const blockMatch of lines.join('\n').matchAll(THREAT_MODEL_BLOCK_RE)) {
313
+ for (const line of blockMatch[1].split('\n')) {
314
+ const row = line.match(THREAT_REGISTER_ROW_RE);
315
+ if (row)
316
+ ids.push(row[1]);
317
+ }
318
+ }
319
+ return ids;
320
+ }
273
321
  module.exports = planDocument;
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+ /**
3
+ * Planning Document — the parse -> mutate -> serialize seam for a `.planning/`
4
+ * root artifact BODY (ADR-4910, epic #4906 Phase 1, #4917).
5
+ *
6
+ * Composes the existing structural seams — never reimplements them:
7
+ * - `markdown-sectionizer.cjs` (`tokenizeHeadings`, `collectSections`,
8
+ * `scanFencedBlocks`, `scanInlineCodeSpans`) for headings/sections and
9
+ * fence/inline-code awareness.
10
+ * - `markdown-table.cjs` (`splitTableRow`, `isDelimiterRow`,
11
+ * `parseMarkdownTable`) for GFM table detection and validation.
12
+ * - `artifacts.cjs` (`isCanonicalPlanningFile`) for the artifact-kind gate.
13
+ *
14
+ * This phase migrates NO call site — it is purely additive (ADR-4910 §7).
15
+ * Only `boldField` nodes are writable; `table`/`checklist` nodes parse and
16
+ * read only. Phase 3 (#4958) checked its own evidence (#4736, #4793) and
17
+ * found neither needed a table/checklist writer here — see ADR-4910's
18
+ * 2026-09-24 amendment. A writer for either kind is unclaimed until a real
19
+ * call site names it.
20
+ *
21
+ * Hyrum's Law commitment (row 3 of the design's behaviour table): `serialize`
22
+ * with zero staged edits returns `doc.source` BYTE-IDENTICAL — never a
23
+ * re-render (#4499's root cause). Every byte outside an edited `valueSpan` is
24
+ * the ORIGINAL source, spliced, never regenerated.
25
+ *
26
+ * ADR-457 build-at-publish: source in src/planning-document.cts, compiled to
27
+ * gsd-core/bin/lib/planning-document.cjs (gitignored).
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.PLANNING_ARTIFACTS = void 0;
34
+ exports.parsePlanningDoc = parsePlanningDoc;
35
+ exports.findField = findField;
36
+ exports.readNode = readNode;
37
+ exports.setFieldValue = setFieldValue;
38
+ exports.hasUnreadableNodes = hasUnreadableNodes;
39
+ exports.serialize = serialize;
40
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
41
+ const markdown_table_cjs_1 = require("./markdown-table.cjs");
42
+ const artifacts_cjs_1 = require("./artifacts.cjs");
43
+ // `frontmatter.cts` uses `export =` (CJS-style single export object), so it
44
+ // is imported as a default import (esModuleInterop), not a named import.
45
+ const frontmatter_cjs_1 = __importDefault(require("./frontmatter.cjs"));
46
+ const { frontmatterRegion } = frontmatter_cjs_1.default;
47
+ /**
48
+ * Canonical `.planning/` root artifact basenames this seam recognises,
49
+ * derived from the SAME registry `isCanonicalPlanningFile` consults
50
+ * (`artifacts.cts`'s `CANONICAL_EXACT`) — never a second, independently
51
+ * maintained list.
52
+ *
53
+ * Filtered to `.md` names only: `CANONICAL_EXACT` also carries non-markdown
54
+ * artifacts (`config.json`, `state.json`, `milestone.lock`, …) that this
55
+ * parser has no grammar for. Handing that JSON/lock content to the markdown
56
+ * parser below returns a successful EMPTY document (`nodes: []`), which reads
57
+ * as "this document records nothing" when the truth is "wrong kind entirely"
58
+ * — the empty-vs-error confusion #4917 / ADR-4910 §5 exists to eliminate. Do
59
+ * NOT remove this filter to "restore" the full registry.
60
+ */
61
+ exports.PLANNING_ARTIFACTS = Object.freeze(Array.from(artifacts_cjs_1.CANONICAL_EXACT).filter((name) => name.endsWith('.md')));
62
+ // ─── Internal helpers ───────────────────────────────────────────────────────
63
+ let nodeCounter = 0;
64
+ function mintId(kind) {
65
+ nodeCounter += 1;
66
+ return `${kind}-${nodeCounter}-${Math.random().toString(36).slice(2, 8)}`;
67
+ }
68
+ function splitLinesInfo(source) {
69
+ const out = [];
70
+ let offset = 0;
71
+ const rawLines = source.split('\n');
72
+ for (let i = 0; i < rawLines.length; i++) {
73
+ const raw = rawLines[i];
74
+ const hasCR = raw.endsWith('\r');
75
+ const text = hasCR ? raw.slice(0, -1) : raw;
76
+ out.push({ text, start: offset, end: offset + text.length });
77
+ offset += raw.length + 1; // +1 for the '\n' split on ('\r' already counted in raw.length)
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * Locate the frontmatter block, if any, by COMPOSING `frontmatter.cts`'s
83
+ * `frontmatterRegion` — the fence-detection grammar (byte-0 rule, BOM strip,
84
+ * `\n---` search, CR handling) lives there, once, and this seam never
85
+ * re-derives it (ADR-4910 Decision 1).
86
+ *
87
+ * `frontmatterRegion` reports the YAML body's own bounds (`region`,
88
+ * `terminated`, and the possibly BOM-stripped `content`), not this seam's
89
+ * `Span` shape (an absolute byte range into the UNSTRIPPED `source`,
90
+ * inclusive of both fences). This adapter translates one into the other by
91
+ * reading ONLY the two boundary characters `frontmatterRegion` already
92
+ * anchored (whether the YAML end / closing fence sit on a CRLF line) — it
93
+ * does not re-scan for the fences themselves.
94
+ */
95
+ function findFrontmatterSpan(source) {
96
+ const found = frontmatterRegion(source);
97
+ if (!found)
98
+ return null;
99
+ // `found.content` may be `source` with a single leading BOM stripped;
100
+ // every offset below is relative to `found.content`, so translate back to
101
+ // `source` coordinates by the same delta.
102
+ const bomDelta = source.length - found.content.length;
103
+ const content = found.content;
104
+ if (!found.terminated) {
105
+ return { span: { start: bomDelta, end: bomDelta + content.length }, terminated: false };
106
+ }
107
+ // `frontmatterRegion` already did fence DETECTION — `found` being non-null
108
+ // and `terminated` IS that result. It reports only the YAML body's bounds
109
+ // (`region`), not an absolute span, so recover the closing fence's end
110
+ // from `region`'s length. The one thing still read directly here is the
111
+ // opening fence's fixed-width line ending (`\n` vs `\r\n`), needed to
112
+ // translate `region`'s length into a `content` offset — not a re-scan for
113
+ // the fence itself.
114
+ const headerEnd = content.startsWith('---\r\n') ? 5 : 4;
115
+ const yamlEnd = headerEnd + found.region.length;
116
+ const closingLineStart = content[yamlEnd] === '\r' ? yamlEnd + 1 : yamlEnd;
117
+ const fenceLineStart = closingLineStart + 1;
118
+ let fenceEnd = fenceLineStart + 3;
119
+ if (content[fenceEnd] === '\r')
120
+ fenceEnd += 1;
121
+ return { span: { start: bomDelta, end: bomDelta + fenceEnd }, terminated: true };
122
+ }
123
+ /** Build the set of 0-based line indices that fall inside a fenced code
124
+ * block (opening/closing delimiter lines included), so `**Label:**`/table/
125
+ * checklist scanning never treats fenced content as a node (rows 9/14). */
126
+ function fencedLineIndices(lines) {
127
+ const raw = lines.map((l) => l.text);
128
+ const blocks = (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(raw);
129
+ const set = new Set();
130
+ for (const b of blocks) {
131
+ const end = b.closeLineIdx === -1 ? raw.length - 1 : b.closeLineIdx;
132
+ for (let i = b.openLineIdx; i <= end; i++)
133
+ set.add(i);
134
+ }
135
+ return set;
136
+ }
137
+ /** Matches both shipped bold-field spellings: colon-inside (`**Label:**`,
138
+ * the original grammar) and colon-outside (`**Label**:`, the canonical form
139
+ * used throughout `templates/roadmap.md`). Each alternative's trailing
140
+ * marker is exactly 3 characters (`:**` or `**:`), so `token.slice(2, -3)`
141
+ * in `parseBoldFieldLine` strips the leading `**` and the spelling-specific
142
+ * trailing marker identically for both, yielding the same `label` either
143
+ * way. Deliberately excludes a bare unbolded `Label:` form — see Phase 1's
144
+ * prose-vs-field disambiguation design. */
145
+ const BOLD_FIELD_RE = /^(\s*)(\*\*[^*\r\n]+(?::\*\*|\*\*:))([ \t]*)([^\r\n]*)$/;
146
+ /** Boundary marking a hand-written trailing annotation on a field line —
147
+ * the token owner must never destroy prose past this separator. */
148
+ const TRAILING_SEPARATOR_RE = / — /;
149
+ function parseBoldFieldLine(line) {
150
+ const m = BOLD_FIELD_RE.exec(line.text);
151
+ if (!m)
152
+ return null;
153
+ const [, leading, token, spacing, rest] = m;
154
+ const labelStart = line.start + leading.length;
155
+ const labelSpan = { start: labelStart, end: labelStart + token.length };
156
+ const label = token.slice(2, -3);
157
+ const restStart = labelSpan.end + spacing.length;
158
+ const sepMatch = TRAILING_SEPARATOR_RE.exec(rest);
159
+ const valueRaw = sepMatch ? rest.slice(0, sepMatch.index) : rest;
160
+ const trimmedValue = valueRaw.replace(/\s+$/, '');
161
+ const valueSpan = { start: restStart, end: restStart + trimmedValue.length };
162
+ const trailingSpan = { start: valueSpan.end, end: line.end };
163
+ return {
164
+ kind: 'boldField',
165
+ id: mintId('boldField'),
166
+ span: { start: labelSpan.start, end: line.end },
167
+ error: null,
168
+ label,
169
+ labelSpan,
170
+ valueSpan,
171
+ trailingSpan,
172
+ value: trimmedValue,
173
+ };
174
+ }
175
+ /** A checklist line is one whose SOLE bullet, per `iterateBullets` (the same
176
+ * grammar the repo's other bullet consumers use), is a checkbox marker, OR
177
+ * whose bullet TEXT begins with a task-list marker.
178
+ *
179
+ * `iterateBullets` owns bullet *structure* — is this a bullet, where does its
180
+ * text start — and continues to own that here unchanged. It only classifies
181
+ * `-`-prefixed bullets as `checkbox-checked`/`checkbox-unchecked`; GFM also
182
+ * permits `*` and `+` as bullet markers, and `* [ ] x` / `+ [x] y` are valid
183
+ * GFM task-list items that `iterateBullets` reports as plain `dash`-family
184
+ * bullets with the `[ ]`/`[x]` left in the bullet's own text. Widening
185
+ * `iterateBullets` itself is forbidden by ADR-2143 §2's extend-never-mutate
186
+ * lock (inherited by this epic), so the task-list-marker interpretation is
187
+ * layered on here, over the bullet's already-extracted text — never by
188
+ * re-scanning the raw line with a new hand-rolled regex.
189
+ *
190
+ * Known limit inherited from `iterateBullets`, not introduced here:
191
+ * `-\t[ ] text` (a tab between the marker and the text) is not recognised as
192
+ * a bullet at all, so it can never become a checklist line. That is a
193
+ * pre-existing `markdown-sectionizer` boundary affecting every consumer of
194
+ * `iterateBullets`, and fixing it would mean altering the locked seam. */
195
+ function isChecklistLine(text) {
196
+ const items = (0, markdown_sectionizer_cjs_1.iterateBullets)(text);
197
+ if (items.length !== 1)
198
+ return false;
199
+ const item = items[0];
200
+ if (item.marker === 'checkbox-checked' || item.marker === 'checkbox-unchecked')
201
+ return true;
202
+ return /^\[[ xX]\] /.test(item.text);
203
+ }
204
+ /**
205
+ * Scan the document body (everything outside the frontmatter block and
206
+ * outside fenced code) for `boldField`, `table`, and `checklist` nodes, in
207
+ * document order.
208
+ */
209
+ function scanBodyNodes(source, lines, frontmatterEnd) {
210
+ const fenced = fencedLineIndices(lines);
211
+ const nodes = [];
212
+ let i = 0;
213
+ while (i < lines.length) {
214
+ const line = lines[i];
215
+ if (fenced.has(i) || line.start < frontmatterEnd) {
216
+ i += 1;
217
+ continue;
218
+ }
219
+ const trimmed = line.text.trim();
220
+ // Table: a pipe-shaped header line followed by a valid delimiter row.
221
+ if (trimmed.startsWith('|') && trimmed.indexOf('|', 1) !== -1 && i + 1 < lines.length) {
222
+ const delimiterLine = lines[i + 1];
223
+ const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(delimiterLine.text);
224
+ const headerCells = (0, markdown_table_cjs_1.splitTableRow)(line.text);
225
+ if (delimiterLine.text.trim().startsWith('|')
226
+ && (0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells)
227
+ && delimiterCells.length === headerCells.length
228
+ && !fenced.has(i + 1)) {
229
+ let last = i + 1;
230
+ while (last + 1 < lines.length && lines[last + 1].text.trim().startsWith('|') && !fenced.has(last + 1)) {
231
+ last += 1;
232
+ }
233
+ const span = { start: line.start, end: lines[last].end };
234
+ const tableText = source.slice(span.start, span.end);
235
+ const parsed = (0, markdown_table_cjs_1.parseMarkdownTable)(tableText);
236
+ nodes.push(parsed.ok
237
+ ? {
238
+ kind: 'table',
239
+ id: mintId('table'),
240
+ span,
241
+ error: null,
242
+ columns: parsed.value.columns,
243
+ }
244
+ : {
245
+ kind: 'table',
246
+ id: mintId('table'),
247
+ span,
248
+ error: { reason: parsed.reason, span },
249
+ columns: null,
250
+ });
251
+ i = last + 1;
252
+ continue;
253
+ }
254
+ }
255
+ // Checklist: a contiguous run of checkbox-bullet lines.
256
+ if (isChecklistLine(line.text)) {
257
+ let last = i;
258
+ let count = 0;
259
+ while (last < lines.length && !fenced.has(last) && isChecklistLine(lines[last].text)) {
260
+ count += 1;
261
+ last += 1;
262
+ }
263
+ last -= 1;
264
+ const span = { start: line.start, end: lines[last].end };
265
+ nodes.push({ kind: 'checklist', id: mintId('checklist'), span, error: null, items: count });
266
+ i = last + 1;
267
+ continue;
268
+ }
269
+ // Bold field.
270
+ const field = parseBoldFieldLine(line);
271
+ if (field) {
272
+ nodes.push(field);
273
+ i += 1;
274
+ continue;
275
+ }
276
+ i += 1;
277
+ }
278
+ return nodes;
279
+ }
280
+ // ─── Public API ─────────────────────────────────────────────────────────────
281
+ /**
282
+ * Parse `source` (the raw text of a `.planning/` root artifact) into a
283
+ * `PlanningDoc`. Document-level `Result` failure is reserved for: `artifact`
284
+ * not a recognised planning artifact kind, `source` not a readable string, or
285
+ * an opened-but-never-closed frontmatter fence (ADR-4910 §5's reservation).
286
+ * A malformed SUB-structure (a ragged table, say) never fails the whole
287
+ * document — it is recorded as that one node's `error`, and every sibling
288
+ * node stays readable (row 7). `nodes: []` on a genuinely empty document is
289
+ * success, not an error (row 15).
290
+ */
291
+ function parsePlanningDoc(source, artifact) {
292
+ if (typeof source !== 'string') {
293
+ return { ok: false, reason: 'unreadable: source is not a string' };
294
+ }
295
+ if (typeof artifact !== 'string' ||
296
+ !(0, artifacts_cjs_1.isCanonicalPlanningFile)(artifact) ||
297
+ !exports.PLANNING_ARTIFACTS.includes(artifact)) {
298
+ return {
299
+ ok: false,
300
+ reason: `not a markdown planning document (artifact: ${String(artifact)})`,
301
+ };
302
+ }
303
+ const nodes = [];
304
+ let frontmatterEnd = 0;
305
+ const fm = findFrontmatterSpan(source);
306
+ if (fm) {
307
+ if (!fm.terminated) {
308
+ return { ok: false, reason: 'no frontmatter terminator' };
309
+ }
310
+ nodes.push({ kind: 'frontmatter', id: mintId('frontmatter'), span: fm.span, error: null });
311
+ frontmatterEnd = fm.span.end;
312
+ }
313
+ if (source.length === 0) {
314
+ return { ok: true, value: { source, artifact, nodes: [], staged: new Map() } };
315
+ }
316
+ const lines = splitLinesInfo(source);
317
+ // Sections: one per heading, in document order — every heading is its own
318
+ // boundary (`collectSections(source, () => true)`), so a nested `####`
319
+ // still gets its own SectionNode rather than being folded into its parent.
320
+ const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(source);
321
+ if (headings.length > 0) {
322
+ const sections = (0, markdown_sectionizer_cjs_1.collectSections)(source, () => true);
323
+ for (const s of sections) {
324
+ nodes.push({
325
+ kind: 'section',
326
+ id: mintId('section'),
327
+ span: { start: s.heading.offset, end: s.bodyEnd },
328
+ error: null,
329
+ heading: s.heading.text,
330
+ level: s.heading.level,
331
+ });
332
+ }
333
+ }
334
+ nodes.push(...scanBodyNodes(source, lines, frontmatterEnd));
335
+ nodes.sort((a, b) => a.span.start - b.span.start);
336
+ return { ok: true, value: { source, artifact, nodes, staged: new Map() } };
337
+ }
338
+ /** Find the id of the (first, document-order) `boldField` node whose label
339
+ * exactly matches `label`, or `null` when none does. */
340
+ function findField(doc, label) {
341
+ for (const n of doc.nodes) {
342
+ if (n.kind === 'boldField' && n.label === label)
343
+ return n.id;
344
+ }
345
+ return null;
346
+ }
347
+ /** Read a node by id. Node-scoped failure only — an unknown id or a node
348
+ * that failed to parse never throws. */
349
+ function readNode(doc, id) {
350
+ const node = doc.nodes.find((n) => n.id === id);
351
+ if (!node) {
352
+ return { ok: false, reason: 'unknown node id', span: { start: 0, end: 0 } };
353
+ }
354
+ if (node.error) {
355
+ return { ok: false, reason: node.error.reason, span: node.error.span };
356
+ }
357
+ if (node.kind === 'boldField') {
358
+ return { ok: true, value: doc.staged.get(id) ?? node.value };
359
+ }
360
+ return { ok: true, value: doc.source.slice(node.span.start, node.span.end) };
361
+ }
362
+ /**
363
+ * Stage a new value for a `boldField` node, returning a NEW `PlanningDoc`
364
+ * (immutable — `doc` itself is never mutated). Refuses an id this doc did
365
+ * not mint, and refuses any node kind other than `boldField` — only the
366
+ * `valueSpan` is ever writable this phase (ADR-4910 §1).
367
+ */
368
+ function setFieldValue(doc, id, value) {
369
+ const node = doc.nodes.find((n) => n.id === id);
370
+ if (!node) {
371
+ return { ok: false, reason: 'unknown node id' };
372
+ }
373
+ if (node.kind !== 'boldField') {
374
+ return { ok: false, reason: `node kind '${node.kind}' is not writable this phase` };
375
+ }
376
+ // #4917 / ADR-4910 Decision 2 & 4: a boldField's token boundary is a LINE
377
+ // boundary, not just an offset range — a value containing \n or \r escapes
378
+ // the field's own span and reparses as sibling structure (a forged field)
379
+ // once spliced back into the source. Decision 4 licenses refusal for any
380
+ // value the grammar cannot represent; Phase 3 may widen this to escaping,
381
+ // but Phase 1 refuses outright. Do not remove this as an over-restriction.
382
+ if (/[\r\n]/.test(value)) {
383
+ return { ok: false, reason: 'field value must not contain a line break (\\r or \\n)' };
384
+ }
385
+ // #4917 / ADR-4910 Decision 4: "a value that cannot be represented in the
386
+ // grammar is refused by the writer, with a report." This is a GENERAL
387
+ // round-trip representability check, not a blacklist of forbidden
388
+ // substrings — the `\r`/`\n` guard above is a narrower special case kept
389
+ // for its clearer message, but THIS check is the backstop. It rebuilds the
390
+ // line exactly as it would be written (existing leading/label/spacing +
391
+ // the new value + the existing trailing text) and re-parses that line
392
+ // through the SAME `parseBoldFieldLine` grammar the reader uses. If the
393
+ // value the grammar reads back is not byte-identical to what the caller
394
+ // staged, the grammar cannot represent this value (e.g. it contains the
395
+ // ` — ` trailing-separator token, which would silently reclassify the
396
+ // rest of the value as trailing prose) and the write is refused. Do NOT
397
+ // replace this with a list of forbidden characters/substrings — the next
398
+ // separator the grammar grows would silently slip past a blacklist.
399
+ const leadingText = doc.source.slice(node.span.start, node.labelSpan.start);
400
+ const tokenText = doc.source.slice(node.labelSpan.start, node.labelSpan.end);
401
+ const spacingText = doc.source.slice(node.labelSpan.end, node.valueSpan.start);
402
+ const trailingText = doc.source.slice(node.trailingSpan.start, node.trailingSpan.end);
403
+ const candidateLine = `${leadingText}${tokenText}${spacingText}${value}${trailingText}`;
404
+ const candidateInfo = { text: candidateLine, start: 0, end: candidateLine.length };
405
+ const reparsed = parseBoldFieldLine(candidateInfo);
406
+ if (!reparsed || reparsed.value !== value) {
407
+ return {
408
+ ok: false,
409
+ reason: 'field value is not representable in the boldField grammar (would not round-trip)',
410
+ };
411
+ }
412
+ const staged = new Map(doc.staged);
413
+ staged.set(id, value);
414
+ return { ok: true, value: { source: doc.source, artifact: doc.artifact, nodes: doc.nodes, staged } };
415
+ }
416
+ /** True when any node in `doc` failed to parse. */
417
+ function hasUnreadableNodes(doc) {
418
+ return doc.nodes.some((n) => n.error !== null);
419
+ }
420
+ /**
421
+ * Splice every staged edit into `doc.source` and return the resulting text.
422
+ * With zero staged edits, returns `doc.source` BYTE-IDENTICAL — never a
423
+ * re-render (row 3). Refuses outright — even with zero staged edits — when
424
+ * `hasUnreadableNodes(doc)` is true (the ADR-4910 amendment): `serialize`
425
+ * re-emits the WHOLE document, so the refusal is document-scoped, not
426
+ * mutation-scoped.
427
+ */
428
+ function serialize(doc) {
429
+ if (hasUnreadableNodes(doc)) {
430
+ return {
431
+ ok: false,
432
+ reason: 'unreadable-nodes',
433
+ nodes: doc.nodes
434
+ .filter((n) => n.error !== null)
435
+ .map((n) => ({ id: n.id, kind: n.kind, span: n.error.span, reason: n.error.reason })),
436
+ };
437
+ }
438
+ if (doc.staged.size === 0) {
439
+ return { ok: true, value: doc.source };
440
+ }
441
+ const edits = [];
442
+ for (const [id, value] of doc.staged) {
443
+ const node = doc.nodes.find((n) => n.id === id);
444
+ if (!node || node.kind !== 'boldField')
445
+ continue; // unreachable: setFieldValue already gated this
446
+ edits.push({ start: node.valueSpan.start, end: node.valueSpan.end, value });
447
+ }
448
+ edits.sort((a, b) => a.start - b.start);
449
+ let out = '';
450
+ let cursor = 0;
451
+ for (const e of edits) {
452
+ out += doc.source.slice(cursor, e.start) + e.value;
453
+ cursor = e.end;
454
+ }
455
+ out += doc.source.slice(cursor);
456
+ return { ok: true, value: out };
457
+ }
458
+ // Consumers: require('../gsd-core/bin/lib/planning-document.cjs')
459
+ // Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
@@ -94,6 +94,9 @@ const { parseMarkdownTable, matchTableSchema } = markdownTable;
94
94
  // eslint-disable-next-line @typescript-eslint/no-require-imports
95
95
  const coreUtilsMod = require("./core-utils.cjs");
96
96
  const { normalizeLineEndings } = coreUtilsMod;
97
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
98
+ const securityMod = require("./security.cjs");
99
+ const { tryWithinRoot, PathAcceptance, isContainedIn } = securityMod;
97
100
  /**
98
101
  * The wire schema version. A consumer MUST reject any value other than this
99
102
  * one rather than best-effort-parsing an unknown shape.
@@ -184,32 +187,45 @@ function toPosix(value) {
184
187
  * directory like `.planning-evil/` that merely shares a string prefix. Pure
185
188
  * string comparison, no I/O — callers own their own `fs.realpathSync` call
186
189
  * (and its own not-found/broken-symlink handling).
190
+ *
191
+ * NOT an independent containment implementation — it is the comparison step
192
+ * of one, and that comparison now comes from `security.cts`'s exported
193
+ * `isContainedIn` rather than being redeclared here. `readDocument` below
194
+ * realpaths target and root itself (to keep its own exists-vs-escaped
195
+ * tri-state) and calls `isContainedIn` directly; `isPathContained` gets its
196
+ * containment DECISION from the canonical `tryWithinRoot` predicate instead
197
+ * (ADR-4650 decision 6) and never called this comparison directly. Every
198
+ * caller owns its own resolution.
187
199
  */
188
- function isWithinRoot(resolvedTarget, resolvedRoot) {
189
- return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + node_path_1.default.sep);
190
- }
191
200
  /**
192
- * Containment check for a path (file OR directory) that resolves its own
193
- * `fs.realpathSync`, then delegates the actual boundary comparison to
194
- * `isWithinRoot`. Used where the caller does not need to distinguish "target
195
- * vanished / broken symlink" from "target resolved but escapes root" — both
196
- * degrade the same way at every call site that uses this (an escaped or
197
- * unresolvable phase directory is treated identically to an unreadable one).
198
- * `readDocument` below needs that distinction for its own exists/readable
199
- * tri-state, so it keeps its own inline `realpathSync` calls and calls
200
- * `isWithinRoot` directly instead of this wrapper.
201
+ * Containment check for a path (file OR directory), used where the caller
202
+ * does not need to distinguish "target vanished / broken symlink" from
203
+ * "target resolved but escapes root" — both degrade the same way at every
204
+ * call site that uses this (an escaped or unresolvable phase directory is
205
+ * treated identically to an unreadable one). `readDocument` below needs
206
+ * that distinction for its own exists/readable tri-state, so it keeps its
207
+ * own inline `realpathSync` calls and calls `isContainedIn` directly instead
208
+ * of this wrapper.
209
+ *
210
+ * The containment DECISION comes from the canonical `tryWithinRoot`
211
+ * predicate (ADR-4650 decision 6: a wrapper may decide HOW to degrade,
212
+ * never WHETHER a path is contained). Must-exist stays this module's OWN
213
+ * degradation condition, applied after: `tryWithinRoot` deliberately accepts
214
+ * a not-yet-created path under the root (ancestor-walk realpath), but every
215
+ * caller of `isPathContained` guards an `fs` read that is about to happen
216
+ * against an already-existing directory, so a vanished/unresolvable path
217
+ * must still degrade the same as an escaped one.
201
218
  */
202
219
  function isPathContained(target, root) {
203
- let realTarget;
204
- let realRoot;
220
+ if (tryWithinRoot(target, root, PathAcceptance.AbsoluteInsideRoot) === null)
221
+ return false;
205
222
  try {
206
- realTarget = node_fs_1.default.realpathSync(target);
207
- realRoot = node_fs_1.default.realpathSync(root);
223
+ node_fs_1.default.realpathSync(target);
208
224
  }
209
225
  catch {
210
226
  return false;
211
227
  }
212
- return isWithinRoot(realTarget, realRoot);
228
+ return true;
213
229
  }
214
230
  function readDocument(filePath, root) {
215
231
  let stat;
@@ -234,7 +250,7 @@ function readDocument(filePath, root) {
234
250
  // here — the same non-answer `readDocument` already gives "not exists".
235
251
  return { text: null, exists: false, readable: false };
236
252
  }
237
- if (!isWithinRoot(realTarget, realRoot)) {
253
+ if (!isContainedIn(realTarget, realRoot)) {
238
254
  return { text: null, exists: true, readable: false };
239
255
  }
240
256
  try {
@@ -901,7 +917,24 @@ function extractDependencyTokens(sectionBody) {
901
917
  const m = DEPENDS_ON_LINE_RE.exec(sectionBody);
902
918
  if (!m)
903
919
  return [];
904
- return sortedUnique([...m[1].matchAll(/\d+(?:\.\d+)*/g)].map((t) => t[0]));
920
+ // #4764: phase REFERENCES, not digit runs — the same prose-anchored grammar
921
+ // init.manager's dep_phases extraction uses (owner: phase-id.cts's
922
+ // PHASE_DEP_REF_SOURCE). The whole-field token scrape this replaces pulled
923
+ // calendar dates, git shas and ledger ids in as dependencies. The grammar's
924
+ // capture group 1 already excludes the "Phase(s)" anchor word, so no
925
+ // prefix-strip literal is needed here. Self-exclusion (init.manager drops
926
+ // the row's own number) is deliberately NOT applied: this reader has no row
927
+ // context at the extraction site and reports informationally, it does not
928
+ // gate.
929
+ const refRe = new RegExp(phaseIdMod.PHASE_DEP_REF_SOURCE, 'gi');
930
+ const tokenRe = new RegExp(phaseIdMod.PHASE_NUMBER_TOKEN_SOURCE, 'g');
931
+ const tokens = [];
932
+ let refMatch;
933
+ while ((refMatch = refRe.exec(m[1])) !== null) {
934
+ for (const t of refMatch[1].matchAll(tokenRe))
935
+ tokens.push(t[0]);
936
+ }
937
+ return sortedUnique(tokens);
905
938
  }
906
939
  /**
907
940
  * This phase's own ROADMAP.md section body — milestone-scoped via the SAME