@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
@@ -19,6 +19,7 @@ const fs = require('node:fs');
19
19
  const path = require('node:path');
20
20
 
21
21
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
22
+ const { normalizeEol } = require('../gsd-core/bin/lib/text-lines.cjs');
22
23
 
23
24
  const ROOT = path.resolve(__dirname, '..');
24
25
  const CAPABILITIES_DIR = path.join(ROOT, 'capabilities');
@@ -808,19 +809,6 @@ function stripGeneratedComment(content) {
808
809
  .join('\n');
809
810
  }
810
811
 
811
- /**
812
- * Normalize line endings to LF.
813
- * The generator always writes LF, but Windows git (autocrlf) checks out committed files with
814
- * CRLF. The --check comparison must be line-ending-agnostic so it only fails on REAL content
815
- * differences, not on checkout-introduced whitespace differences.
816
- *
817
- * @param {string} content
818
- * @returns {string}
819
- */
820
- function normalizeLineEndings(content) {
821
- return content.replace(/\r/g, '');
822
- }
823
-
824
812
  // ─── Main ─────────────────────────────────────────────────────────────────────
825
813
 
826
814
 
@@ -859,7 +847,7 @@ function main() {
859
847
  }
860
848
 
861
849
  const committed = fs.readFileSync(REGISTRY_PATH, 'utf8');
