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
@@ -70,9 +70,10 @@
70
70
  * @module scripts/lib/maintenance-due-banner
71
71
  */
72
72
 
73
- import { closeSync, existsSync, fstatSync, openSync, readSync, statSync } from 'node:fs';
73
+ import { existsSync, statSync } from 'node:fs';
74
74
  import path from 'node:path';
75
75
 
76
+ import { SCAN_CHUNK_BYTES, scanEventsBackwards } from './events.mjs';
76
77
  import { computeReconcileNudge } from './reconcile-nudge-banner.mjs';
77
78
  import { sweepExpiredLearnings } from './learnings/expiry-sweep.mjs';
78
79
  import { shouldDispatchAutoDialectic } from './auto-dialectic.mjs';
@@ -152,104 +153,64 @@ function isoDay(ts) {
152
153
  }
153
154
 
154
155
  /**
155
- * Backwards-scan chunk size (#1290 item 2).
156
- *
157
- * NAMED CEILING: 256 KiB is ~800 records in this repo's ledger, so the common
158
- * case — a repo that ran /evolve within its recent history — answers after a
159
- * handful of reads instead of loading the whole 7.9 MB file. The scan is
160
- * UNBOUNDED in the worst case ON PURPOSE: "never ran" is a claim about every
161
- * line and cannot be made from a tail, so a repo with no `evolve.completed`
162
- * record still walks the file to its start — just in chunks, never all at once
163
- * in one string.
164
- *
165
- * REVISIT TRIGGER: the maintenance probe's median passes 1000 ms (half
166
- * `PROBE_BUDGET_MS`), or one repo's `events.jsonl` passes 50 MB. Either means
167
- * the "never ran" walk has become the cost that matters and the answer needs an
168
- * index rather than a scan.
156
+ * Backwards-scan chunk size (#1290 item 2) — the shared reader's constant, kept
157
+ * under this module's own name because it is the number this probe's cost is
158
+ * reasoned about in. NAMED CEILING + revisit trigger live at
159
+ * {@link SCAN_CHUNK_BYTES}.
169
160
  */
170
- export const TAIL_CHUNK_BYTES = 256 * 1024;
161
+ export const TAIL_CHUNK_BYTES = SCAN_CHUNK_BYTES;
171
162
 
172
163
  /**
173
- * Scan a buffer of COMPLETE lines backwards for the newest evolve record.
164
+ * Wall-clock budget for the "did /evolve ever run?" walk (#1414).
165
+ *
166
+ * NAMED CEILING (BV-004): 1000 ms is half of `PROBE_BUDGET_MS` (2000 ms in
167
+ * `session-start-probes.mjs`), which this probe shares with six other signals.
168
+ * The walk is UNBOUNDED in principle — "never ran" is a claim about every line
169
+ * of every source — so it needs an own limit now that it spans the archives
170
+ * too, and running out must report `truncated`, never a clean "never".
174
171
  *
175
- * @param {Buffer} buf
176
- * @returns {{lastAt: string|null}|null} null ⇒ no record in this buffer
172
+ * REVISIT TRIGGER: a repo where this reports `truncated` more than rarely. That
173
+ * means the answer needs an index rather than a scan, not a bigger budget
174
+ * (HR-101).
177
175
  */
178
- function scanEvolveLines(buf) {
179
- if (buf.length === 0) return null;
180
- const lines = buf.toString('utf8').split('\n');
181
- for (let i = lines.length - 1; i >= 0; i -= 1) {
182
- const line = lines[i];
183
- if (!line || !line.includes(EVOLVE_EVENT)) continue; // cheap pre-filter before JSON.parse
184
- try {
185
- const rec = JSON.parse(line);
186
- if (rec?.event !== EVOLVE_EVENT) continue;
187
- return { lastAt: typeof rec.timestamp === 'string' ? rec.timestamp : null };
188
- } catch {
189
- continue; // a malformed line is not evidence either way — keep scanning
190
- }
191
- }
192
- return null;
193
- }
176
+ export const EVOLVE_SCAN_BUDGET_MS = 1000;
194
177
 
