session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * block-preprocess.mjs — THE line-preprocessing contract for the block parsers
3
+ * under `scripts/lib/config/` (#1162).
4
+ *
5
+ * Every block parser here begins with `content.split(/\r?\n/)`, then walks lines
6
+ * looking for its own header and the indented sub-keys under it. Two markdown
7
+ * shapes that are ordinary in a CLAUDE.md / AGENTS.md were invisible to that
8
+ * walk, and both failed in the dangerous direction:
9
+ *
10
+ * (a) A block commented OUT with a multi-line `<!-- … -->` was read as LIVE
11
+ * config. Commenting a key out is the most ordinary way to disable it; it
12
+ * must not arm it. (`_extractConfigSection` learned this in #1097 — the
13
+ * block parsers never did.)
14
+ * (b) The bold-bullet rendering of a SUB-key (`- **enabled:** true`) matched
15
+ * no sub-key regex, so the key was absent and its DEFAULT applied. For a
16
+ * boolean that default is `false`, which reads exactly like a deliberately
17
+ * disabled block — no error, in any log. #823 fixed this for the block
18
+ * HEADER only (see `block-header.mjs`); this fixes it for the body.
19
+ *
20
+ * ZERO IMPORTS beyond the sibling `section-extractor.mjs` skipper, stdlib only —
21
+ * `tests/lib/config/cycle-guard.test.mjs` forbids any `scripts/lib/config/*.mjs`
22
+ * from importing `../config.mjs`, and the RAW-PARITY constraint (block-header.mjs
23
+ * docblock) requires the normalisation to live in the helper each parser calls,
24
+ * never in a preprocessing step in `config.mjs`.
25
+ *
26
+ * DELIBERATE NON-GOAL — fenced code blocks. These parsers are fence-UNAWARE, and
27
+ * that is a pinned contract, not an oversight. Two tests, cited by NAME because
28
+ * line numbers rot: `tests/lib/config/frontend-slop-hook.test.mjs` — "parses a
29
+ * block wrapped in a fenced code block — the parser is fence-unaware", and
30
+ * `tests/lib/config/dispatcher-autonomy-capture.test.mjs` — "treats a header
31
+ * inside a fenced code block as PRESENT". Both assert that a ```-fenced block IS
32
+ * parsed. Teaching this module about
33
+ * fences would flip ~36 parsers at once and break those pins, so it does not.
34
+ * Revisit only as its own change, with those two tests as the blast radius.
35
+ */
36
+
37
+ import { htmlCommentSkipper } from './section-extractor.mjs';
38
+
39
+ /**
40
+ * Drop every line that lies inside — or opens — a multi-line `<!-- … -->` HTML
41
+ * comment (#1162a).
42
+ *
43
+ * Wraps `htmlCommentSkipper()` from `section-extractor.mjs` rather than
44
+ * re-deriving the state machine, so the block parsers and the Session Config
45
+ * KV extractor share ONE definition of "this line is commented-out
46
+ * documentation" by construction. A fresh closure is built per call: the skipper
47
+ * carries per-document state, and a shared instance would leak an unterminated
48
+ * comment from one document into the next.
49
+ *
50
+ * Inherits the skipper's deliberate ceiling: the opener must be at the START of
51
+ * a line (`/^\s*<!--/`), so `waves: 5 <!-- note` does NOT swallow the following
52
+ * lines — the trailing single-line comment form this repo's own convention
53
+ * encourages keeps working.
54
+ *
55
+ * Never touches `- ` list markers, indentation, or line content.
56
+ *
57
+ * FAIL-CLOSED on an UNTERMINATED comment. A stray `<!--` with no `-->` after it
58
+ * puts the skipper in the swallowing state for the REST of the document, so
59
+ * every later block silently disappears and each parser falls back to its
60
+ * defaults — for `config-protection` that is `strict` → `warn`, i.e. a guard
61
+ * disarmed by a typo, with no error anywhere. When the skipper would still be
62
+ * inside a comment at EOF, this returns the INPUT lines unfiltered (the
63
+ * pre-#1162 behaviour: the opener is treated as literal text), so nothing can
64
+ * vanish. The condition is reported via `onUnterminated` and is separately
65
+ * observable through {@link findUnterminatedComment}.
66
+ *
67
+ * @param {string[]} lines
68
+ * @param {(lineNo: number) => void} [onUnterminated] — called with the 1-based
69
+ * line number of the unterminated opener when stripping is disabled.
70
+ * @returns {string[]} a new array; the input is not mutated.
71
+ */
72
+ export function stripHtmlCommentBlocks(lines, onUnterminated) {
73
+ if (!Array.isArray(lines)) return [];
74
+ const unterminatedAt = findUnterminatedComment(lines);
75
+ if (unterminatedAt !== null) {
76
+ if (typeof onUnterminated === 'function') onUnterminated(unterminatedAt);
77
+ return lines.slice();
78
+ }
79
+ const skip = htmlCommentSkipper();
80
+ return lines.filter((line) => !skip(typeof line === 'string' ? line : ''));
81
+ }
82
+
83
+ /**
84
+ * The 1-based line number of an `<!--` that is never closed, or `null` when
85
+ * every comment in `lines` is terminated.
86
+ *
87
+ * Derived from the SAME `htmlCommentSkipper()` state machine rather than a
88
+ * second copy of it (the RAW-PARITY constraint): the skipper swallows the
89
+ * closing `-->` line too, so the document ends inside a comment exactly when
90
+ * the LAST swallowed line carries no `-->`. A swallowed line that follows a
91
+ * closing line (or the first one of all) is an opener, which is how the
92
+ * reported line number is obtained.
93
+ *
94
+ * @param {string[]} lines
95
+ * @returns {number|null}
96
+ */
97
+ export function findUnterminatedComment(lines) {
98
+ if (!Array.isArray(lines)) return null;
99
+ const skip = htmlCommentSkipper();
100
+ let openerLine = null;
101
+ /** @type {string|null} */
102
+ let lastSwallowed = null;
103
+ for (let i = 0; i < lines.length; i += 1) {
104
+ const line = typeof lines[i] === 'string' ? lines[i] : '';
105
+ if (!skip(line)) continue;
106
+ if (lastSwallowed === null || lastSwallowed.includes('-->')) openerLine = i + 1;
107
+ lastSwallowed = line;
108
+ }
109
+ if (lastSwallowed === null || lastSwallowed.includes('-->')) return null;
110
+ return openerLine;
111
+ }
112
+
113
+ /**
114
+ * `- **key:** value` / `- **key**: value` / `**key:** value` → `key: value`,
115
+ * preserving the leading indentation EXACTLY (#1162b).
116
+ *
117
+ * The indent is load-bearing: every block parser ends its block at the first
118
+ * non-empty non-indented line, so an indent-eating rewrite would truncate the
119
+ * block one line early. Only the `**` markers and the optional `- ` bullet
120
+ * immediately around the `key:` token at line start are removed; the value
121
+ * portion (including any literal `**` inside it) is untouched — same rule as the
122
+ * `_matchKVLine` Format-1 branch and the #823 header tolerance.
123
+ *
124
+ * A line that is not a bold key/value pair is returned byte-identical.
125
+ *
126
+ * @internal — exported for this module's own tests. Production callers use
127
+ * {@link preprocessBlockLines} / {@link preprocessBlockLinesNoDash}.
128
+ *
129
+ * NAMED RISK — dash-record blocks. In any block whose body uses `- ` as a RECORD
130
+ * boundary, de-dashing the first key of a record merges it into the PREVIOUS
131
+ * record, silently producing one record where two were written. Those parsers
132
+ * must use
133
+ * `preprocessBlockLinesNoDash` instead. `tests/lib/config/block-preprocess.test.mjs`
134
+ * demonstrates the merge so the next reader sees why the NoDash variant exists.
135
+ *
136
+ * @param {string[]} lines
137
+ * @returns {string[]} a new array; the input is not mutated.
138
+ */
139
+ export function normalizeBoldSubkeys(lines) {
140
+ if (!Array.isArray(lines)) return [];
141
+ return lines.map((line) => {
142
+ if (typeof line !== 'string') return line;
143
+ // (1) indent (2) optional bullet (3) **key:** or **key**: (4) rest
144
+ const m = line.match(/^(\s*)(?:-\s+)?\*\*([^*:]+?)(?::\*\*|\*\*:)\s*(.*)$/);
145
+ if (!m) return line;
146
+ const value = m[3];
147
+ return value === '' ? `${m[1]}${m[2]}:` : `${m[1]}${m[2]}: ${value}`;
148
+ });
149
+ }
150
+
151
+ /**
152
+ * THE drop-in replacement for `content.split(/\r?\n/)` in a flat or
153
+ * nested-OBJECT block parser: strip commented-out lines, then normalise bold
154
+ * sub-keys.
155
+ *
156
+ * @param {string} content — full file contents
157
+ * @returns {string[]} preprocessed lines (empty array for non-string input)
158
+ */
159
+ export function preprocessBlockLines(content) {
160
+ if (typeof content !== 'string') return [];
161
+ return normalizeBoldSubkeys(stripHtmlCommentBlocks(content.split(/\r?\n/)));
162
+ }
163
+
164
+ /**
165
+ * THE drop-in for every dash-RECORD parser — any parser whose block body uses
166
+ * `- ` as a record boundary, so the dash must survive: HTML-comment stripping
167
+ * only, no bold-subkey normalisation. (Enumerating them here goes stale; the
168
+ * rule is the criterion. `grep -l preprocessBlockLinesNoDash scripts/lib/config/`
169
+ * lists the current set.)
170
+ *
171
+ * @param {string} content — full file contents
172
+ * @returns {string[]} preprocessed lines (empty array for non-string input)
173
+ */
174
+ export function preprocessBlockLinesNoDash(content) {
175
+ if (typeof content !== 'string') return [];
176
+ return stripHtmlCommentBlocks(content.split(/\r?\n/));
177
+ }
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * broken-window.mjs — Parser for the top-level `broken-window-budget:` YAML block
@@ -48,7 +49,7 @@ export function _parseBrokenWindow(content) {
48
49
  'due-days': 7,
49
50
  };
50
51
 
51
- const lines = String(content ?? '').split(/\r?\n/);
52
+ const lines = preprocessBlockLines(String(content ?? ''));
52
53
  let inBlock = false;
53
54
  const blockLines = [];
54
55
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * cold-start.mjs — Parser for the top-level `cold-start:` YAML block (PRD F1.3 /
@@ -30,7 +31,7 @@ export function _parseColdStart(content) {
30
31
  'silence-after-sessions': 1,
31
32
  };
32
33
 
33
- const lines = content.split(/\r?\n/);
34
+ const lines = preprocessBlockLines(content);
34
35
  let inBlock = false;
35
36
  const blockLines = [];
36
37
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { findUnterminatedComment, preprocessBlockLines, preprocessBlockLinesNoDash } from './block-preprocess.mjs';
2
3
  import { isSessionConfigHeading } from './section-extractor.mjs';
3
4
 
4
5
  /**
@@ -45,7 +46,7 @@ export function _parseConfigProtection(content) {
45
46
 
46
47
  if (typeof content !== 'string' || content.length === 0) return defaults;
47
48
 
48
- const lines = content.split(/\r?\n/);
49
+ const lines = preprocessBlockLines(content);
49
50
  let inBlock = false;
50
51
  const blockLines = [];
51
52
 
@@ -107,7 +108,26 @@ export function _parseConfigProtection(content) {
107
108
  export function _isConfigWeakeningAllowed(content) {
108
109
  if (typeof content !== 'string' || content.length === 0) return false;
109
110
 
110
- const lines = content.split(/\r?\n/);
111
+ // HTML-comment stripping ONLY (`preprocessBlockLinesNoDash`), never the full
112
+ // `preprocessBlockLines`: bold-subkey normalisation would make
113
+ // `- **allow-config-weakening:** true` arm the bypass, and
114
+ // `tests/lib/config/config-protection.test.mjs` pins the opposite — only the
115
+ // plain form is the supported bypass. Fail-closed beats markdown tolerance there.
116
+ // The comment strip is the OTHER direction and is mandatory (W4/F1): with a raw
117
+ // `content.split(/\r?\n/)` a COMMENTED-OUT `<!--\nallow-config-weakening: true\n-->`
118
+ // inside `## Session Config` ARMED the bypass — commenting a key out is the most
119
+ // ordinary way to disable it and must never enable it (same class as #1162a).
120
+ //
121
+ // UNTERMINATED `<!--` — the fail-closed direction INVERTS for a bypass scan.
122
+ // `stripHtmlCommentBlocks` returns the lines UNFILTERED when a comment is never
123
+ // closed, which is right for a block PARSER (nothing may silently vanish) and
124
+ // wrong here: it hands the commented-out bypass line straight back and re-arms
125
+ // the guard's own off switch. For this scan "I cannot tell where the comments
126
+ // end" must mean NOT ARMED — the bypass is opt-in, so refusing to grant it is
127
+ // the safe reading in every ambiguous document.
128
+ if (findUnterminatedComment(content.split(/\r?\n/)) !== null) return false;
129
+
130
+ const lines = preprocessBlockLinesNoDash(content);
111
131
  let inConfig = false;
112
132
  for (const line of lines) {
113
133
  if (isSessionConfigHeading(line)) { inConfig = true; continue; }
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * context-coverage.mjs — Parser for the top-level `context-coverage:` YAML block.
@@ -37,7 +38,7 @@ import { matchBlockHeader } from './block-header.mjs';
37
38
  export function _parseContextCoverage(content) {
38
39
  const defaults = { enabled: false, mode: 'warn' };
39
40
 
40
- const lines = content.split(/\r?\n/);
41
+ const lines = preprocessBlockLines(content);
41
42
  let inBlock = false;
42
43
  const blockLines = [];
43
44
 
@@ -20,6 +20,7 @@ import { homedir } from 'node:os';
20
20
  import { join } from 'node:path';
21
21
  import { readConfigFile } from './io.mjs';
22
22
  import { matchBlockHeader } from './block-header.mjs';
23
+ import { preprocessBlockLines } from './block-preprocess.mjs';
23
24
 
24
25
  /**
25
26
  * Resolve the cross-repo confinement root. Defaults to ~/Projects.
@@ -47,7 +48,7 @@ const SAFE_PATH_RE = /^[A-Za-z0-9._~/-]+$/;
47
48
  * @returns {string[]} list of project paths, or [] when absent/empty/none/null
48
49
  */
49
50
  export function _parseCrossRepo(content) {
50
- const lines = content.split(/\r?\n/);
51
+ const lines = preprocessBlockLines(content);
51
52
  let inBlock = false;
52
53
 
53
54
  for (const rawLine of lines) {
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLinesNoDash } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * custom-phases.mjs — Parser for the `custom-phases:` Session Config block (#637).
@@ -65,7 +66,7 @@ const SAFE_PATH_RE = /^[A-Za-z0-9._~/-]+$/;
65
66
  * @returns {Array<{name: string, when: string, command: string, mode: string, review: string|null}>}
66
67
  */
67
68
  export function _parseCustomPhases(content) {
68
- const lines = content.split(/\r?\n/);
69
+ const lines = preprocessBlockLinesNoDash(content);
69
70
  let inBlock = false;
70
71
  const blockLines = [];
71
72
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * dialectic.mjs — Parser for top-level `dialectic:` YAML block (#506).
@@ -30,7 +31,7 @@ const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
30
31
  export function _parseDialectic(content) {
31
32
  const defaults = { cadence: 5, model: 'haiku', 'budget-tokens': 8000 };
32
33
 
33
- const lines = content.split(/\r?\n/);
34
+ const lines = preprocessBlockLines(content);
34
35
  let inBlock = false;
35
36
  const blockLines = [];
36
37
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * discovery-validator.mjs — Parser for the top-level `discovery-validator:`
@@ -36,7 +37,7 @@ export function _parseDiscoveryValidator(content) {
36
37
  enabled: false,
37
38
  };
38
39
 
39
- const lines = content.split(/\r?\n/);
40
+ const lines = preprocessBlockLines(content);
40
41
  let inBlock = false;
41
42
  const blockLines = [];
42
43
 
@@ -39,6 +39,7 @@
39
39
  import { readFileSync, writeFileSync } from 'node:fs';
40
40
 
41
41
  import { hasBlockHeader } from './block-header.mjs';
42
+ import { preprocessBlockLinesNoDash } from './block-preprocess.mjs';
42
43
 
43
44
  const ALLOWED_AUTONOMY = ['off', 'advisory', 'autonomous-gated'];
44
45
  const DEFAULT_CONFIDENCE_FLOOR = 0.5;
@@ -113,6 +114,18 @@ export function getDispatcherAutonomyQuestion() {
113
114
  * the shared `matchBlockHeader()` matcher (block-header.mjs). `hasBlockHeader()`
114
115
  * splits on `/\r?\n/` and tests each line, so `\r` and trailing whitespace are
115
116
  * eaten by the `\s*$` tail and the identical set of inputs matches.
117
+ *
118
+ * PARSER PARITY, both halves (#1222): the parser preprocesses its content with
119
+ * `preprocessBlockLines()` — it STRIPS `<!-- … -->` HTML comments and is
120
+ * fence-BLIND. This guard therefore preprocesses too, with
121
+ * `preprocessBlockLinesNoDash()` (comment-strip only; the full
122
+ * `preprocessBlockLines` additionally de-dashes bold sub-keys, which this
123
+ * whole-file presence scan has no use for), and stays fence-blind as well. Before
124
+ * that, a block commented OUT (`<!--\ndispatcher-autonomy:\n autonomy: off\n-->`)
125
+ * parsed to DEFAULTS while being reported PRESENT — so the one-time capture AUQ
126
+ * never fired and the operator was never asked. The two halves must move
127
+ * together: teach one about comments or fences and the other follows in
128
+ * lock-step, or the guard and the parser disagree again.
116
129
  * Verified-equivalent edge cases:
117
130
  * - CRLF line endings (`dispatcher-autonomy:\r\n`) → PRESENT (`\s*$` eats the `\r`).
118
131
  * - Trailing spaces / tabs on the header line → PRESENT (`\s*$`).
@@ -130,6 +143,15 @@ export function getDispatcherAutonomyQuestion() {
130
143
  * - Header present but BODY malformed (garbage non-yaml lines) → PRESENT. A
131
144
  * malformed block is the operator's to fix, never a re-prompt trigger — the
132
145
  * parser tolerantly falls back to defaults, and capture must not re-ask.
146
+ * - Header inside a `<!-- … -->` comment → ABSENT (#1222), matching the parser.
147
+ * - Header inside a comment that is never closed (a stray `<!--` with no
148
+ * `-->`) → PRESENT. `stripHtmlCommentBlocks()` returns the lines UNFILTERED
149
+ * in that case, so nothing silently vanishes, and the parser — which uses the
150
+ * same helper — sees the header too. Unlike `_isConfigWeakeningAllowed()` in
151
+ * config-protection.mjs, this scan does NOT invert that direction: there the
152
+ * scanned line ARMS a bypass, so ambiguity must mean "not armed"; here the
153
+ * only cost of a stray PRESENT is one un-asked question, while diverging from
154
+ * the parser would re-open exactly the bug this fix closes.
133
155
  *
134
156
  * No-throw: null / non-string / empty → false.
135
157
  *
@@ -138,7 +160,8 @@ export function getDispatcherAutonomyQuestion() {
138
160
  */
139
161
  export function isDispatcherAutonomyBlockPresent(claudeMdContent) {
140
162
  if (typeof claudeMdContent !== 'string' || claudeMdContent.length === 0) return false;
141
- return hasBlockHeader(claudeMdContent, 'dispatcher-autonomy');
163
+ const preprocessed = preprocessBlockLinesNoDash(claudeMdContent).join('\n');
164
+ return hasBlockHeader(preprocessed, 'dispatcher-autonomy');
142
165
  }
143
166
 
144
167
  /**
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * dispatcher-autonomy.mjs — Parser + resolver for the top-level
@@ -40,7 +41,7 @@ export function _parseDispatcherAutonomy(content) {
40
41
  'confidence-floor': 0.5,
41
42
  };
42
43
 
43
- const lines = content.split(/\r?\n/);
44
+ const lines = preprocessBlockLines(content);
44
45
  let inBlock = false;
45
46
  const blockLines = [];
46
47
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * docs-orchestrator.mjs — Parser for the top-level `docs-orchestrator:` YAML block.
@@ -13,7 +14,7 @@ import { matchBlockHeader } from './block-header.mjs';
13
14
  export function _parseDocsOrchestrator(content) {
14
15
  const defaults = { enabled: false, audiences: ['user', 'dev', 'vault'], mode: 'warn' };
15
16
 
16
- const lines = content.split(/\r?\n/);
17
+ const lines = preprocessBlockLines(content);
17
18
  let inBlock = false;
18
19
  const blockLines = [];
19
20
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * docs-staleness.mjs — Parser for the top-level `docs-staleness:` YAML block.
@@ -29,7 +30,7 @@ export function _parseDocsStaleness(content) {
29
30
  mode: 'warn',
30
31
  };
31
32
 
32
- const lines = content.split(/\r?\n/);
33
+ const lines = preprocessBlockLines(content);
33
34
  let inBlock = false;
34
35
  const blockLines = [];
35
36
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * drift-check.mjs — Parser for the top-level `drift-check:` YAML block.
@@ -39,7 +40,7 @@ export function _parseDriftCheck(content) {
39
40
  'check-docs-parity': true,
40
41
  };
41
42
 
42
- const lines = content.split(/\r?\n/);
43
+ const lines = preprocessBlockLines(content);
43
44
  let inBlock = false;
44
45
  const blockLines = [];
45
46
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * eval.mjs — Parser for the top-level `eval:` YAML block (#809, Epic #803).
@@ -51,7 +52,7 @@ const ALLOWED_REPORT = new Set(['html', 'none']);
51
52
  export function _parseEval(content) {
52
53
  const defaults = { enabled: false, mode: 'warn', judge: 'off', report: 'html', handle: null };
53
54
 
54
- const lines = content.split(/\r?\n/);
55
+ const lines = preprocessBlockLines(content);
55
56
  let inBlock = false;
56
57
  const blockLines = [];
57
58
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * events-rotation.mjs — Parser for the top-level `events-rotation:` YAML block.
@@ -17,7 +18,7 @@ import { matchBlockHeader } from './block-header.mjs';
17
18
  export function _parseEventsRotation(content) {
18
19
  const defaults = { enabled: true, 'max-size-mb': 10, 'max-backups': 5 };
19
20
 
20
- const lines = content.split(/\r?\n/);
21
+ const lines = preprocessBlockLines(content);
21
22
  let inBlock = false;
22
23
  const blockLines = [];
23
24
 
@@ -35,6 +35,7 @@
35
35
  import pathModule from 'node:path';
36
36
 
37
37
  import { matchBlockHeader } from './block-header.mjs';
38
+ import { preprocessBlockLines, preprocessBlockLinesNoDash } from './block-preprocess.mjs';
38
39
 
39
40
  /** Per-entry defaults (none beyond the required field-set; documented for symmetry). */
40
41
  export const EVOLVE_EXTRA_SOURCE_DEFAULTS = Object.freeze({
@@ -67,7 +68,9 @@ const SAFE_PATH_RE = /^[A-Za-z0-9._~/-]+$/;
67
68
  * @returns {Array<{path: string, kind: string, 'learning-type': string}>}
68
69
  */
69
70
  export function _parseEvolve(content) {
70
- const lines = content.split(/\r?\n/);
71
+ // NoDash: `- ` is the RECORD boundary under `extra-sources:` — de-dashing the
72
+ // first key of a record would merge it into the previous one (#1162).
73
+ const lines = preprocessBlockLinesNoDash(content);
71
74
  let inEvolve = false;
72
75
  let inExtraSources = false;
73
76
  const blockLines = [];
@@ -254,7 +257,10 @@ export function _parseEvolveDecay(content) {
254
257
  'floor-factor': EVOLVE_DECAY_DEFAULTS['floor-factor'],
255
258
  };
256
259
 
257
- const lines = content.split(/\r?\n/);
260
+ // Standard variant: this parser reads only FLAT `decay-*` sub-keys and builds
261
+ // no records, so bold-subkey normalisation is safe here even though the same
262
+ // `evolve:` block also carries the `extra-sources:` dash records (#1162).
263
+ const lines = preprocessBlockLines(content);
258
264
  let inEvolve = false;
259
265
  const blockLines = [];
260
266
 
@@ -18,8 +18,9 @@
18
18
  import { promises as fs } from 'node:fs';
19
19
  import path from 'node:path';
20
20
 
21
- import { SO_PROJECT_DIR } from '../platform.mjs';
21
+ import { getProjectDir } from '../platform.mjs';
22
22
  import { matchBlockHeader } from './block-header.mjs';
23
+ import { preprocessBlockLines } from './block-preprocess.mjs';
23
24
 
24
25
  /**
25
26
  * Parse the top-level `frontend-slop-hook:` YAML block from markdown content.
@@ -35,7 +36,7 @@ import { matchBlockHeader } from './block-header.mjs';
35
36
  export function _parseFrontendSlopHook(content) {
36
37
  const defaults = { enabled: false };
37
38
 
38
- const lines = content.split(/\r?\n/);
39
+ const lines = preprocessBlockLines(content);
39
40
  let inBlock = false;
40
41
  const blockLines = [];
41
42
 
@@ -86,7 +87,10 @@ export function _parseFrontendSlopHook(content) {
86
87
  * @returns {Promise<{ enabled: boolean }>}
87
88
  */
88
89
  export async function loadFrontendSlopHookConfig(opts = {}) {
89
- const root = opts.repoRoot || SO_PROJECT_DIR;
90
+ // Called at USE time, never at module load: the deprecated `SO_PROJECT_DIR`
91
+ // named export is a live binding that stays `undefined` until some getter has
92
+ // run in the process, so a bare call resolved the root to undefined (#1153 P5).
93
+ const root = opts.repoRoot || getProjectDir();
90
94
  const candidates = [
91
95
  path.join(root, 'CLAUDE.md'),
92
96
  path.join(root, 'AGENTS.md'),
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * gitlab-portfolio.mjs — Parser for the top-level `gitlab-portfolio:` YAML block.
@@ -66,7 +67,7 @@ export function coerceGitlabPortfolio(raw) {
66
67
  * @returns {{ enabled: boolean, mode: 'warn'|'strict'|'off', 'stale-days': number, 'critical-labels': string[] }}
67
68
  */
68
69
  export function _parseGitlabPortfolio(content) {
69
- const lines = content.split(/\r?\n/);
70
+ const lines = preprocessBlockLines(content);
70
71
  let inBlock = false;
71
72
  const blockLines = [];
72
73
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * handover-gate.mjs — Parser for the top-level `handover-gate:` YAML block
@@ -43,7 +44,7 @@ export function _parseHandoverGate(content) {
43
44
  'max-open-questions': 3,
44
45
  };
45
46
 
46
- const lines = content.split(/\r?\n/);
47
+ const lines = preprocessBlockLines(content);
47
48
  let inBlock = false;
48
49
  const blockLines = [];
49
50
 
@@ -63,6 +63,7 @@
63
63
  */
64
64
 
65
65
  import { matchBlockHeader, matchBlockHeaderDetailed } from './block-header.mjs';
66
+ import { preprocessBlockLines, preprocessBlockLinesNoDash } from './block-preprocess.mjs';
66
67
  import { _coerceBoolean } from './coercers.mjs';
67
68
 
68
69
  /**
@@ -315,7 +316,9 @@ function assignKV(entry, text) {
315
316
  export function _parseHealthEndpoints(content) {
316
317
  if (typeof content !== 'string' || content === '') return null;
317
318
 
318
- const lines = content.split(/\r?\n/);
319
+ // NoDash: `- name:` / `- url:` / `- {name, url}` list items are RECORD
320
+ // boundaries here — de-dashing would merge two endpoints into one (#1162).
321
+ const lines = preprocessBlockLinesNoDash(content);
319
322
  for (let i = 0; i < lines.length; i++) {
320
323
  const line = lines[i].replace(/\r$/, '');
321
324
  const m = matchBlockHeaderDetailed(line, 'health-endpoints');
@@ -353,7 +356,9 @@ export function _parseHealthEndpoints(content) {
353
356
  export function _parseEcosystemHealthBlockEnabled(content) {
354
357
  if (typeof content !== 'string' || content === '') return null;
355
358
 
356
- const lines = content.split(/\r?\n/);
359
+ // Standard variant: this parser reads only the flat `enabled:` sub-key and
360
+ // builds no records (#1162).
361
+ const lines = preprocessBlockLines(content);
357
362
  let inBlock = false;
358
363
  const blockLines = [];
359
364
  for (const rawLine of lines) {
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * issue-budget.mjs — Parser for the `issue-budget:` block-style Session Config key.
@@ -65,7 +66,7 @@ export function _parseIssueBudget(content) {
65
66
 
66
67
  if (typeof content !== 'string' || content === '') return { ...defaults };
67
68
 
68
- const lines = content.split(/\r?\n/);
69
+ const lines = preprocessBlockLines(content);
69
70
  let inBlock = false;
70
71
  const blockLines = [];
71
72
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * loop-guard.mjs — Parser for the top-level `loop-guard:` YAML block
@@ -57,7 +58,7 @@ export function _parseLoopGuard(content) {
57
58
  window: DEFAULT_WINDOW,
58
59
  };
59
60
 
60
- const lines = content.split(/\r?\n/);
61
+ const lines = preprocessBlockLines(content);
61
62
  let inBlock = false;
62
63
  const blockLines = [];
63
64
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * memory.mjs — Parser for the top-level `memory:` YAML block.
@@ -55,7 +56,7 @@ export function _parseMemory(content) {
55
56
  },
56
57
  };
57
58
 
58
- const lines = content.split(/\r?\n/);
59
+ const lines = preprocessBlockLines(content);
59
60
  let inBlock = false;
60
61
  const blockLines = [];
61
62