@opengsd/gsd-core 1.17.0 → 1.19.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 (369) 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-advisor-researcher.compact.md +1 -1
  5. package/agents/gsd-advisor-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  7. package/agents/gsd-assumptions-analyzer.md +1 -1
  8. package/agents/gsd-code-fixer.compact.md +3 -3
  9. package/agents/gsd-code-fixer.md +5 -4
  10. package/agents/gsd-code-reviewer.compact.md +1 -1
  11. package/agents/gsd-code-reviewer.md +1 -1
  12. package/agents/gsd-codebase-mapper.compact.md +1 -1
  13. package/agents/gsd-codebase-mapper.md +1 -1
  14. package/agents/gsd-debugger.md +1 -1
  15. package/agents/gsd-doc-verifier.compact.md +4 -4
  16. package/agents/gsd-doc-verifier.md +4 -3
  17. package/agents/gsd-doc-writer.compact.md +23 -5
  18. package/agents/gsd-doc-writer.md +19 -6
  19. package/agents/gsd-eval-auditor.compact.md +1 -1
  20. package/agents/gsd-eval-auditor.md +1 -1
  21. package/agents/gsd-executor.md +7 -3
  22. package/agents/gsd-integration-checker.compact.md +1 -1
  23. package/agents/gsd-integration-checker.md +1 -1
  24. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  25. package/agents/gsd-nyquist-auditor.md +1 -1
  26. package/agents/gsd-phase-researcher.md +1 -1
  27. package/agents/gsd-plan-checker.md +2 -1
  28. package/agents/gsd-planner.md +5 -4
  29. package/agents/gsd-project-researcher.compact.md +1 -1
  30. package/agents/gsd-project-researcher.md +1 -1
  31. package/agents/gsd-research-synthesizer.compact.md +5 -5
  32. package/agents/gsd-research-synthesizer.md +5 -5
  33. package/agents/gsd-roadmapper.compact.md +1 -1
  34. package/agents/gsd-roadmapper.md +1 -1
  35. package/agents/gsd-security-auditor.compact.md +1 -1
  36. package/agents/gsd-security-auditor.md +1 -1
  37. package/agents/gsd-ui-auditor.compact.md +1 -1
  38. package/agents/gsd-ui-auditor.md +1 -1
  39. package/agents/gsd-ui-checker.compact.md +1 -1
  40. package/agents/gsd-ui-checker.md +1 -1
  41. package/agents/gsd-ui-researcher.compact.md +13 -1
  42. package/agents/gsd-ui-researcher.md +28 -1
  43. package/agents/gsd-verifier.md +2 -2
  44. package/bin/install.js +460 -105
  45. package/commands/gsd/code-review.md +8 -0
  46. package/commands/gsd/pause-work.md +1 -1
  47. package/gsd-core/bin/gsd-tools.cjs +124 -18
  48. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  49. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  50. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  51. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  52. package/gsd-core/bin/lib/capability-lock.cjs +41 -8
  53. package/gsd-core/bin/lib/capability-registry.cjs +162 -74
  54. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  55. package/gsd-core/bin/lib/capability-state.cjs +20 -11
  56. package/gsd-core/bin/lib/capability-validator.cjs +23 -4
  57. package/gsd-core/bin/lib/commands.cjs +5 -1
  58. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  59. package/gsd-core/bin/lib/config.cjs +256 -58
  60. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  61. package/gsd-core/bin/lib/decision-coverage-support.cjs +93 -0
  62. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  63. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +21 -1
  64. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +19 -1
  65. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  66. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  67. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  68. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +114 -18
  69. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  70. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  72. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  73. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  74. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  75. package/gsd-core/bin/lib/init.cjs +76 -5
  76. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  77. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  78. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  79. package/gsd-core/bin/lib/install-profiles.cjs +14 -7
  80. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  81. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  82. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  83. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  84. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  85. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  86. package/gsd-core/bin/lib/milestone.cjs +43 -1
  87. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  88. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  89. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  91. package/gsd-core/bin/lib/phase.cjs +235 -37
  92. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  93. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  94. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  95. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  96. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  97. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  98. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  99. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  100. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  101. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  102. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  103. package/gsd-core/bin/lib/roadmap.cjs +89 -30
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +428 -16
  105. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  106. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  108. package/gsd-core/bin/lib/shell-command-projection.cjs +123 -16
  109. package/gsd-core/bin/lib/smart-entry.cjs +37 -16
  110. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  111. package/gsd-core/bin/lib/state-document.cjs +47 -5
  112. package/gsd-core/bin/lib/state-transition.cjs +150 -34
  113. package/gsd-core/bin/lib/state.cjs +140 -88
  114. package/gsd-core/bin/lib/surface.cjs +9 -0
  115. package/gsd-core/bin/lib/task-command-router.cjs +7 -28
  116. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  117. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  118. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  119. package/gsd-core/bin/lib/verification.cjs +96 -9
  120. package/gsd-core/bin/lib/verify.cjs +38 -1
  121. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  122. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  123. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  124. package/gsd-core/references/agent-contracts.md +1 -1
  125. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  126. package/gsd-core/references/git-integration.md +14 -3
  127. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  128. package/gsd-core/references/planner-reversibility.md +18 -4
  129. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  130. package/gsd-core/references/planning-config.md +2 -0
  131. package/gsd-core/references/tdd.md +5 -4
  132. package/gsd-core/references/verifier-phase-gates.md +8 -1
  133. package/gsd-core/templates/DEBUG.md +16 -0
  134. package/gsd-core/templates/config.json +2 -1
  135. package/gsd-core/templates/verification-report.md +1 -1
  136. package/gsd-core/workflows/add-backlog.md +10 -2
  137. package/gsd-core/workflows/add-phase.md +5 -1
  138. package/gsd-core/workflows/add-tests.md +5 -1
  139. package/gsd-core/workflows/add-todo.md +5 -1
  140. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  141. package/gsd-core/workflows/audit-fix.md +2 -1
  142. package/gsd-core/workflows/audit-milestone.md +21 -9
  143. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  144. package/gsd-core/workflows/autonomous.md +52 -64
  145. package/gsd-core/workflows/check-todos.md +5 -1
  146. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  147. package/gsd-core/workflows/code-review-fix.md +5 -1
  148. package/gsd-core/workflows/code-review.md +66 -8
  149. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  150. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  151. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  152. package/gsd-core/workflows/complete-milestone.md +64 -157
  153. package/gsd-core/workflows/debug.md +5 -1
  154. package/gsd-core/workflows/diagnose-issues.md +34 -6
  155. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  156. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  157. package/gsd-core/workflows/discuss-phase-assumptions.md +44 -5
  158. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  159. package/gsd-core/workflows/discuss-phase.md +11 -11
  160. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  161. package/gsd-core/workflows/docs-update.md +27 -5
  162. package/gsd-core/workflows/edit-phase.md +5 -1
  163. package/gsd-core/workflows/eval-review.md +11 -3
  164. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  165. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  166. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +19 -8
  167. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +21 -3
  168. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  169. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +7 -3
  170. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  171. package/gsd-core/workflows/execute-phase.md +46 -11
  172. package/gsd-core/workflows/execute-plan.md +8 -4
  173. package/gsd-core/workflows/extract-learnings.md +6 -2
  174. package/gsd-core/workflows/forensics.md +5 -1
  175. package/gsd-core/workflows/health.md +2 -1
  176. package/gsd-core/workflows/import.md +6 -2
  177. package/gsd-core/workflows/insert-phase.md +5 -1
  178. package/gsd-core/workflows/manager.md +10 -2
  179. package/gsd-core/workflows/map-codebase.md +5 -1
  180. package/gsd-core/workflows/milestone-summary.md +10 -2
  181. package/gsd-core/workflows/new-milestone.md +13 -5
  182. package/gsd-core/workflows/new-project.md +9 -5
  183. package/gsd-core/workflows/next.md +8 -4
  184. package/gsd-core/workflows/pause-work.md +3 -3
  185. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  186. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +7 -3
  187. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  188. package/gsd-core/workflows/plan-phase.md +54 -86
  189. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  190. package/gsd-core/workflows/plant-seed.md +8 -4
  191. package/gsd-core/workflows/progress.md +6 -2
  192. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  193. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  194. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  195. package/gsd-core/workflows/quick-batch.md +5 -1
  196. package/gsd-core/workflows/quick.md +23 -8
  197. package/gsd-core/workflows/remove-phase.md +5 -1
  198. package/gsd-core/workflows/resume-project.md +5 -1
  199. package/gsd-core/workflows/review.md +22 -15
  200. package/gsd-core/workflows/scan.md +5 -1
  201. package/gsd-core/workflows/section-manifest.json +5 -0
  202. package/gsd-core/workflows/secure-phase.md +6 -2
  203. package/gsd-core/workflows/settings.md +4 -3
  204. package/gsd-core/workflows/ship.md +16 -14
  205. package/gsd-core/workflows/spec-phase.md +3 -6
  206. package/gsd-core/workflows/thread.md +10 -2
  207. package/gsd-core/workflows/transition.md +5 -1
  208. package/gsd-core/workflows/ui-phase.md +5 -1
  209. package/gsd-core/workflows/ui-review.md +6 -2
  210. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  211. package/gsd-core/workflows/update.md +5 -1
  212. package/gsd-core/workflows/validate-phase.md +6 -2
  213. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  214. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  215. package/gsd-core/workflows/verify-work.md +15 -6
  216. package/hooks/dist/gsd-agent-isolation-guard.js +14 -2
  217. package/hooks/dist/gsd-cursor-subagent-start.js +8 -3
  218. package/hooks/dist/gsd-path-export.js +95 -0
  219. package/hooks/dist/gsd-secret-read-guard.js +244 -53
  220. package/hooks/dist/gsd-statusline.js +35 -2
  221. package/hooks/dist/gsd-validate-commit.sh +27 -5
  222. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  223. package/hooks/dist/gsd-workflow-guard.js +1 -1
  224. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  225. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  226. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  227. package/hooks/gsd-agent-isolation-guard.js +14 -2
  228. package/hooks/gsd-cursor-subagent-start.js +8 -3
  229. package/hooks/gsd-path-export.js +95 -0
  230. package/hooks/gsd-secret-read-guard.js +244 -53
  231. package/hooks/gsd-statusline.js +35 -2
  232. package/hooks/gsd-validate-commit.sh +27 -5
  233. package/hooks/gsd-windsurf-pre-write.js +28 -1
  234. package/hooks/gsd-workflow-guard.js +1 -1
  235. package/hooks/gsd-worktree-path-guard.js +28 -1
  236. package/hooks/hooks.json +1 -0
  237. package/hooks/lib/isolation-sentinel.js +44 -0
  238. package/hooks/managed-hooks-registry.cjs +1 -0
  239. package/package.json +13 -6
  240. package/pi/gsd.cjs +1047 -10
  241. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  242. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  243. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  244. package/pi/package-agents/gsd-code-fixer.md +769 -0
  245. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  246. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  247. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  248. package/pi/package-agents/gsd-debugger.md +1274 -0
  249. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  250. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  251. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  252. package/pi/package-agents/gsd-doc-writer.md +626 -0
  253. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  254. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  255. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  256. package/pi/package-agents/gsd-eval-planner.md +149 -0
  257. package/pi/package-agents/gsd-executor.md +894 -0
  258. package/pi/package-agents/gsd-framework-selector.md +159 -0
  259. package/pi/package-agents/gsd-integration-checker.md +476 -0
  260. package/pi/package-agents/gsd-intel-updater.md +338 -0
  261. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  262. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  263. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  264. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  265. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  266. package/pi/package-agents/gsd-planner.md +1003 -0
  267. package/pi/package-agents/gsd-project-researcher.md +611 -0
  268. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  269. package/pi/package-agents/gsd-roadmapper.md +779 -0
  270. package/pi/package-agents/gsd-security-auditor.md +171 -0
  271. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  272. package/pi/package-agents/gsd-ui-checker.md +417 -0
  273. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  274. package/pi/package-agents/gsd-user-profiler.md +174 -0
  275. package/pi/package-agents/gsd-verifier.md +963 -0
  276. package/pi/package-agents/manifest.json +39 -0
  277. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  278. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  279. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  280. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  281. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  282. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  283. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  284. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  285. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  286. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  287. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  288. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  289. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  290. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  291. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  292. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  293. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  294. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  295. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  296. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  297. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  298. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  299. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  300. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  301. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  302. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  303. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  304. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  305. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  306. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  307. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  308. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  309. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  310. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  311. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  312. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  313. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  314. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  315. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  316. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  317. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  318. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  319. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  320. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  321. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  322. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  323. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  324. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  325. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  326. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  327. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  328. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  329. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  330. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  331. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  332. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  333. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  334. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  335. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  336. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  337. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  338. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  339. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  340. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  341. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  342. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  343. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  344. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  345. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  346. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  347. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  348. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  349. package/scripts/audit-workflow-script-paths.cjs +1 -1
  350. package/scripts/build-hooks.js +4 -0
  351. package/scripts/command-contract-helpers.cjs +1 -1
  352. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  353. package/scripts/docs-guard-registry.cjs +22 -7
  354. package/scripts/gen-health-docs.cjs +13 -1
  355. package/scripts/gen-pi-package-skills.cjs +217 -0
  356. package/scripts/lib/macos-conformance-tier.generated.cjs +19 -0
  357. package/scripts/lib/platform-conformance-tier.generated.cjs +41 -0
  358. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  359. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  360. package/scripts/lint-retired-runtime-name.cjs +5 -0
  361. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  362. package/scripts/npm-audit-baseline.cjs +41 -7
  363. package/scripts/release-tarball-smoke.cjs +1 -0
  364. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  365. package/scripts/select-docs-guards.cjs +1 -1
  366. package/scripts/self-test-report-policy.cjs +350 -0
  367. package/skills/gsd-code-review/SKILL.md +8 -0
  368. package/skills/gsd-pause-work/SKILL.md +1 -1
  369. package/vscode/package.json +1 -1