195
178
  /**
196
- * Find the most recent `orchestrator.evolve.completed` record.
197
- *
198
- * Reads the ledger BACKWARDS in {@link TAIL_CHUNK_BYTES} chunks and stops at
199
- * the first hit, because the interesting answer is the LAST occurrence. The
200
- * former implementation `readFileSync`-ed the whole file (7.9 MB here, 30–44 ms)
201
- * to answer a question the last few kilobytes usually settle.
179
+ * Find the most recent `orchestrator.evolve.completed` record — across ROTATION
180
+ * boundaries (#1414).
202
181
  *
203
- * The one bug a naive chunked scan introduces is a record SPLIT across a chunk
204
- * boundary: the bytes before the first newline of a chunk are the tail of a line
205
- * whose head is in the chunk not read yet, so they are CARRIED, never parsed
206
- * here. Splitting on the 0x0A byte is safe on UTF-8 — no continuation byte can
207
- * equal a newline — so a multibyte character never splits a line either.
182
+ * Walks the ledger backwards through every source, newest first, and stops at
183
+ * the first hit, because the interesting answer is the LAST occurrence. Reading
184
+ * the active file alone was the bug: "never ran" is a claim about EVERY line
185
+ * ever written, and after a rotation the only `evolve.completed` record on the
186
+ * host can sit in `_archive/` — the probe then nags a repo that has run
187
+ * /evolve, which is the HR-101 failure mode this module exists to avoid.
208
188
  *
209
189
  * @param {string} repoRoot
210
- * @returns {{ok: boolean, lastAt: string|null}} `ok: false` ⇒ the ledger exists
211
- * but could not be read — the caller must record `undeterminable`, never clean.
190
+ * @returns {{ok: boolean, lastAt: string|null, truncated: boolean}} `ok: false`
191
+ * ⇒ a source exists but could not be read; `truncated: true` ⇒ the budget ran
192
+ * out before the walk finished. In BOTH cases the caller must record
193
+ * `undeterminable`, never clean — a walk that did not finish has not proven
194
+ * "never".
212
195
  */
213
196
  function readLastEvolveRun(repoRoot) {
214
197
  const file = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
215
- if (!existsSync(file)) return { ok: true, lastAt: null }; // fresh repo: genuinely never
216
- let fd;
217
- try {
218
- fd = openSync(file, 'r');
219
- let pos = fstatSync(fd).size;
220
- /** Partial line at the FRONT of everything read so far. */
221
- let carry = Buffer.alloc(0);
222
-
223
- while (pos > 0) {
224
- const length = Math.min(TAIL_CHUNK_BYTES, pos);
225
- pos -= length;
226
- const buf = Buffer.alloc(length);
227
- readSync(fd, buf, 0, length, pos);
228
- const block = carry.length > 0 ? Buffer.concat([buf, carry]) : buf;
229
- const firstNewline = block.indexOf(0x0a);
230
- if (firstNewline === -1) {
231
- carry = block; // no complete line yet — a line longer than one chunk
232
- continue;
233
- }
234
- const hit = scanEvolveLines(block.subarray(firstNewline + 1));
235
- if (hit) return { ok: true, lastAt: hit.lastAt };
236
- carry = block.subarray(0, firstNewline);
237
- }
238
-
239
- // pos === 0: the carry is the file's FIRST line, complete by construction.
240
- const hit = scanEvolveLines(carry);
241
- return { ok: true, lastAt: hit ? hit.lastAt : null };
242
- } catch {
243
- return { ok: false, lastAt: null };
244
- } finally {
245
- if (fd !== undefined) {
246
- try {
247
- closeSync(fd);
248
- } catch {
249
- /* best-effort */
250
- }
251
- }
252
- }
198
+ let lastAt = null;
199
+ const scan = scanEventsBackwards({
200
+ filePath: file,
201
+ chunkBytes: TAIL_CHUNK_BYTES,
202
+ budgetMs: EVOLVE_SCAN_BUDGET_MS,
203
+ // Cheap substring pre-filter before JSON.parse — the property the previous
204
+ // hand-rolled scan was written for, kept here rather than re-derived.
205
+ filter: EVOLVE_EVENT,
206
+ onRecord: (rec) => {
207
+ if (rec?.event !== EVOLVE_EVENT) return false;
208
+ lastAt = typeof rec.timestamp === 'string' ? rec.timestamp : null;
209
+ return true;
210
+ },
211
+ });
212
+ if (scan.unreadable.length > 0) return { ok: false, lastAt: null, truncated: false };
213
+ return { ok: true, lastAt, truncated: scan.truncated };
253
214
  }
254
215
 
