@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * Health Diagnostic — Root existence + PROJECT.md rules (Phase 11, #3309,
4
+ * ADR-3180 §8.2/§8.3/§8.5).
5
+ *
6
+ * Group: "Root existence + PROJECT.md" (design doc, "Rule table organization"
7
+ * table) — E002, E003, E004, W001. E001 (the `.planning/` root missing guard)
8
+ * stays OUTSIDE the rule table entirely per the design doc's "Two guards that
9
+ * stay OUTSIDE the rule table entirely" section — it is not a row here.
10
+ *
11
+ * Ported behavior-preserving from `cmdValidateHealth`
12
+ * (`src/verify.cts:1681-1705`), the exact call sites for E002/E003/E004/W001.
13
+ *
14
+ * E002's original message interpolates `${slash('new-project')}`
15
+ * (`verify.cts:1682`, ``Run ${slash('new-project')} to create``) and E003's
16
+ * interpolates `${slash('new-milestone')}` (`verify.cts:1694`, ``Run
17
+ * ${slash('new-milestone')} to create roadmap``) — both per-project
18
+ * runtime-resolved values (`formatGsdSlash`, `src/runtime-slash.cts`) this
19
+ * rule's `(snapshot) => Diagnostic[]` signature has no access to (§8.1 rule
20
+ * 1 forbids ambient I/O, including `cwd`, inside `check`). Hardcodes the
21
+ * canonical `/gsd-new-project`/`/gsd-new-milestone` hyphen form instead,
22
+ * mirroring the sibling "config.json validation" group's W016 rule
23
+ * (`src/health-diagnostic-rules/config-validation.cts`), which hardcodes
24
+ * `/gsd-ai-integration-phase` the same way for the identical reason.
25
+ *
26
+ * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
27
+ *
28
+ * ADR-457 build-at-publish: source in src/health-diagnostic-rules/root-existence.cts,
29
+ * compiled to gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs (gitignored).
30
+ */
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
33
+ const { SEVERITY, REMEDY_ACTION, REMEDY_RISK, adviseRemedy } = healthDiagnosticMod;
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const planningScopeMod = require("../planning-scope.cjs");
36
+ const { SCOPE } = planningScopeMod;
37
+ // ─── E002 — PROJECT.md not found (verify.cts:1682) ─────────────────────────
38
+ function checkE002(snapshot) {
39
+ if (snapshot.projectSections.exists)
40
+ return [];
41
+ return [
42
+ {
43
+ code: 'E002',
44
+ severity: SEVERITY.ERROR,
45
+ message: 'PROJECT.md not found',
46
+ remedy: adviseRemedy('/gsd-new-project'),
47
+ },
48
+ ];
49
+ }
50
+ // ─── E003 — ROADMAP.md not found (verify.cts:1694) ─────────────────────────
51
+ //
52
+ // Condition uses `snapshot.milestone.scope === SCOPE.UNREADABLE`
53
+ // (`getMilestoneInfo`, `src/roadmap-parser.cts`). KNOWN AMBIGUITY (flagged in
54
+ // this batch's report, not silently papered over): `getMilestoneInfo` returns
55
+ // `SCOPE.UNREADABLE` for TWO distinct causes it does not otherwise
56
+ // distinguish — (1) ROADMAP.md absent (`platformReadSync` returns `null` ->
57
+ // synthetic `Error('missing')`, no errno, `reportUnreadableRoadmap` finds no
58
+ // `.code` and stays silent) and (2) ROADMAP.md present but unreadable (a real
59
+ // read fault, e.g. EACCES/EISDIR, which DOES carry an errno and fires
60
+ // `warnUnusableInput(ROADMAP_UNREADABLE)`). Unlike `config`/`projectSections`,
61
+ // `milestone` carries no `exists` discriminator, so this rule cannot tell the
62
+ // two apart from the snapshot alone without adding cwd/fs access to `check`
63
+ // (forbidden by §8.1 rule 1). This is a best-effort port of the pre-migration
64
+ // condition (`!fs.existsSync(roadmapPath)`), which itself only asked "does
65
+ // the file exist" — this rule now also fires (message-mismatched, but
66
+ // error-preserving) on a present-but-corrupt ROADMAP.md.
67
+ function checkE003(snapshot) {
68
+ if (snapshot.milestone.scope !== SCOPE.UNREADABLE)
69
+ return [];
70
+ return [
71
+ {
72
+ code: 'E003',
73
+ severity: SEVERITY.ERROR,
74
+ message: 'ROADMAP.md not found',
75
+ remedy: adviseRemedy('/gsd-new-milestone'),
76
+ },
77
+ ];
78
+ }
79
+ // ─── E004 — STATE.md not found (verify.cts:1697) ───────────────────────────
80
+ //
81
+ // Condition uses `snapshot.currentPhaseLabel.scope === SCOPE.UNREADABLE`
82
+ // (`buildStateFields`, `src/planning-snapshot.cts:210-249`). KNOWN GAP
83
+ // (flagged in this batch's report): `buildStateFields` collapses TWO distinct
84
+ // causes into the same `UNREADABLE` scope with no discriminator field at
85
+ // all — STATE.md absent (`platformReadSync` returns `null`, a real
86
+ // non-answer, `warnUnusableInput` NOT called) and STATE.md present but
87
+ // unreadable (any other read error, e.g. EISDIR, corruption,
88
+ // `warnUnusableInput(STATE_UNREADABLE)` fires). Unlike `config`, there is no
89
+ // `exists` flag on `currentPhaseLabel` (or on `PlanningSnapshot` generally)
90
+ // to distinguish "STATE.md was never created" from "STATE.md exists but
91
+ // could not be read" — this is a REAL gap in the current 15-field
92
+ // `PlanningSnapshot` shape, not something this rule can work around without
93
+ // extending that snapshot (out of this batch's scope per the brief). This
94
+ // rule is therefore a best-effort port: it fires E004 ("STATE.md not found")
95
+ // for both causes, exactly mirroring what `snapshot.currentPhaseLabel.scope`
96
+ // can express today.
97
+ //
98
+ // Remedy is `regenerateState`, one of the two DESTRUCTIVE-risk actions (loses
99
+ // session history, design doc "Risk assignment" section) — per §8.3 rule 3
100
+ // `--repair` will refuse to auto-apply it once `applyRepairs`'s dispatch
101
+ // wires this rule in; the remedy is still described (ADVISE-shaped for
102
+ // display, per `applyRepairs`'s own contract) but never executed.
103
+ function checkE004(snapshot) {
104
+ if (snapshot.currentPhaseLabel.scope !== SCOPE.UNREADABLE)
105
+ return [];
106
+ return [
107
+ {
108
+ code: 'E004',
109
+ severity: SEVERITY.ERROR,
110
+ message: 'STATE.md not found',
111
+ remedy: {
112
+ action: REMEDY_ACTION.REGENERATE_STATE,
113
+ risk: REMEDY_RISK.DESTRUCTIVE,
114
+ args: {},
115
+ },
116
+ },
117
+ ];
118
+ }
119
+ // ─── W001 — PROJECT.md missing a required section (verify.cts:1684-1690) ──
120
+ //
121
+ // `REQUIRED_SECTIONS` carries the exact `## `-prefixed strings
122
+ // `verify.cts:1685` uses in its message text; membership is tested against
123
+ // `snapshot.projectSections.value`, which `buildProjectSectionsField`
124
+ // (`src/planning-snapshot.cts:367-381`) stores WITHOUT the `##` prefix (its
125
+ // `/^##\s+(.+)$/gm` capture group), so each required string's own `## `
126
+ // prefix is stripped before the membership check. `projectSections.value ===
127
+ // null` (PROJECT.md absent OR unreadable) emits zero diagnostics — E002
128
+ // already reports absence; this rule does not double-report it.
129
+ const REQUIRED_SECTIONS = ['## What This Is', '## Core Value', '## Requirements'];
130
+ function checkW001(snapshot) {
131
+ const { value } = snapshot.projectSections;
132
+ if (value === null)
133
+ return [];
134
+ const diagnostics = [];
135
+ for (const required of REQUIRED_SECTIONS) {
136
+ const heading = required.replace(/^##\s+/, '');
137
+ if (!value.includes(heading)) {
138
+ diagnostics.push({
139
+ code: 'W001',
140
+ severity: SEVERITY.WARNING,
141
+ message: `PROJECT.md missing section: ${required}`,
142
+ remedy: adviseRemedy('Add section manually'),
143
+ });
144
+ }
145
+ }
146
+ return diagnostics;
147
+ }
148
+ // ─── Exports ────────────────────────────────────────────────────────────────
149
+ const RULES = [
150
+ { code: 'E002', severity: SEVERITY.ERROR, description: 'PROJECT.md not found', repairable: false, check: checkE002 },
151
+ { code: 'E003', severity: SEVERITY.ERROR, description: 'ROADMAP.md not found', repairable: false, check: checkE003 },
152
+ { code: 'E004', severity: SEVERITY.ERROR, description: 'STATE.md not found', repairable: false, check: checkE004 },
153
+ {
154
+ code: 'W001',
155
+ severity: SEVERITY.WARNING,
156
+ description: 'PROJECT.md missing required section',
157
+ repairable: false,
158
+ check: checkW001,
159
+ },
160
+ ];
161
+ module.exports = { RULES };
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ /**
3
+ * Health Diagnostic Rules — STATE.md consistency group (Phase 11, #3309,
4
+ * ADR-3180 §8.2/§8.3/§8.5).
5
+ *
6
+ * Five rules, each a near-mechanical extraction of an already-working
7
+ * `addIssue` call site in `cmdValidateHealth` (Gall's Law, design doc "Rule
8
+ * table organization" / "Laws applied"):
9
+ *
10
+ * - W024 (`verify.cts:1709-1729`) — STATE.md `state_head` commit-age
11
+ * freshness vs. git HEAD. GENUINE GAP, deliberately NOT migrated — see the
12
+ * `RULE_W024` comment below for exactly why.
13
+ * - W002 (`verify.cts:1731-1774`) — STATE.md references a phase token not
14
+ * declared anywhere (disk or ROADMAP).
15
+ * - W011 (`verify.cts:2104-2134`) — STATE's current-phase status disagrees
16
+ * with ROADMAP's `[x]` checkbox for that same phase.
17
+ * - W021 (`verify.cts:2270-2299`, the FIRST `addIssue('warning', 'W021', ...)`
18
+ * call site) — under the `'milestone-prefixed'` `phase_id_convention`, a
19
+ * phase's integer prefix implies a different milestone than the ROADMAP
20
+ * section it is actually listed under.
21
+ * - W026 (`verify.cts:2356-2399`, the SECOND `addIssue('warning', 'W021', ...)`
22
+ * call site — split off per the design doc's "New codes for the two split
23
+ * subjects" section, since one code covering two unrelated subjects is a
24
+ * genuine conflation) — STATE says the milestone is complete/archived, but
25
+ * ROADMAP (scoped to that same milestone) still lists a phase with no
26
+ * matching disk directory.
27
+ *
28
+ * - W002's original message interpolates `${slash('health')}`
29
+ * (`verify.cts:1770`) and W011's interpolates `${slash('progress')}`
30
+ * (`verify.cts:2126`) — both per-project runtime-resolved values
31
+ * (`formatGsdSlash`, `src/runtime-slash.cts`) this rule's
32
+ * `(snapshot) => Diagnostic[]` signature has no access to. Hardcodes the
33
+ * canonical `/gsd-health`/`/gsd-progress` hyphen form instead, mirroring
34
+ * the sibling "config.json validation" group's W016 rule
35
+ * (`src/health-diagnostic-rules/config-validation.cts`), which hardcodes
36
+ * `/gsd-ai-integration-phase` the same way for the identical reason.
37
+ *
38
+ * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
39
+ */
40
+ // Runtime values (SEVERITY/REMEDY_ACTION/REMEDY_RISK) are needed here, not
41
+ // just types, so this is a normal (non type-only) `import ... = require(...)`
42
+ // — unlike `health-diagnostic.cts`'s own type-only import of
43
+ // `planning-snapshot.cjs`, which never touches that module's runtime values.
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- export= CommonJS module
45
+ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
46
+ const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
47
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
48
+ const phaseIdMod = require("../phase-id.cjs");
49
+ const { getMilestoneFromPhaseId, matchPhaseDirs, normalizePhaseName, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- the single owner of the persisted status vocabulary
51
+ const stateDocumentMod = require("../state-document.cjs");
52
+ const { normalizeStateStatus } = stateDocumentMod;
53
+ // ─── W024 — STATE.md commit-age freshness (DELIBERATELY INERT) ─────────────
54
+ /**
55
+ * W024's real check (`verify.cts:1709-1729`) calls
56
+ * `readStateHeadFreshness(cwd, fm['state_head'])`, which shells out to `git
57
+ * log` to count commits between the frontmatter's `state_head` and the
58
+ * current HEAD. That is ambient I/O (git history), not `.planning/` content —
59
+ * confirmed against `src/planning-snapshot.cts`'s full 15-field
60
+ * `PlanningSnapshot` interface: no field wraps `readStateHeadFreshness` or
61
+ * exposes a commits-behind count.
62
+ *
63
+ * §8.1 rule 1 requires a rule's signature to be `(snapshot) => Diagnostic[]`
64
+ * with no ambient I/O inside `check` — so this rule does NOT call
65
+ * `readStateHeadFreshness` itself (that would violate the constraint the
66
+ * skeleton's own `Rule.check` type exists to enforce). Adding a 16th
67
+ * `PlanningSnapshot` field (e.g. `stateHeadFreshness: {value:
68
+ * {commitsBehind, stateHead}, scope}`) is the fix, but is out of this
69
+ * group's scope (`src/planning-snapshot.cts` is a shared file this task was
70
+ * not dispatched to extend).
71
+ *
72
+ * Registered here, `check` always returning `[]`, so the code table stays
73
+ * complete per §8.2's 1:1 invariant (every code the old `verify.cts` emitted
74
+ * has exactly one `Rule` entry) rather than silently dropping W024 from the
75
+ * table. This is a documented, deliberate deferral pending the 16th snapshot
76
+ * field — flagged prominently rather than quietly ported as a no-op.
77
+ */
78
+ const RULE_W024 = {
79
+ code: 'W024',
80
+ severity: SEVERITY.WARNING,
81
+ description: 'STATE.md was written many commits ago — treat its contents as approximate',
82
+ repairable: false,
83
+ check: (_snapshot) => [],
84
+ };
85
+ // ─── W002 — STATE.md references an undeclared phase token ──────────────────
86
+ /**
87
+ * The "valid phase" set the original code builds from
88
+ * `collectDiskPhases(planBase)` (disk dir tokens) + ROADMAP heading tokens +
89
+ * `forEachArchivedPhaseToken` (archived milestone-phase-dir tokens,
90
+ * `verify.cts:1748`). This rebuilds the disk+ROADMAP two-thirds from parsed
91
+ * snapshot fields only: `phaseDirs.value` (disk dir names, tokenized the same
92
+ * way `collectDiskPhaseEntries` does — via `extractPhaseToken`) and
93
+ * `roadmapDeclaredPhases.value.map(p => p.phaseId)` (ROADMAP-declared phase
94
+ * ids). Archived-phase-token coverage is NOT included — no
95
+ * `PlanningSnapshot` field exposes archived milestone-phase-dir tokens
96
+ * (confirmed against the 15-field interface). Omitting it makes this valid
97
+ * set a SUBSET of the original's, which can only make MORE STATE.md phase
98
+ * tokens look "invalid" (never fewer) — a conservative, safe direction; a
99
+ * project with archived phases still referenced from STATE.md is the
100
+ * fixture shape that would expose a false positive, and none of this
101
+ * group's fixtures exercise archives, so this gap is disclosed rather than
102
+ * silently absorbed.
103
+ *
104
+ * UPDATE (#3652): archived-phase-token coverage IS now included, via the
105
+ * additive `snapshot.archivedPhaseTokens` field
106
+ * (`src/planning-snapshot.cts`, added for this fix) — the disclosed gap
107
+ * above is closed; the paragraph is kept for the "conservative direction"
108
+ * reasoning, which still explains why every OTHER omission in this
109
+ * function is safe.
110
+ */
111
+ function buildValidPhaseSet(snapshot) {
112
+ const valid = new Set();
113
+ for (const dir of snapshot.phaseDirs.value) {
114
+ const token = extractPhaseToken(dir);
115
+ if (token)
116
+ valid.add(token);
117
+ }
118
+ for (const entry of snapshot.roadmapDeclaredPhases.value) {
119
+ valid.add(entry.phaseId);
120
+ }
121
+ for (const token of snapshot.archivedPhaseTokens.value) {
122
+ valid.add(token);
123
+ }
124
+ return valid;
125
+ }
126
+ /** Mirrors `verify.cts:1749-1758`'s zero-padding normalization exactly. */
127
+ function normalizePhaseTokenSet(valid) {
128
+ const normalized = new Set();
129
+ for (const p of valid) {
130
+ normalized.add(p);
131
+ const dotIdx = p.indexOf('.');
132
+ const head = dotIdx === -1 ? p : p.slice(0, dotIdx);
133
+ const tail = dotIdx === -1 ? '' : p.slice(dotIdx);
134
+ if (/^\d+$/.test(head)) {
135
+ normalized.add(head.padStart(2, '0') + tail);
136
+ }
137
+ }
138
+ return normalized;
139
+ }
140
+ const RULE_W002 = {
141
+ code: 'W002',
142
+ severity: SEVERITY.WARNING,
143
+ description: 'STATE.md references invalid phase',
144
+ repairable: false,
145
+ check: (snapshot) => {
146
+ const validPhases = buildValidPhaseSet(snapshot);
147
+ // Mirrors `verify.cts:1765`'s `if (normalizedValid.size > 0)` guard
148
+ // exactly — a project with zero declared phases emits nothing, never a
149
+ // false positive on every STATE.md phase mention.
150
+ if (validPhases.size === 0)
151
+ return [];
152
+ const normalizedValid = normalizePhaseTokenSet(validPhases);
153
+ const sortedValid = [...validPhases].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
154
+ const diagnostics = [];
155
+ for (const ref of snapshot.statePhaseTokens.value) {
156
+ const dotIdx = ref.indexOf('.');
157
+ const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
158
+ const tail = dotIdx === -1 ? '' : ref.slice(dotIdx);
159
+ const padded = /^\d+$/.test(head) ? head.padStart(2, '0') + tail : ref;
160
+ if (normalizedValid.has(ref) || normalizedValid.has(padded))
161
+ continue;
162
+ diagnostics.push({
163
+ code: 'W002',
164
+ severity: SEVERITY.WARNING,
165
+ message: `STATE.md references phase ${ref}, but only phases ${sortedValid.join(', ')} are declared`,
166
+ remedy: adviseRemedy('Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches'),
167
+ });
168
+ }
169
+ return diagnostics;
170
+ },
171
+ };
172
+ // ─── W011 — STATE current-phase status vs. ROADMAP checkbox disagree ───────
173
+ /**
174
+ * `currentPhaseLabel.value` is a prose string (e.g. `"3 of 8 (User Auth)"`),
175
+ * not a clean phase id — the leading integer (optionally letter-suffixed /
176
+ * dotted, the same `PHASE_NUMBER_TOKEN_SOURCE` grammar) is the "current
177
+ * phase" proxy the original `verify.cts:2109-2113` derives via its own
178
+ * `**Current Phase:**`/`Current Phase:` regex + `.replace(/^0+/, '')`. That
179
+ * literal field name does not exist in the current `state.md` template
180
+ * (which uses `Phase: [X] of [Y] ([Phase name])` under `## Current
181
+ * Position`) — `currentPhaseLabel` is the parsed owner of that exact field,
182
+ * so extracting its leading number is the equivalent-intent read against
183
+ * the template STATE.md actually ships. #3280: the label's ladder
184
+ * (`buildStateFields`, `src/planning-snapshot.cts`) now leads with the
185
+ * frontmatter `current_phase` scalar — the key `gsd-tools state update` /
186
+ * `state begin-phase` persist — so a bare `"2"` is the expected value on
187
+ * the machine-readable format, and this leading-number extraction handles
188
+ * it identically.
189
+ */
190
+ function currentPhaseIdFromLabel(label) {
191
+ if (!label)
192
+ return null;
193
+ const m = label.match(new RegExp(`^0*(${PHASE_NUMBER_TOKEN_SOURCE})`));
194
+ return m ? m[1] : null;
195
+ }
196
+ const RULE_W011 = {
197
+ code: 'W011',
198
+ severity: SEVERITY.WARNING,
199
+ description: 'STATE.md current-phase status disagrees with ROADMAP.md checkbox',
200
+ repairable: false,
201
+ check: (snapshot) => {
202
+ const phaseId = currentPhaseIdFromLabel(snapshot.currentPhaseLabel.value);
203
+ if (phaseId === null)
204
+ return [];
205
+ const checked = snapshot.roadmapPhaseCheckboxes.value[phaseId];
206
+ if (checked !== true)
207
+ return [];
208
+ // #3280: the state writer persists `status` through `normalizeStateStatus`
209
+ // (`state.cts`'s syncStateFrontmatter), whose completion token is
210
+ // `completed` — an exact `'complete' || 'done'` comparison rejects the
211
+ // exact vocabulary the product writes and turns every legitimately
212
+ // completed frontmatter STATE.md into a false positive. Route the
213
+ // comparison through the same seam that owns the vocabulary
214
+ // (`state-document.cjs`'s `normalizeStateStatus`) rather than growing a
215
+ // second bespoke token list here.
216
+ const statusVal = (snapshot.stateStatus.value ?? '').trim();
217
+ if (normalizeStateStatus(statusVal, null) === 'completed')
218
+ return [];
219
+ return [
220
+ {
221
+ code: 'W011',
222
+ severity: SEVERITY.WARNING,
223
+ message: `STATE.md says current phase is ${phaseId} (status: ${statusVal.toLowerCase() || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
224
+ remedy: adviseRemedy('Run /gsd-progress to re-derive current position, or manually update STATE.md'),
225
+ },
226
+ ];
227
+ },
228
+ };
229
+ // ─── W021 — phase_id_convention integer-prefix/milestone mismatch ──────────
230
+ const RULE_W021 = {
231
+ code: 'W021',
232
+ severity: SEVERITY.WARNING,
233
+ description: "Phase's integer prefix implies a different milestone than its ROADMAP section (phase_id_convention: milestone-prefixed)",
234
+ repairable: false,
235
+ check: (snapshot) => {
236
+ const convention = snapshot.config.value?.['phase_id_convention'];
237
+ if (convention !== 'milestone-prefixed')
238
+ return [];
239
+ const diagnostics = [];
240
+ for (const entry of snapshot.roadmapDeclaredPhases.value) {
241
+ // `entry.milestone === null` means the builder never found this phase
242
+ // heading inside any versioned (`v\d+\.\d+`) section — the original
243
+ // `checkMilestonePrefixMismatches` only ever iterates phases found
244
+ // WITHIN a section, so a phase outside any section is equivalently
245
+ // never checked here.
246
+ if (entry.milestone === null)
247
+ continue;
248
+ const expectedMilestone = getMilestoneFromPhaseId(entry.phaseId);
249
+ if (expectedMilestone === null || expectedMilestone === entry.milestone)
250
+ continue;
251
+ diagnostics.push({
252
+ code: 'W021',
253
+ severity: SEVERITY.WARNING,
254
+ message: `Phase ${entry.phaseId}: integer prefix implies ${expectedMilestone} but listed under ${entry.milestone}`,
255
+ remedy: adviseRemedy('gsd-tools roadmap upgrade --convention milestone-prefixed'),
256
+ });
257
+ }
258
+ return diagnostics;
259
+ },
260
+ };
261
+ // ─── W026 — STATE says milestone complete but ROADMAP lists unstarted phase ─
262
+ const RULE_W026 = {
263
+ code: 'W026',
264
+ severity: SEVERITY.WARNING,
265
+ description: 'STATE says milestone complete but ROADMAP lists an unstarted phase for that milestone',
266
+ repairable: false,
267
+ check: (snapshot) => {
268
+ const statusVal = (snapshot.stateStatus.value ?? '').trim().toLowerCase();
269
+ if (!/milestone complete|archived/.test(statusVal))
270
+ return [];
271
+ // `currentMilestoneRoadmapPhaseIds` is already scoped to the current
272
+ // milestone (`extractCurrentMilestone(roadmapRaw, cwd)`, the same
273
+ // `<details>`/`<summary>`-tolerant owner `verify.cts:2364` used) — no
274
+ // separate `currentMilestone` resolution/filter needed here (see the
275
+ // field's own doc comment on `PlanningSnapshot` for why
276
+ // `roadmapDeclaredPhases`'s `milestone` attribution is the wrong fit).
277
+ const unstarted = [];
278
+ for (const phaseId of snapshot.currentMilestoneRoadmapPhaseIds.value) {
279
+ const normalized = normalizePhaseName(phaseId);
280
+ // `allPhaseDirNames` — every directory under `phases/`, UNWINDOWED by
281
+ // ROADMAP-declaration membership — mirrors the original's own
282
+ // unwindowed `phaseDirNames2` (`verify.cts:2372-2382`, a direct
283
+ // `readdirSync` of the phases dir), not the current-milestone-windowed
284
+ // `phaseDirs`.
285
+ const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, normalized).matches.length > 0;
286
+ if (!hasDirectory)
287
+ unstarted.push(phaseId);
288
+ }
289
+ if (unstarted.length === 0)
290
+ return [];
291
+ return [
292
+ {
293
+ code: 'W026',
294
+ severity: SEVERITY.WARNING,
295
+ message: `STATE says milestone complete but ROADMAP lists ${unstarted.length} unstarted phase(s) (e.g. Phase ${unstarted[0]})`,
296
+ remedy: adviseRemedy('Run validate consistency or re-run complete-milestone after verifying all phases are done'),
297
+ },
298
+ ];
299
+ },
300
+ };
301
+ // ─── Exports ────────────────────────────────────────────────────────────────
302
+ const RULES = [RULE_W024, RULE_W002, RULE_W011, RULE_W021, RULE_W026];
303
+ module.exports = { RULES };
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /**
3
+ * Health Diagnostic — Worktree health rules (Phase 11, #3309, ADR-3180
4
+ * §8.2/§8.3/§8.5).
5
+ *
6
+ * Group: "Worktree health" (design doc, "Rule table organization" table) —
7
+ * W020 (×3 internal conditions, one subject: "the worktree health scan
8
+ * itself is degraded", design doc "Rejected alternatives" §3), W017 (orphan
9
+ * worktree), W027 (NEW — the split-off "stale worktree" subject, design
10
+ * doc's "New codes for the two split subjects" section).
11
+ *
12
+ * Ported behavior-preserving from `cmdValidateHealth`
13
+ * (`src/verify.cts:2193-2268`), the exact call sites for W020/W017/W027 (the
14
+ * pre-migration source still names the split-off stale-worktree site
15
+ * 'W017' — this batch is what actually applies the W027 split).
16
+ *
17
+ * W020's original THREE conditions were git_timed_out / git_list_failed / a
18
+ * per-finding 'unverified' kind, each with its own message. The first two
19
+ * are scan-level failures reported by `inspectWorktreeHealth`'s own `reason`
20
+ * field ('git_timed_out' vs 'git_list_failed' vs 'not_a_git_repo') —
21
+ * `planning-snapshot.cts`'s `buildWorktreeHealthField` now carries `reason`
22
+ * straight through on `PlanningSnapshot.worktreeHealth`, so `checkW020`
23
+ * below reproduces the original's exact branch-per-reason messages instead
24
+ * of collapsing them (a prior version of this file collapsed both into one
25
+ * message AND, worse, warned on 'not_a_git_repo' too — a regression, since
26
+ * the original silently skips a non-git cwd; see `verify.cts:2202-2217`).
27
+ *
28
+ * W027 restores the pre-migration active-worktree exclusion
29
+ * (`verify.cts:2233-2242`) via `PlanningSnapshot.cwd` — see `checkW027`'s own
30
+ * comment below for the mechanism.
31
+ *
32
+ * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
33
+ *
34
+ * ADR-457 build-at-publish: source in
35
+ * src/health-diagnostic-rules/worktree-health.cts, compiled to
36
+ * gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs (gitignored).
37
+ */
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
44
+ const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
45
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
46
+ const planningScopeMod = require("../planning-scope.cjs");
47
+ const { SCOPE } = planningScopeMod;
48
+ // ─── W020 — worktree health scan itself is degraded (verify.cts:2193-2264) ─
49
+ //
50
+ // ONE rule, THREE internal conditions, all the same subject ("the worktree
51
+ // health scan itself is degraded" — design doc "Rejected alternatives" §3):
52
+ // (a) `git worktree list` timed out (verify.cts:2202-2209), (b) `git
53
+ // worktree list` failed outright (verify.cts:2210-2217), (c) a specific
54
+ // 'unverified' finding (existsSync ok, statSync threw,
55
+ // verify.cts:2256-2263). A fourth `!ok` reason, 'not_a_git_repo', and a
56
+ // thrown exception ('exception') are DELIBERATELY silent — the original's
57
+ // `if` ladder never matches 'not_a_git_repo', and the outer try/catch around
58
+ // the whole block is commented "git worktree not available or not a git
59
+ // repo — skip silently".
60
+ function checkW020(snapshot) {
61
+ const diagnostics = [];
62
+ const { scope, reason } = snapshot.worktreeHealth;
63
+ const degraded = scope === SCOPE.UNREADABLE;
64
+ // (a) — git worktree list timed out (verify.cts:2202-2209).
65
+ if (degraded && reason === 'git_timed_out') {
66
+ diagnostics.push({
67
+ code: 'W020',
68
+ severity: SEVERITY.WARNING,
69
+ message: 'Worktree health check degraded: git worktree list timed out after 10s — orphan/stale worktrees could not be inspected',
70
+ remedy: adviseRemedy('Run: git worktree list --porcelain to diagnose; check for .git/index.lock or a hung git process'),
71
+ });
72
+ }
73
+ // (b) — git worktree list failed outright (verify.cts:2210-2217).
74
+ if (degraded && reason === 'git_list_failed') {
75
+ diagnostics.push({
76
+ code: 'W020',
77
+ severity: SEVERITY.WARNING,
78
+ message: 'Worktree health check degraded: git worktree list failed — orphan/stale worktrees could not be inspected',
79
+ remedy: adviseRemedy('Run: git worktree list --porcelain to diagnose; check git repository state and permissions'),
80
+ });
81
+ }
82
+ // (c) — per-finding 'unverified' (existsSync ok, statSync threw).
83
+ for (const finding of snapshot.worktreeHealth.value) {
84
+ if (finding.kind !== 'unverified')
85
+ continue;
86
+ diagnostics.push({
87
+ code: 'W020',
88
+ severity: SEVERITY.WARNING,
89
+ message: `Worktree health check degraded: could not stat ${finding.path} — presence/staleness could not be verified`,
90
+ remedy: adviseRemedy('Check filesystem permissions on the worktree path, or investigate why statSync failed for it'),
91
+ });
92
+ }
93
+ return diagnostics;
94
+ }
95
+ // ─── W017 — orphan git worktree (verify.cts:2222-2229) ─────────────────────
96
+ //
97
+ // `finding.kind === 'orphan'` — path no longer exists on disk. One
98
+ // Diagnostic per orphan finding. Remedy mirrors the exact original literal
99
+ // fix, `verify.cts:2227`: `'Run: git worktree prune'`.
100
+ function checkW017(snapshot) {
101
+ const diagnostics = [];
102
+ for (const finding of snapshot.worktreeHealth.value) {
103
+ if (finding.kind !== 'orphan')
104
+ continue;
105
+ diagnostics.push({
106
+ code: 'W017',
107
+ severity: SEVERITY.WARNING,
108
+ message: `Orphan git worktree: ${finding.path} (path no longer exists on disk)`,
109
+ remedy: adviseRemedy('git worktree prune'),
110
+ });
111
+ }
112
+ return diagnostics;
113
+ }
114
+ // ─── W027 — stale git worktree (verify.cts:2232-2249, the split-off half of
115
+ // the pre-migration 'W017' site) ─────────────────────────────────────────
116
+ //
117
+ // `finding.kind === 'stale'` — age-based. Excludes the active session's own
118
+ // worktree, restored via `snapshot.cwd` (see module doc, gap 2 — RESOLVED):
119
+ // a 'stale' finding is skipped when `snapshot.cwd` equals the finding's path
120
+ // or is nested under it, the exact comparison `verify.cts:2238-2241` made
121
+ // against `process.cwd()`. Per this batch's brief: the interpolated command
122
+ // (with the real path) lives in `message`; `remedy.args.command` stays a
123
+ // static `<path>` template, mirroring the split the brief specifies.
124
+ function checkW027(snapshot) {
125
+ const diagnostics = [];
126
+ const activeCwd = snapshot.cwd;
127
+ for (const finding of snapshot.worktreeHealth.value) {
128
+ if (finding.kind !== 'stale')
129
+ continue;
130
+ const normalizedWorktree = node_path_1.default.resolve(finding.path);
131
+ const isActiveWorktree = activeCwd === normalizedWorktree || activeCwd.startsWith(normalizedWorktree + node_path_1.default.sep);
132
+ if (isActiveWorktree)
133
+ continue;
134
+ diagnostics.push({
135
+ code: 'W027',
136
+ severity: SEVERITY.WARNING,
137
+ // #3280: staleness is a pure mtime heuristic and carries no information
138
+ // about whether the tree is clean — git's own refusal of a non-forced
139
+ // removal on a dirty tree is the safety net, so the remediation must
140
+ // direct the operator (or an agent executing this literally) to check
141
+ // for uncommitted work FIRST and keep `--force` an explicit discard
142
+ // opt-in, never the default instruction.
143
+ message: `Stale git worktree: ${finding.path} (last modified ${finding.ageMinutes} minutes ago). Inspect uncommitted work first: git -C ${finding.path} status --porcelain; if clean run: git worktree remove ${finding.path}; only add --force to discard changes`,
144
+ remedy: adviseRemedy('git -C <path> status --porcelain; if clean: git worktree remove <path>; add --force only to discard changes'),
145
+ });
146
+ }
147
+ return diagnostics;
148
+ }
149
+ // ─── Exports ────────────────────────────────────────────────────────────────
150
+ const RULES = [
151
+ {
152
+ code: 'W020',
153
+ severity: SEVERITY.WARNING,
154
+ description: 'Worktree health scan degraded — git worktree list timed out, failed, or a finding could not be verified',
155
+ repairable: false,
156
+ check: checkW020,
157
+ },
158
+ {
159
+ code: 'W017',
160
+ severity: SEVERITY.WARNING,
161
+ description: 'Orphan git worktree (path no longer exists on disk)',
162
+ repairable: false,
163
+ check: checkW017,
164
+ },
165
+ {
166
+ code: 'W027',
167
+ severity: SEVERITY.WARNING,
168
+ description: 'Stale git worktree (not modified in a long time)',
169
+ repairable: false,
170
+ check: checkW027,
171
+ },
172
+ ];
173
+ module.exports = { RULES };