session-orchestrator 5.2.0 → 5.4.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 (427) 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 +4 -2
  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/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -0,0 +1,316 @@
1
+ /**
2
+ * orphan-reaper.mjs — Teil B of Epic #1425: the NET under the process-group kill.
3
+ *
4
+ * Teil A (`process-group.mjs`) prevents orphans at the source. This module finds
5
+ * the ones that happen anyway — a session that crashed, a harness SIGKILL, a
6
+ * leak from a foreign repo on the same host — and reaps them.
7
+ *
8
+ * The 2026-09-20 incident this exists for: four orphaned `tsgo --noEmit`
9
+ * processes, two at PPID 1, 86-588% CPU and up to 8.0 GB RSS each; the Mac at
10
+ * 13% free memory, load 67.4. The existing detector
11
+ * (`countZombieProcesses`, `resource-probe/parsers.mjs`) would have counted ZERO:
12
+ * its `ps` carries no `ppid` and no `rss`, its name filter knows only
13
+ * `claude`/`node`, and it defines a zombie as IDLE (`cpu <= 1.0`) — the exact
14
+ * opposite of a runaway.
15
+ *
16
+ * ## Shape: pure decision, impure execution
17
+ *
18
+ * ps text → `parsePsSnapshot` → `decideReapCandidates` (PURE)
19
+ * → targeted ps + identity re-check BEFORE EVERY SIGNAL
20
+ * → `deps.killProcessGroup` → wait → re-measure (IMPURE)
21
+ *
22
+ * ## Layout (#1437)
23
+ *
24
+ * This file is the stable entry: the public re-exports below plus the CLI the
25
+ * trigger hooks spawn. The implementation lives in `orphan-reaper/`:
26
+ *
27
+ * defaults.mjs `REAPER_DEFAULTS` + the repo-path join (node:path only)
28
+ * scan-throttle.mjs B4 throttle marker — the trigger hooks import THIS
29
+ * ps-snapshot.mjs the binding `ps` format, its parser and runners
30
+ * reaper-decide.mjs the PURE candidate decision and its identity checks
31
+ * reaper-audit.mjs B5 audit record, writer, bounded reader, pruner, HR-101 rate
32
+ * scan.mjs `runOrphanScan` + its `resolveDeps` adapter
33
+ *
34
+ * Explicit named re-exports, never `export *`: a star barrel hides which module
35
+ * a name comes from, and the import census reads it as no reference at all.
36
+ */
37
+
38
+ import { isMainModule } from './is-main-module.mjs';
39
+ import { REAPER_DEFAULTS } from './orphan-reaper/defaults.mjs';
40
+ import { runOrphanScan } from './orphan-reaper/scan.mjs';
41
+
42
+ export { REAPER_DEFAULTS } from './orphan-reaper/defaults.mjs';
43
+ export {
44
+ SCAN_MARKER_RELPATH,
45
+ scanMarkerPath,
46
+ shouldScanNow,
47
+ touchScanMarker,
48
+ } from './orphan-reaper/scan-throttle.mjs';
49
+ export {
50
+ PS_ARGS,
51
+ parsePsSnapshot,
52
+ parsePsSnapshotDetailed,
53
+ psPidArgs,
54
+ } from './orphan-reaper/ps-snapshot.mjs';
55
+ export {
56
+ READ_ONLY_COMMAND_PATTERNS,
57
+ decideReapCandidates,
58
+ isReadOnlyCommand,
59
+ verifyGroupMemberIdentity,
60
+ } from './orphan-reaper/reaper-decide.mjs';
61
+ export {
62
+ FALSE_ALARM_SUSPECT_RATE,
63
+ REAPER_AUDIT_RELPATH,
64
+ auditPath,
65
+ falseAlarmRate,
66
+ } from './orphan-reaper/reaper-audit.mjs';
67
+ export { REAPER_SCAN_EVENT, resolveDeps, runOrphanScan } from './orphan-reaper/scan.mjs';
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // CLI (#1432 B4 follow-up)
71
+ // ---------------------------------------------------------------------------
72
+ //
73
+ // The two trigger hooks (`hooks/on-stop.mjs`,
74
+ // `hooks/post-tool-batch-wave-signal.mjs`) spawn the scan as a DETACHED child.
75
+ // Before this tail existed they had to hand `node` an `--input-type=module -e
76
+ // <program>` string that dynamically imported this module — a program the
77
+ // hooks carried as source text, in two byte-identical copies. This entry point
78
+ // replaces it with a plain argv call, so the hooks spawn `node
79
+ // scripts/lib/orphan-reaper.mjs --repo-root <p> --mode <m> …` and the child's
80
+ // contract lives HERE, in one place, next to the function it drives.
81
+
82
+ /**
83
+ * Parse the CLI argv — PURE, and exported so the flag contract is testable
84
+ * without starting a process (the argv form is what the two hooks build, so a
85
+ * typo in it fails silently as a dead detached child).
86
+ *
87
+ * Never throws: an unknown flag, a missing value and a non-numeric value all
88
+ * land in `errors`, which the tail maps to exit 2. Returning the errors rather
89
+ * than throwing keeps this usable from a test and from the tail alike.
90
+ *
91
+ * @param {string[]} [argv] `process.argv.slice(2)`
92
+ * @returns {{help: boolean, json: boolean, repoRoot: string|null, mode: 'report'|'kill',
93
+ * dryRun: boolean, minAgeSeconds: number, killGraceMs: number, verifyWaitMs: number,
94
+ * falseAlarmWindow: number, errors: string[]}}
95
+ */
96
+ export function parseReaperCliArgs(argv = []) {
97
+ const args = Array.isArray(argv) ? argv : [];
98
+ /** @type {string[]} */
99
+ const errors = [];
100
+ const out = {
101
+ help: false,
102
+ json: false,
103
+ repoRoot: null,
104
+ mode: /** @type {'report'|'kill'} */ ('report'),
105
+ dryRun: true,
106
+ minAgeSeconds: REAPER_DEFAULTS.minAgeSeconds,
107
+ killGraceMs: REAPER_DEFAULTS.killGraceMs,
108
+ verifyWaitMs: REAPER_DEFAULTS.verifyWaitMs,
109
+ falseAlarmWindow: REAPER_DEFAULTS.falseAlarmWindow,
110
+ errors,
111
+ };
112
+
113
+ /**
114
+ * Consume the value of `--flag <value>`; records an error when it is absent.
115
+ * @param {string} flag @param {number} i @returns {string|null}
116
+ */
117
+ const valueAt = (flag, i) => {
118
+ const value = args[i + 1];
119
+ if (value === undefined || value.startsWith('--')) {
120
+ errors.push(`missing value for ${flag}`);
121
+ return null;
122
+ }
123
+ return value;
124
+ };
125
+
126
+ /**
127
+ * A count of seconds/milliseconds: finite and non-negative. A `NaN` here
128
+ * would reach `runOrphanScan` as a threshold that compares false against
129
+ * everything — a silently disarmed gate, which is why it is an error and
130
+ * never a fallback to the default.
131
+ * @param {string} flag @param {string} raw @returns {number|null}
132
+ */
133
+ const numberFrom = (flag, raw) => {
134
+ const n = Number(raw);
135
+ if (!Number.isFinite(n) || n < 0) {
136
+ errors.push(`${flag} expects a non-negative number, got ${JSON.stringify(raw)}`);
137
+ return null;
138
+ }
139
+ return n;
140
+ };
141
+
142
+ for (let i = 0; i < args.length; i += 1) {
143
+ const arg = args[i];
144
+ switch (arg) {
145
+ case '--help':
146
+ case '-h':
147
+ out.help = true;
148
+ break;
149
+ case '--json':
150
+ out.json = true;
151
+ break;
152
+ case '--repo-root': {
153
+ const value = valueAt(arg, i);
154
+ if (value !== null) { out.repoRoot = value; i += 1; }
155
+ break;
156
+ }
157
+ case '--mode': {
158
+ const value = valueAt(arg, i);
159
+ if (value === null) break;
160
+ i += 1;
161
+ if (value !== 'report' && value !== 'kill') {
162
+ errors.push(`--mode expects report|kill, got ${JSON.stringify(value)}`);
163
+ break;
164
+ }
165
+ out.mode = value;
166
+ break;
167
+ }
168
+ case '--min-age-seconds':
169
+ case '--kill-grace-ms':
170
+ case '--verify-wait-ms': {
171
+ const value = valueAt(arg, i);
172
+ if (value === null) break;
173
+ i += 1;
174
+ const n = numberFrom(arg, value);
175
+ if (n === null) break;
176
+ if (arg === '--min-age-seconds') out.minAgeSeconds = n;
177
+ else if (arg === '--kill-grace-ms') out.killGraceMs = n;
178
+ else out.verifyWaitMs = n;
179
+ break;
180
+ }
181
+ case '--false-alarm-window': {
182
+ const value = valueAt(arg, i);
183
+ if (value === null) break;
184
+ i += 1;
185
+ const n = numberFrom(arg, value);
186
+ if (n === null) break;
187
+ // A window of 0 is not "no window" — `falseAlarmRate` reads it as "every
188
+ // firing ever recorded", which is the opposite of the rolling window the
189
+ // key names. The config parser clamps at 1; the CLI refuses instead, so
190
+ // a hand-run diagnosis never measures a different population than the
191
+ // hook does.
192
+ if (n < 1) {
193
+ errors.push(`${arg} expects a number >= 1, got ${JSON.stringify(value)}`);
194
+ break;
195
+ }
196
+ out.falseAlarmWindow = n;
197
+ break;
198
+ }
199
+ default:
200
+ errors.push(`unknown argument: ${arg}`);
201
+ }
202
+ }
203
+
204
+ // `kill` is the ONLY value that disarms the dry run — same direction as
205
+ // `runOrphanScan`'s own `dryRun = true` default: a caller that mistypes the
206
+ // mode scans and reports, it does not signal. (A rejected `--mode` value
207
+ // already left `mode` at `report`, so this stays `true` there too.)
208
+ out.dryRun = out.mode !== 'kill';
209
+ return out;
210
+ }
211
+
212
+ /** `--help` text. Exit codes documented here are the ones the tail returns. */
213
+ const REAPER_HELP_TEXT = `orphan-reaper.mjs — scan for orphaned read-only gate processes (#1425 Teil B)
214
+
215
+ USAGE
216
+ node scripts/lib/orphan-reaper.mjs [options]
217
+
218
+ DESCRIPTION
219
+ Runs ONE orphan scan: ps -> decide -> (with --mode kill) re-verify identity
220
+ against a fresh snapshot -> kill the process group -> audit. Candidates are a
221
+ CONJUNCTION: recorded in this repo's gate-process ledger, PPID 1, older than
222
+ --min-age-seconds, a read-only gate command, and an identity that still
223
+ verifies. Spawned detached by hooks/on-stop.mjs and
224
+ hooks/post-tool-batch-wave-signal.mjs; runnable by hand for diagnosis.
225
+
226
+ OPTIONS
227
+ --repo-root <path> Repo whose ledger, audit and marker are used
228
+ (default: cwd).
229
+ --mode report|kill report = dry run, signals nothing (default);
230
+ kill = send the SIGTERM/SIGKILL ladder.
231
+ --min-age-seconds <n> Minimum elapsed time before a process is reapable
232
+ (default: ${REAPER_DEFAULTS.minAgeSeconds}).
233
+ --kill-grace-ms <n> Grace between SIGTERM and SIGKILL
234
+ (default: ${REAPER_DEFAULTS.killGraceMs}).
235
+ --verify-wait-ms <n> Wait before re-measuring the effect
236
+ (default: ${REAPER_DEFAULTS.verifyWaitMs}).
237
+ --false-alarm-window <n> Rolling window of audit DECISIONS the HR-101
238
+ false-alarm rate is judged over; >= 1
239
+ (default: ${REAPER_DEFAULTS.falseAlarmWindow}).
240
+ --json Emit the full scan result as one JSON object.
241
+ --help, -h Show this help and exit.
242
+
243
+ EXIT CODES
244
+ 0 the scan ran (0 candidates is a normal, successful scan)
245
+ 2 usage error, or a degraded scan that measured nothing (\`skipped\`)
246
+ `;
247
+
248
+ /**
249
+ * CLI body: one scan, one line (or one JSON object) of output.
250
+ *
251
+ * A degraded scan — `skipped: 'ps-failed' | 'ledger-unreadable' | …` — exits 2,
252
+ * not 0: it measured NOTHING, and an exit 0 there is exactly the "missing
253
+ * measurement looks like a zero" shape this module refuses everywhere else.
254
+ * The detached hook child ignores the code; a human or a CI caller does not.
255
+ *
256
+ * @param {string[]} argv `process.argv.slice(2)`
257
+ * @returns {Promise<number>} process exit code
258
+ */
259
+ async function mainCli(argv) {
260
+ const cli = parseReaperCliArgs(argv);
261
+
262
+ if (cli.help) {
263
+ process.stdout.write(REAPER_HELP_TEXT);
264
+ return 0;
265
+ }
266
+ if (cli.errors.length > 0) {
267
+ for (const message of cli.errors) process.stderr.write(`orphan-reaper: ${message}\n`);
268
+ process.stderr.write('Run with --help for usage.\n');
269
+ return 2;
270
+ }
271
+
272
+ const repoRoot = cli.repoRoot ?? process.cwd();
273
+ let result;
274
+ try {
275
+ result = await runOrphanScan({
276
+ repoRoot,
277
+ dryRun: cli.dryRun,
278
+ minAgeSeconds: cli.minAgeSeconds,
279
+ killGraceMs: cli.killGraceMs,
280
+ verifyWaitMs: cli.verifyWaitMs,
281
+ falseAlarmWindow: cli.falseAlarmWindow,
282
+ });
283
+ } catch (err) {
284
+ // runOrphanScan carries a NO-THROW contract; this catch exists so a broken
285
+ // contract surfaces as a tool error instead of an unhandled rejection.
286
+ process.stderr.write(`orphan-reaper: scan failed — ${err?.message ?? String(err)}\n`);
287
+ return 2;
288
+ }
289
+
290
+ if (cli.json) {
291
+ process.stdout.write(`${JSON.stringify({ repoRoot, mode: cli.mode, ...result })}\n`);
292
+ } else {
293
+ process.stdout.write(
294
+ `orphan-reaper: scanned=${result.scanned} candidates=${result.candidates.length} `
295
+ + `reported=${result.reported.length} unattributed=${result.unattributed} `
296
+ + `peer_liveness=${result.peerLiveness ?? 'none'} killed=${result.killed.length} `
297
+ + `malformed=${result.malformed} mode=${cli.mode}`
298
+ + `${result.skipped ? ` skipped=${result.skipped}` : ''}\n`,
299
+ );
300
+ }
301
+ return result.skipped ? 2 : 0;
302
+ }
303
+
304
+ // Entry guard: a bare `import()` of this module must do NOTHING — tests and
305
+ // any caller of the re-exports above import it. Written
306
+ // in the one symlink-safe form `scripts/lib/validate/check-entry-guard.mjs`
307
+ // and `check-hook-entry-guards.mjs` accept.
308
+ //
309
+ // `process.exitCode` rather than `process.exit()`: `--json` carries the whole
310
+ // result, whose `rejected` array materialises every PPID-1 row not in the
311
+ // ledger — measured 2026-09-22 on this host, 40.224 bytes for 834 processes,
312
+ // the same order as the 64 KiB pipe buffer above which `process.exit()`
313
+ // DISCARDS the pending write and turns a full result into a truncated one.
314
+ if (isMainModule(import.meta.url)) {
315
+ process.exitCode = await mainCli(process.argv.slice(2));
316
+ }
@@ -36,6 +36,16 @@
36
36
  * dispatcher: (optional; host-local cross-repo dispatcher autonomy override — #679)
37
37
  * autonomy: string ('' = no override; resolver enum off | advisory | autonomous-gated;
38
38
  * precedence env SO_DISPATCHER_AUTONOMY > this > committed > off)
39
+ * vault-integration: (optional; host-local switch for the vault-integration gate — SO#1448)
40
+ * enabled: boolean (false = off on this host; true is ignored — the host may not enable)
41
+ * mode: 'off' | 'warn' | 'strict' (applied only when weaker than the committed mode;
42
+ * precedence env SO_VAULT_INTEGRATION > this > committed.
43
+ * An invalid value drops ONLY this section, see loadOwnerConfig)
44
+ * vault-dirs: (optional list; per-directory vault-dir — agents/vault#319)
45
+ * - path: string (the vault directory)
46
+ * match: { path-prefix: string } (required; cwd directory prefix, first match wins)
47
+ * name: string (optional label, defaults to path)
48
+ * precedence env SO_VAULT_DIR > this > paths.vault-dir > committed
39
49
  *
40
50
  * ── Exports ───────────────────────────────────────────────────────────────────
41
51
  *
@@ -44,7 +54,8 @@
44
54
  * validateOwnerSections(obj) — pure validation, no I/O; bucketed per section (#820)
45
55
  * validateOwnerConfig(obj) — pure validation, no I/O; thin wrapper over validateOwnerSections
46
56
  * loadOwnerConfig({path?}) — reads file; per-section tolerance for OPTIONAL
47
- * sections (paths/dispatcher/vaults/baselines) — #820
57
+ * sections (paths/dispatcher/vault-integration/vaults/
58
+ * baselines/vault-dirs) — #820
48
59
  * writeOwnerConfig(config, {path?}) — validates, writes YAML, creates dir
49
60
  * getDefaults() — returns sensible default config object
50
61
  *
@@ -202,9 +213,12 @@ const REQUIRED_SECTIONS = /** @type {const} */ (['owner', 'tone', 'efficiency',
202
213
  * sections — currently `scripts/lib/owner-config-banner.mjs`, which derived the
203
214
  * #1244 "survived a whole-file discard" set from a hand-mirrored local copy.
204
215
  * Frozen so a consumer cannot mutate the shared array.
216
+ *
217
+ * `vault-integration` has no `getDefaults()` entry: absent means "no host
218
+ * override", so a dropped section simply leaves the committed gate in force.
205
219
  */
206
220
  export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
207
- Object.freeze(['paths', 'dispatcher'])
221
+ Object.freeze(['paths', 'dispatcher', 'vault-integration'])
208
222
  );
209
223
 
210
224
  /**
@@ -213,7 +227,10 @@ export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
213
227
  * `parseNamedVaults`/`parseBaselines` — that already drops bad entries with
214
228
  * its own WARN). Surfaced here only via `sectionWarnings`, never dropped.
215
229
  */
216
- const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines']);
230
+ const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines', 'vault-dirs']);
231
+
232
+ /** Host-settable vault-integration modes (mirrors VAULT_MODE_VALUES in config-schema.mjs). */
233
+ const VALID_VAULT_INTEGRATION_MODES = ['off', 'warn', 'strict'];
217
234
 
218
235
  /** Canonical validation-order — MUST match the pre-#820 inline validation order. */
219
236
  const SECTION_ORDER = /** @type {const} */ ([
@@ -421,6 +438,35 @@ export function validateOwnerSections(obj) {
421
438
  sections.dispatcher = { valid: errors.length === 0, errors };
422
439
  }
423
440
 
441
+ // ── vault-integration (optional; host-local gate switch — SO#1448) ──────────
442
+ // STRICT enum validation, unlike `dispatcher`: a typo like `mode: of` must not
443
+ // fall through to the committed `strict` silently while the operator believes
444
+ // the host is off. An invalid section is dropped (and reported) on its own —
445
+ // `paths` and every other section survive (#820 per-section tolerance).
446
+ {
447
+ const errors = [];
448
+ const vi = obj['vault-integration'];
449
+ if (vi !== undefined && vi !== null) {
450
+ if (!isPlainObject(vi)) {
451
+ errors.push('vault-integration must be an object when present');
452
+ } else {
453
+ if (vi.enabled !== undefined && vi.enabled !== null && typeof vi.enabled !== 'boolean') {
454
+ errors.push(`vault-integration.enabled must be a boolean, got: ${typeof vi.enabled}`);
455
+ }
456
+ if (
457
+ vi.mode !== undefined &&
458
+ vi.mode !== null &&
459
+ !VALID_VAULT_INTEGRATION_MODES.includes(vi.mode)
460
+ ) {
461
+ errors.push(
462
+ `vault-integration.mode must be one of ${VALID_VAULT_INTEGRATION_MODES.join(', ')}, got: ${JSON.stringify(vi.mode)}`,
463
+ );
464
+ }
465
+ }
466
+ }
467
+ sections['vault-integration'] = { valid: errors.length === 0, errors };
468
+ }
469
+
424
470
  // ── vaults (optional; N named vaults for walk-up resolution — #700) ──────────
425
471
  // Drop-and-WARN on malformed entries is handled by parseNamedVaults() in
426
472
  // named-vault-resolver.mjs. Here we only validate the container shape to
@@ -512,6 +558,47 @@ export function validateOwnerSections(obj) {
512
558
  sections.baselines = { valid: errors.length === 0, errors };
513
559
  }
514
560
 
561
+ // ── vault-dirs (optional; per-directory vault-dir — agents/vault#319) ────────
562
+ // Same entry shape as baselines: (and parsed by the same parseBaselines() with
563
+ // section 'vault-dirs'), except `name` is optional. List section: surfaced via
564
+ // sectionWarnings, never dropped — the resolver drops bad entries itself.
565
+ {
566
+ const errors = [];
567
+ const vaultDirs = obj['vault-dirs'];
568
+ if (vaultDirs !== undefined && vaultDirs !== null) {
569
+ if (!Array.isArray(vaultDirs)) {
570
+ errors.push('vault-dirs must be an array when present');
571
+ } else {
572
+ for (let i = 0; i < vaultDirs.length; i++) {
573
+ const entry = vaultDirs[i];
574
+ if (!isPlainObject(entry)) {
575
+ errors.push(`vault-dirs[${i}] must be an object`);
576
+ continue;
577
+ }
578
+ if (typeof entry.path !== 'string' || entry.path.trim() === '') {
579
+ errors.push(`vault-dirs[${i}].path must be a non-empty string`);
580
+ }
581
+ if (
582
+ entry.name !== undefined &&
583
+ entry.name !== null &&
584
+ (typeof entry.name !== 'string' || entry.name.trim() === '')
585
+ ) {
586
+ errors.push(`vault-dirs[${i}].name must be a non-empty string when present`);
587
+ }
588
+ if (!isPlainObject(entry.match)) {
589
+ errors.push(`vault-dirs[${i}].match must be an object`);
590
+ } else if (
591
+ typeof entry.match['path-prefix'] !== 'string' ||
592
+ entry.match['path-prefix'].trim() === ''
593
+ ) {
594
+ errors.push(`vault-dirs[${i}].match.path-prefix must be a non-empty string`);
595
+ }
596
+ }
597
+ }
598
+ }
599
+ sections['vault-dirs'] = { valid: errors.length === 0, errors };
600
+ }
601
+
515
602
  const errors = SECTION_ORDER.flatMap((name) => sections[name].errors);
516
603
  return { sections, errors };
517
604
  }
@@ -540,10 +627,10 @@ export function validateOwnerConfig(obj) {
540
627
  * Per-section tolerance (#820): an invalid REQUIRED section (owner, tone,
541
628
  * efficiency, hardware-sharing) still discards the whole file (legacy
542
629
  * behaviour, unchanged). An invalid OPTIONAL object section (paths,
543
- * dispatcher) is instead replaced by its default value — the rest of the
630
+ * dispatcher, vault-integration) is instead replaced by its default value — the rest of the
544
631
  * file survives, `source` becomes `'partial'`, and the drop is reported via
545
632
  * `droppedSections` + a stderr WARN. OPTIONAL list sections (vaults,
546
- * baselines) are passed through UNTOUCHED even when strict-invalid — their
633
+ * baselines, vault-dirs) are passed through UNTOUCHED even when strict-invalid — their
547
634
  * consumers already run a lenient parse-at-point-of-use pass — and are
548
635
  * surfaced only via `sectionWarnings` (never dropped, never counted towards
549
636
  * `'partial'`).
@@ -658,6 +745,14 @@ export function loadOwnerConfig(opts = {}) {
658
745
  discardDropped.push({ section: name, errors: sec.errors });
659
746
  }
660
747
  }
748
+ // `vault-dirs:` (agents/vault#319) survives the discard too, raw — exactly as
749
+ // on the required-valid branch below; parseBaselines() drops bad entries at
750
+ // point-of-use. Dropping it would hand a repo under a declared prefix the
751
+ // host-wide `paths.vault-dir` (which DOES survive above): the silent vault
752
+ // override #319 exists to prevent, triggered by an unrelated `tone:` typo.
753
+ if (parsed['vault-dirs'] !== undefined && parsed['vault-dirs'] !== null) {
754
+ discardConfig['vault-dirs'] = parsed['vault-dirs'];
755
+ }
661
756
  const discardResult = { config: discardConfig, source: 'defaults', errors: allErrors };
662
757
  if (discardDropped.length > 0) discardResult.droppedSections = discardDropped;
663
758
  return discardResult;
@@ -673,7 +768,10 @@ export function loadOwnerConfig(opts = {}) {
673
768
  for (const name of OPTIONAL_OBJECT_SECTIONS) {
674
769
  const sec = sections[name];
675
770
  if (sec && !sec.valid) {
676
- config[name] = defaults[name];
771
+ // A section without a default (vault-integration) is removed, not set to
772
+ // `undefined`: js-yaml's dump rejects undefined values on a write-back.
773
+ if (defaults[name] === undefined) delete config[name];
774
+ else config[name] = defaults[name];
677
775
  droppedSections.push({ section: name, errors: sec.errors });
678
776
  const firstError = sec.errors[0] ?? 'invalid section';
679
777
  console.warn(
@@ -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
  }