@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
@@ -30,7 +30,7 @@ const modelProfiles = require("./model-profiles.cjs");
30
30
  const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
31
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
32
  const configSchema = require("./config-schema.cjs");
33
- const { VALID_CONFIG_KEYS, isValidConfigKey, getCapabilityConfigSchema } = configSchema;
33
+ const { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS, isValidConfigKey, getCapabilityConfigSchema } = configSchema;
34
34
  const secrets_cjs_1 = require("./secrets.cjs");
35
35
  const review_reviewer_selection_cjs_1 = require("./review-reviewer-selection.cjs");
36
36
  const configuration_cjs_1 = require("./configuration.cjs");
@@ -66,6 +66,8 @@ const SHIP_PR_BODY_TEMPLATE_TOKENS = new Set([
66
66
  'padded_phase',
67
67
  ]);
68
68
  const SHIP_PR_BODY_SOURCE_RE = /^(ROADMAP|PLAN|SUMMARY|VERIFICATION|STATE|REQUIREMENTS|CONTEXT)\.md\s+##\s+[^\r\n#][^\r\n]*$/;
69
+ /** Allowed values of `git.repo_tagging` (#5706): `single` is the default. */
70
+ const VALID_REPO_TAGGING_MODES = Object.freeze(['single', 'per_repo']);
69
71
  // ADR-612 PR-5: configuration accepts every convention the runtime can read.
70
72
  // Keep this distinct from roadmap-upgrade's supported target set: sequential
71
73
  // is valid project configuration but is not a migration destination.
@@ -87,6 +89,9 @@ const SCHEMA_DEFAULTS = {
87
89
  'planner.stall_detect_interval_minutes': 5,
88
90
  'planner.stall_threshold_minutes': 10,
89
91
  'git.create_tag': true,
92
+ // #5706 (epic #3628): per-repository milestone tagging mode. `single` is
93
+ // exactly the pre-existing behavior; `per_repo` is opt-in.
94
+ 'git.repo_tagging': 'single',
90
95
  // #1689: per-plan agent_hint executor routing — default-on. A no-op for plans
91
96
  // without an agent_hint field, so existing dispatch is byte-identical.
92
97
  'workflow.agent_hint_routing': true,
@@ -191,6 +196,18 @@ function validateKnownConfigKeyPath(keyPath) {
191
196
  error(`Unknown config key: ${keyPath}. Did you mean ${suggested}?`, ERROR_REASON.CONFIG_INVALID_KEY);
192
197
  }
193
198
  }
