session-orchestrator 5.2.0 → 5.3.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 (295) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. package/pi/prompts/npm-publish.md +0 -11
@@ -49,7 +49,7 @@ const SENTINEL_BEGIN_RE = new RegExp(`<!--\\s*BEGIN\\s+MANAGED:\\s*(${SECTION_NA
49
49
 
50
50
  /**
51
51
  * @typedef {Object} Conflict
52
- * @property {'duplicate-section' | 'orphan-begin'} type
52
+ * @property {'duplicate-section' | 'orphan-begin' | 'unmapped-preamble' | 'multi-heading-region'} type
53
53
  * @property {string} [name] — section name (for duplicate-section and orphan-begin)
54
54
  */
55
55
 
@@ -317,8 +317,14 @@ function escapeRegex(s) {
317
317
  // the two, so `/evolve dialectic --apply` could not complete (#1310, correcting
318
318
  // #1303 point 3 — the signatures line up, the seam did not).
319
319
  //
320
- // The mapping is DERIVED, not invented: every managed region in the live cards
321
- // wraps exactly one `## ` heading, so the heading IS the section unit. Existing
320
+ // The mapping is DERIVED, not invented: a managed region wrapping exactly one
321
+ // `## ` heading makes that heading the section unit. A region wrapping SEVERAL
322
+ // headings cannot be addressed per heading without splitting the region (a
323
+ // storage-format change), so it is fail-closed (#1380): `deriveManagedUpdates`
324
+ // never targets it, and every proposed heading that lives inside it is skipped
325
+ // — neither replaced (which would delete its sibling headings' hand text) nor
326
+ // appended (which would duplicate the heading) — and reported as a
327
+ // `multi-heading-region` conflict by `mergeDerivedBody`. Existing
322
328
  // names are read back out of the card rather than re-slugified, because the live
323
329
  // names are NOT a pure function of their headings — measured 2026-09-11 in
324
330
  // `.orchestrator/peers/AGENT.md`: "Guard and protocol-migration discipline" →
@@ -359,6 +365,9 @@ function slugifyHeading(heading) {
359
365
  * • Text BEFORE the first `## ` heading → returned as `preamble`. It is NOT
360
366
  * written into any section (it has no sentinel to own it); `mergeDerivedBody`
361
367
  * surfaces it as an `unmapped-preamble` conflict so a caller cannot miss it.
368
+ * • Heading that lives inside an existing managed region wrapping MORE than one
369
+ * `## ` heading → SKIPPED (not in `managedUpdates`, not in `mapping`) and
370
+ * listed in `skippedRegions`; that region is never a target (#1380).
362
371
  * • Content under `###`+ headings stays inside its parent `##` section.
363
372
  * • A proposed body that already carries BEGIN/END sentinels keeps them inside
364
373
  * the section content — it is the deriver's job not to emit them (see
@@ -370,7 +379,8 @@ function slugifyHeading(heading) {
370
379
  * @param {string} [existingBody] — the on-disk body, for existing-name lookup
371
380
  * @returns {{ managedUpdates: Record<string,string>,
372
381
  * mapping: Array<{heading: string, section: string, origin: 'existing'|'new'}>,
373
- * preamble: string }}
382
+ * preamble: string,
383
+ * skippedRegions: Array<{region: string, headings: string[], skipped: string[]}> }}
374
384
  */
375
385
  export function deriveManagedUpdates(proposedBody, existingBody = '') {
376
386
  if (typeof proposedBody !== 'string') {
@@ -383,13 +393,25 @@ export function deriveManagedUpdates(proposedBody, existingBody = '') {
383
393
  // heading-key → existing section name, read out of the live card
384
394
  const existingByHeading = new Map();
385
395
  const existingNames = new Set();
396
+ // Fail-closed bookkeeping for regions wrapping more than one `## ` heading (#1380).
397
+ /** @type {Map<string, {region: string, headings: string[], skipped: string[]}>} */
398
+ const multiHeading = new Map();
399
+ const multiHeadingByKey = new Map();
386
400
  if (existingBody.length > 0) {
387
401
  for (const s of parseSections(existingBody).sections) {
388
402
  if (s.type !== 'managed') continue;
389
403
  existingNames.add(s.name);
390
- const h = s.content.match(/^##[ \t]+(.+?)[ \t]*$/m);
391
- if (h && !existingByHeading.has(headingKey(h[1]))) {
392
- existingByHeading.set(headingKey(h[1]), s.name);
404
+ H2_RE.lastIndex = 0;
405
+ const regionHeads = [...s.content.matchAll(H2_RE)].map(h => h[1]);
406
+ if (regionHeads.length > 1) {
407
+ const entry = multiHeading.get(s.name) ?? { region: s.name, headings: [], skipped: [] };
408
+ entry.headings.push(...regionHeads);
409
+ multiHeading.set(s.name, entry);
410
+ for (const h of regionHeads) multiHeadingByKey.set(headingKey(h), entry);
411
+ continue;
412
+ }
413
+ if (regionHeads.length === 1 && !existingByHeading.has(headingKey(regionHeads[0]))) {
414
+ existingByHeading.set(headingKey(regionHeads[0]), s.name);
393
415
  }
394
416
  }
395
417
  }
@@ -409,6 +431,12 @@ export function deriveManagedUpdates(proposedBody, existingBody = '') {
409
431
  const end = i + 1 < heads.length ? heads[i + 1].index : proposedBody.length;
410
432
  const content = proposedBody.slice(start, end).trim();
411
433
 
434
+ const blocked = multiHeadingByKey.get(headingKey(heading));
435
+ if (blocked !== undefined) {
436
+ blocked.skipped.push(heading);
437
+ continue;
438
+ }
439
+
412
440
  const existing = existingByHeading.get(headingKey(heading));
413
441
  let section;
414
442
  let origin;
@@ -419,7 +447,9 @@ export function deriveManagedUpdates(proposedBody, existingBody = '') {
419
447
  const base = slugifyHeading(heading);
420
448
  let candidate = base;
421
449
  let n = 2;
422
- while (used.has(candidate)) {
450
+ // A multi-heading region is never a target — not even via a slug that
451
+ // happens to equal its name.
452
+ while (used.has(candidate) || multiHeading.has(candidate)) {
423
453
  candidate = `${base}-${n++}`;
424
454
  }
425
455
  section = candidate;
@@ -431,7 +461,8 @@ export function deriveManagedUpdates(proposedBody, existingBody = '') {
431
461
  mapping.push({ heading, section, origin });
432
462
  }
433
463
 
434
- return { managedUpdates, mapping, preamble };
464
+ const skippedRegions = [...multiHeading.values()].filter(e => e.skipped.length > 0);
465
+ return { managedUpdates, mapping, preamble, skippedRegions };
435
466
  }
436
467
 
437
468
  /**
@@ -439,13 +470,20 @@ export function deriveManagedUpdates(proposedBody, existingBody = '') {
439
470
  * This is the function `/evolve dialectic --apply` calls; `mergePeerCard` stays
440
471
  * the section-map primitive its existing callers already use.
441
472
  *
473
+ * A managed region wrapping several `## ` headings is left byte-unchanged; each
474
+ * skipped proposed heading surfaces as a
475
+ * `{ type: 'multi-heading-region', region, headings, skipped }` conflict (#1380).
476
+ *
442
477
  * @param {string} existingBody
443
478
  * @param {string} proposedBody
444
479
  * @returns {MergeResult & { mapping: Array<{heading: string, section: string, origin: 'existing'|'new'}>, preamble: string }}
445
480
  */
446
481
  export function mergeDerivedBody(existingBody, proposedBody) {
447
- const { managedUpdates, mapping, preamble } = deriveManagedUpdates(proposedBody, existingBody);
482
+ const { managedUpdates, mapping, preamble, skippedRegions } = deriveManagedUpdates(proposedBody, existingBody);
448
483
  const result = mergePeerCard(existingBody, managedUpdates);
484
+ for (const { region, headings, skipped } of skippedRegions) {
485
+ result.conflicts.push({ type: 'multi-heading-region', region, headings, skipped });
486
+ }
449
487
  if (preamble.length > 0) {
450
488
  result.conflicts.push({ type: 'unmapped-preamble', content: preamble });
451
489
  }
@@ -104,11 +104,11 @@ async function readOneCard(absPath, now) {
104
104
  };
105
105
  }
106
106
 
107
- const parsed = parseStateMd(content);
107
+ const parsed = parseStateMd(foldMultilineLists(content));
108
108
  if (parsed === null) {
109
109
  return {
110
110
  frontmatter: null,
111
- body: content,
111
+ body: stripFrontmatterBlock(content),
112
112
  stalenessDays: Infinity,
113
113
  isStale: true,
114
114
  validation: { ok: false, errors: ['no frontmatter or malformed YAML'] },
@@ -123,3 +123,79 @@ async function readOneCard(absPath, now) {
123
123
 
124
124
  return { frontmatter, body, stalenessDays, isStale, validation };
125
125
  }
126
+
127
+ // `---\n<frontmatter>\n---\n` at the very start — same shape `parseStateMd` matches.
128
+ const FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/;
129
+
130
+ /**
131
+ * Fold multi-line list values of top-level frontmatter keys into the one-line
132
+ * flow form (`key: [a, b]`) that `parseStateMd` understands. Two shapes (#1380):
133
+ * • a multi-line flow list — `key:\n [\n "a",\n "b",\n ]` — which is how
134
+ * Prettier reformats the one-line list `writer.mjs` emits;
135
+ * • a block list — `key:\n - a\n - b` — as a hand edit would write it.
136
+ * Done here, not in the shared STATE.md parser: its mutators rely on `null` for
137
+ * shapes it does not understand. A block list whose items are mappings
138
+ * (`- name: x`) is left untouched — that is the parser's own block-seq form.
139
+ * Content outside the frontmatter block is never modified.
140
+ *
141
+ * @param {string} content — full file content
142
+ * @returns {string}
143
+ */
144
+ function foldMultilineLists(content) {
145
+ const m = FRONTMATTER_BLOCK_RE.exec(content);
146
+ if (!m) return content;
147
+ const lines = m[1].split(/\r?\n/);
148
+ const out = [];
149
+ for (let i = 0; i < lines.length; i++) {
150
+ const key = /^([\w-]+):\s*$/.exec(lines[i]);
151
+ const next = lines[i + 1];
152
+ if (!key || next === undefined || !/^\s+\S/.test(next)) {
153
+ out.push(lines[i]);
154
+ continue;
155
+ }
156
+ if (next.trim().startsWith('[')) {
157
+ let j = i + 1;
158
+ const parts = [];
159
+ while (j < lines.length && /^\s+\S/.test(lines[j])) {
160
+ parts.push(lines[j].trim());
161
+ if (lines[j].trim().endsWith(']')) break;
162
+ j++;
163
+ }
164
+ if (j < lines.length && lines[j].trim().endsWith(']')) {
165
+ const inner = parts.join(' ').slice(1, -1).trim().replace(/,\s*$/, '');
166
+ out.push(`${key[1]}: [${inner.replace(/\s*,\s*/g, ', ')}]`);
167
+ i = j;
168
+ continue;
169
+ }
170
+ } else {
171
+ let j = i + 1;
172
+ const items = [];
173
+ while (j < lines.length && /^\s+-\s+\S/.test(lines[j])) {
174
+ items.push(lines[j].trim().slice(1).trim());
175
+ j++;
176
+ }
177
+ const endsBlock = j >= lines.length || !/^\s/.test(lines[j]);
178
+ if (items.length > 0 && endsBlock && !items.some((it) => /^[\w-]+:(\s|$)/.test(it))) {
179
+ out.push(`${key[1]}: [${items.join(', ')}]`);
180
+ i = j - 1;
181
+ continue;
182
+ }
183
+ }
184
+ out.push(lines[i]);
185
+ }
186
+ return `---\n${out.join('\n')}\n---${m[2]}${content.slice(m[0].length)}`;
187
+ }
188
+
189
+ /**
190
+ * Remove a leading `---…---` frontmatter block so an unparseable one never reaches
191
+ * a body consumer (the dialectic deriver payload) as if it were markdown (#1380).
192
+ *
193
+ * @param {string} content
194
+ * @returns {string}
195
+ */
196
+ function stripFrontmatterBlock(content) {
197
+ const m = FRONTMATTER_BLOCK_RE.exec(content);
198
+ if (!m) return content;
199
+ const rest = content.slice(m[0].length);
200
+ return rest.startsWith('\n') ? rest.slice(1) : rest;
201
+ }