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
@@ -11,6 +11,10 @@
11
11
  * records that between them said nothing happened (#1151). Its count is
12
12
  * carried by the run event's `skipped` total and `action_breakdown`, so the
13
13
  * class stays measured — only its per-line locators are gone.
14
+ * `skipped-duplicate-session` (#1291) is one of these per-entry records:
15
+ * a `--kind session` line whose `session_id` a later line in the same
16
+ * source superseded, so it was never dispatched. It carries `record_id`
17
+ * and `line` and never `path` — no target file exists for it.
14
18
  * - `orchestrator.vault.mirror_run_completed` — ONE record per CLI run,
15
19
  * carrying the DENOMINATOR (`total` plus the per-class counts).
16
20
  *
@@ -162,7 +166,10 @@ export async function emitMirrorEvent({
162
166
  * @param {number} opts.updated — entries whose action was `updated`.
163
167
  * @param {number} opts.skipped — entries skipped for a NON-failure reason
164
168
  * (`skipped-noop`, `skipped-handwritten`, `skipped-quality-low`,
165
- * `skipped-collision-resolved`, `skipped-abandoned`).
169
+ * `skipped-collision-resolved`, `skipped-abandoned`,
170
+ * `skipped-duplicate-session`). The last one (#1291) is what keeps
171
+ * `created + updated + skipped + failed === total` true for a `--kind
172
+ * session` run whose dedup pass collapsed a duplicate `session_id` line.
166
173
  * @param {number} opts.failed — entries that produced `skipped-invalid`
167
174
  * (validation error or mapper crash). Split out from `skipped` because these
168
175
  * are the runs where a session silently ends up WITHOUT its vault note.
@@ -12,8 +12,9 @@
12
12
  *
13
13
  * Telemetry: every {@link mirrorBoard} call — and therefore every
14
14
  * {@link sweepBoard} call — emits exactly ONE {@link BOARD_EVENT} record,
15
- * including the no-op paths (`skipped-vault-disabled`, `skipped-handwritten`,
16
- * `skipped-noop`, `skipped-write-failed`). Those are the states that previously
15
+ * including the no-op paths (`skipped-vault-disabled`,
16
+ * `skipped-vault-not-canonical`, `skipped-handwritten`, `skipped-noop`,
17
+ * `skipped-write-failed`). Those are the states that previously
17
18
  * looked identical to a healthy write from outside the process. Emission is
18
19
  * best-effort and can never fail a board write.
19
20
  *
@@ -43,7 +44,8 @@
43
44
  * 2. Defense-in-depth: writeBoard hard-refuses to touch `_overview.md`
44
45
  * (sven-owned, must NEVER be written by this generator).
45
46
  *
46
- * No console noise — library code. Plain Node ESM. No external deps.
47
+ * No console noise — library code — except the one stderr line on a
48
+ * canonical-vault refusal (#1450). Plain Node ESM. No external deps.
47
49
  */
48
50
 
49
51
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
@@ -60,6 +62,8 @@ import { enumerateCandidates } from '../dispatcher/enumerate.mjs';
60
62
  import { atomicWriteWithBackup } from '../io.mjs';
61
63
  import { withBoardLock } from './board-lock.mjs';
62
64
  import { expandTilde } from '../common.mjs';
65
+ import { checkCanonicalVault, describeOriginForLog } from '../named-vault-resolver.mjs';
66
+ import { loadHostPaths } from '../config/host-paths.mjs';
63
67
 
64
68
  /** Frontmatter sentinel that identifies generator-owned board files. */
65
69
  export const GENERATOR_MARKER = 'session-orchestrator-active-sessions@1';
@@ -825,6 +829,8 @@ async function emitBoardEvent({ repoRoot, caller, action, path: outputPath, rows
825
829
  * Vault path assertion (Epic #673 safety): the resolved vault dir MUST live
826
830
  * under $HOME — a vault outside the home tree is refused as `skipped-vault-disabled`
827
831
  * so a misconfigured path can never drive a write into an arbitrary location.
832
+ * It must also be a canonical Meta-Vault ({@link checkCanonicalVault}, #1450):
833
+ * otherwise `skipped-vault-not-canonical`, nothing written, no lock, dry-run included.
828
834
  *
829
835
  * @param {object} opts
830
836
  * @param {string} opts.repoRoot — the repo whose row is being updated.
@@ -840,11 +846,14 @@ async function emitBoardEvent({ repoRoot, caller, action, path: outputPath, rows
840
846
  * @param {Date} [opts.now]
841
847
  * @param {boolean} [opts.dryRun]
842
848
  * @param {object} [opts.fs] — injectable fs for tests.
843
- * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
849
+ * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object,
850
+ * readOriginUrl?: (vaultDir: string) => string }} [opts.hostPaths]
844
851
  * — forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653).
852
+ * The same ctx feeds {@link checkCanonicalVault}; `readOriginUrl` replaces its
853
+ * `git remote get-url origin` read (tests only — tmp vaults have no origin).
845
854
  * Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig: undefined }`) when
846
855
  * asserting a fixture's committed `vault-dir` — omitting it reads the REAL host
847
- * `owner.yaml`, whose `paths.vault-dir` override (if set) wins over the fixture value
856
+ * `owner.yaml`, whose `vault-dirs:` cwd match or `paths.vault-dir` override (if set) wins over the fixture value
848
857
  * and bleeds into the assertion (issue #783). Production callers omit this — the
849
858
  * default (real owner.yaml resolution) is the correct host-local behavior there.
850
859
  * @returns {Promise<{ result: { action: string, path?: string }, rows?: number,
@@ -861,10 +870,19 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
861
870
  }
862
871
 
863
872
  // Read + parse Session Config. Any failure → silent no-op.
873
+ // The host context is resolved ONCE here and reused by the canonical-vault
874
+ // guard below: loadOwnerConfig() has no cache and its section-drop WARNs are
875
+ // not rate-limited, so a second load would print every WARN of a broken
876
+ // owner.yaml twice per board write (#1450 follow-up). The config file is read
877
+ // BEFORE owner.yaml — the order narrative-mirror.mjs shares (#1479); vault-dir
878
+ // precedence is applied in parseSessionConfig, so the order changes only
879
+ // whether a repo without a config file reads owner.yaml at all.
864
880
  let config;
881
+ let hostCtx;
865
882
  try {
866
883
  const text = await readConfigFile(repoRoot);
867
- config = parseSessionConfig(text, { hostPaths });
884
+ hostCtx = hostPaths ?? loadHostPaths();
885
+ config = parseSessionConfig(text, { hostPaths: hostCtx });
868
886
  } catch {
869
887
  return { result: { action: 'skipped-vault-disabled' } };
870
888
  }
@@ -886,6 +904,24 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
886
904
  return { result: { action: 'skipped-vault-disabled' } };
887
905
  }
888
906
 
907
+ // Safety: the vault must be a canonical Meta-Vault (#1450) — the same guard
908
+ // vault-mirror applies, so a vault-dir that resolved to a FOREIGN vault can
909
+ // never receive the board. Reuses the `hostCtx` the config parse used, so a
910
+ // hermetic ctx never reads the real owner.yaml and owner.yaml is loaded at
911
+ // most once. Runs before the dry-run branch and the lock: fail-closed.
912
+ const canonical = checkCanonicalVault({
913
+ vaultDir: expandedVault,
914
+ ownerConfig: hostCtx.ownerConfig,
915
+ env: hostCtx.env ?? process.env,
916
+ readOriginUrl: hostCtx.readOriginUrl,
917
+ });
918
+ if (!canonical.ok) {
919
+ process.stderr.write(
920
+ `vault-status board: refusing to write — vault is not the canonical Meta-Vault (expected git origin ending in one of: ${canonical.expected.join(', ')}; got ${describeOriginForLog(canonical.got) || 'no git origin'})\n`,
921
+ );
922
+ return { result: { action: 'skipped-vault-not-canonical' } };
923
+ }
924
+
889
925
  // `vault-name` (#660) overrides the git-derived repo slug for per-project
890
926
  // vault namespacing. Without it `collectRows` falls back to
891
927
  // `path.basename(repoRoot)`, so a repo whose directory name differs from its
@@ -1112,10 +1148,10 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
1112
1148
  * Public {@link mirrorBoardInner} wrapper that emits exactly ONE
1113
1149
  * {@link BOARD_EVENT} per call — on EVERY path, no-ops included.
1114
1150
  *
1115
- * The wrapper exists so the emit cannot be forgotten: the six return points
1116
- * inside {@link mirrorBoardInner} (five `skipped-vault-disabled` guards plus
1117
- * whatever {@link writeBoard} decides) all funnel through here, and so does the
1118
- * seventh someone adds next. Emitting per-return instead would leave each new
1151
+ * The wrapper exists so the emit cannot be forgotten: the seven return points
1152
+ * inside {@link mirrorBoardInner} (five `skipped-vault-disabled` guards, the
1153
+ * `skipped-vault-not-canonical` guard, plus whatever {@link writeBoard}
1154
+ * decides) all funnel through here, and so does the eighth someone adds next. Emitting per-return instead would leave each new
1119
1155
  * early return silent by default — which is the exact defect being fixed.
1120
1156
  *
1121
1157
  * A THROW from the inner function is deliberately NOT converted into an event:
@@ -1276,7 +1312,8 @@ export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
1276
1312
  * @param {object} [opts.deps] — injectable deps for {@link enumerateCandidates}
1277
1313
  * (test seam: `readdirSync`, `existsSync`, `readLock`, `isLockLive`,
1278
1314
  * `getCrossRepoProjects`, `validatePathInsideProject`, `now`).
1279
- * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
1315
+ * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object,
1316
+ * readOriginUrl?: (vaultDir: string) => string }} [opts.hostPaths]
1280
1317
  * — forwarded verbatim to every {@link mirrorBoard} call below (both the happy-path
1281
1318
  * and the enumeration-failure-fallback path), which in turn forwards it to
1282
1319
  * {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653/#783). See
@@ -43,6 +43,9 @@ import { readConfigFile, parseSessionConfig } from '../config.mjs';
43
43
  import { validatePathInsideProject } from '../path-utils.mjs';
44
44
  import { createSecretValueMasker } from '../secret-masker.mjs';
45
45
  import { expandTilde } from '../common.mjs';
46
+ import { readVaultSlug } from '../vault-yaml.mjs';
47
+ import { checkCanonicalVault, describeOriginForLog } from '../named-vault-resolver.mjs';
48
+ import { loadHostPaths } from '../config/host-paths.mjs';
46
49
 
47
50
  /** Frontmatter sentinel that identifies generator-owned narrative files. */
48
51
  export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
@@ -762,9 +765,10 @@ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
762
765
  * before any write.
763
766
  *
764
767
  * THIS FUNCTION WRITES TO THE OPERATOR'S REAL VAULT UNLESS YOU STOP IT. `vault-dir`
765
- * resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `paths.vault-dir` >
766
- * committed Session Config (`scripts/lib/config/host-paths.mjs`) — so the literal
767
- * value in a CLAUDE.md you just wrote into a tmp dir LOSES to both overrides. A
768
+ * resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `vault-dirs:` cwd match >
769
+ * `owner.yaml` `paths.vault-dir` > committed Session Config (`resolveVaultDir` in
770
+ * `scripts/lib/config.mjs`) — so the literal value in a CLAUDE.md you just wrote
771
+ * into a tmp dir LOSES to every override. A
768
772
  * probe that passes a synthetic `repoRoot` and nothing else has already written
769
773
  * into the live vault once (#1025 review). Any caller that must not touch the real
770
774
  * vault passes `hostPaths: { env: {}, ownerConfig: undefined }` (see below),
@@ -776,13 +780,16 @@ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
776
780
  * now?: Date,
777
781
  * dryRun?: boolean,
778
782
  * fs?: object,
779
- * hostPaths?: { env?: Record<string, string|undefined>, ownerConfig?: object },
783
+ * hostPaths?: { env?: Record<string, string|undefined>, ownerConfig?: object,
784
+ * readOriginUrl?: (vaultDir: string) => string },
780
785
  * }} opts
781
786
  * `hostPaths` is forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI
782
- * seam (issue #653). Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig:
787
+ * seam (issue #653). The same ctx feeds {@link checkCanonicalVault} (#1450);
788
+ * `readOriginUrl` replaces its `git remote get-url origin` read (tests only —
789
+ * tmp vaults have no origin). Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig:
783
790
  * undefined }`) when asserting a fixture's committed `vault-dir` — omitting it reads
784
- * the REAL host `owner.yaml`, whose `paths.vault-dir` override (if set) wins over the
785
- * fixture value and bleeds into the assertion (issue #783). Production callers omit
791
+ * the REAL host `owner.yaml`, whose `vault-dirs:` cwd match or `paths.vault-dir`
792
+ * override (if set) wins over the fixture value and bleeds into the assertion (issue #783). Production callers omit
786
793
  * this — the default (real owner.yaml resolution) is the correct host-local behavior.
787
794
  * @returns {Promise<{ action: string, path?: string }>}
788
795
  */
@@ -828,7 +835,7 @@ export async function mirrorNarrative(opts) {
828
835
  * Split out so that the two emit sites in `mirrorNarrative` (the narrative
829
836
  * event AND, since #1028, the masker event) each cover EVERY outcome from
830
837
  * ONE call: a future early return added inside this function is telemetered
831
- * by construction, whereas hand-placing an emit beside each of the seven
838
+ * by construction, whereas hand-placing an emit beside each of the eight
832
839
  * `return`s makes "forgot the new one" the default failure. The `chars` companion travels beside the result
833
840
  * rather than inside it because the returned object is a PUBLIC shape that
834
841
  * callers (and tests) compare with `toEqual` — adding a key there would be an
@@ -854,10 +861,19 @@ async function runNarrativeMirror(opts) {
854
861
  const masker = createSecretValueMasker(process.env);
855
862
 
856
863
  // Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
864
+ // The host context is loaded ONCE and shared by the Session Config parse and
865
+ // the canonical-vault guard below, so owner.yaml is read a single time per run.
866
+ // Order (#1479, same as board-writer.mjs): config file first, owner.yaml
867
+ // second. The `vault-dir` precedence (SO_VAULT_DIR > owner.yaml > committed)
868
+ // is applied inside parseSessionConfig, after both are in hand, so the order
869
+ // cannot change the resolved dir; it only spares a repo without a config file
870
+ // the owner.yaml read and its section-drop WARNs on a run that skips anyway.
857
871
  let config;
872
+ let hostCtx;
858
873
  try {
859
874
  const configText = await readConfigFile(repoRoot);
860
- config = parseSessionConfig(configText, { hostPaths });
875
+ hostCtx = hostPaths ?? loadHostPaths();
876
+ config = parseSessionConfig(configText, { hostPaths: hostCtx });
861
877
  } catch {
862
878
  return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
863
879
  }
@@ -869,15 +885,29 @@ async function runNarrativeMirror(opts) {
869
885
 
870
886
  // Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
871
887
  // it from the operator-configured `vault-name` override (#660/#832) when set,
872
- // else the repoRoot basename — never silently mis-file under 'unknown' (#675
873
- // review). Precedence: explicit `repo` opt > `vault-name` > basename.
888
+ // else the `.vault.yaml` slug (#1131), else the repoRoot basename — never
889
+ // silently mis-file under 'unknown' (#675 review).
874
890
  const vaultNameOverride =
875
891
  typeof vaultIntegration['vault-name'] === 'string' && vaultIntegration['vault-name'].trim()
876
892
  ? vaultIntegration['vault-name'].trim()
877
893
  : null;
878
- const repoName = (typeof repo === 'string' && repo.trim().length > 0)
894
+ const explicitName = (typeof repo === 'string' && repo.trim().length > 0)
879
895
  ? repo
880
- : vaultNameOverride ?? path.basename(path.resolve(repoRoot));
896
+ : vaultNameOverride;
897
+
898
+ // #1131: with NO explicit override, the repo's own `.vault.yaml`
899
+ // `metadata.slug` — the CANONICAL vault registration — outranks the
900
+ // directory basename. Precedence: explicit `repo` opt > `vault-name` >
901
+ // `.vault.yaml` slug > basename.
902
+ //
903
+ // WHY the two overrides still win: both are things a CALLER/operator states
904
+ // for THIS call (`repo` is only ever passed to override the derived name —
905
+ // the sole production caller, `skills/session-end/session-metrics-write.md`,
906
+ // passes `repoRoot` alone), so honouring the file over them would make an
907
+ // explicit override unhonourable. The file wins over the basename, which is
908
+ // a guess nobody stated.
909
+ const vaultYamlSlug = explicitName ? null : readVaultSlug(repoRoot);
910
+ const repoName = explicitName ?? vaultYamlSlug ?? path.basename(path.resolve(repoRoot));
881
911
 
882
912
  const rawVaultDir = vaultIntegration['vault-dir'];
883
913
  if (!rawVaultDir || typeof rawVaultDir !== 'string') {
@@ -885,11 +915,36 @@ async function runNarrativeMirror(opts) {
885
915
  }
886
916
 
887
917
  const vaultDir = path.resolve(expandTilde(rawVaultDir));
918
+
919
+ // #1450: same shared guard as board-writer.mjs and scripts/vault-mirror.mjs —
920
+ // refuse a vault-dir whose git origin is not a canonical Meta-Vault. The
921
+ // stderr line names the expected suffixes and the origin, never the vault
922
+ // path itself.
923
+ const canonical = checkCanonicalVault({
924
+ vaultDir,
925
+ ownerConfig: hostCtx.ownerConfig,
926
+ env: hostCtx.env ?? process.env,
927
+ readOriginUrl: hostCtx.readOriginUrl,
928
+ });
929
+ if (!canonical.ok) {
930
+ process.stderr.write(`narrative-mirror: refusing to write — vault is not the canonical Meta-Vault (expected git origin ending in one of: ${canonical.expected.join(', ')}; got ${describeOriginForLog(canonical.got) || 'no git origin'})\n`);
931
+ return { result: { action: 'skipped-vault-not-canonical' }, needleCount: masker.needleCount, hits: 0, dryRun };
932
+ }
933
+
888
934
  const candidateSlug = subjectToSlug(repoName) || 'unknown';
889
- // Loose-match against existing 01-projects/ folders before minting a new
890
- // slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
935
+ // A declared `.vault.yaml` slug needs no healing — it IS the canonical folder
936
+ // name, so resolveLooseSlug is SKIPPED for it. Running the healer over it
937
+ // would re-open the bug this fixes: a vault that also carries an unhealed
938
+ // legacy folder (e.g. `foobarapp` beside the declared `foo-bar-app`) makes
939
+ // the loose match ambiguous, and ambiguity falls back to the candidate —
940
+ // which is how a duplicate pair, once created, keeps being written to.
941
+ //
942
+ // Otherwise: loose-match against existing 01-projects/ folders before minting
943
+ // a new slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
891
944
  // rules. Falls through to `candidateSlug` unchanged on any read failure.
892
- const repoSlug = resolveLooseSlug(vaultDir, candidateSlug, { readdirSync: injectedFs?.readdirSync });
945
+ const repoSlug = vaultYamlSlug
946
+ ? candidateSlug
947
+ : resolveLooseSlug(vaultDir, candidateSlug, { readdirSync: injectedFs?.readdirSync });
893
948
  const outputPath = resolveNarrativePath(vaultDir, repoSlug);
894
949
 
895
950
  // Defense-in-depth: ensure the resolved file stays inside the vault root.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * vault-yaml.mjs — Reader for a repo's `.vault.yaml` registration slug (#1131).
3
+ *
4
+ * A repo registered in the Meta-Vault carries `.vault.yaml` at its root, whose
5
+ * `metadata.slug` is the CANONICAL vault slug for that project — the name of
6
+ * its `01-projects/<slug>/` folder. Until #1131 nothing READ that key: every
7
+ * vault writer derived the slug from the checkout DIRECTORY name instead
8
+ * (`scripts/lib/vault-status/narrative-mirror.mjs` via `subjectToSlug`,
9
+ * `scripts/lib/vault-mirror/namespace.mjs` via `basename(process.cwd())`), so a
10
+ * repo whose directory name differs lexically from its registered slug had its
11
+ * narrative and its learnings/sessions filed under a second, wrong folder — and
12
+ * the loose-slug healer in narrative-mirror (#829) only repairs case and
13
+ * punctuation drift, never a genuinely different name.
14
+ *
15
+ * Exports:
16
+ * VAULT_YAML_FILE — the basename this module reads
17
+ * readVaultSlug — repoRoot → validated `metadata.slug` string, or null
18
+ *
19
+ * Contract: NEVER throws. Absent file, unreadable file, malformed YAML, missing
20
+ * key, non-string value, or a value that fails the repo's slug predicate all
21
+ * return `null`, leaving every caller on its pre-#1131 derivation chain.
22
+ *
23
+ * Import closure is deliberately tiny (`node:fs`, `node:path`, `node:module`,
24
+ * `./vault-mirror/utils.mjs`) — `namespace.mjs` is documented as a leaf-ward
25
+ * module and must stay one.
26
+ */
27
+
28
+ import { readFileSync } from 'node:fs';
29
+ import path from 'node:path';
30
+ import { createRequire } from 'node:module';
31
+
32
+ import { isValidSlug } from './vault-mirror/utils.mjs';
33
+
34
+ /** Basename of the per-repo vault registration file. */
35
+ export const VAULT_YAML_FILE = '.vault.yaml';
36
+
37
+ // ── Lazy js-yaml resolution ──────────────────────────────────────────────────
38
+ //
39
+ // Same shape (and same reason) as `getYaml()` in scripts/lib/owner-yaml.mjs:
40
+ // a static `import yaml from 'js-yaml'` crashes any consumer running without
41
+ // `node_modules`, at MODULE LOAD, where the failure is not catchable. Both
42
+ // consumers of this reader are synchronous, so `createRequire` — not
43
+ // `await import()` — is the resolution mechanism.
44
+
45
+ /** @type {null | false | { load: Function }} */
46
+ let _yaml = null;
47
+ let _yamlWarned = false;
48
+
49
+ function getYaml() {
50
+ if (_yaml !== null) return _yaml === false ? null : _yaml;
51
+ try {
52
+ _yaml = createRequire(import.meta.url)('js-yaml');
53
+ } catch {
54
+ _yaml = false;
55
+ }
56
+ return _yaml === false ? null : _yaml;
57
+ }
58
+
59
+ /**
60
+ * One-per-process actionable WARN, emitted ONLY when a `.vault.yaml` actually
61
+ * exists and could therefore have changed the resolved slug. A repo without the
62
+ * file loses nothing by the parser being absent, so it stays silent.
63
+ */
64
+ function warnYamlMissing() {
65
+ if (_yamlWarned) return;
66
+ _yamlWarned = true;
67
+ process.stderr.write(
68
+ "WARN vault-yaml: 'js-yaml' is not installed — .vault.yaml cannot be parsed; " +
69
+ 'the vault slug falls back to the directory-derived name (run \'npm install\' in the plugin directory)\n',
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Read the canonical vault slug declared at `metadata.slug` in
75
+ * `<repoRoot>/.vault.yaml`.
76
+ *
77
+ * The value becomes a FILESYSTEM PATH SEGMENT under the operator's vault, so it
78
+ * is validated with the repo's existing slug predicate ({@link isValidSlug},
79
+ * `/^[a-z0-9]+(?:-[a-z0-9]+)*$/`) rather than trusted. That rejects `/`, `..`,
80
+ * leading dots, whitespace and uppercase — this validation is a trust-boundary
81
+ * control, not hygiene: `.vault.yaml` is repo-supplied data, and a slug such as
82
+ * `../../etc` would otherwise escape the vault root.
83
+ *
84
+ * @param {string} repoRoot - absolute or relative path to the repository root.
85
+ * @returns {string|null} the validated slug, or `null` when it cannot be read.
86
+ */
87
+ export function readVaultSlug(repoRoot) {
88
+ if (typeof repoRoot !== 'string' || repoRoot.trim() === '') return null;
89
+
90
+ let text;
91
+ try {
92
+ text = readFileSync(path.join(repoRoot, VAULT_YAML_FILE), 'utf8');
93
+ } catch {
94
+ // Absent, unreadable, a directory, … — all "no declared slug".
95
+ return null;
96
+ }
97
+
98
+ const yaml = getYaml();
99
+ if (!yaml) {
100
+ warnYamlMissing();
101
+ return null;
102
+ }
103
+
104
+ let doc;
105
+ try {
106
+ doc = yaml.load(text);
107
+ } catch {
108
+ // Malformed YAML must never break a vault write.
109
+ return null;
110
+ }
111
+
112
+ if (!doc || typeof doc !== 'object') return null;
113
+ const metadata = doc.metadata;
114
+ if (!metadata || typeof metadata !== 'object') return null;
115
+
116
+ const slug = metadata.slug;
117
+ return isValidSlug(slug) ? slug : null;
118
+ }
@@ -21,6 +21,14 @@
21
21
  * exists to be.
22
22
  */
23
23
 
24
+ /**
25
+ * Re-exported, not re-declared: the value lives in `process-group.mjs` since
26
+ * 773a994b, but this module exported it in 5.3.x and the package ships
27
+ * `scripts/` with no `exports` map — so a deep importer still links against
28
+ * this name. One definition, two import paths.
29
+ */
30
+ export { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
31
+
24
32
  /**
25
33
  * Roles that may NEVER run on a foreign model. Copied verbatim from
26
34
  * account-switch `tools/routing/routing.yaml` § never_foreign (read 2026-08-25).
@@ -44,22 +52,6 @@ export const NEVER_FOREIGN_ROLES = Object.freeze([
44
52
  * manufactures timeouts and reads as model failure. */
45
53
  export const DEFAULT_TIMEOUT_SEC = 900;
46
54
 
47
- /**
48
- * Grace period between SIGTERM and SIGKILL, in ms.
49
- *
50
- * `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
51
- * ignores it never emits `close`, and this module's only resolve paths are
52
- * `close` and `error` — so without escalation a wedged `cursor-agent` hangs
53
- * `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
54
- * catchable, so the escalation always terminates.
55
- *
56
- * Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
57
- * stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
58
- * measured needing longer than that to shut down — raise the constant, never
59
- * drop the escalation.
60
- */
61
- export const DEFAULT_KILL_GRACE_MS = 10_000;
62
-
63
55
  /** A runId names a directory and a log file. Anything outside this alphabet —
64
56
  * or the two relative-path literals the alphabet happens to admit — can escape
65
57
  * the parent it is joined to. */
@@ -90,8 +82,8 @@ export function isNeverForeignRole(role) {
90
82
  *
91
83
  * The escalation is the difference between a bounded dispatch and a hung wave:
92
84
  * this function's only resolve paths are the child's `close` and `error`
93
- * events, so a child that ignores SIGTERM never lets the promise settle. See
94
- * {@link DEFAULT_KILL_GRACE_MS}.
85
+ * events, so a child that ignores SIGTERM never lets the promise settle. The
86
+ * callers pass `DEFAULT_KILL_GRACE_MS` from `process-group.mjs`.
95
87
  *
96
88
  * @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
97
89
  * timerFired: boolean, killSignals: string[], errorCode: string|null}>}
@@ -54,8 +54,8 @@ import path from 'node:path';
54
54
 
55
55
  import { emitEvent, sessionAttribution } from '../events.mjs';
56
56
  import { isPathInside } from '../path-utils.mjs';
57
+ import { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
57
58
  import {
58
- DEFAULT_KILL_GRACE_MS,
59
59
  DEFAULT_TIMEOUT_SEC,
60
60
  isNeverForeignRole,
61
61
  isSafeRunId,
@@ -34,8 +34,8 @@ import path from 'node:path';
34
34
 
35
35
  import { emitEvent, sessionAttribution } from '../events.mjs';
36
36
  import { isPathInside } from '../path-utils.mjs';
37
+ import { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
37
38
  import {
38
- DEFAULT_KILL_GRACE_MS,
39
39
  DEFAULT_TIMEOUT_SEC,
40
40
  isNeverForeignRole,
41
41
  isSafeRunId,
@@ -123,6 +123,14 @@ async function applyDecisionRules(measurements, opts) {
123
123
  * declares a remote host that accepts this wave role, route the wave to that
124
124
  * host at its full planned agent count instead of shrinking it.
125
125
  *
126
+ * `offload-first: true` in Session Config (#1465) widens that trigger to a
127
+ * `proceed` verdict too: an offloadable wave role goes to a ready host even when
128
+ * the local host has headroom. Host selection, the witness contract and the
129
+ * HR-004 cap below are identical on both paths. With the switch on, a wave that
130
+ * stays local anyway (no host declared, none ready) carries a named
131
+ * `offload-first:` reason, so the switch never fails silently; a role absent
132
+ * from OFFLOADABLE_WAVE_ROLES stays local without one.
133
+ *
126
134
  * The gate does NOT probe the network — a placement decision must stay a pure
127
135
  * function of its inputs. The caller supplies a readiness WITNESS:
128
136
  * - `opts.remoteReady` — `{ [alias]: boolean }`, e.g. built from the
@@ -140,13 +148,24 @@ async function applyDecisionRules(measurements, opts) {
140
148
  * @returns {Promise<{decision: string, agents: number, reasons: string[], measurements: object, host?: string}>}
141
149
  */
142
150
  async function applyOffloadDecision(result, opts) {
143
- if (result.decision !== 'reduce' && result.decision !== 'coordinator-direct') return result;
151
+ const offloadFirst = opts.config?.['offload-first'] === true;
152
+ if (!offloadFirst && result.decision !== 'reduce' && result.decision !== 'coordinator-direct') {
153
+ return result;
154
+ }
144
155
 
145
156
  const { config, plannedAgents, waveRole, remoteReady, probeFn = null } = opts;
157
+ const role = String(waveRole ?? '').trim().toLowerCase();
146
158
  const hosts = config?.['remote-hosts'];
147
- if (!Array.isArray(hosts) || hosts.length === 0) return result;
159
+ if (!Array.isArray(hosts) || hosts.length === 0) {
160
+ // Only an offloadable role could have been placed, so only it gets the note.
161
+ return offloadFirst && OFFLOADABLE_WAVE_ROLES[role] !== undefined
162
+ ? {
163
+ ...result,
164
+ reasons: [...result.reasons, 'offload-first: true but no remote-hosts declared — staying local'],
165
+ }
166
+ : result;
167
+ }
148
168
 
149
- const role = String(waveRole ?? '').trim().toLowerCase();
150
169
  // No isNeverForeignRole() check here (D8 #1204 LOW-1): the invariant is
151
170
  // enforced STRUCTURALLY by this mapping table, not by an extra guard.
152
171
  // NEVER_FOREIGN_ROLES {impl-core, security-review, migration, release,
@@ -186,9 +205,10 @@ async function applyOffloadDecision(result, opts) {
186
205
  }
187
206
  }
188
207
  if (host === undefined) {
189
- return probeFailures.length === 0
190
- ? result
191
- : { ...result, reasons: [...result.reasons, ...probeFailures] };
208
+ const notes = offloadFirst
209
+ ? [...probeFailures, `offload-first: no ready host for '${role}' — staying local`]
210
+ : probeFailures;
211
+ return notes.length === 0 ? result : { ...result, reasons: [...result.reasons, ...notes] };
192
212
  }
193
213
 
194
214
  // The wave runs at its planned size again — but never above the HR-004 static
@@ -207,7 +227,9 @@ async function applyOffloadDecision(result, opts) {
207
227
  host: host.alias,
208
228
  reasons: [
209
229
  ...result.reasons,
210
- `offload: ${role} routed to host '${host.alias}' instead of reducing to ${result.agents}`,
230
+ result.decision === 'proceed'
231
+ ? `offload-first: ${role} routed to host '${host.alias}' (verdict was proceed)`
232
+ : `offload: ${role} routed to host '${host.alias}' instead of reducing to ${result.agents}`,
211
233
  ],
212
234
  measurements: result.measurements,
213
235
  };
@@ -383,14 +405,18 @@ export async function evaluateWaveResourceGate(opts) {
383
405
  }
384
406
 
385
407
  // Rule 2: probe the system (or use override for tests).
408
+ // A failed probe yields `proceed`. Under `offload-first: true` (#1465) that
409
+ // result still goes through placement: routing to a witnessed-ready host needs
410
+ // no local measurement, so a broken probe must not silently disable the switch.
386
411
  const measured = await extractMeasurements(opts);
387
412
  if ('probeFailed' in measured) {
388
- return {
413
+ const failed = {
389
414
  decision: 'proceed',
390
415
  agents: plannedAgents,
391
416
  reasons: ['probe failed (ignored)'],
392
417
  measurements: {},
393
418
  };
419
+ return config['offload-first'] === true ? applyOffloadDecision(failed, opts) : failed;
394
420
  }
395
421
 
396
422
  // Rules 3-8: apply decision rule sequence.