199
+ /**
200
+ * The key gate `config-set` and `config-new-project` share (#5268): a known
201
+ * misspelling gets its suggestion, and anything else outside the schema is
202
+ * refused with the valid-key list. One function, so the two verbs cannot
203
+ * disagree about which keys they accept or about the error text.
204
+ */
205
+ function assertKnownConfigKey(cwd, kp) {
206
+ validateKnownConfigKeyPath(kp);
207
+ if (!isValidConfigKey(kp, cwd)) {
208
+ error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>, phase_commit_docs.<phase-id>`, ERROR_REASON.CONFIG_INVALID_KEY);
209
+ }
210
+ }
194
211
  /**
195
212
  * Is `value` an acceptable `git.protected_branches` list (#3552)?
196
213
  *
@@ -271,9 +288,11 @@ function validateShipPrBodySections(value) {
271
288
  }
272
289
  });
273
290
  }
274
- // ─── Core config operations ───────────────────────────────────────────────────
275
291
  /**
276
- * Build a fully-materialized config object for a new project.
292
+ * Plan a fully-materialized config object for a new project. Writes nothing:
293
+ * the user defaults rewrite comes back as data, for the caller to persist once
294
+ * it has decided to write at all (#5268: a `--dry-run` never does, and a
295
+ * refused build never gets that far).
277
296
  *
278
297
  * Merges (increasing priority):
279
298
  * 1. Hardcoded defaults — every key that loadConfig() resolves, plus mode/granularity
@@ -284,11 +303,10 @@ function validateShipPrBodySections(value) {
284
303
  * Uses the canonical `git` namespace for branching keys (consistent with VALID_CONFIG_KEYS
285
304
  * and the settings workflow). loadConfig() handles both flat and nested formats, so this
286
305
  * is backward-compatible with existing projects that have flat keys.
287
- *
288
- * Returns a plain object — does NOT write any files.
289
306
  */
290
- function buildNewProjectConfig(userChoices) {
307
+ function planNewProjectConfig(userChoices) {
291
308
  const choices = userChoices || {};
309
+ let defaultsRewrite = null;
292
310
  // #4976: the GSD-owned store resolves exactly as the config loader resolves
293
311
  // it (`GSD_HOME || homedir()`), so the defaults.json seeding this project is
294
312
  // the one the loader and its #3532 shadow warning read in the same run.
@@ -319,10 +337,9 @@ function buildNewProjectConfig(userChoices) {
319
337
  const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
320
338
  userDefaults['granularity'] = depthToGranularity[userDefaults['depth']] || userDefaults['depth'];
321
339
  delete userDefaults['depth'];
322
- try {
323
- (0, shell_command_projection_cjs_1.platformWriteSync)(globalDefaultsPath, JSON.stringify(userDefaults, null, 2));
324
- }
325
- catch { /* intentionally empty */ }
340
+ // The migrated value is used for this build whether or not the
341
+ // rewrite is ever persisted.
342
+ defaultsRewrite = { path: globalDefaultsPath, contents: JSON.stringify(userDefaults, null, 2) };
326
343
  }
327
344
  }
328
345
  }
@@ -344,6 +361,7 @@ function buildNewProjectConfig(userChoices) {
344
361
  git: {
345
362
  branching_strategy: CONFIG_DEFAULTS.branching_strategy,
346
363
  create_tag: true,
364
+ repo_tagging: 'single',
347
365
  phase_branch_template: CONFIG_DEFAULTS.phase_branch_template,
348
366
  milestone_branch_template: CONFIG_DEFAULTS.milestone_branch_template,
349
367
  quick_branch_template: CONFIG_DEFAULTS.quick_branch_template,
@@ -444,7 +462,134 @@ function buildNewProjectConfig(userChoices) {
444
462
  },
445
463
  };
446
464
  validateShipPrBodySections(config['ship']['pr_body_sections']);
447
- return config;
465
+ return { config, defaultsRewrite };
466
+ }
467
+ /** Persist a plan's user defaults rewrite, best-effort as it always was. */
468
+ function persistDefaultsRewrite(plan) {
469
+ if (plan.defaultsRewrite === null)
470
+ return;
471
+ try {
472
+ (0, shell_command_projection_cjs_1.platformWriteSync)(plan.defaultsRewrite.path, plan.defaultsRewrite.contents);
473
+ }
474
+ catch { /* intentionally empty */ }
475
+ }
476
+ /**
477
+ * Build a new project's config and persist the user defaults rewrite its plan
478
+ * carries. For callers that write the config unconditionally; one that may not
479
+ * (a `--dry-run`) plans with `planNewProjectConfig` and persists only on write.
480
+ */
481
+ function buildNewProjectConfig(userChoices) {
482
+ const plan = planNewProjectConfig(userChoices);
483
+ persistDefaultsRewrite(plan);
484
+ return plan.config;
485
+ }
486
+ function isPlainObject(value) {
487
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
488
+ }
489
+ /**
490
+ * Does any schema key live under `keyPath`? True for a section (`workflow`),
491
+ * for the top level of a dynamic key family (`agent_skills`, whose keys are
492
+ * `agent_skills.<agent-type>`), and for the settings that are also parents
493
+ * (`review.max_prompt_tokens_per_reviewer` and its per-reviewer keys;
494
+ * `agent_tools`, a setting whose entries are the `agent_tools.<selector>`
495
+ * family).
496
+ */
497
+ function hasChildConfigKeys(cwd, keyPath) {
498
+ const childPrefix = `${keyPath}.`;
499
+ for (const known of VALID_CONFIG_KEYS) {
500
+ if (known.startsWith(childPrefix))
501
+ return true;
502
+ }
503
+ if (DYNAMIC_KEY_PATTERNS.some((family) => family['topLevel'] === keyPath))
504
+ return true;
505
+ return Object.keys(getCapabilityConfigSchema(cwd)).some((known) => known.startsWith(childPrefix));
506
+ }
507
+ /**
508
+ * Is `keyPath` a section a choice may nest settings under? The same test as
509
+ * `hasChildConfigKeys`. A dynamic family's deeper levels
510
+ * (`model_profile_overrides.<runtime>`) are not recognised; an empty object
511
+ * there is refused as an unknown key, and it would set nothing anyway.
512
+ */
513
+ function isConfigSection(cwd, keyPath) {
514
+ return hasChildConfigKeys(cwd, keyPath);
515
+ }
516
+ /**
517
+ * Hold `config-new-project` choices to the rules `config-set` applies (#5268).
518
+ *
519
+ * Choices arrive nested (`{"workflow":{"research":true}}`) while the schema
520
+ * and `config-set` address dotted leaf paths (`workflow.research`), so the
521
+ * walk descends through section objects and checks each leaf under its dotted
522
+ * path: the key through `assertKnownConfigKey`, the value through
523
+ * `validateConfigValue`. An object whose own path is a settable key with
524
+ * nothing under it (`model_overrides.<agent>`) is a leaf, as it is for
525
+ * `config-set`.
526
+ *
527
+ * Returns the checked choices as a new object and leaves `choices` as given:
528
+ * each value is the one `config-set` would persist (a normalizing rule's
529
+ * result where one applies). An empty object is a section with nothing set:
530
+ * refused as an unknown key unless its name is a real section, and left out
531
+ * of the result either way.
532
+ */
533
+ function validateNewProjectChoices(cwd, choices, prefix = '') {
534
+ const checked = {};
535
+ for (const key of Object.keys(choices)) {
536
+ const kp = prefix ? `${prefix}.${key}` : key;
537
+ // An empty key names no setting. Refused here because an empty section
538
+ // name would otherwise be walked as if its contents sat one level up.
539
+ if (key === '')
540
+ assertKnownConfigKey(cwd, kp);
541
+ // A key is one path segment. A dot inside it spells a deeper path, which
542
+ // would pass the key gate while the merge wrote it as one literal key no
543
+ // reader resolves. The exception is a dot inside an id: one dynamic key
544
+ // family admits both the path cut at the dot and the whole path, so the
545
+ // dot is part of what that family calls a single key (`phase_commit_docs`
546
+ // is a map keyed by phase id, and "2.1" is a phase id).
547
+ const dot = key.indexOf('.');
548
+ if (dot !== -1) {
549
+ const head = prefix ? `${prefix}.${key.slice(0, dot)}` : key.slice(0, dot);
550
+ const dotIsInsideAnId = prefix !== '' && DYNAMIC_KEY_PATTERNS.some((family) => family.test(head) && family.test(kp));
551
+ if (!dotIsInsideAnId) {
552
+ error(`Invalid choice key "${key}" for config-new-project. Nest it instead of using a dotted key, e.g. {"workflow":{"research":true}}.`, ERROR_REASON.CONFIG_INVALID_KEY);
553
+ }
554
+ }
555
+ const value = choices[key];
556
+ if (isPlainObject(value) && (!isValidConfigKey(kp, cwd) || hasChildConfigKeys(cwd, kp))) {
557
+ // A non-empty section has its name checked through its leaves. An empty
558
+ // one has no leaves, so its name is checked here: config-set refuses
559
+ // `bogus '{}'` as an unknown key, and so does this.
560
+ if (Object.keys(value).length === 0 && !isValidConfigKey(kp, cwd) && !isConfigSection(cwd, kp)) {
561
+ assertKnownConfigKey(cwd, kp);
562
+ }
563
+ const section = validateNewProjectChoices(cwd, value, kp);
564
+ // Empty as given, or emptied by null choices: it sets nothing.
565
+ if (Object.keys(section).length > 0)
566
+ checked[key] = section;
567
+ continue;
568
+ }
569
+ // A section given a value that is not an object. config-set would call
570
+ // the name unknown, which misdescribes a real section; say what is wrong.
571
+ if (!isValidConfigKey(kp, cwd) && isConfigSection(cwd, kp)) {
572
+ error(`Invalid choice "${kp}" for config-new-project: it is a section, so its value must be an object of settings, e.g. {"workflow":{"research":true}}.`, ERROR_REASON.CONFIG_INVALID_KEY);
573
+ }
574
+ assertKnownConfigKey(cwd, kp);
575
+ // `config-set <key> null` clears the key: it skips the typed rules and
576
+ // leaves the key to its default. A null choice is the same request, so it
577
+ // skips them too and is left unchosen, where it used to be written as a
578
+ // literal null that config-set can never store.
579
+ const checkedValue = value === null
580
+ ? null
581
+ : validateConfigValue(cwd, kp, value, typeof value === 'string' ? value : JSON.stringify(value));
582
+ // config-set's setter refuses these segments after the key and value
583
+ // rules have run (`_setNestedValue`); same refusal, same place in the order.
584
+ // It also runs before the assignment below, so no such key reaches `checked`.
585
+ if (kp.split('.').some((segment) => segment === '__proto__' || segment === 'prototype' || segment === 'constructor')) {
586
+ error('Invalid config key (prototype pollution guard): ' + kp, ERROR_REASON.CONFIG_PARSE_FAILED);
587
+ }
588
+ if (value === null)
589
+ continue;
590
+ checked[key] = checkedValue;
591
+ }
592
+ return checked;
448
593
  }
449
594
  /**
450
595
  * Command: create a fully-materialized .planning/config.json for a new project.
@@ -453,25 +598,54 @@ function buildNewProjectConfig(userChoices) {
453
598
  * configured during /gsd:new-project). All remaining keys are filled from
454
599
  * hardcoded defaults and optional $GSD_HOME/.gsd/defaults.json.
455
600
  *
456
- * Idempotent: if config.json already exists, returns { created: false }.
601
+ * Choices are validated with `config-set`'s key and value rules before
602
+ * anything is written (#5268). With `dryRun`, prints the config that would be
603
+ * created and writes nothing.
604
+ *
605
+ * Idempotent: if config.json already exists, returns { created: false } once
606
+ * the choices have been checked; a refused choice is refused either way.
457
607
  */
458
- function cmdConfigNewProject(cwd, choicesJson, raw) {
608
+ function cmdConfigNewProject(cwd, choicesJson, raw, options = {}) {
609
+ const dryRun = options.dryRun === true;
459
610
  const planningBase = planningDir(cwd);
460
611
  const configPath = node_path_1.default.join(planningBase, 'config.json');
461
- // Idempotent: don't overwrite existing config
462
- if (node_fs_1.default.existsSync(configPath)) {
463
- output({ created: false, reason: 'already_exists' }, raw, 'exists');
464
- return;
465
- }
466
612
  // Parse user choices
467
613
  let userChoices = {};
468
614
  if (choicesJson && choicesJson.trim() !== '') {
615
+ let parsedChoices;
469
616
  try {
470
- userChoices = JSON.parse(choicesJson);
617
+ parsedChoices = JSON.parse(choicesJson);
471
618
  }
472
619
  catch (err) {
473
620
  error('Invalid JSON for config-new-project: ' + err.message);
474
621
  }
622
+ // #5268: an array or a string parses as JSON and was then spread into the
623
+ // config as index keys.
624
+ if (!isPlainObject(parsedChoices)) {
625
+ error('Invalid choices for config-new-project: expected a JSON object of config keys.', ERROR_REASON.USAGE);
626
+ }
627
+ userChoices = parsedChoices;
628
+ }
629
+ // #5268: refuse what config-set refuses, before the first write — and
630
+ // before the existing-config check, so a refused choice is reported
631
+ // whether or not the project already has a config.
632
+ const checkedChoices = validateNewProjectChoices(cwd, userChoices);
633
+ // Idempotent: don't overwrite existing config
634
+ if (node_fs_1.default.existsSync(configPath)) {
635
+ output({ created: false, reason: 'already_exists' }, raw, 'exists');
636
+ return;
637
+ }
638
+ const plan = planNewProjectConfig(checkedChoices);
639
+ const config = plan.config;
640
+ if (dryRun) {
641
+ // The preview is CLI output, so secret keys are masked exactly as
642
+ // config-set and config-get mask them. See lib/secrets.cjs.
643
+ const preview = { ...config };
644
+ for (const key of Object.keys(preview)) {
645
+ preview[key] = (0, secrets_cjs_1.maskIfSecret)(key, preview[key]);
646
+ }
647
+ output({ dry_run: true, would_create: true, path: '.planning/config.json', config: preview }, raw, 'would create (dry run)');
648
+ return;
475
649
  }
476
650
  // Ensure .planning directory exists
477
651
  try {
@@ -480,14 +654,16 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
480
654
  catch (err) {
481
655
  error('Failed to create .planning directory: ' + err.message);
482
656
  }
483
- const config = buildNewProjectConfig(userChoices);
484
657
  try {
485
658
  (0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(config, null, 2));
486
- output({ created: true, path: '.planning/config.json' }, raw, 'created');
487
659
  }
488
660
  catch (err) {
489
661
  error('Failed to write config.json: ' + err.message);
490
662
  }
663
+ // The user defaults rewrite follows the config it was planned for: a
664
+ // creation that fails before here leaves the defaults file as it was.
665
+ persistDefaultsRewrite(plan);
666
+ output({ created: true, path: '.planning/config.json' }, raw, 'created');
491
667
  }
492
668
  /**
493
669
  * Ensures the config file exists (creates it if needed).
@@ -772,10 +948,7 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
772
948
  // TypeScript doesn't always infer never-return narrowing through error(), so we assert.
773
949
  const kp = keyPath;
774
950
  const val = value;
775
- validateKnownConfigKeyPath(kp);
776
- if (!isValidConfigKey(kp, cwd)) {
777
- error(`Unknown config key: "${kp}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>, features.<feature_name>, phase_commit_docs.<phase-id>`, ERROR_REASON.CONFIG_INVALID_KEY);
778
- }
951
+ assertKnownConfigKey(cwd, kp);
779
952
  // Parse value (handle booleans, numbers, and JSON arrays/objects)
780
953
  let parsedValue = val;
781
954
  if (val === 'true')
@@ -832,6 +1005,60 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
832
1005
  if (kp === 'project_code') {
833
1006
  parsedValue = val;
834
1007
  }
1008
+ parsedValue = validateConfigValue(cwd, kp, parsedValue, val);
1009
+ if (dryRun) {
1010
+ const preview = previewConfigValue(cwd, kp, parsedValue);
1011
+ if ((0, secrets_cjs_1.isSecretKey)(kp)) {
1012
+ const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
1013
+ const maskedPrev = preview.previousValue === undefined
1014
+ ? undefined
1015
+ : (0, secrets_cjs_1.maskSecret)(preview.previousValue);
1016
+ output({ dry_run: true, would_update: true, key: kp, value: masked, previousValue: maskedPrev, masked: true }, raw, `${kp}=${masked} (dry run)`);
1017
+ return;
1018
+ }
1019
+ output({ dry_run: true, would_update: true, key: kp, value: parsedValue, previousValue: preview.previousValue }, raw, `${kp}=${String(parsedValue)} (dry run)`);
1020
+ return;
1021
+ }
1022
+ const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
1023
+ // Mask secrets in both JSON and text output. The plaintext is written
1024
+ // to config.json (that's where secrets live on disk); the CLI output
1025
+ // must never echo it. See lib/secrets.cjs.
1026
+ if ((0, secrets_cjs_1.isSecretKey)(kp)) {
1027
+ // parsedValue is unknown at this point; maskSecret accepts MaskableValue
1028
+ const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
1029
+ const maskedPrev = setConfigValueResult.previousValue === undefined
1030
+ ? undefined
1031
+ : (0, secrets_cjs_1.maskSecret)(setConfigValueResult.previousValue);
1032
+ const maskedResult = {
1033
+ ...setConfigValueResult,
1034
+ value: masked,
1035
+ previousValue: maskedPrev,
1036
+ masked: true,
1037
+ };
1038
+ output(maskedResult, raw, `${kp}=${masked}`);
1039
+ return;
1040
+ }
1041
+ output(setConfigValueResult, raw, `${kp}=${String(parsedValue)}`);
1042
+ }
1043
+ /**
1044
+ * The per-key VALUE rules, shared by `config-set` and `config-new-project`
1045
+ * (#5268). They were inline in `cmdConfigSet`, so `config-new-project` had no
1046
+ * way to apply them and wrote values `config-set` refuses for the same key.
1047
+ *
1048
+ * `parsedValue` is the typed value about to be persisted and `val` its text
1049
+ * form, used only in error messages. Returns the value to persist for `kp`:
1050
+ * the input itself, or its normalized form where a rule normalizes
1051
+ * (`review.default_reviewers`). A value the key does not accept has no value
1052
+ * to persist, so it is refused through `error()` instead.
1053
+ */
1054
+ function validateConfigValue(cwd, kp, parsedValue, val) {
1055
+ // #1581: project_code is an identifier, and config-set stores it as the text
1056
+ // typed. Through config-set this never fires; it holds a JSON choice (#5268)
1057
+ // to the type config-set stores, rather than writing a number readers cast
1058
+ // to string.
1059
+ if (kp === 'project_code' && typeof parsedValue !== 'string') {
1060
+ error(`Invalid project_code '${val}'. Must be a string identifier, e.g. "PROJ".`);
1061
+ }
835
1062
  const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
836
1063
  if (kp === 'context')
837
1064
  assertEnumValue(parsedValue, val, VALID_CONTEXT_VALUES, 'context value');
@@ -912,6 +1139,9 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
912
1139
  error(`Invalid git.create_tag '${val}'. Must be a boolean (true or false).`);
913
1140
  }