255
216
  /**
@@ -335,7 +296,9 @@ export async function computeMaintenanceDue(opts = {}) {
335
296
  } else {
336
297
  // --- evolve (S1) -------------------------------------------------------
337
298
  const evolve = readLastEvolveRun(repoRoot);
338
- if (!evolve.ok) {
299
+ // `truncated` is NOT "not found": a walk that ran out of budget proves
300
+ // nothing about the lines it never reached (#1414).
301
+ if (!evolve.ok || evolve.truncated) {
339
302
  undeterminable.push('evolve');
340
303
  } else if (evolve.lastAt === null && nudge.activeLearnings >= MAINTENANCE_MIN_LEARNINGS) {
341
304
  markDue('evolve', `never, ${nudge.activeLearnings} active learnings`);
@@ -344,7 +307,9 @@ export async function computeMaintenanceDue(opts = {}) {
344
307
  // --- reconcile (S3) ----------------------------------------------------
345
308
  if (nudge.nudge === true) {
346
309
  // HR-106: report what the rule JUDGED. `computeReconcileNudge` nudges on
347
- // a BACKLOG (eligible-unmaterialized learnings), never on a date — so
310
+ // a BACKLOG — rule-eligible learnings minus those already materialized
311
+ // in the sidecar or `.claude/rules/` (`countReconcileBacklog`, #1380;
312
+ // before #1380 it counted all eligible ones) — never on a date — so
348
313
  // printing `lastRunAt` here put today's date next to the word "due" and
349
314
  // read as "last run today and already due again". Measured 2026-09-09
350
315
  // (learning 013a45ba): with 108 learnings capped under
@@ -7,7 +7,8 @@
7
7
  * during a wave, reviewed by the memory-cleanup flow, and either accepted
8
8
  * (appended to learnings.jsonl) or discarded.
9
9
  *
10
- * Pure leaf module — imports only node:crypto. No I/O, no quota enforcement
10
+ * Pure module — imports only node:crypto and the pure learnings/schema.mjs
11
+ * (type registry, repo-relative path predicate). No I/O, no quota enforcement
11
12
  * (see store.mjs for those responsibilities).
12
13
  *
13
14
  * Canonical schema (schema_version: 1) — ALL required fields:
@@ -30,7 +31,8 @@
30
31
  */
31
32
 
32
33
  import { randomUUID } from 'node:crypto';
33
- import { LEARNING_TYPE_REGISTRY } from '../learnings/schema.mjs';
34
+
35
+ import { isRepoRelativePathEntry, LEARNING_TYPE_REGISTRY } from '../learnings/schema.mjs';
34
36
 
35
37
  // ---------------------------------------------------------------------------
36
38
  // Constants
@@ -251,24 +253,28 @@ export function validateProposalRecord(record) {
251
253
  }
252
254
 
253
255
  // file_paths (optional, but if present must be a non-empty array of
254
- // non-empty, newline-free, glob-metacharacter-free strings) — issue #900 C,
256
+ // repo-relative, newline-free, glob-metacharacter-free strings) — issue #900 C,
255
257
  // Q3-MED fix pass: a glob metacharacter (* ? [ ] { }) surviving this schema
256
258
  // gate would later reach emitter.mjs::globsFromFilePaths verbatim for a
257
259
  // top-level (dirname==='.') entry, effectively producing an always-on rule
258
260
  // glob (e.g. `file_paths: ['**']`). `createProposalRecord` never sets an
259
261
  // empty array (it omits the key), so a present-but-empty `[]` is itself a
260
262
  // signal of a malformed/tampered record — reject it explicitly.
263
+ // Repo-relative is the SAME predicate validateLearning applies at write time
264
+ // (isRepoRelativePathEntry): a record read back from proposals.jsonl is not
265
+ // re-validated before writeApproved → appendLearning, so an absolute, home-
266
+ // relative or URL entry must be refused here, not silently at promotion.
261
267
  if ('file_paths' in record) {
262
268
  const fp = record.file_paths;
263
269
  const isValidArray =
264
270
  Array.isArray(fp) &&
265
271
  fp.length > 0 &&
266
272
  fp.every(
267
- (p) => typeof p === 'string' && p.length > 0 && !/[\r\n]/.test(p) && !/[*?[\]{}]/.test(p),
273
+ (p) => isRepoRelativePathEntry(p) && !/[\r\n]/.test(p) && !/[*?[\]{}]/.test(p),
268
274
  );
269
275
  if (!isValidArray) {
270
276
  errors.push(
271
- 'file_paths must be a non-empty array of non-empty, newline-free, glob-metacharacter-free strings when present',
277
+ 'file_paths must be a non-empty array of repo-relative, newline-free, glob-metacharacter-free strings when present',
272
278
  );
273
279
  }
274
280
  }