@@ -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,8 @@ 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");
43
45
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
46
  const phaseIdModule = require("./phase-id.cjs");
45
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -521,6 +523,36 @@ function findOrphanSummaries(planFiles, summaryFiles) {
521
523
  }
522
524
  return summaryFiles.filter((s) => !claimed.has(s));
523
525
  }
526
+ /**
527
+ * #5542: does `summaryFile` pair with ANY of `planFiles`, by the same
528
+ * `summaryCandidates` rule the counts use? The ROADMAP plan-checkbox writers
529
+ * tick a row only for a summary that does — never by id prefix, which let a
530
+ * leftover `47-1` summary survive because a live `47-10` plan starts with `47-1`.
531
+ */
532
+ function summaryPairsWithAnyPlan(planFiles, summaryFile) {
533
+ return planFiles.some((plan) => countMatchedSummaries([plan], [summaryFile]) > 0);
534
+ }
535
+ /**
536
+ * #5542: lookahead that ends a plan id inside a ROADMAP checkbox row, so `47-1`
537
+ * never matches the start of `47-10-PLAN.md`. The id ends at `.md` or at any
538
+ * character that is not a word character or a `.`/`-` followed by a digit: a
539
+ * dotted sub-id (`01-01.1`) or a longer numeric id (`05-01-02`) is a distinct
540
+ * plan, not a continuation of `01-01`/`05-01`, while sentence punctuation
541
+ * (`01-01. Setup`) and a slug (`05-01-scaffold`) still end the id.
542
+ */
543
+ const PLAN_ID_ROW_BOUNDARY_SOURCE = '(?=\\.md|(?!\\w|[.-]\\d))';
544
+ /**
545
+ * #5542: the checkbox-row regex for one plan id. By default it matches an
546
+ * UNCHECKED row (the tick target); `anyState` matches either box state, for
547
+ * presence detection; `anchored` pins the row to a line start (the
548
+ * `phase complete` bullet flip). Capture group 1 is the opening `- [`, group 2
549
+ * the rest after the box, so a caller ticks with `$1x$2`.
550
+ */
551
+ function planRowPattern(planId, opts = {}) {
552
+ const box = opts.anyState ? '[x ]' : ' ';
553
+ const lead = opts.anchored ? '^(\\s*-\\s*\\[)' : '(-\\s*\\[)';
554
+ return new RegExp(`${lead}${box}(\\]\\s*(?:\\*\\*)?${(0, pattern_cjs_1.escapeRegex)(planId)}${PLAN_ID_ROW_BOUNDARY_SOURCE}(?:\\*\\*)?)`, 'i');
555
+ }
524
556
  module.exports = {
525
557
  toPosixPath,
526
558
  normalizeLineEndings,
@@ -536,4 +568,6 @@ module.exports = {
536
568
  countMatchedSummaries,
537
569
  findUnsummarizedPlans,
538
570
  findOrphanSummaries,
571
+ summaryPairsWithAnyPlan,
572
+ planRowPattern,
539
573
  };
@@ -20,11 +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;
25
+ exports.phaseDirProblem = phaseDirProblem;
24
26
  exports.loadPlanContents = loadPlanContents;
25
27
  exports.loadSummaryContents = loadSummaryContents;
26
28
  exports.loadDecisionExtraction = loadDecisionExtraction;
27
29
  exports.extractPlanDesignatedSections = extractPlanDesignatedSections;
30
+ exports.resolveTrackerBodies = resolveTrackerBodies;
28
31
  exports.buildPlanMessage = buildPlanMessage;
29
32
  exports.buildVerifyMessage = buildVerifyMessage;
30
33
  exports.phaseCommitMessages = phaseCommitMessages;
@@ -45,6 +48,12 @@ const { scanPhasePlans } = planScanMod;
45
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports
46
49
  const planningScopeMod = require("./planning-scope.cjs");
47
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;
48
57
  // ─── Decision matching ────────────────────────────────────────────────────────
49
58
  function normalizePhrase(text) {
50
59
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -70,6 +79,24 @@ function decisionMentioned(haystack, decision) {
70
79
  return phrase ? normalizePhrase(haystack).includes(phrase) : false;
71
80
  }
72
81
  // ─── File reading ─────────────────────────────────────────────────────────────
82
+ // #4939: loadPlanContents answers no files for a path that is not there, and that reads
83
+ // as "no plan cites anything" — so a phase-dir argument that names no directory
84
+ // (the phase NUMBER in the phase-dir slot) must be told apart BEFORE the scan.
85
+ // Answers null for a directory, else what is wrong with the path and what to do
86
+ // about it. Like the plan gate's contextKind (#4794), the message names the real
87
+ // stat outcome: a permission or I/O failure (EACCES, ELOOP, EIO) is reported as
88
+ // unreadable, not as a missing directory. Only ENOENT/ENOTDIR mean the path is not there.
89
+ function phaseDirProblem(dirPath) {
90
+ const passDirectory = 'Pass the phase DIRECTORY (e.g. .planning/phases/01-slug), not the phase number.';
91
+ const st = (0, gate_evidence_cjs_1.statEvidence)(dirPath);
92
+ if (st.kind === 'none')
93
+ return { reason: 'phase directory not found', what: 'does not exist', hint: passDirectory, unreadable: false };
94
+ if (st.kind === 'unreadable')
95
+ return { reason: 'phase directory unreadable', what: `could not be read (${st.reason})`, hint: 'Check that the directory is readable.', unreadable: true };
96
+ if (st.value.isDirectory())
97
+ return null;
98
+ return { reason: 'phase path is not a directory', what: st.value.isFile() ? 'is a file, not a directory' : 'is not a directory', hint: passDirectory, unreadable: false };
99
+ }
73
100
  /**
74
101
  * The contents of the phase files `pick` selects (#5170, ADR-5057 §4). An ABSENT phase directory
75
102
  * is "no files" (`found []`); a directory or file that exists but cannot be read is `unreadable` —
@@ -170,6 +197,72 @@ function extractPlanDesignatedSections(planContent) {
170
197
  parts.push((0, markdown_sectionizer_cjs_1.extractXmlTagBodies)(cleaned));
171
198
  return parts.join('\n\n');
172
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
+ }
173
266
  function buildPlanMessage(uncovered) {
174
267
  if (uncovered.length === 0)
175
268
  return 'All trackable CONTEXT.md decisions are covered by plans.';
@@ -34,7 +34,10 @@ const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
34
34
  const planningWorkspace = require("./planning-workspace.cjs");
35
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
36
36
  const verificationMod = require("./verification.cjs");
37
- const { planningDir } = planningWorkspace;
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
38
+ const phaseIdMod = require("./phase-id.cjs");
39
+ const { scopeToPhase } = phaseIdMod;
40
+ const { planningDir, isPhaseSpecFile, findPhaseSpecIn } = planningWorkspace;
38
41
  const { defaultPhaseCleanCommitTimesMs } = verificationMod;
39
42
  /**
40
43
  * Pure comparator: which of `entries` have an effective last-changed time
@@ -54,7 +57,7 @@ function buildContextDriftMessage(staleArtifacts, phaseArg, projectDir) {
54
57
  if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
55
58
  parts.push(`Regenerate patterns: delete the PATTERNS.md file, then re-run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, 'plan-phase')}.`);
56
59
  }
57
- if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
60
+ if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || isPhaseSpecFile(f))) {
58
61
  parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
59
62
  }
60
63
  parts.push('Do not hand-inject the newer decisions into a prompt as a substitute for regenerating — that carries the staleness forward.');
@@ -98,7 +101,9 @@ function runContextDriftGate(cwd, phaseArg) {
98
101
  const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
99
102
  const patternsFile = phaseFiles.find((f) => f.endsWith('-PATTERNS.md'));
100
103
  const validationFile = phaseFiles.find((f) => f.endsWith('-VALIDATION.md'));
101
- const specFile = phaseFiles.find((f) => f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md'));
104
+ // #5629: the shared phase-SPEC predicate (the init payloads' `has_spec` / `spec_path` use the same one).
105
+ // Scoped to this phase's own artifacts, as init scopes it, so a misfiled `02-SPEC.md` agrees in both.
106
+ const specFile = findPhaseSpecIn(scopeToPhase(phaseFiles, node_path_1.default.basename(phaseDir)));
102
107
  const upstreamFiles = [researchFile, patternsFile, validationFile, specFile].filter((f) => !!f);
103
108
  if (upstreamFiles.length === 0) {
104
109
  return skip('no-upstream-artifacts');