914
1141
  }
1142
+ // #5706 (epic #3628): per-repository milestone tagging mode, enum only.
1143
+ if (kp === 'git.repo_tagging')
1144
+ assertEnumValue(parsedValue, val, VALID_REPO_TAGGING_MODES, 'git.repo_tagging');
915
1145
  if (kp === 'git.protected_branches') {
916
1146
  if (!isValidProtectedBranches(parsedValue)) {
917
1147
  error(`Invalid git.protected_branches '${val}'. Must be a non-empty array of non-empty branch names.`);
@@ -1076,39 +1306,7 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
1076
1306
  }
1077
1307
  }
1078
1308
  }
1079
- if (dryRun) {
1080
- const preview = previewConfigValue(cwd, kp, parsedValue);
1081
- if ((0, secrets_cjs_1.isSecretKey)(kp)) {
1082
- const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
1083
- const maskedPrev = preview.previousValue === undefined
1084
- ? undefined
1085
- : (0, secrets_cjs_1.maskSecret)(preview.previousValue);
1086
- output({ dry_run: true, would_update: true, key: kp, value: masked, previousValue: maskedPrev, masked: true }, raw, `${kp}=${masked} (dry run)`);
1087
- return;
1088
- }
1089
- output({ dry_run: true, would_update: true, key: kp, value: parsedValue, previousValue: preview.previousValue }, raw, `${kp}=${String(parsedValue)} (dry run)`);
1090
- return;
1091
- }
1092
- const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
1093
- // Mask secrets in both JSON and text output. The plaintext is written
1094
- // to config.json (that's where secrets live on disk); the CLI output
1095
- // must never echo it. See lib/secrets.cjs.
1096
- if ((0, secrets_cjs_1.isSecretKey)(kp)) {
1097
- // parsedValue is unknown at this point; maskSecret accepts MaskableValue
1098
- const masked = (0, secrets_cjs_1.maskSecret)(parsedValue);
1099
- const maskedPrev = setConfigValueResult.previousValue === undefined
1100
- ? undefined
1101
- : (0, secrets_cjs_1.maskSecret)(setConfigValueResult.previousValue);
1102
- const maskedResult = {
1103
- ...setConfigValueResult,
1104
- value: masked,
1105
- previousValue: maskedPrev,
1106
- masked: true,
1107
- };
1108
- output(maskedResult, raw, `${kp}=${masked}`);
1109
- return;
1110
- }
1111
- output(setConfigValueResult, raw, `${kp}=${String(parsedValue)}`);
1309
+ return parsedValue;
1112
1310
  }