862
- if (normalizeLineEndings(stripGeneratedComment(committed)) !== normalizeLineEndings(stripGeneratedComment(live))) {
850
+ if (normalizeEol(stripGeneratedComment(committed)) !== normalizeEol(stripGeneratedComment(live))) {
863
851
  process.stderr.write(
864
852
  'gsd-core/bin/lib/capability-registry.cjs is stale. Run:\n' +
865
853
  ' node scripts/gen-capability-registry.cjs --write\n',
@@ -931,7 +919,7 @@ module.exports = {
931
919
  serializeRegistry,
932
920
  computeRequiresClosure,
933
921
  topoSortSteps,
934
- normalizeLineEndings,
922
+ normalizeLineEndings: normalizeEol,
935
923
  stripGeneratedComment,
936
924
  validateConfigSliceEntry,
937
925
  VALID_CONFIG_SLICE_TYPES,
@@ -46,6 +46,7 @@ const fs = require('node:fs');
46
46
  const path = require('node:path');
47
47
 
48
48
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
49
+ const { normalizeEol } = require('../gsd-core/bin/lib/text-lines.cjs');
49
50
 
50
51
  const ROOT = path.resolve(__dirname, '..');
51
52
  const CONTEXT_PREDICATES_LIB_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'context-predicates.cjs');
@@ -144,16 +145,6 @@ function serializeIndex(index) {
144
145
  return JSON.stringify(index, null, 2) + '\n';
145
146
  }
146
147
 
147
- /**
148
- * Normalize line endings to LF for CRLF-agnostic full-content comparison.
149
- *
150
- * @param {string} content
151
- * @returns {string}
152
- */
153
- function normalizeLineEndings(content) {
154
- return content.replace(/\r/g, '');
155
- }
156
-
157
148
  /**
158
149
  * Extract the sorted list of duplicate predicate ids from a built index.
159
150
  *
@@ -431,7 +422,7 @@ module.exports = {
431
422
  readContextMarkdown,
432
423
  buildFreshIndex,
433
424
  serializeIndex,
434
- normalizeLineEndings,
425
+ normalizeLineEndings: normalizeEol,
435
426
  duplicateIds,
436
427
  checkReport,
437
428
  parseArgs,
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Generates the `<error_codes>` and `<repair_actions>` tables in
6
+ * `gsd-core/workflows/health.md` from `src/health-diagnostic.cts`'s `RULES`
7
+ * table (Phase 11 follow-up, #3309 "Proposed behavior": "health.md's tables
8
+ * are generated rather than hand-maintained, closing the 16-vs-30+
9
+ * documentation gap structurally").
10
+ *
11
+ * Sources:
12
+ * - The 31 real rules in the compiled `RULES` array
13
+ * (`gsd-core/bin/lib/health-diagnostic.cjs`, built from
14
+ * `src/health-diagnostic.cts` + `src/health-diagnostic-rules/*.cts`),
15
+ * each carrying a static `description`/`repairable` (see
16
+ * `src/health-diagnostic-types.cts`'s `Rule` interface).
17
+ * - `PRECHECK_CODES` below — E001, E010, I010 — the three diagnostics
18
+ * `cmdValidateHealth` (`src/verify.cts`) emits as pre-checks OUTSIDE the
19
+ * rule table entirely (ADR-3180 §8.2 rule 4, "no precedence system" —
20
+ * see `.gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md`,
21
+ * "Two guards that stay OUTSIDE the rule table entirely"). These will
22
+ * never appear in `RULES`, so they are a small, static, clearly-labeled
23
+ * list merged in here instead.
24
+ * - `REMEDY_ACTION_METADATA` below — the Effect/Risk prose for each of the
25
+ * 6 real repair actions (`REMEDY_ACTION`, excluding `ADVISE`, which never
26
+ * acts). Static because the compiled module carries no Effect/Risk text
27
+ * of its own — only the action identifier.
28
+ *
29
+ * Deliberately EXCLUDED from the generated `<error_codes>` table: `W025`
30
+ * (the `workflow.use_worktrees`/`dispatch.isolation` check, #2486). It is a
31
+ * workflow-layer diagnostic emitted directly by this same file's own
32
+ * `run_health_check` step (a bash block in `health.md` itself), never by
33
+ * `cmdValidateHealth`/`RULES` — it has no `Rule` entry and is not one of the
34
+ * three pre-checks above. It stays fully documented in prose at its own step
35
+ * (`<step name="run_health_check">`), which is the authoritative, more
36
+ * detailed source `<error_codes>` used to merely summarize; dropping the
37
+ * redundant table row is not a loss of information, and folding it back in
38
+ * here would require this generator to parse bash, which it does not do.
39
+ * Same reasoning for `I002` (stale Windows task-directory cleanup,
40
+ * `<stale_task_cleanup>` step) — it was never part of the `<error_codes>`
41
+ * tagged region even before this generator existed.
42
+ *
43
+ * Usage:
44
+ * node scripts/gen-health-docs.cjs # print both tables to stdout
45
+ * node scripts/gen-health-docs.cjs --write # rewrite the tagged regions in health.md
46
+ * node scripts/gen-health-docs.cjs --check # exit 1 if either region is stale
47
+ * node scripts/gen-health-docs.cjs --write --target <path> # test-only: target a fixture file
48
+ */
49
+
50
+ const fs = require('node:fs');
51
+ const path = require('node:path');
52
+
53
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
54
+
55
+ const ROOT = path.resolve(__dirname, '..');
56
+ const HEALTH_MD_REL = 'gsd-core/workflows/health.md';
57
+ const HEALTH_MD_PATH = path.join(ROOT, HEALTH_MD_REL);
58
+ const COMPILED_MODULE_REL = 'gsd-core/bin/lib/health-diagnostic.cjs';
59
+ const COMPILED_MODULE_PATH = path.join(ROOT, COMPILED_MODULE_REL);
60
+
61
+ const ERROR_CODES_START = '<error_codes>';
62
+ const ERROR_CODES_END = '</error_codes>';
63
+ const REPAIR_ACTIONS_START = '<repair_actions>';
64
+ const REPAIR_ACTIONS_END = '</repair_actions>';
65
+
66
+ /**
67
+ * The 3 pre-check diagnostics `cmdValidateHealth` emits OUTSIDE the rule
68
+ * table (see module header). All three are non-repairable safety rails, not
69
+ * `.planning/` findings a remedy could act on.
70
+ */
71
+ const PRECHECK_CODES = [
72
+ {
73
+ code: 'E001',
74
+ severity: 'error',
75
+ description: '.planning/ directory not found',
76
+ repairable: false,
77
+ },
78
+ {
79
+ code: 'E010',
80
+ severity: 'error',
81
+ description: "CWD resolves to the user's home directory — health check would target the wrong .planning/",
82
+ repairable: false,
83
+ },
84
+ {
85
+ code: 'I010',
86
+ severity: 'info',
87
+ description: 'Resolved CWD reported alongside the E010 home-directory guard',
88
+ repairable: false,
89
+ },
90
+ ];
91
+
92
+ /**
93
+ * Effect/Risk prose per real `REMEDY_ACTION` (everything except `ADVISE`,
94
+ * which never acts and has no row in `<repair_actions>`). Text for the 5
95
+ * actions the hand-written table already documented is reused VERBATIM;
96
+ * `addAiIntegrationPhaseKey` is new — #3309 itself notes it was "live in
97
+ * code, missing from docs" (mirrors `addNyquistKey`, its structural sibling:
98
+ * same shape, one config key each).
99
+ */
100
+ const REMEDY_ACTION_METADATA = new Map([
101
+ ['createConfig', { effect: 'Create config.json with defaults', risk: 'None' }],
102
+ ['resetConfig', { effect: 'Delete + recreate config.json', risk: 'Loses custom settings' }],
103
+ [
104
+ 'regenerateState',
105
+ {
106
+ effect: 'Create STATE.md from ROADMAP structure when it is missing',
107
+ risk: 'Loses session history',
108
+ },
109
+ ],
110
+ [
111
+ 'addNyquistKey',
112
+ { effect: 'Add workflow.nyquist_validation: true to config.json', risk: 'None — matches existing default' },
113
+ ],
114
+ [
115
+ 'addAiIntegrationPhaseKey',
116
+ { effect: 'Add workflow.ai_integration_phase: true to config.json', risk: 'None — matches existing default' },
117
+ ],
118
+ [
119
+ 'backfillMilestones',
120
+ {
121
+ effect: 'Synthesize missing MILESTONES.md entries from `.planning/milestones/vX.Y-ROADMAP.md` snapshots',
122
+ risk: 'None — additive only; triggered by `--backfill` flag',
123
+ },
124
+ ],
125
+ ]);
126
+
127
+ /** Order the Effect/Risk table renders in — matches `REMEDY_ACTION`'s own declaration order. */
128
+ const REMEDY_ACTION_ORDER = [
129
+ 'createConfig',
130
+ 'resetConfig',
131
+ 'regenerateState',
132
+ 'addNyquistKey',
133
+ 'addAiIntegrationPhaseKey',
134
+ 'backfillMilestones',
135
+ ];
136
+
137
+ /**
138
+ * Per-code override for the "Repairable" cell's display text, for codes
139
+ * whose remedy is conditional on a flag the plain `Yes`/`No` can't express
140
+ * (mirrors the hand-written table's pre-existing `W018` row: `Yes (--backfill)`).
141
+ */
142
+ const REPAIRABLE_DISPLAY_OVERRIDE = new Map([['W018', 'Yes (`--backfill`)']]);
143
+
144
+ const STATIC_NOT_REPAIRABLE_BULLETS = [
145
+ 'PROJECT.md, ROADMAP.md content',
146
+ 'Phase directory renaming',
147
+ 'Orphaned plan cleanup',
148
+ ];
149
+
150
+ const FOOTNOTE_PARAGRAPH =
151
+ 'Note: this table is **generated** — do not hand-edit it. It is produced by ' +
152
+ '`node scripts/gen-health-docs.cjs --write` from `src/health-diagnostic.cts`\'s `RULES` table ' +
153
+ '(31 rules as of #3309, each carrying a static `description`/`repairable` on its `Rule` entry — ' +
154
+ 'see `src/health-diagnostic-types.cts`) plus the 3 pre-checks that stay outside the rule table by ' +
155
+ 'design (`E001`, `E010`, `I010` — safety rails in `cmdValidateHealth`, `src/verify.cts`, never ' +
156
+ '`.planning/` findings). `scripts/lint-health-diagnostic-rule-table.cjs` already enforces the 1:1 ' +
157
+ 'code invariant this table depends on (no duplicate codes; severity is always a `Rule` property, ' +
158
+ 'never set per emit call) — before assigning a new code, add a `Rule` entry under ' +
159
+ '`src/health-diagnostic-rules/` (its `code` is simply the next free number the lint guard has not ' +
160
+ 'yet seen) and run `node scripts/gen-health-docs.cjs --write` to regenerate this table; ' +
161
+ '`npm run lint:generated-sync` fails if it drifts. `W025` (the `workflow.use_worktrees`/' +
162
+ '`dispatch.isolation` check, #2486) is a workflow-layer diagnostic emitted directly by this file\'s ' +
163
+ 'own `run_health_check` step, not by `cmdValidateHealth` — it stays documented in that step, not in ' +
164
+ 'this generated table.';
165
+
166
+ /**
167
+ * Load the compiled health-diagnostic module. Throws a clear ExitError (not
168
+ * a raw MODULE_NOT_FOUND) if `npm run build:lib` has not run — mirrors
169
+ * `scripts/lint-health-diagnostic-rule-table.cjs`'s `loadCompiledModule`.
170
+ */
171
+ function loadCompiledModule(compiledPath = COMPILED_MODULE_PATH) {
172
+ if (!fs.existsSync(compiledPath)) {
173
+ throw new ExitError(
174
+ 2,
175
+ `gen-health-docs: compiled artifact not found at ${COMPILED_MODULE_REL}.\n` +
176
+ 'Run `npm run build:lib` first.',
177
+ );
178
+ }
179
+ return require(compiledPath);
180
+ }
181
+
182
+ /**
183
+ * Sort order for the `<error_codes>` table: E-codes, then W-codes
184
+ * numerically, then I-codes — matching the hand-written table's pre-existing
185
+ * order. NOT insertion order from `RULES` (which is grouped by
186
+ * subject-area file, not sorted by code).
187
+ */
188
+ const PREFIX_RANK = { E: 0, W: 1, I: 2 };
189
+
190
+ function parseCode(code) {
191
+ const m = code.match(/^([A-Z]+)(\d+)$/);
192
+ if (!m) throw new Error(`gen-health-docs: unparseable diagnostic code "${code}"`);
193
+ return { prefix: m[1], number: Number(m[2]) };
194
+ }
195
+
196
+ function compareCodes(a, b) {
197
+ const pa = parseCode(a.code);
198
+ const pb = parseCode(b.code);
199
+ const rankA = PREFIX_RANK[pa.prefix] ?? 99;
200
+ const rankB = PREFIX_RANK[pb.prefix] ?? 99;
201
+ if (rankA !== rankB) return rankA - rankB;
202
+ return pa.number - pb.number;
203
+ }
204
+
205
+ /**
206
+ * Combine the 31 real rules + the 3 static pre-checks into one sorted row
207
+ * list for the `<error_codes>` table.
208
+ *
209
+ * @param {Array<{code: string, severity: string, description: string, repairable: boolean}>} rules
210
+ */
211
+ function buildErrorCodeRows(rules) {
212
+ const seen = new Set();
213
+ const rows = [];
214
+ for (const entry of [...rules, ...PRECHECK_CODES]) {
215
+ if (seen.has(entry.code)) {
216
+ throw new Error(`gen-health-docs: duplicate diagnostic code "${entry.code}" across RULES + PRECHECK_CODES`);
217
+ }
218
+ seen.add(entry.code);
219
+ rows.push(entry);
220
+ }
221
+ rows.sort(compareCodes);
222
+ return rows;
223
+ }
224
+
225
+ /** Escape a cell's markdown-table-hostile characters (mirrors gen-adr-index.cjs's `cellText`). */
226
+ function cellText(text) {
227
+ return String(text)
228
+ .replace(/\\/g, '\\\\')
229
+ .replace(/\|/g, '\\|')
230
+ .replace(/</g, '&lt;')
231
+ .replace(/>/g, '&gt;')
232
+ .replace(/\r?\n/g, ' ')
233
+ .trim();
234
+ }
235
+
236
+ function repairableCell(row) {
237
+ if (REPAIRABLE_DISPLAY_OVERRIDE.has(row.code)) return REPAIRABLE_DISPLAY_OVERRIDE.get(row.code);
238
+ return row.repairable ? 'Yes' : 'No';
239
+ }
240
+
241
+ function renderErrorCodesRegion(rules) {
242
+ const rows = buildErrorCodeRows(rules);
243
+ const lines = ['', '| Code | Severity | Description | Repairable |', '|------|----------|-------------|------------|'];
244
+ for (const row of rows) {
245
+ lines.push(`| ${row.code} | ${row.severity} | ${cellText(row.description)} | ${repairableCell(row)} |`);
246
+ }
247
+ lines.push('', FOOTNOTE_PARAGRAPH, '');
248
+ return lines.join('\n');
249
+ }
250
+
251
+ function renderRepairActionsRegion() {
252
+ const lines = ['', '| Action | Effect | Risk |', '|--------|--------|------|'];
253
+ for (const action of REMEDY_ACTION_ORDER) {
254
+ const meta = REMEDY_ACTION_METADATA.get(action);
255
+ if (!meta) {
256
+ throw new Error(
257
+ `gen-health-docs: no Effect/Risk metadata registered for repair action "${action}" — add an entry to REMEDY_ACTION_METADATA.`,
258
+ );
259
+ }
260
+ lines.push(`| ${action} | ${meta.effect} | ${meta.risk} |`);
261
+ }
262
+ lines.push('', '**Not repairable (too risky):**');
263
+ for (const bullet of STATIC_NOT_REPAIRABLE_BULLETS) lines.push(`- ${bullet}`);
264
+ lines.push('');
265
+ return lines.join('\n');
266
+ }
267
+
268
+ /**
269
+ * Splice `newInner` between `${startTag}`/`${endTag}` inside `text`. Throws
270
+ * if either tag is missing, or if the tags appear more than once (this
271
+ * generator only ever targets the FIRST occurrence pair, and a duplicate
272
+ * tag anywhere in the file would silently corrupt the splice).
273
+ */
274
+ function spliceRegion(text, startTag, endTag, newInner) {
275
+ const startIdx = text.indexOf(startTag);
276
+ const endIdx = text.indexOf(endTag);
277
+ if (startIdx === -1 || endIdx === -1) {
278
+ throw new ExitError(
279
+ 1,
280
+ `gen-health-docs: ${HEALTH_MD_REL} is missing the ${startTag}/${endTag} tags.`,
281
+ );
282
+ }
283
+ if (text.indexOf(startTag, startIdx + 1) !== -1 || text.indexOf(endTag, endIdx + 1) !== -1) {
284
+ throw new ExitError(1, `gen-health-docs: ${HEALTH_MD_REL} has more than one ${startTag}/${endTag} pair.`);
285
+ }
286
+ const before = text.slice(0, startIdx + startTag.length);
287
+ const after = text.slice(endIdx);
288
+ return `${before}${newInner}\n${after}`;
289
+ }
290
+
291
+ /**
292
+ * Regenerate `health.md`'s full text from `rules` (the compiled `RULES`
293
+ * array) and the current on-disk `health.md` content.
294
+ */
295
+ function regenerateHealthMd(rules, currentText) {
296
+ let out = spliceRegion(currentText, ERROR_CODES_START, ERROR_CODES_END, renderErrorCodesRegion(rules));
297
+ out = spliceRegion(out, REPAIR_ACTIONS_START, REPAIR_ACTIONS_END, renderRepairActionsRegion());
298
+ return out;
299
+ }
300
+
301
+ /**
302
+ * @param {string[]} argv
303
+ * @returns {{write: boolean, check: boolean, targetPath: string|null}}
304
+ */
305
+ function parseArgs(argv) {
306
+ const opts = { write: false, check: false, targetPath: null };
307
+ for (let i = 0; i < argv.length; i++) {
308
+ const arg = argv[i];
309
+ if (arg === '--write') opts.write = true;
310
+ else if (arg === '--check') opts.check = true;
311
+ else if (arg === '--target') {
312
+ const value = argv[i + 1];
313
+ if (value === undefined) throw new ExitError(1, '--target requires a path argument.');
314
+ opts.targetPath = value;
315
+ i++;
316
+ } else {
317
+ throw new ExitError(1, `unknown flag: ${arg}\nRecognized flags: --write, --check, --target <path>.`);
318
+ }
319
+ }
320
+ return opts;
321
+ }
322
+
323
+ function main() {
324
+ const { write, check, targetPath } = parseArgs(process.argv.slice(2));
325
+ const { RULES } = loadCompiledModule();
326
+
327
+ // `--target` overrides the real committed health.md path, exclusively for
328
+ // test isolation (mirrors gen-section-manifest.cjs's `--manifest-path`
329
+ // override) — no production caller ever passes it.
330
+ const resolvedPath = targetPath ? path.resolve(targetPath) : HEALTH_MD_PATH;
331
+ const displayPath = targetPath ? targetPath : HEALTH_MD_REL;
332
+
333
+ const currentText = fs.existsSync(resolvedPath) ? fs.readFileSync(resolvedPath, 'utf8') : null;
334
+ if (currentText === null) {
335
+ throw new ExitError(1, `gen-health-docs: ${displayPath} not found.`);
336
+ }
337
+
338
+ const expected = regenerateHealthMd(RULES, currentText);
339
+
340
+ if (write) {
341
+ fs.writeFileSync(resolvedPath, expected, 'utf8');
342
+ process.stdout.write(
343
+ `Wrote ${displayPath} — ${RULES.length + PRECHECK_CODES.length} error/warning/info code(s), ` +
344
+ `${REMEDY_ACTION_ORDER.length} repair action(s).\n`,
345
+ );
346
+ return 0;
347
+ }
348
+
349
+ if (check) {
350
+ if (expected !== currentText) {
351
+ process.stderr.write(
352
+ `${displayPath} is stale — its <error_codes>/<repair_actions> tables do not match ` +
353
+ "src/health-diagnostic.cts's RULES table.\nRun:\n node scripts/gen-health-docs.cjs --write\n\n",
354
+ );
355
+ throw new ExitError(1);
356
+ }
357
+ process.stdout.write(
358
+ `${displayPath} is up to date (${RULES.length + PRECHECK_CODES.length} codes, ${REMEDY_ACTION_ORDER.length} repair actions).\n`,
359
+ );
360
+ return 0;
361
+ }
362
+
363
+ process.stdout.write(renderErrorCodesRegion(RULES) + '\n\n' + renderRepairActionsRegion() + '\n');
364
+ return 0;
365
+ }
366
+
367
+ // Guarded: requiring this module (the test suite imports the pure render
368
+ // functions directly) must not also run the generator as a side effect.
369
+ if (require.main === module) runMain(main);
370
+
371
+ module.exports = {
372
+ loadCompiledModule,
373
+ buildErrorCodeRows,
374
+ renderErrorCodesRegion,
375
+ renderRepairActionsRegion,
376
+ regenerateHealthMd,
377
+ spliceRegion,
378
+ compareCodes,
379
+ parseCode,
380
+ PRECHECK_CODES,
381
+ REMEDY_ACTION_METADATA,
382
+ REMEDY_ACTION_ORDER,
383
+ REPAIRABLE_DISPLAY_OVERRIDE,
384
+ HEALTH_MD_PATH,
385
+ COMPILED_MODULE_PATH,
386
+ ERROR_CODES_START,
387
+ ERROR_CODES_END,
388
+ REPAIR_ACTIONS_START,
389
+ REPAIR_ACTIONS_END,
390
+ };
@@ -117,6 +117,47 @@ function statOrNull(p) {
117
117
  }
118
118
  }
119
119
 
120
+ /**
121
+ * Collect `<dir>/<subdir>/<file>` entries as `<subdir>/<file>` keys — ONE level of
122
+ * subdirectory beneath `dir` itself, where the subdirectory's NAME is the thing being
123
+ * collected (unlike `collectNested`, there is no fixed subdir name to look for; every
124
+ * child directory of `dir` is scanned). This is what makes `gsd-core/bin/lib/<subdir>/*.cjs`
125
+ * (e.g. `health-diagnostic-rules/`, `installer-migrations/`, `host-integration-adapters/`,
126
+ * `observability/`) visible to the `cli_modules` family, mirroring the shape
127
+ * `docs/INVENTORY.md`'s CLI Modules table already uses for these files.
128
+ *
129
+ * Same defensive `statOrNull`-based style as `collectNested`: a stat/readdir failure on
130
+ * one entry is swallowed rather than thrown, so one unreadable subdirectory cannot take
131
+ * down `--check` for the whole repo.
132
+ */
133
+ function collectOneLevelSubdirs({ dir, filter }) {
134
+ if (!fs.existsSync(dir)) return [];
135
+ const out = [];
136
+ let children;
137
+ try {
138
+ children = fs.readdirSync(dir);
139
+ } catch {
140
+ return [];
141
+ }
142
+ for (const child of children) {
143
+ const childStat = statOrNull(path.join(dir, child));
144
+ if (!childStat || !childStat.isDirectory()) continue;
145
+ const subdirPath = path.join(dir, child);
146
+ let files;
147
+ try {
148
+ files = fs.readdirSync(subdirPath);
149
+ } catch {
150
+ continue;
151
+ }
152
+ for (const file of files) {
153
+ const fileStat = statOrNull(path.join(subdirPath, file));
154
+ if (!fileStat || !fileStat.isFile() || !filter(file)) continue;
155
+ out.push([child, file].join('/'));
156
+ }
157
+ }
158
+ return out.sort();
159
+ }
160
+
120
161
  function collectNested({ root, subdir, filter }) {
121
162
  if (!fs.existsSync(root)) return [];
122
163
  const out = [];
@@ -150,11 +191,16 @@ function collectNested({ root, subdir, filter }) {
150
191
  function buildManifest() {
151
192
  const manifest = { families: {} };
152
193
  for (const { name, dir, filter, toName } of FAMILIES) {
153
- manifest.families[name] = fs
194
+ const flat = fs
154
195
  .readdirSync(dir)
155
196
  .filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
156
- .map(toName)
157
- .sort();
197
+ .map(toName);
198
+ // `cli_modules` also ships subdirectory modules (`health-diagnostic-rules/`,
199
+ // `installer-migrations/`, `host-integration-adapters/`, `observability/`) invisible to
200
+ // the flat readdirSync above; merge them into the SAME sorted array, matching the single
201
+ // "CLI Modules" table shape docs/INVENTORY.md already uses (#3309).
202
+ const nested = name === 'cli_modules' ? collectOneLevelSubdirs({ dir, filter }) : [];
203
+ manifest.families[name] = [...flat, ...nested].sort();
158
204
  }
159
205
  for (const family of NESTED_FAMILIES) {
160
206
  manifest.families[family.name] = collectNested(family);
@@ -209,4 +255,4 @@ if (require.main === module) {
209
255
  // `DEFECT.GENERATIVE-FIX` divergence class: adding a family here while the test kept
210
256
  // its own list meant the test silently verified fewer families than shipped, and still
211
257
  // passed. The test now imports these, so the two surfaces cannot drift.
212
- module.exports = { FAMILIES, NESTED_FAMILIES, collectNested, buildManifest };
258
+ module.exports = { FAMILIES, NESTED_FAMILIES, collectNested, collectOneLevelSubdirs, buildManifest };
@@ -20,6 +20,8 @@ const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
 
22
22
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
23
+ const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
24
+ const { normalizeEol } = require('../gsd-core/bin/lib/text-lines.cjs');
23
25
 
24
26
  const ROOT = path.resolve(__dirname, '..');
25
27
  const WORKFLOWS_DIR = path.join(ROOT, 'gsd-core', 'workflows');
@@ -191,13 +193,6 @@ function parseLoopHostBlock(content, fileName) {
191
193
  * @param {string} fileName For error messages
192
194
  * @returns {string[]} Array of error strings; empty = OK
193
195
  */
194
- /**
195
- * Escape a string for literal use in a RegExp.
196
- */
197
- function escapeRegExp(s) {
198
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
199
- }
200
-
201
196
  function crossCheckRoles(content, agentRoles, fileName) {
202
197
  const errors = [];
203
198
  for (const role of agentRoles) {
@@ -375,21 +370,6 @@ function serializeContract(contract) {
375
370
  return lines.join('\n');
376
371
  }
377
372
 
378
- // ─── --check diff helper ──────────────────────────────────────────────────────
379
-
380
- /**
381
- * Normalize line endings to LF for CRLF-agnostic comparison.
382
- * FIX 4: The serializer has no nondeterministic content (no timestamp), so
383
- * the generated-by-line stripping that was here has been removed — full content
384
- * comparison is now used so header drift is caught by --check.
385
- *
386
- * @param {string} content
387
- * @returns {string}
388
- */
389
- function normalizeLineEndings(content) {
390
- return content.replace(/\r/g, '');
391
- }
392
-
393
373
  // ─── Main ─────────────────────────────────────────────────────────────────────
394
374
 
395
375
  function main() {
@@ -415,7 +395,7 @@ function main() {
415
395
 
416
396
  const committed = fs.readFileSync(CONTRACT_PATH, 'utf8');
417
397
  // FIX 4: Compare full content (no generated-by stripping) so header drift is caught.
418
- if (normalizeLineEndings(committed) !== normalizeLineEndings(live)) {
398
+ if (normalizeEol(committed) !== normalizeEol(live)) {
419
399
  process.stderr.write(
420
400
  'gsd-core/bin/lib/loop-host-contract.cjs is stale. Run:\n' +
421
401
  ' node scripts/gen-loop-host-contract.cjs --write\n',
@@ -509,7 +489,7 @@ module.exports = {
509
489
  assertPointsCoverage,
510
490
  buildContract,
511
491
  serializeContract,
512
- normalizeLineEndings,
492
+ normalizeLineEndings: normalizeEol,
513
493
  STEP_WORKFLOWS,
514
494
  HOST_LOOP_FILES,
515
495
  CANONICAL_POINTS,
@@ -35,6 +35,7 @@ const path = require('node:path');
35
35
 
36
36
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
37
37
  const { renderMarkdown } = require('./registry-schema.cjs');
38
+ const { normalizeEol } = require('../gsd-core/bin/lib/text-lines.cjs');
38
39
 
39
40
  const SOURCES = [
40
41
  { type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
@@ -42,18 +43,6 @@ const SOURCES = [
42
43
  { type: 'reviewer', jsonFile: 'reviewers.json', mdFile: 'reviewer-registry.md', optional: true },
43
44
  ];
44
45
 
45
- /**
46
- * The generator always writes LF; a Windows checkout (autocrlf) may present
47
- * committed files with CRLF. Normalize before comparing so `--check` only
48
- * fails on real content drift, not checkout-introduced line-ending noise.
49
- *
50
- * @param {string} content
51
- * @returns {string}
52
- */
53
- function normalizeLineEndings(content) {
54
- return content.replace(/\r/g, '');
55
- }
56
-
57
46
  function getRegistriesDir() {
58
47
  return path.join(process.cwd(), 'docs', 'registries');
59
48
  }
@@ -126,7 +115,7 @@ function main() {
126
115
  continue;
127
116
  }
128
117
  const committed = fs.readFileSync(mdPath, 'utf8');
129
- if (normalizeLineEndings(committed) !== normalizeLineEndings(rendered)) {
118
+ if (normalizeEol(committed) !== normalizeEol(rendered)) {
130
119
  process.stderr.write(`${mdFile} is stale. Run:\n node scripts/gen-registry.cjs --write\n`);
131
120
  anyDrift = true;
132
121
  }
@@ -149,4 +138,4 @@ function main() {
149
138
 
150
139
  if (require.main === module) runMain(main);
151
140
 
152
- module.exports = { main, renderFor, SOURCES, normalizeLineEndings };
141
+ module.exports = { main, renderFor, SOURCES, normalizeLineEndings: normalizeEol };