@@ -29,11 +29,21 @@
29
29
  * also YIELDS to tier 1: when SO_BASELINE_PATH is set (non-blank), resolveNamedBaseline
30
30
  * returns the null-fallback ({source:null}) so the caller's env tier wins.
31
31
  *
32
+ * ── Other sections (agents/vault#319) ────────────────────────────────────────
33
+ *
34
+ * The same directory-prefix match serves any owner.yaml list of the shape
35
+ * `{ path, match: { path-prefix } }`. `section` picks the list and `envKey` the
36
+ * env var this tier yields to. The live second consumer is `vault-dirs:`
37
+ * (section 'vault-dirs', envKey 'SO_VAULT_DIR'), driven by
38
+ * `resolveVaultDir` in scripts/lib/config/host-paths.mjs. `name` is required
39
+ * only for `baselines:` (the #819 contract); elsewhere it is optional and
40
+ * defaults to `path`, which is what the ambiguity WARN then names.
41
+ *
32
42
  * ── Exports ──────────────────────────────────────────────────────────────────
33
43
  *
34
- * parseBaselines(ownerConfig)
35
- * matchBaselineForPath(absPath, baselines)
36
- * resolveNamedBaseline({ cwd, ownerConfig, env })
44
+ * parseBaselines(ownerConfig, section?)
45
+ * matchBaselineForPath(absPath, baselines, section?)
46
+ * resolveNamedBaseline({ cwd, ownerConfig, env, section?, envKey? })
37
47
  */
38
48
 
39
49
  import { normalize, sep } from 'node:path';
@@ -43,6 +53,9 @@ import { expandTilde } from './common.mjs';
43
53
  // Internal helpers
44
54
  // ---------------------------------------------------------------------------
45
55
 
56
+ /** Sections whose entries MUST carry a `name` (the #819 baselines contract). */
57
+ const NAME_REQUIRED_SECTIONS = new Set(['baselines']);
58
+
46
59
  /** @param {unknown} v */
47
60
  function isPlainObject(v) {
48
61
  return v !== null && typeof v === 'object' && !Array.isArray(v);
@@ -70,18 +83,23 @@ function _normalizePath(p) {
70
83
  // ---------------------------------------------------------------------------
71
84
 
72
85
  /**
73
- * Extract and validate the `baselines:` list from a raw ownerConfig object.
86
+ * Extract and validate a directory-prefix list (default `baselines:`) from a raw
87
+ * ownerConfig object.
74
88
  *
75
89
  * Drop-and-WARN on malformed entries; never throw. Returns [] when the section
76
- * is absent, null, or empty — the backward-compat no-op path.
90
+ * is absent, null, or empty — the backward-compat no-op path. Every WARN names
91
+ * the section it came from.
77
92
  *
78
- * Each valid entry: { name: string, path: string, match: { 'path-prefix': string } }
93
+ * Each valid entry: { name: string, path: string, match: { 'path-prefix': string } }.
94
+ * `name` is required for `baselines`; for other sections an absent name falls
95
+ * back to `path` (a present-but-blank or non-string name is still dropped).
79
96
  *
80
97
  * @param {object|undefined} ownerConfig — raw parsed owner.yaml (NOT merged with defaults)
98
+ * @param {string} [section='baselines'] — owner.yaml list to read (e.g. 'vault-dirs')
81
99
  * @returns {Array<{name:string, path:string, match:{'path-prefix':string}}>}
82
100
  */
83
- export function parseBaselines(ownerConfig) {
84
- const raw = ownerConfig?.baselines;
101
+ export function parseBaselines(ownerConfig, section = 'baselines') {
102
+ const raw = ownerConfig?.[section];
85
103
 
86
104
  // Absent or explicit null/empty → no-op
87
105
  if (raw === undefined || raw === null) return [];
@@ -89,54 +107,56 @@ export function parseBaselines(ownerConfig) {
89
107
 
90
108
  if (!Array.isArray(raw)) {
91
109
  process.stderr.write(
92
- 'WARN named-baseline-resolver: owner.yaml baselines: must be an array; ignoring\n',
110
+ `WARN named-baseline-resolver: owner.yaml ${section}: must be an array; ignoring\n`,
93
111
  );
94
112
  return [];
95
113
  }
96
114
 
115
+ const nameRequired = NAME_REQUIRED_SECTIONS.has(section);
97
116
  const result = [];
98
117
  for (let i = 0; i < raw.length; i++) {
99
118
  const entry = raw[i];
100
119
  if (!isPlainObject(entry)) {
101
120
  process.stderr.write(
102
- `WARN named-baseline-resolver: owner.yaml baselines[${i}] is not an object; dropping\n`,
121
+ `WARN named-baseline-resolver: owner.yaml ${section}[${i}] is not an object; dropping\n`,
103
122
  );
104
123
  continue;
105
124
  }
106
125
 
107
126
  const { name, path: entryPath, match } = entry;
108
127
 
109
- if (typeof name !== 'string' || name.trim() === '') {
128
+ const nameAbsent = name === undefined || name === null;
129
+ if ((nameRequired || !nameAbsent) && (typeof name !== 'string' || name.trim() === '')) {
110
130
  process.stderr.write(
111
- `WARN named-baseline-resolver: owner.yaml baselines[${i}].name must be a non-empty string; dropping entry\n`,
131
+ `WARN named-baseline-resolver: owner.yaml ${section}[${i}].name must be a non-empty string; dropping entry\n`,
112
132
  );
113
133
  continue;
114
134
  }
115
135
  if (typeof entryPath !== 'string' || entryPath.trim() === '') {
116
136
  process.stderr.write(
117
- `WARN named-baseline-resolver: owner.yaml baselines[${i}].path must be a non-empty string; dropping entry\n`,
137
+ `WARN named-baseline-resolver: owner.yaml ${section}[${i}].path must be a non-empty string; dropping entry\n`,
118
138
  );
119
139
  continue;
120
140
  }
121
141
 
122
- // match is REQUIRED (unlike vaults) — a baseline with no path-prefix can
142
+ // match is REQUIRED (unlike vaults) — an entry with no path-prefix can
123
143
  // never be selected, so it is dropped rather than kept.
124
144
  if (!isPlainObject(match)) {
125
145
  process.stderr.write(
126
- `WARN named-baseline-resolver: owner.yaml baselines[${i}].match must be an object; dropping entry\n`,
146
+ `WARN named-baseline-resolver: owner.yaml ${section}[${i}].match must be an object; dropping entry\n`,
127
147
  );
128
148
  continue;
129
149
  }
130
150
  const pathPrefix = match['path-prefix'];
131
151
  if (typeof pathPrefix !== 'string' || pathPrefix.trim() === '') {
132
152
  process.stderr.write(
133
- `WARN named-baseline-resolver: owner.yaml baselines[${i}].match.path-prefix must be a non-empty string; dropping entry\n`,
153
+ `WARN named-baseline-resolver: owner.yaml ${section}[${i}].match.path-prefix must be a non-empty string; dropping entry\n`,
134
154
  );
135
155
  continue;
136
156
  }
137
157
 
138
158
  result.push({
139
- name: name.trim(),
159
+ name: nameAbsent ? entryPath.trim() : name.trim(),
140
160
  path: entryPath.trim(),
141
161
  match: { 'path-prefix': pathPrefix.trim() },
142
162
  });
@@ -162,9 +182,10 @@ export function parseBaselines(ownerConfig) {
162
182
  *
163
183
  * @param {string} absPath — the working directory to classify
164
184
  * @param {Array<{name:string, path:string, match:{'path-prefix':string}}>} baselines
185
+ * @param {string} [section='baselines'] — owner.yaml list name, used only in the WARN
165
186
  * @returns {{name:string, path:string, match:{'path-prefix':string}}|null}
166
187
  */
167
- export function matchBaselineForPath(absPath, baselines) {
188
+ export function matchBaselineForPath(absPath, baselines, section = 'baselines') {
168
189
  if (!absPath || !Array.isArray(baselines) || baselines.length === 0) return null;
169
190
 
170
191
  const target = _normalizePath(absPath);
@@ -181,7 +202,7 @@ export function matchBaselineForPath(absPath, baselines) {
181
202
 
182
203
  if (matched.length > 1) {
183
204
  process.stderr.write(
184
- `WARN named-baseline-resolver: multiple baselines match path "${absPath}": ${matched
205
+ `WARN named-baseline-resolver: multiple ${section} match path "${absPath}": ${matched
185
206
  .map((b) => b.name)
186
207
  .join(', ')}; using first match "${matched[0].name}"\n`,
187
208
  );
@@ -195,13 +216,13 @@ export function matchBaselineForPath(absPath, baselines) {
195
216
  // ---------------------------------------------------------------------------
196
217
 
197
218
  /**
198
- * Resolve the `baselines:` match tier for a given cwd.
219
+ * Resolve the directory-prefix match tier (default `baselines:`) for a given cwd.
199
220
  *
200
221
  * Returns:
201
- * { path, name, source:'match' } when a baseline directory-prefix matches cwd.
222
+ * { path, name, source:'match' } when an entry's directory-prefix matches cwd.
202
223
  * { path:null, name:null, source:null } (the null-fallback) otherwise — including
203
- * when SO_BASELINE_PATH env is set (this tier yields to the higher env tier the
204
- * caller owns), when no `baselines:` are configured, or when nothing matches.
224
+ * when `env[envKey]` is set (this tier yields to the higher env tier the caller
225
+ * owns), when the section is not configured, or when nothing matches.
205
226
  *
206
227
  * Pure + synchronous — no git calls, no disk reads. Never throws.
207
228
  *
@@ -209,22 +230,26 @@ export function matchBaselineForPath(absPath, baselines) {
209
230
  * cwd?: string,
210
231
  * ownerConfig?: object,
211
232
  * env?: Record<string, string|undefined>,
212
- * }} [opts]
233
+ * section?: string,
234
+ * envKey?: string,
235
+ * }} [opts] — `section` defaults to 'baselines', `envKey` to 'SO_BASELINE_PATH'
213
236
  * @returns {{ path: string|null, name: string|null, source: 'match'|null }}
214
237
  */
215
238
  export function resolveNamedBaseline({
216
239
  cwd = process.cwd(),
217
240
  ownerConfig,
218
241
  env = process.env,
242
+ section = 'baselines',
243
+ envKey = 'SO_BASELINE_PATH',
219
244
  } = {}) {
220
- // Yield to the higher-precedence env tier the caller owns (SO_BASELINE_PATH).
221
- const envVal = env?.SO_BASELINE_PATH;
245
+ // Yield to the higher-precedence env tier the caller owns.
246
+ const envVal = env?.[envKey];
222
247
  if (typeof envVal === 'string' && envVal.trim() !== '') {
223
248
  return { path: null, name: null, source: null };
224
249
  }
225
250
 
226
- const baselines = parseBaselines(ownerConfig);
227
- const matched = baselines.length > 0 ? matchBaselineForPath(cwd, baselines) : null;
251
+ const entries = parseBaselines(ownerConfig, section);
252
+ const matched = entries.length > 0 ? matchBaselineForPath(cwd, entries, section) : null;
228
253
  if (matched !== null) {
229
254
  return { path: matched.path, name: matched.name, source: 'match' };
230
255
  }
@@ -47,11 +47,14 @@
47
47
  * resolveCanonicalSuffixes({ ownerConfig, env })
48
48
  * findRepoRoot(cwd, { existsSync, realpathSync })
49
49
  * resolveNamedVault({ vaultName, cwd, ownerConfig, env, gitRemote, existsSync, realpathSync })
50
+ * normalizeRemote(url)
51
+ * checkCanonicalVault({ vaultDir, ownerConfig, env, readOriginUrl })
50
52
  */
51
53
 
52
54
  import { join, dirname } from 'node:path';
53
55
  import { existsSync as nodeExistsSync, realpathSync as nodeRealpathSync } from 'node:fs';
54
- import { resolvePreferredRemote, isQueryFailure } from './vcs-repo-spec.mjs';
56
+ import { spawnSync } from 'node:child_process';
57
+ import { resolvePreferredRemote, isQueryFailure, redactUrlCredentials } from './vcs-repo-spec.mjs';
55
58
 
56
59
  // ---------------------------------------------------------------------------
57
60
  // Internal helpers
@@ -255,6 +258,73 @@ export function resolveCanonicalSuffixes({ ownerConfig, env = process.env } = {}
255
258
  return canonicalSuffixesFromVaults(vaults, env?.VAULT_MIRROR_CANONICAL_SUFFIX);
256
259
  }
257
260
 
261
+ // ---------------------------------------------------------------------------
262
+ // checkCanonicalVault — the canonical-vault write guard (#1450)
263
+ // ---------------------------------------------------------------------------
264
+
265
+ /**
266
+ * Default origin reader: `git -C <vaultDir> remote get-url origin`. Returns
267
+ * `''` on a spawn error or a non-zero exit, so every failure reads as "no
268
+ * origin" and the guard stays fail-closed.
269
+ * @param {string} vaultDir
270
+ * @returns {string}
271
+ */
272
+ function _defaultReadOriginUrl(vaultDir) {
273
+ const res = spawnSync('git', ['-C', vaultDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8' });
274
+ if (res.error || res.status !== 0) return '';
275
+ return String(res.stdout ?? '').trim();
276
+ }
277
+
278
+ /**
279
+ * Decide whether `vaultDir` is a canonical Meta-Vault, i.e. whether its git
280
+ * `origin` ends in one of {@link resolveCanonicalSuffixes}. Shared by every
281
+ * writer into the vault so none of them can write into a foreign vault that
282
+ * `vault-dir` happened to resolve to.
283
+ *
284
+ * Fail-closed: a missing origin, a failing or throwing reader, and a suffix
285
+ * resolution that throws all return `ok: false`. The only bypass is the
286
+ * existing `VAULT_MIRROR_SKIP_CANONICAL_CHECK === '1'` switch, read from the
287
+ * `env` passed in (never from the ambient process.env when `env` is given).
288
+ * It is evaluated before any suffix resolution, so a skipped check neither
289
+ * parses `ownerConfig.vaults` nor prints its WARN lines, and returns
290
+ * `expected: []`.
291
+ *
292
+ * @param {{
293
+ * vaultDir: string,
294
+ * ownerConfig?: object,
295
+ * env?: Record<string, string|undefined>,
296
+ * readOriginUrl?: (vaultDir: string) => string,
297
+ * }} opts
298
+ * @returns {{ ok: boolean, reason?: 'skipped-by-env'|'no-git-origin'|'not-canonical', got?: string, expected: string[] }}
299
+ */
300
+ export function checkCanonicalVault({ vaultDir, ownerConfig, env = process.env, readOriginUrl = _defaultReadOriginUrl } = {}) {
301
+ if (env?.VAULT_MIRROR_SKIP_CANONICAL_CHECK === '1') {
302
+ // Empty on purpose: suffix resolution is exactly what the switch skips, and
303
+ // callers read `expected` only on `ok: false`.
304
+ return { ok: true, reason: 'skipped-by-env', expected: [] };
305
+ }
306
+ let expected;
307
+ try {
308
+ expected = resolveCanonicalSuffixes({ ownerConfig, env });
309
+ } catch {
310
+ return { ok: false, reason: 'not-canonical', expected: [] };
311
+ }
312
+
313
+ let url;
314
+ try {
315
+ url = String(readOriginUrl(vaultDir) ?? '').trim();
316
+ } catch {
317
+ url = '';
318
+ }
319
+ if (!url) return { ok: false, reason: 'no-git-origin', expected };
320
+
321
+ const normalized = normalizeRemote(url);
322
+ if (!expected.some((s) => normalized.endsWith(s))) {
323
+ return { ok: false, reason: 'not-canonical', got: url, expected };
324
+ }
325
+ return { ok: true, expected };
326
+ }
327
+
258
328
  // ---------------------------------------------------------------------------
259
329
  // findRepoRoot — injectable IO
260
330
  // ---------------------------------------------------------------------------
@@ -414,7 +484,7 @@ export function resolveNamedVault({
414
484
 
415
485
  // Guard: if this repo IS one of the vaults, skip (don't self-mirror)
416
486
  const canonicalSuffixes = canonicalSuffixesFromVaults(vaults, env?.VAULT_MIRROR_CANONICAL_SUFFIX);
417
- const normalized = _normalizeRemoteUrl(remoteUrl);
487
+ const normalized = normalizeRemote(remoteUrl);
418
488
  if (canonicalSuffixes.some((s) => normalized.endsWith(s))) {
419
489
  // This is the vault itself — fall through to fallback
420
490
  } else {
@@ -460,11 +530,14 @@ function _resolveEnvSuffix(env) {
460
530
  }
461
531
 
462
532
  /**
463
- * Normalize a git remote URL to host/path tail (mirrors vault-mirror.mjs _normalizeRemote).
533
+ * Normalize a git remote URL to a host/path tail for canonical-suffix matching.
534
+ * Strips `.git`, the `git@host:` / scheme prefixes, and trailing slashes —
535
+ * byte-identical to vault-mirror.mjs `_normalizeRemote`, which predates this
536
+ * export and can import it from here.
464
537
  * @param {string} url
465
538
  * @returns {string}
466
539
  */
467
- function _normalizeRemoteUrl(url) {
540
+ export function normalizeRemote(url) {
468
541
  return String(url ?? '')
469
542
  .trim()
470
543
  .replace(/\.git$/, '')
@@ -473,6 +546,32 @@ function _normalizeRemoteUrl(url) {
473
546
  .replace(/\/+$/, '');
474
547
  }
475
548
 
549
+ /**
550
+ * Render a git origin URL for a stderr line (#1479). The vault writers print
551
+ * the origin {@link checkCanonicalVault} rejected, and that stderr lands in CI
552
+ * logs and session transcripts — so a LOCAL origin must not carry its path
553
+ * (the operator's home dir, `.claude/rules/security.md` § Owner-Privacy).
554
+ *
555
+ * - `file:` URL, or a bare/relative path (git's rule: no `://` and no `:`
556
+ * before the first `/`, a Windows drive letter counting as a path)
557
+ * → `file://<local path>`.
558
+ * - Every other form (`https://`, `ssh://`, scp-like `git@host:path`) keeps its
559
+ * host/path and goes through {@link redactUrlCredentials}, so an
560
+ * `https://user:token@host/…` origin cannot leak its credential either.
561
+ *
562
+ * Returns `''` for an empty/absent origin; callers supply their own wording.
563
+ * @param {string|undefined|null} url
564
+ * @returns {string}
565
+ */
566
+ export function describeOriginForLog(url) {
567
+ const s = String(url ?? '').trim();
568
+ if (!s) return '';
569
+ if (/^file:/i.test(s)) return 'file://<local path>';
570
+ if (s.includes('://')) return redactUrlCredentials(s);
571
+ const scpLike = /^[^/\\]*:/.test(s) && !/^[a-z]:[\\/]/i.test(s);
572
+ return scpLike ? redactUrlCredentials(s) : 'file://<local path>';
573
+ }
574
+
476
575
  /**
477
576
  * Derive an org/repo slug from a git remote URL.
478
577
  * e.g. 'git@github.com:my-org/my-repo.git' → 'my-org/my-repo'
@@ -480,7 +579,7 @@ function _normalizeRemoteUrl(url) {
480
579
  * @returns {string}
481
580
  */
482
581
  function _deriveSlugFromRemote(url) {
483
- const normalized = _normalizeRemoteUrl(url);
582
+ const normalized = normalizeRemote(url);
484
583
  // normalized is now 'host/path/to/repo' — take the last two segments
485
584
  const parts = normalized.split('/').filter(Boolean);
486
585
  if (parts.length >= 2) {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * orphan-reaper/defaults.mjs — the reaper's parameter table and its repo-path join.
3
+ *
4
+ * Imports nothing but `node:path` on purpose: the trigger hooks reach it through
5
+ * `scan-throttle.mjs` on a hot path whose import closure is measured (#1432), and
6
+ * every other reaper module shares these numbers instead of re-spelling them.
7
+ */
8
+
9
+ import path from 'node:path';
10
+
11
+ /**
12
+ * Stufe-1 parameters. Every number carries its provenance from the PRD
13
+ * parameter table (`docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md`
14
+ * § 4) — NOT calibrated, so the provenance is what makes a later re-measurement
15
+ * possible ("keine Zahl ohne ihre Population").
16
+ */
17
+ export const REAPER_DEFAULTS = Object.freeze({
18
+ /** `reaper.min-age-seconds`: DevWatchdogs' hard limit for `tsgo`; the
19
+ * 2026-09-20 orphans were 7-17 min old, comfortably above it. */
20
+ minAgeSeconds: 300,
21
+ /** `reaper.min-scan-interval-seconds`: DevWatchdogs' normal scan cadence —
22
+ * keeps a `PostToolBatch` storm from taxing every tool call. */
23
+ minScanIntervalSeconds: 30,
24
+ /** `reaper.kill-grace-ms`: `DEFAULT_KILL_GRACE_MS` from `process-group.mjs` —
25
+ * a repo convention, not a new number. A literal rather than an import so this
26
+ * module stays out of `process-group.mjs`'s closure. */
27
+ killGraceMs: 10_000,
28
+ /** `reaper.verify-wait-ms`: without it the 2026-09-20 hand-run cleanup
29
+ * reported "still alive" for processes that were already gone. */
30
+ verifyWaitMs: 500,
31
+ /** `reaper.max-hook-latency-ms`: ceiling a scan may delay a hook by. */
32
+ maxHookLatencyMs: 50,
33
+ /** `reaper.false-alarm-window`: last N audit decisions — a ROLLING window
34
+ * rather than calendar time, so a quiet host still has a population (HR-101). */
35
+ falseAlarmWindow: 50,
36
+ });
37
+
38
+ /** @param {string} repoRoot @param {string} relpath @returns {string} */
39
+ export function underRepo(repoRoot, relpath) {
40
+ return path.join(repoRoot, ...relpath.split('/'));
41
+ }