1113
1311
  function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
1114
1312
  const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
@@ -40,6 +40,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
40
40
  const node_path_1 = __importDefault(require("node:path"));
41
41
  // frontmatter-fence.cjs is a leaf module (no imports), so this adds no cycle.
42
42
  const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
43
+ // pattern.cjs is a leaf module (escapeRegex only), so this adds no cycle.
44
+ const pattern_cjs_1 = require("./pattern.cjs");
45
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
43
46
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
47
  const phaseIdModule = require("./phase-id.cjs");
45
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -521,6 +524,45 @@ function findOrphanSummaries(planFiles, summaryFiles) {
521
524
  }
522
525
  return summaryFiles.filter((s) => !claimed.has(s));
523
526
  }
527
+ /**
528
+ * #5542: does `summaryFile` pair with ANY of `planFiles`, by the same
529
+ * `summaryCandidates` rule the counts use? The ROADMAP plan-checkbox writers
530
+ * tick a row only for a summary that does — never by id prefix, which let a
531
+ * leftover `47-1` summary survive because a live `47-10` plan starts with `47-1`.
532
+ */
533
+ function summaryPairsWithAnyPlan(planFiles, summaryFile) {
534
+ return planFiles.some((plan) => countMatchedSummaries([plan], [summaryFile]) > 0);
535
+ }
536
+ /**
537
+ * #5542: lookahead that ends a plan id inside a ROADMAP checkbox row, so `47-1`
538
+ * never matches the start of `47-10-PLAN.md`. The id ends at `.md` or at any
539
+ * character that is not a word character or a `.`/`-` followed by a digit: a
540
+ * dotted sub-id (`01-01.1`) or a longer numeric id (`05-01-02`) is a distinct
541
+ * plan, not a continuation of `01-01`/`05-01`, while sentence punctuation
542
+ * (`01-01. Setup`) and a slug (`05-01-scaffold`) still end the id.
543
+ */
544
+ const PLAN_ID_ROW_BOUNDARY_SOURCE = '(?=\\.md|(?!\\w|[.-]\\d))';
545
+ /**
546
+ * #5542: the checkbox-row regex for one plan id. By default it matches an
547
+ * UNCHECKED row (the tick target); `anyState` matches either box state, for
548
+ * presence detection; `anchored` pins the row to a line start (the
549
+ * `phase complete` bullet flip). Capture group 1 is the opening `- [`, group 2
550
+ * the rest after the box, so a caller ticks with `$1x$2`.
551
+ */
552
+ function planRowPattern(planId, opts = {}) {
553
+ const box = opts.anyState ? '[x ]' : ' ';
554
+ const lead = opts.anchored ? '^(\\s*-\\s*\\[)' : '(-\\s*\\[)';
555
+ return new RegExp(`${lead}${box}(\\]\\s*(?:\\*\\*)?${(0, pattern_cjs_1.escapeRegex)(planId)}${PLAN_ID_ROW_BOUNDARY_SOURCE}(?:\\*\\*)?)`, 'i');
556
+ }
557
+ /**
558
+ * Tick the first line-anchored unchecked row for `planId` in `text` (the
559
+ * `phase complete` / `roadmap update-plan-progress` bullet flip): a bullet
560
+ * mutation through `updateBullet`, so never a fenced line or mid-line match.
561
+ */
562
+ function tickAnchoredPlanRow(text, planId) {
563
+ const row = planRowPattern(planId, { anchored: true });
564
+ return (0, markdown_sectionizer_cjs_1.updateBullet)(text, (_bulletText, rawLine) => row.test(rawLine), (rawLine) => rawLine.replace(row, '$1x$2'));
565
+ }
524
566
  module.exports = {
525
567
  toPosixPath,
526
568
  normalizeLineEndings,
@@ -536,4 +578,7 @@ module.exports = {
536
578
  countMatchedSummaries,
537
579
  findUnsummarizedPlans,
538
580
  findOrphanSummaries,
581
+ summaryPairsWithAnyPlan,
582
+ planRowPattern,
583
+ tickAnchoredPlanRow,
539
584
  };
@@ -20,12 +20,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.TRACKER_RESOLVE_BUDGET_MS = void 0;
23
24
  exports.decisionMentioned = decisionMentioned;
24
25
  exports.phaseDirProblem = phaseDirProblem;
25
26
  exports.loadPlanContents = loadPlanContents;
26
27
  exports.loadSummaryContents = loadSummaryContents;
27
28
  exports.loadDecisionExtraction = loadDecisionExtraction;
28
29
  exports.extractPlanDesignatedSections = extractPlanDesignatedSections;
30
+ exports.resolveTrackerBodies = resolveTrackerBodies;
29
31
  exports.buildPlanMessage = buildPlanMessage;
30
32
  exports.buildVerifyMessage = buildVerifyMessage;
31
33
  exports.phaseCommitMessages = phaseCommitMessages;
@@ -46,6 +48,12 @@ const { scanPhasePlans } = planScanMod;
46
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports
47
49
  const planningScopeMod = require("./planning-scope.cjs");
48
50
  const { SCOPE } = planningScopeMod;
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const planDocumentMod = require("./plan-document.cjs");
53
+ const { parsePlanDocument } = planDocumentMod;
54
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
55
+ const taskContentResolutionMod = require("./task-content-resolution.cjs");
56
+ const { resolveTaskContent, splitTrackerId, loadResolverCapabilities, isResolverFailure, contentTexts, } = taskContentResolutionMod;
49
57
  // ─── Decision matching ────────────────────────────────────────────────────────
50
58
  function normalizePhrase(text) {
51
59
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -189,6 +197,72 @@ function extractPlanDesignatedSections(planContent) {
189
197
  parts.push((0, markdown_sectionizer_cjs_1.extractXmlTagBodies)(cleaned));
190
198
  return parts.join('\n\n');
191
199
  }
200
+ /** Aggregate wall-clock budget for all resolver spawns in one gate run; registry load and each spawn's timeout are inside it (spawn timeouts are clamped to the remainder), so this is the true bound for resolver work. */
201
+ exports.TRACKER_RESOLVE_BUDGET_MS = 120_000;
202
+ const unreadable = (reason, span) => ({ kind: 'unreadable', reason, span });
203
+ /**
204
+ * Extra scannable text per plan (same index) from tracker-resolved task content; '' for plans without `tracker-id=`.
205
+ * `missingResolver` names the first tracker id with no installed resolver: coverage is monotone (a mention anywhere counts), so
206
+ * that can only cause a false "uncovered" — callers go `unreadable` only when a decision would otherwise be uncovered.
207
+ */
208
+ function resolveTrackerBodies(projectDir, planContents, deps = {}) {
209
+ let missingResolver = null;
210
+ let capabilities = null;
211
+ const seen = new Map();
212
+ const resolveFn = deps.resolveTaskContentFn ?? resolveTaskContent;
213
+ const bodies = [];
214
+ const now = deps.now ?? (() => performance.now());
215
+ const startedAt = now();
216
+ for (const raw of planContents) {
217
+ const content = stripCommentsAndFences(raw);
218
+ if (!/\btracker-id\s*=/i.test(content)) {
219
+ bodies.push('');
220
+ continue;
221
+ }
222
+ const parts = [];
223
+ for (const task of parsePlanDocument(content).tasks) {
224
+ const trackerId = task.trackerId;
225
+ if (trackerId === null)
226
+ continue;
227
+ const cached = seen.get(trackerId);
228
+ if (cached) {
229
+ parts.push(...cached);
230
+ continue;
231
+ }
232
+ if (capabilities === null) {
233
+ try {
234
+ capabilities = (deps.loadCapabilities ?? loadResolverCapabilities)(projectDir);
235
+ }
236
+ catch (err) {
237
+ return unreadable(`capability registry unreadable: ${err instanceof Error ? err.message : String(err)}`, trackerId);
238
+ }
239
+ }
240
+ // Floor: spawnSync rejects a fractional timeout, and a 0 timeout means unbounded, so a sub-ms remainder is a rejection.
241
+ const remainingMs = Math.floor(exports.TRACKER_RESOLVE_BUDGET_MS - (now() - startedAt));
242
+ if (remainingMs <= 0) {
243
+ return unreadable(`tracker resolution exceeded the ${exports.TRACKER_RESOLVE_BUDGET_MS} ms aggregate budget`, trackerId);
244
+ }
245
+ try {
246
+ const result = resolveFn({ trackerId, capabilities, timeoutOverrideMs: remainingMs });
247
+ if (result.kind === 'no-resolver') {
248
+ missingResolver ??= { span: trackerId, reason: `no task-content resolver installed for tracker prefix "${splitTrackerId(trackerId)?.prefix}"` };
249
+ }
250
+ // Per field: an unclosed fence in one field must not swallow the rest.
251
+ const texts = result.kind === 'resolved' ? contentTexts(result.content).map(stripCommentsAndFences) : [];
252
+ seen.set(trackerId, texts);
253
+ parts.push(...texts);
254
+ }
255
+ catch (err) {
256
+ if (isResolverFailure(err)) {
257
+ return unreadable(`tracker task content unresolved: ${err.message}`, trackerId);
258
+ }
259
+ throw err;
260
+ }
261
+ }
262
+ bodies.push(parts.join('\n'));
263
+ }
264
+ return (0, gate_evidence_cjs_1.evidenceFound)({ bodies, missingResolver });
265
+ }
192
266
  function buildPlanMessage(uncovered) {
193
267
  if (uncovered.length === 0)
194
268
  return 'All trackable CONTEXT.md decisions are covered by plans.';
@@ -851,6 +851,17 @@ function frontmatterListEntries(content, key) {
851
851
  * no closed, parseable frontmatter mapping or the key is not an own key of it.
852
852
  */
853
853
  function rawFrontmatterField(content, key) {
854
+ const mapping = rawFrontmatterMapping(content);
855
+ if (!mapping || !Object.prototype.hasOwnProperty.call(mapping, key))
856
+ return null;
857
+ return { value: mapping[key] };
858
+ }
859
+ /**
860
+ * The top-level frontmatter mapping VERBATIM, off `rawFrontmatterField`'s guarded parse path.
861
+ * Null when the document has no closed, parseable frontmatter mapping — so a caller can tell
862
+ * "parsed, without that key" (#4998 review) from "no usable frontmatter".
863
+ */
864
+ function rawFrontmatterMapping(content) {
854
865
  const found = frontmatterRegion(content);
855
866
  if (!found || !found.terminated)
856
867
  return null;
@@ -867,9 +878,7 @@ function rawFrontmatterField(content, key) {
867
878
  }
868
879
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
869
880
  return null;
870
- if (!Object.prototype.hasOwnProperty.call(raw, key))
871
- return null;
872
- return { value: raw[key] };
881
+ return raw;
873
882
  }
874
883
  /**
875
884
  * Escape a string for emission inside a YAML double-quoted scalar (#1779). ADR-3473 §8.1
@@ -940,6 +949,14 @@ function scalarNeedsDoubleQuoting(s) {
940
949
  // round-trip.
941
950
  if (/[\uD800-\uDFFF]/.test(s))
942
951
  return true;
952
+ // #4998: U+2028/U+2029 are not YAML line breaks, so a bare one round-trips
953
+ // through js-yaml, but every line-anchored reader of a planning document
954
+ // (`^key:\s*(.+)` with the `m` flag) ends the line there and reads only the
955
+ // part before it. (A value holding `:` was already quoted by the callers, so
956
+ // no key could be forged this way.) The quoted path escapes them (`\L`,
957
+ // `\P`), which no such reader splits on.
958
+ if (/[\u2028\u2029]/.test(s))
959
+ return true;
943
960
  return false;
944
961
  }
945
962
  /**
@@ -1806,6 +1823,7 @@ module.exports = {
1806
1823
  // parse path — the decision-coverage gates read `must_haves`/`truths`/`objective` and the
1807
1824
  // SUMMARY `files_modified` list through it instead of a hand-rolled `^key:` scan / regex.
1808
1825
  rawFrontmatterField,
1826
+ rawFrontmatterMapping,
1809
1827
  // #5139: one key's block as unparsed text, for a citation scan that must survive frontmatter
1810
1828
  // the YAML parser refuses.
1811
1829
  frontmatterKeyBlockText,
@@ -39,7 +39,7 @@ const planningWorkspaceMod = require("./planning-workspace.cjs");
39
39
  const { planningDir } = planningWorkspaceMod;
40
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
41
41
  const phaseLocatorMod = require("./phase-locator.cjs");
42
- const { findPhaseInternal } = phaseLocatorMod;
42
+ const { phaseDirectoryOptions, findPhaseInternal } = phaseLocatorMod;
43
43
  const security_cjs_1 = require("./security.cjs");
44
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
45
45
  const roadmapModule = require("./roadmap.cjs");
@@ -171,7 +171,7 @@ function evaluateApiCoverageVerifyPre(input) {
171
171
  let resolvedDir = null;
172
172
  let phaseNumber = '';
173
173
  if (token) {
174
- const found = findPhaseInternal(projectDir, token);
174
+ const found = findPhaseInternal(projectDir, token, undefined, phaseDirectoryOptions(projectDir));
175
175
  if (found && found.directory) {
176
176
  // findPhaseInternal's `directory` is relative to the project root; anchor it to `projectDir`
177
177
  // (not the process cwd) so the gate reads the same files in-process as it does from the CLI.