@opengsd/gsd-core 1.18.0 → 1.20.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 (308) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-code-fixer.compact.md +7 -7
  5. package/agents/gsd-code-fixer.md +13 -7
  6. package/agents/gsd-code-reviewer.compact.md +49 -8
  7. package/agents/gsd-code-reviewer.md +53 -14
  8. package/agents/gsd-executor.md +8 -4
  9. package/agents/gsd-verifier.md +1 -1
  10. package/bin/install.js +422 -103
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +179 -30
  13. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  14. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  15. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  16. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  17. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  18. package/gsd-core/bin/lib/capability-registry.cjs +376 -103
  19. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  20. package/gsd-core/bin/lib/capability-validator.cjs +32 -0
  21. package/gsd-core/bin/lib/commands.cjs +27 -17
  22. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  23. package/gsd-core/bin/lib/config.cjs +256 -58
  24. package/gsd-core/bin/lib/core-utils.cjs +45 -0
  25. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  26. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  27. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  28. package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
  29. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  30. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  33. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  34. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  35. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  36. package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
  37. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
  38. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  39. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  40. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  41. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  42. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  43. package/gsd-core/bin/lib/init.cjs +245 -68
  44. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  45. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  47. package/gsd-core/bin/lib/io.cjs +6 -0
  48. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  49. package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
  50. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  51. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  52. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  54. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  55. package/gsd-core/bin/lib/milestone.cjs +61 -51
  56. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  57. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  58. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  59. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  60. package/gsd-core/bin/lib/phase-id.cjs +629 -38
  61. package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
  62. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  63. package/gsd-core/bin/lib/phase.cjs +3066 -653
  64. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  65. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  66. package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
  67. package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
  68. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  69. package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
  70. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  71. package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
  72. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  73. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  74. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  75. package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
  76. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  77. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
  78. package/gsd-core/bin/lib/roadmap.cjs +383 -123
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  82. package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
  83. package/gsd-core/bin/lib/smart-entry.cjs +35 -11
  84. package/gsd-core/bin/lib/state-contract.cjs +39 -15
  85. package/gsd-core/bin/lib/state-document.cjs +344 -27
  86. package/gsd-core/bin/lib/state-transition.cjs +156 -32
  87. package/gsd-core/bin/lib/state.cjs +527 -186
  88. package/gsd-core/bin/lib/surface.cjs +9 -0
  89. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  90. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  91. package/gsd-core/bin/lib/template.cjs +2 -2
  92. package/gsd-core/bin/lib/validate.cjs +3 -2
  93. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  94. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  95. package/gsd-core/bin/lib/verification.cjs +96 -9
  96. package/gsd-core/bin/lib/verify.cjs +4 -2
  97. package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
  98. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  99. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  100. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  101. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  102. package/gsd-core/references/cicd-review-patterns.md +45 -0
  103. package/gsd-core/references/git-integration.md +11 -0
  104. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  105. package/gsd-core/references/necessity-review.md +48 -0
  106. package/gsd-core/references/planning-config.md +2 -0
  107. package/gsd-core/references/tdd.md +2 -2
  108. package/gsd-core/templates/DEBUG.md +16 -0
  109. package/gsd-core/templates/config.json +2 -1
  110. package/gsd-core/templates/verification-report.md +1 -1
  111. package/gsd-core/workflows/audit-fix.md +2 -1
  112. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  113. package/gsd-core/workflows/code-review-fix.md +46 -5
  114. package/gsd-core/workflows/code-review.md +64 -7
  115. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  116. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  117. package/gsd-core/workflows/complete-milestone.md +37 -150
  118. package/gsd-core/workflows/diagnose-issues.md +29 -5
  119. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  120. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  121. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  122. package/gsd-core/workflows/discuss-phase.md +6 -10
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  124. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  126. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  127. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  128. package/gsd-core/workflows/execute-phase.md +10 -6
  129. package/gsd-core/workflows/execute-plan.md +1 -1
  130. package/gsd-core/workflows/health.md +2 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  133. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  134. package/gsd-core/workflows/plan-phase.md +14 -14
  135. package/gsd-core/workflows/pr-branch.md +2 -0
  136. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  137. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  138. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  139. package/gsd-core/workflows/quick.md +1 -1
  140. package/gsd-core/workflows/reapply-patches.md +104 -0
  141. package/gsd-core/workflows/review.md +70 -154
  142. package/gsd-core/workflows/section-manifest.json +5 -0
  143. package/gsd-core/workflows/secure-phase.md +1 -1
  144. package/gsd-core/workflows/settings-advanced.md +57 -9
  145. package/gsd-core/workflows/settings.md +1 -0
  146. package/gsd-core/workflows/ship.md +15 -4
  147. package/gsd-core/workflows/spec-phase.md +3 -6
  148. package/gsd-core/workflows/verify-work.md +4 -3
  149. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  150. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  151. package/hooks/dist/gsd-graphify-update.sh +6 -0
  152. package/hooks/dist/gsd-path-export.js +95 -0
  153. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  154. package/hooks/dist/gsd-statusline.js +37 -3
  155. package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
  156. package/hooks/dist/gsd-workflow-guard.js +3 -2
  157. package/hooks/dist/gsd-worktree-path-guard.js +29 -2
  158. package/hooks/dist/lib/git-probe.js +16 -0
  159. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  160. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  161. package/hooks/gsd-agent-isolation-guard.js +11 -1
  162. package/hooks/gsd-cursor-subagent-start.js +6 -2
  163. package/hooks/gsd-graphify-update.sh +6 -0
  164. package/hooks/gsd-path-export.js +95 -0
  165. package/hooks/gsd-secret-read-guard.js +172 -39
  166. package/hooks/gsd-statusline.js +37 -3
  167. package/hooks/gsd-windsurf-pre-write.js +29 -2
  168. package/hooks/gsd-workflow-guard.js +3 -2
  169. package/hooks/gsd-worktree-path-guard.js +29 -2
  170. package/hooks/hooks.json +1 -0
  171. package/hooks/lib/git-probe.js +16 -0
  172. package/hooks/lib/isolation-sentinel.js +44 -0
  173. package/hooks/managed-hooks-registry.cjs +1 -0
  174. package/package.json +15 -8
  175. package/pi/gsd.cjs +1047 -10
  176. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  177. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  178. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  179. package/pi/package-agents/gsd-code-fixer.md +774 -0
  180. package/pi/package-agents/gsd-code-reviewer.md +441 -0
  181. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  182. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  183. package/pi/package-agents/gsd-debugger.md +1274 -0
  184. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  185. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  186. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  187. package/pi/package-agents/gsd-doc-writer.md +626 -0
  188. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  189. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  190. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  191. package/pi/package-agents/gsd-eval-planner.md +149 -0
  192. package/pi/package-agents/gsd-executor.md +898 -0
  193. package/pi/package-agents/gsd-framework-selector.md +159 -0
  194. package/pi/package-agents/gsd-integration-checker.md +476 -0
  195. package/pi/package-agents/gsd-intel-updater.md +338 -0
  196. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  197. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  198. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  199. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  200. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  201. package/pi/package-agents/gsd-planner.md +1003 -0
  202. package/pi/package-agents/gsd-project-researcher.md +611 -0
  203. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  204. package/pi/package-agents/gsd-roadmapper.md +779 -0
  205. package/pi/package-agents/gsd-security-auditor.md +171 -0
  206. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  207. package/pi/package-agents/gsd-ui-checker.md +417 -0
  208. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  209. package/pi/package-agents/gsd-user-profiler.md +174 -0
  210. package/pi/package-agents/gsd-verifier.md +963 -0
  211. package/pi/package-agents/manifest.json +39 -0
  212. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  213. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  214. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  215. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  216. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  217. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  218. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  219. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  220. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  221. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  222. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  223. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  224. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  225. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  226. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  227. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  228. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  229. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  230. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  231. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  232. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  233. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  234. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  235. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  236. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  237. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  238. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  239. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  240. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  241. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  242. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  243. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  244. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  245. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  246. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  247. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  248. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  249. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  250. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  251. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  252. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  253. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  254. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  255. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  256. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  257. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  258. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  259. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  260. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  261. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  262. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  263. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  264. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  265. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  266. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  267. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  268. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  269. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  270. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  271. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  272. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  273. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  274. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  275. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  276. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  277. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  278. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  279. package/pi/package-skills/gsd-update/SKILL.md +54 -0
  280. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  281. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  282. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  283. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  284. package/scripts/build-hooks.js +4 -0
  285. package/scripts/ci-next-health.cjs +246 -23
  286. package/scripts/command-contract-helpers.cjs +1 -1
  287. package/scripts/docs-guard-registry.cjs +22 -7
  288. package/scripts/gen-features.cjs +14 -2
  289. package/scripts/gen-health-docs.cjs +13 -1
  290. package/scripts/gen-pi-package-skills.cjs +217 -0
  291. package/scripts/lib/drift-scan.cjs +27 -0
  292. package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
  293. package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
  294. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  295. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  296. package/scripts/lint-retired-runtime-name.cjs +5 -0
  297. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  298. package/scripts/mutation-matrix.cjs +23 -1
  299. package/scripts/npm-audit-baseline.cjs +41 -7
  300. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  301. package/scripts/select-docs-guards.cjs +1 -1
  302. package/scripts/self-test-report-policy.cjs +350 -0
  303. package/scripts/sync-next-version.cjs +38 -6
  304. package/skills/gsd-update/SKILL.md +5 -2
  305. package/vscode/package.json +1 -1
  306. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  307. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  308. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -0,0 +1,440 @@
1
+ "use strict";
2
+ /**
3
+ * Milestone Release Tags — #5705 (epic #3628, Phase 1).
4
+ *
5
+ * `gsd-tools milestone release-tags --map <file> [--dry-run]`: preflight and
6
+ * create annotated tags across the repositories listed in `sub_repos` from a
7
+ * caller-supplied map `[{ repo, tag, ref?, message?, skip? }]`.
8
+ *
9
+ * Contract (the result shape is read by Phases 2-3 of the epic, so it is fixed
10
+ * here and pinned by tests):
11
+ *
12
+ * { ok, phase: 'preflight' | 'apply' | 'dry_run',
13
+ * repos: { [key]: { status, tag?, commit?, reason?, error?,
14
+ * existing_commit?, requested_commit?, detached_head? } },
15
+ * created: string[], // "repo:tag" for every tag written
16
+ * errors: [{ repo?, tag?, reason, error? }] }
17
+ *
18
+ * `key` is the normalized repo path. When one repo receives more than one
19
+ * non-skipped entry (two tags), every entry of that repo is keyed `repo:tag`
20
+ * instead, so no result is overwritten.
21
+ *
22
+ * Every entry is preflighted before the first write, so a predictable failure
23
+ * (unknown repo, bad tag name, unresolved ref, conflicting tag) writes nothing
24
+ * anywhere. Tag creation itself is not transactional across repositories: a
25
+ * failure after the first write is reported with the list of tags already
26
+ * written (`created`), never rolled back, and a re-run with the same map
27
+ * reports those tags as `already_tagged` and retries only the failed ones.
28
+ *
29
+ * Nothing is pushed, no existing tag is ever overwritten, and every git call is
30
+ * an argv array run with the target repository as cwd under a 15 s bound.
31
+ */
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RELEASE_TAG_REASONS = exports.MAX_RELEASE_TAG_ENTRIES = void 0;
37
+ exports.preflightReleaseTags = preflightReleaseTags;
38
+ exports.createReleaseTags = createReleaseTags;
39
+ exports.cmdMilestoneReleaseTags = cmdMilestoneReleaseTags;
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
43
+ const security_cjs_1 = require("./security.cjs");
44
+ const command_arg_projection_cjs_1 = require("./command-arg-projection.cjs");
45
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
46
+ const ioMod = require("./io.cjs");
47
+ const { output, ERROR_REASON } = ioMod;
48
+ const error = ioMod.error;
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const configLoaderMod = require("./config-loader.cjs");
51
+ const { loadConfig } = configLoaderMod;
52
+ // ─── Constants ────────────────────────────────────────────────────────────
53
+ /** Maximum entries accepted in one request. Exactly this many are accepted. */
54
+ exports.MAX_RELEASE_TAG_ENTRIES = 256;
55
+ /** Bound on every git subprocess (ms). */
56
+ const RELEASE_TAG_GIT_TIMEOUT_MS = 15000;
57
+ /** Largest `--map` file read from disk (bytes). */
58
+ const MAX_MAP_FILE_BYTES = 1_048_576;
59
+ /** Every reason code this module can emit — a closed set, pinned against CONTEXT.md. */
60
+ exports.RELEASE_TAG_REASONS = Object.freeze([
61
+ 'no_sub_repos',
62
+ 'invalid_request',
63
+ 'too_many_entries',
64
+ 'repo_not_configured',
65
+ 'repo_path_invalid',
66
+ 'repo_missing',
67
+ 'not_a_git_repo',
68
+ 'tag_invalid',
69
+ 'ref_unresolved',
70
+ 'tag_conflict',
71
+ 'duplicate_entry',
72
+ 'git_timeout',
73
+ 'tag_create_failed',
74
+ ]);
75
+ // ─── Helpers ──────────────────────────────────────────────────────────────
76
+ function newResult(phase) {
77
+ // Null prototype: a hostile repo key such as `__proto__` is plain data.
78
+ return { ok: true, phase, repos: Object.create(null), created: [], errors: [] };
79
+ }
80
+ function topLevelError(reason, message) {
81
+ const result = newResult('preflight');
82
+ result.ok = false;
83
+ result.errors.push({ reason, error: message });
84
+ return result;
85
+ }
86
+ function isPlainObject(v) {
87
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
88
+ }
89
+ /**
90
+ * Normalize a repository path to a forward-slash relative form, or null when
91
+ * it is absolute, contains a `..` segment (either separator), a NUL, or is
92
+ * empty. Backslashes are converted FIRST so `a\..\b` cannot slip past.
93
+ */
94
+ function normalizeRepoPath(raw) {
95
+ const s = raw.replace(/\\/g, '/');
96
+ if (s === '' || s.includes('\0') || s.startsWith('/') || /^[A-Za-z]:/.test(s))
97
+ return null;
98
+ const segments = s.split('/');
99
+ if (segments.includes('..'))
100
+ return null;
101
+ const kept = segments.filter((seg) => seg !== '' && seg !== '.');
102
+ return kept.length === 0 ? null : kept.join('/');
103
+ }
104
+ function readSubRepos(cwd) {
105
+ const config = loadConfig(cwd);
106
+ const raw = config['sub_repos'];
107
+ const out = new Set();
108
+ if (!Array.isArray(raw))
109
+ return out;
110
+ for (const item of raw) {
111
+ if (typeof item !== 'string')
112
+ continue;
113
+ const n = normalizeRepoPath(item);
114
+ if (n !== null)
115
+ out.add(n);
116
+ }
117
+ return out;
118
+ }
119
+ /** Validate request shape. Returns a top-level failure, or null when well-formed. */
120
+ function validateRequest(entries) {
121
+ if (!Array.isArray(entries)) {
122
+ return topLevelError('invalid_request', 'request must be a JSON array of entry objects');
123
+ }
124
+ if (entries.length > exports.MAX_RELEASE_TAG_ENTRIES) {
125
+ return topLevelError('too_many_entries', `request has ${entries.length} entries; the limit is ${exports.MAX_RELEASE_TAG_ENTRIES}`);
126
+ }
127
+ const result = newResult('preflight');
128
+ entries.forEach((entry, index) => {
129
+ const where = `entry ${index}`;
130
+ if (!isPlainObject(entry)) {
131
+ result.errors.push({ reason: 'invalid_request', error: `${where}: must be an object` });
132
+ return;
133
+ }
134
+ if (typeof entry['repo'] !== 'string') {
135
+ result.errors.push({ reason: 'invalid_request', error: `${where}: "repo" must be a string` });
136
+ return;
137
+ }
138
+ const repo = entry['repo'];
139
+ if (entry['skip'] === true)
140
+ return;
141
+ if (entry['skip'] !== undefined && typeof entry['skip'] !== 'boolean') {
142
+ result.errors.push({ repo, reason: 'invalid_request', error: `${where}: "skip" must be a boolean` });
143
+ }
144
+ if (typeof entry['tag'] !== 'string') {
145
+ result.errors.push({ repo, reason: 'invalid_request', error: `${where}: "tag" must be a string` });
146
+ }
147
+ for (const field of ['ref', 'message']) {
148
+ if (entry[field] !== undefined && typeof entry[field] !== 'string') {
149
+ result.errors.push({ repo, reason: 'invalid_request', error: `${where}: "${field}" must be a string` });
150
+ }
151
+ }
152
+ });
153
+ if (result.errors.length > 0) {
154
+ result.ok = false;
155
+ return result;
156
+ }
157
+ return null;
158
+ }
159
+ // ─── Preflight ────────────────────────────────────────────────────────────
160
+ function runPreflight(cwd, entries, options, phase) {
161
+ const shape = validateRequest(entries);
162
+ if (shape)
163
+ return { result: shape, plans: [] };
164
+ const list = entries;
165
+ const exec = options.execGit ?? shell_command_projection_cjs_1.execGit;
166
+ const timeout = options.timeoutMs ?? RELEASE_TAG_GIT_TIMEOUT_MS;
167
+ const run = (dir, args) => exec(args, { cwd: dir, timeout });
168
+ const subRepos = readSubRepos(cwd);
169
+ if (subRepos.size === 0) {
170
+ return { result: topLevelError('no_sub_repos', 'no sub_repos configured in .planning/config.json'), plans: [] };
171
+ }
172
+ // A repo that receives more than one non-skipped entry is keyed `repo:tag`.
173
+ const perRepo = new Map();
174
+ for (const entry of list) {
175
+ if (entry.skip === true)
176
+ continue;
177
+ const k = normalizeRepoPath(entry.repo) ?? entry.repo;
178
+ perRepo.set(k, (perRepo.get(k) ?? 0) + 1);
179
+ }
180
+ // A repo given both a skip entry and a tag entry is contradictory; the check is
181
+ // order-independent so preflight never reports `skipped` for a repo that gets tagged.
182
+ const skippedRepos = new Set();
183
+ for (const entry of list) {
184
+ if (entry.skip === true)
185
+ skippedRepos.add(normalizeRepoPath(entry.repo) ?? entry.repo);
186
+ }
187
+ const result = newResult(phase);
188
+ const plans = [];
189
+ const seen = new Set();
190
+ for (const entry of list) {
191
+ const rawRepo = entry.repo;
192
+ const normalized = normalizeRepoPath(rawRepo);
193
+ const repo = normalized ?? rawRepo;
194
+ if (entry.skip === true) {
195
+ const skipKey = `skip\0${repo}`;
196
+ if (seen.has(skipKey)) {
197
+ result.ok = false;
198
+ result.errors.push({ repo, reason: 'duplicate_entry' });
199
+ }
200
+ else {
201
+ seen.add(skipKey);
202
+ // When the repo also has a tag entry, that entry records the duplicate_entry error
203
+ // under the same key; writing `skipped` here would overwrite it order-dependently.
204
+ if ((perRepo.get(repo) ?? 0) === 0)
205
+ result.repos[repo] = { status: 'skipped' };
206
+ }
207
+ continue;
208
+ }
209
+ const tag = entry.tag;
210
+ const key = (perRepo.get(repo) ?? 0) > 1 ? `${repo}:${tag}` : repo;
211
+ const fail = (reason, extra = {}) => {
212
+ result.ok = false;
213
+ result.repos[key] = { status: 'error', tag, reason, ...extra };
214
+ result.errors.push({ repo, tag, reason, ...(extra.error ? { error: extra.error } : {}) });
215
+ };
216
+ if (skippedRepos.has(repo)) {
217
+ fail('duplicate_entry', { error: 'repo has both a skip entry and a tag entry' });
218
+ continue;
219
+ }
220
+ if (normalized === null || (0, security_cjs_1.tryWithinRoot)(normalized, cwd) === null) {
221
+ fail('repo_path_invalid', { error: 'repo path must be relative and stay inside the project root' });
222
+ continue;
223
+ }
224
+ if (!subRepos.has(normalized)) {
225
+ fail('repo_not_configured', { error: 'repo is not listed in sub_repos' });
226
+ continue;
227
+ }
228
+ const pairKey = `${normalized}\0${tag}`;
229
+ if (seen.has(pairKey)) {
230
+ result.ok = false;
231
+ result.errors.push({ repo, tag, reason: 'duplicate_entry' });
232
+ continue;
233
+ }
234
+ seen.add(pairKey);
235
+ const dir = node_path_1.default.join(cwd, normalized);
236
+ let isDir = false;
237
+ try {
238
+ isDir = node_fs_1.default.statSync(dir).isDirectory();
239
+ }
240
+ catch {
241
+ isDir = false;
242
+ }
243
+ if (!isDir) {
244
+ fail('repo_missing', { error: 'repo directory does not exist' });
245
+ continue;
246
+ }
247
+ const top = run(dir, ['rev-parse', '--show-toplevel']);
248
+ if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(top)) {
249
+ fail('git_timeout', { error: 'git rev-parse timed out' });
250
+ continue;
251
+ }
252
+ let isOwnWorkTree = false;
253
+ if (top.exitCode === 0) {
254
+ try {
255
+ // `.native` expands Windows 8.3 short names (RUNNER~1) the way git's long-form path does.
256
+ isOwnWorkTree = node_fs_1.default.realpathSync.native(top.stdout) === node_fs_1.default.realpathSync.native(dir);
257
+ }
258
+ catch {
259
+ isOwnWorkTree = false;
260
+ }
261
+ }
262
+ if (!isOwnWorkTree) {
263
+ fail('not_a_git_repo', { error: 'directory is not the root of a git work tree' });
264
+ continue;
265
+ }
266
+ if (tag === '' || tag.startsWith('-') || tag.includes('\0')) {
267
+ fail('tag_invalid', { error: 'tag name is empty or starts with "-"' });
268
+ continue;
269
+ }
270
+ const fmt = run(dir, ['check-ref-format', `refs/tags/${tag}`]);
271
+ if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(fmt)) {
272
+ fail('git_timeout', { error: 'git check-ref-format timed out' });
273
+ continue;
274
+ }
275
+ if (fmt.exitCode !== 0) {
276
+ fail('tag_invalid', { error: 'tag name rejected by git check-ref-format' });
277
+ continue;
278
+ }
279
+ const refOmitted = entry.ref === undefined;
280
+ const ref = entry.ref ?? 'HEAD';
281
+ if (ref === '' || ref.startsWith('-') || ref.includes('\0')) {
282
+ fail('ref_unresolved', { error: 'ref is empty or starts with "-"' });
283
+ continue;
284
+ }
285
+ const resolved = run(dir, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
286
+ if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(resolved)) {
287
+ fail('git_timeout', { error: 'git rev-parse timed out' });
288
+ continue;
289
+ }
290
+ if (resolved.exitCode !== 0 || resolved.stdout === '') {
291
+ fail('ref_unresolved', { error: `ref "${ref}" does not resolve to a commit` });
292
+ continue;
293
+ }
294
+ const commit = resolved.stdout;
295
+ let detached;
296
+ if (refOmitted) {
297
+ const head = run(dir, ['symbolic-ref', '-q', 'HEAD']);
298
+ if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(head)) {
299
+ fail('git_timeout', { error: 'git symbolic-ref timed out' });
300
+ continue;
301
+ }
302
+ if (head.exitCode === 1)
303
+ detached = true;
304
+ }
305
+ const existing = run(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{commit}`]);
306
+ if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(existing)) {
307
+ fail('git_timeout', { error: 'git rev-parse timed out' });
308
+ continue;
309
+ }
310
+ if (existing.exitCode === 0 && existing.stdout !== '') {
311
+ if (existing.stdout === commit) {
312
+ result.repos[key] = { status: 'already_tagged', tag, commit, ...(detached ? { detached_head: true } : {}) };
313
+ }
314
+ else {
315
+ fail('tag_conflict', {
316
+ error: 'tag already exists at a different commit',
317
+ existing_commit: existing.stdout,
318
+ requested_commit: commit,
319
+ });
320
+ }
321
+ continue;
322
+ }
323
+ const anyRef = run(dir, ['show-ref', '--verify', '--quiet', `refs/tags/${tag}`]);
324
+ if ((0, shell_command_projection_cjs_1.isSpawnTimeout)(anyRef)) {
325
+ fail('git_timeout', { error: 'git show-ref timed out' });
326
+ continue;
327
+ }
328
+ if (anyRef.exitCode === 0) {
329
+ fail('tag_conflict', {
330
+ error: 'tag already exists and does not point at a commit',
331
+ existing_commit: null,
332
+ requested_commit: commit,
333
+ });
334
+ continue;
335
+ }
336
+ result.repos[key] = { status: 'would_create', tag, commit, ...(detached ? { detached_head: true } : {}) };
337
+ plans.push({
338
+ key,
339
+ repo: normalized,
340
+ dir,
341
+ tag,
342
+ message: entry.message === undefined || entry.message === '' ? tag : entry.message,
343
+ commit,
344
+ ...(detached ? { detached: true } : {}),
345
+ });
346
+ }
347
+ return { result, plans };
348
+ }
349
+ /** Validate a request without writing anything. */
350
+ function preflightReleaseTags(cwd, entries, options = {}) {
351
+ return runPreflight(cwd, entries, options, 'preflight').result;
352
+ }
353
+ /**
354
+ * Preflight, then (unless `dryRun`) create each annotated tag in its own repo.
355
+ * A preflight failure returns before any write. A creation failure does not
356
+ * stop the remaining repos; the result lists every tag already written.
357
+ */
358
+ function createReleaseTags(cwd, entries, options = {}) {
359
+ const dryRun = options.dryRun === true;
360
+ const { result, plans } = runPreflight(cwd, entries, options, dryRun ? 'dry_run' : 'apply');
361
+ if (!result.ok) {
362
+ result.phase = 'preflight';
363
+ return result;
364
+ }
365
+ if (dryRun)
366
+ return result;
367
+ const exec = options.execGit ?? shell_command_projection_cjs_1.execGit;
368
+ const timeout = options.timeoutMs ?? RELEASE_TAG_GIT_TIMEOUT_MS;
369
+ for (const plan of plans) {
370
+ const r = exec(['tag', '-a', '--message', plan.message, '--', plan.tag, plan.commit], { cwd: plan.dir, timeout });
371
+ if (r.exitCode === 0) {
372
+ result.repos[plan.key] = {
373
+ status: 'created',
374
+ tag: plan.tag,
375
+ commit: plan.commit,
376
+ ...(plan.detached ? { detached_head: true } : {}),
377
+ };
378
+ result.created.push(`${plan.repo}:${plan.tag}`);
379
+ continue;
380
+ }
381
+ const reason = (0, shell_command_projection_cjs_1.isSpawnTimeout)(r) ? 'git_timeout' : 'tag_create_failed';
382
+ const message = r.stderr || r.stdout || ((0, shell_command_projection_cjs_1.isSpawnTimeout)(r) ? 'git tag timed out' : 'git tag failed');
383
+ result.ok = false;
384
+ result.repos[plan.key] = { status: 'failed', tag: plan.tag, commit: plan.commit, reason, error: message };
385
+ result.errors.push({ repo: plan.repo, tag: plan.tag, reason, error: message });
386
+ }
387
+ return result;
388
+ }
389
+ // ─── CLI adapter ──────────────────────────────────────────────────────────
390
+ /**
391
+ * `milestone release-tags --map <file> [--dry-run]`. `args` is the full argv
392
+ * tail (`['milestone', 'release-tags', ...flags]`). Prints the result as JSON
393
+ * and sets a non-zero exit code when `ok` is false.
394
+ */
395
+ function cmdMilestoneReleaseTags(cwd, args, raw) {
396
+ const parsed = (0, command_arg_projection_cjs_1.parseNamedArgsOrExit)(args, { valueFlags: ['map'], booleanFlags: ['dry-run'], positionals: 2 }, error);
397
+ const mapArg = parsed['map'];
398
+ if (typeof mapArg !== 'string' || mapArg === '') {
399
+ error('--map <path-to-json-file> required for milestone release-tags', ERROR_REASON.USAGE);
400
+ return;
401
+ }
402
+ // A relative --map is relative to the shell's working directory, not the project root.
403
+ const mapPath = node_path_1.default.resolve(process.cwd(), mapArg);
404
+ let mapStat = null;
405
+ let statError = '';
406
+ try {
407
+ mapStat = node_fs_1.default.statSync(mapPath);
408
+ }
409
+ catch (e) {
410
+ statError = e instanceof Error ? e.message : String(e);
411
+ }
412
+ if (mapStat === null) {
413
+ error(`cannot read --map file: ${statError}`, ERROR_REASON.USAGE);
414
+ return;
415
+ }
416
+ // A FIFO or device reports size 0 and would bypass the size cap on read.
417
+ if (!mapStat.isFile()) {
418
+ error('--map must be a regular file', ERROR_REASON.USAGE);
419
+ return;
420
+ }
421
+ if (mapStat.size > MAX_MAP_FILE_BYTES) {
422
+ error(`--map file exceeds ${MAX_MAP_FILE_BYTES} byte limit (got ${mapStat.size})`, ERROR_REASON.USAGE);
423
+ return;
424
+ }
425
+ let text = '';
426
+ try {
427
+ text = node_fs_1.default.readFileSync(mapPath, 'utf-8');
428
+ }
429
+ catch (e) {
430
+ error(`cannot read --map file: ${e instanceof Error ? e.message : String(e)}`, ERROR_REASON.USAGE);
431
+ return;
432
+ }
433
+ const json = (0, security_cjs_1.safeJsonParse)(text, { maxLength: MAX_MAP_FILE_BYTES, label: '--map file' });
434
+ const result = json.ok
435
+ ? createReleaseTags(cwd, json.value, { dryRun: parsed['dry-run'] === true })
436
+ : topLevelError('invalid_request', json.error);
437
+ output(result, raw);
438
+ if (!result.ok)
439
+ process.exitCode = 1;
440
+ }
@@ -38,8 +38,9 @@ const stateContract = require("./state-contract.cjs");
38
38
  const { publishStateContract } = stateContract;
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
40
  const phaseIdMod = require("./phase-id.cjs");
41
- const { normalizePhaseName, matchPhaseDirs, isSentinelPhaseId, isSentinelPhaseDir, buildPhaseHeadingScanRegex, PHASE_HEADING_BASELINE } = phaseIdMod;
41
+ const { isSentinelPhaseId, isSentinelPhaseDir, buildPhaseHeadingScanRegex, PHASE_HEADING_BASELINE } = phaseIdMod;
42
42
  const pattern_cjs_1 = require("./pattern.cjs");
43
+ const milestone_release_map_cjs_1 = require("./milestone-release-map.cjs");
43
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
45
  const roadmapParserMod = require("./roadmap-parser.cjs");
45
46
  const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, } = roadmapParserMod;
@@ -54,7 +55,7 @@ const planScanMod = require("./plan-scan.cjs");
54
55
  const { scanPhasePlans } = planScanMod;
55
56
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
56
57
  const phaseLocatorMod = require("./phase-locator.cjs");
57
- const { listMilestonePhaseDirs } = phaseLocatorMod;
58
+ const { phaseDirectoryOptions, listMilestonePhaseDirs, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
58
59
  const { planningPaths, resolvePhaseIdConvention } = planningWorkspace;
59
60
  const { extractFrontmatter } = frontmatterMod;
60
61
  // ADR-3408 §8.3 / #3469: `writeStateMd` gets sync and NO preservation — the
@@ -72,43 +73,6 @@ const { syncAndPreserveStateMd, withStateLock, readModifyWriteStateMd, assertVer
72
73
  // the caller-supplied `--archive-version` override and the STATE.md-derived
73
74
  // live-read value, so a crafted value cannot escape `.planning/milestones/`.
74
75
  const ARCHIVE_VERSION_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
75
- /**
76
- * Scope an `updateTableCell` call to the `## Traceability` (or
77
- * `## Traceability Status`) heading's own section — up to the next H1/H2
78
- * heading — instead of handing it the WHOLE REQUIREMENTS.md content.
79
- *
80
- * F1 (#2245 review, BLOCKER): `updateTableCell` binds to the FIRST GFM table
81
- * found in whatever text it is given. The shipped requirements template
82
- * (gsd-core/templates/requirements.md) puts an `## Out of Scope` table
83
- * (`| Feature | Reason |`, no `Status` column) BEFORE `## Traceability` — so
84
- * an unscoped whole-file call targets the Out-of-Scope table instead, fails
85
- * with `{ok:false, reason:'unknown column: Status'}`, and the real
86
- * Traceability row is never flipped, while the checkbox surface still flips
87
- * and the command reports success (the #2140 silent-divergence class one
88
- * level deeper). Mirrors phase.cts's `editProgressHeadingSlice` scoping of
89
- * `## Progress` writes to that heading's own slice.
90
- *
91
- * Falls back to running `updateTableCell` against the whole `text` when no
92
- * `## Traceability` heading exists — matching the previous (unscoped)
93
- * behaviour for a REQUIREMENTS.md whose traceability table sits under some
94
- * other heading, or with no heading at all (never worse than before this fix).
95
- */
96
- function updateTraceabilityCell(text, match, column, newValue) {
97
- const headingMatch = text.match(/^##[ \t]+Traceability(?:[ \t]+Status)?\b/im);
98
- if (!headingMatch || headingMatch.index === undefined) {
99
- return (0, markdown_table_cjs_1.updateTableCell)(text, match, column, newValue);
100
- }
101
- const headingOffset = headingMatch.index;
102
- const before = text.slice(0, headingOffset);
103
- const fromHeading = text.slice(headingOffset);
104
- const nextHeadingOffset = fromHeading.search(/\n#{1,2}[ \t]/);
105
- const scoped = nextHeadingOffset >= 0 ? fromHeading.slice(0, nextHeadingOffset) : fromHeading;
106
- const after = nextHeadingOffset >= 0 ? fromHeading.slice(nextHeadingOffset) : '';
107
- const result = (0, markdown_table_cjs_1.updateTableCell)(scoped, match, column, newValue);
108
- if (!result.ok)
109
- return result;
110
- return { ok: true, value: before + result.value + after };
111
- }
112
76
  function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
113
77
  if (!reqIdsRaw || reqIdsRaw.length === 0) {
114
78
  error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
@@ -186,7 +150,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
186
150
  // Complete" gate is folded into the newValue callback so one
187
151
  // updateTableCell call both probes the current value and writes.
188
152
  let tableHit = false;
189
- const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
153
+ const tableUpdate = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
190
154
  // #2788: accept `Gaps Found` as a forward input too — `revert-phase` (the
191
155
  // documented gaps_found response) leaves a row stranded at Gaps Found with
192
156
  // no inverse; a genuinely-satisfied requirement must be able to reach
@@ -229,7 +193,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
229
193
  // that genuinely exists. Probe via a no-op updateTableCell write (its own
230
194
  // tolerant scan) instead of findTableWithColumns (whole-table parse gate).
231
195
  let currentStatusCell = '';
232
- const statusProbe = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
196
+ const statusProbe = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
233
197
  currentStatusCell = current;
234
198
  return current;
235
199
  });
@@ -433,7 +397,7 @@ function cmdRequirementsRevertPhase(cwd, reqIdsRaw, raw) {
433
397
  // "only flip Pending -> Complete" gate, in reverse).
434
398
  const rowMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === reqId.toLowerCase();
435
399
  let tableHit = false;
436
- const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
400
+ const tableUpdate = (0, markdown_table_cjs_1.updateTraceabilityCell)(reqContent, rowMatch, 'Status', (current) => {
437
401
  if (/^complete$/i.test(current.trim())) {
438
402
  tableHit = true;
439
403
  return ' Gaps Found ';
@@ -524,6 +488,41 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
524
488
  const phasesDir = planningPaths(cwd).phases;
525
489
  const today = clock_cjs_1.realClock.localToday();
526
490
  const milestoneName = options.name || version;
491
+ // #5706 (epic #3628): validate the caller-supplied release map BEFORE any
492
+ // read-for-decision or write below, so a bad map leaves the tree untouched
493
+ // (no archive directory, no MILESTONES.md or STATE.md change). An archived
494
+ // map is a recorded value: a differing map for an already-archived version is
495
+ // refused rather than silently overwritten; an identical one is left as is.
496
+ let releaseMap = null;
497
+ let releaseMapFile = null;
498
+ let releaseMapNeedsWrite = false;
499
+ if (typeof options.releaseMap === 'string') {
500
+ const loaded = (0, milestone_release_map_cjs_1.loadReleaseMapFile)(node_path_1.default.resolve(cwd, options.releaseMap));
501
+ if (!loaded.ok) {
502
+ error(`milestone complete: invalid --release-map — nothing has been changed.\n${loaded.errors.map((e) => ` - ${e}`).join('\n')}`);
503
+ }
504
+ else {
505
+ releaseMap = loaded.map;
506
+ releaseMapFile = (0, milestone_release_map_cjs_1.releaseMapArchivePath)(archiveDir, version);
507
+ const serialized = (0, milestone_release_map_cjs_1.serializeReleaseMapArchive)(version, releaseMap);
508
+ let existingText = null;
509
+ try {
510
+ existingText = node_fs_1.default.readFileSync(releaseMapFile, 'utf-8');
511
+ }
512
+ catch (e) {
513
+ if (e.code !== 'ENOENT') {
514
+ error(`milestone complete: ${node_path_1.default.basename(releaseMapFile)} already exists and cannot be read — nothing has been changed.`);
515
+ }
516
+ }
517
+ if (existingText !== null && existingText !== serialized) {
518
+ error(`milestone complete: ${node_path_1.default.basename(releaseMapFile)} already exists with a different release map; the archived map is a recorded value and is not overwritten — nothing has been changed.`);
519
+ }
520
+ releaseMapNeedsWrite = existingText === null;
521
+ }
522
+ }
523
+ const releaseMapReport = releaseMap && releaseMapFile
524
+ ? { path: node_path_1.default.relative(cwd, releaseMapFile).split(node_path_1.default.sep).join('/'), entries: releaseMap.length }
525
+ : null;
527
526
  // ADR-3408 §8.5 / #3469: "liberal but visible" — when the write-seam
528
527
  // composition's preservation stage restores a curated frontmatter value
529
528
  // over a disagreeing freshly-derived one, that divergence is surfaced
@@ -549,7 +548,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
549
548
  // `platformEnsureDir(archiveDir)` below — a refused run (scope not COMPLETE,
550
549
  // no --force) must be a true no-op on disk, and creating the archive
551
550
  // directory first left an empty directory behind even on refusal.
552
- const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
551
+ const isDirInMilestone = getMilestonePhaseFilter(cwd, version, undefined, undefined, phaseDirectoryOptions(cwd));
553
552
  if (isDirInMilestone.missingExplicitVersion) {
554
553
  error(`no phases found for milestone ${version} in ROADMAP.md`);
555
554
  }
@@ -574,6 +573,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
574
573
  `entries, even though the ROADMAP has phase entries elsewhere), so phase scoping ` +
575
574
  `cannot be trusted for this destructive operation. Re-run with --force to override.`);
576
575
  }
576
+ // #2761/#4304: resolve once for every phase-directory consumer in this
577
+ // command. The matcher opts in only for bracket; the milestone enumerator
578
+ // retains the full resolved value it already accepted.
579
+ const phaseConvention = resolvePhaseIdConvention(cwd);
577
580
  // Guard: prevent marking complete when ROADMAP still lists phases that have
578
581
  // no directory on disk (disk_status: no_directory). This catches the case
579
582
  // where the active milestone was erroneously marked complete before phases
@@ -655,8 +658,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
655
658
  // properly-scaffolded bracket phase reads as disk_status: 'no_directory'
656
659
  // — turning a real bracket project's own phases into false "ROADMAP
657
660
  // lists N unstarted phase(s)" failures on `milestone complete`.
658
- const convention = resolvePhaseIdConvention(cwd);
659
- const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, convention);
661
+ const directoryContext = phaseDirectoryOptions(cwd, undefined, scopedContent);
662
+ const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, phaseConvention, directoryContext);
660
663
  const noDirectoryPhases = [];
661
664
  let pm;
662
665
  const phaseDirEntries = (() => {
@@ -680,12 +683,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
680
683
  // #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this local check already covered both 0 and 999; now delegates to the single canonical owner.
681
684
  if (isSentinelPhaseId(phaseNum))
682
685
  continue;
683
- const normalized = normalizePhaseName(phaseNum);
686
+ const lookup = resolvePhaseDirectoryLookup(cwd, phaseNum, undefined, directoryContext);
684
687
  // A phase has disk_status: 'no_directory' when no phase directory
685
688
  // with a matching token exists on disk. Use the same matchPhaseDirs
686
689
  // owner that roadmap.analyze uses to avoid false positives on decimal
687
- // (2.1) and letter-suffix (12A) phase IDs. (#2528)
688
- const hasDirectory = matchPhaseDirs(phaseDirEntries, normalized, convention).matches.length > 0;
690
+ // (2.1) and letter-suffix (12A) phase IDs. (#2528) Under #4304 the
691
+ // current checkout's bracket convention and migration-window legacy
692
+ // fallback are both owned by the shared lookup adapter.
693
+ const hasDirectory = matchPhaseDirsForLookup(phaseDirEntries, lookup, undefined, directoryContext).matches.length > 0;
689
694
  if (!hasDirectory) {
690
695
  noDirectoryPhases.push(phaseNum);
691
696
  }
@@ -715,7 +720,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
715
720
  let totalPlans = 0;
716
721
  let totalTasks = 0;
717
722
  const accomplishments = [];
718
- // #2761 (round-11 BLOCKER): resolved ONCE, ambiently (no explicit `ws` —
723
+ // #2761: resolved ONCE above, ambiently (no explicit `ws` —
719
724
  // this function has none of its own and already resolves everything else
720
725
  // off `cwd` via planningPaths(cwd), matching the ambient-workstream
721
726
  // contract `resolvePhaseIdConvention`/`planningDir` share), and threaded
@@ -730,7 +735,6 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
730
735
  // the silent-inherit path without changing behavior for `null` /
731
736
  // `milestone-prefixed` projects, whose resolved convention is the same
732
737
  // value `undefined` would have lazily produced anyway.
733
- const phaseConvention = resolvePhaseIdConvention(cwd);
734
738
  // #3597 (ADR-3180 Decision 2): SINGLE resolution of "which phase
735
739
  // directories belong to the current milestone" AND the SCOPE discriminator
736
740
  // that resolution came from — shared verbatim by the read-only stats loop
@@ -860,6 +864,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
860
864
  phases_archive_skipped: phasesArchiveSkippedForScope,
861
865
  phases_archive_skip_reason: phasesArchiveSkipReason,
862
866
  quick: quickDirsToArchive,
867
+ ...(releaseMapReport ? { release_map: { target: releaseMapReport.path, entries: releaseMapReport.entries } } : {}),
863
868
  },
864
869
  would_update: {
865
870
  milestones_md: node_path_1.default.relative(cwd, milestonesPath).split(node_path_1.default.sep).join('/'),
@@ -885,6 +890,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
885
890
  // archive directory behind. Reaching this point means the run is
886
891
  // committed to mutating.
887
892
  (0, shell_command_projection_cjs_1.platformEnsureDir)(archiveDir);
893
+ // #5706: archive the validated release map by recorded value.
894
+ if (releaseMap && releaseMapFile && releaseMapNeedsWrite) {
895
+ (0, shell_command_projection_cjs_1.platformWriteSync)(releaseMapFile, (0, milestone_release_map_cjs_1.serializeReleaseMapArchive)(version, releaseMap));
896
+ }
888
897
  // Archive ROADMAP.md
889
898
  if (node_fs_1.default.existsSync(roadmapPath)) {
890
899
  const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
@@ -909,7 +918,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
909
918
  }
910
919
  // Create/append MILESTONES.md entry
911
920
  const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
912
- const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
921
+ const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n${releaseMap ? `${(0, milestone_release_map_cjs_1.renderReleaseMapMarkdown)(releaseMap)}\n` : ''}---\n\n`;
913
922
  // #3685: mirror requirementsUpdated's diff-tracking contract — the result
914
923
  // below used to report `milestones_updated: true` hardcoded, never
915
924
  // consulting whether the MILESTONES.md write actually changed anything.
@@ -1166,6 +1175,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
1166
1175
  milestones_updated: milestonesUpdated,
1167
1176
  state_updated: stateUpdated,
1168
1177
  preservation_warnings: preservationWarnings,
1178
+ ...(releaseMapReport ? { release_map: releaseMapReport } : {}),
1169
1179
  };
1170
1180
  output(result, raw);
1171
1181
  // #3227 (design doc §40 row 26 / "Not-corruption" rule): a refreshed