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
@@ -23,6 +23,7 @@ All keys are optional. Unknown keys are **ignored without error** — adding a n
23
23
  | Field | Type | Required | Meaning | Example |
24
24
  |-------|------|----------|---------|---------|
25
25
  | `globs` | `string[]` (block or flow style) | no | Glob patterns relative to repo root. Rule loads only when a `scopePath` matches at least one. Absent = always-on; `[]` = matches nothing (disabled). | `globs:` then ` - src/**/*.tsx` |
26
+ | `paths` | `string[]` (block or flow style) | no — **required alongside `globs` with the identical list in a path-scoped VENDORED rule** (`rules/` library, #1449) | Alias for `globs` in `rule-loader.mjs` (#795; `globs` wins when both are present). It is the ONLY key Claude Code's native loader reads, and only from a frontmatter that opens on line 1 — a rule without it loads always-on there. A vendored rule therefore carries both keys: `paths` for Claude Code, `globs` for `rule-loader.mjs`, which reads it first. A repo's own consolidated `.claude/rules/` files are `paths`-only (see [Consolidated rules](#consolidated-rules-n-provenance-pairs-in-one-file-the-merge-contract) point 3). | `paths:` then ` - src/**/*.tsx` |
26
27
  | `description` | `string` | no | Human-readable summary of what the rule covers. Surfaced on the rule entry; used by FA2/FA4 tooling and authors. | `description: Tailwind + a11y conventions` |
27
28
  | `mode` | `string` (`housekeeping` \| `feature` \| `deep`) | no | Session-mode gate. Rule loads only in the named session mode. Absent = passes every mode. | `mode: deep` |
28
29
  | `host-class` | `string` | no | Host-class gate. Matched against `host_class` in `.orchestrator/host.json`. Rule loads only on matching hosts. Absent = passes every host. | `host-class: macos-arm64-m4pro` |
@@ -93,27 +94,42 @@ Since issue #743, `rules/opt-in-stack/{backend,backend-data,frontend,swift,secur
93
94
 
94
95
  ### Provenance header + frontmatter coexistence (issue #722)
95
96
 
96
- Vendored rule sources carry a mandatory single-line provenance header **before** any frontmatter block — `rules-sync.mjs` uses that header (`PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin ...'`) to tell "plugin-owned, safe to overwrite on re-sync" apart from "local override, preserve". The recommended shape for a vendored rule with `globs:` frontmatter:
97
+ Vendored rule sources carry a mandatory single-line provenance header — `rules-sync.mjs` uses that header (`PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin ...'`) to tell "plugin-owned, safe to overwrite on re-sync" apart from "local override, preserve". Where it goes depends on whether the rule has frontmatter (#1449):
98
+
99
+ - **Rule with frontmatter** — the frontmatter opens on **line 1** (the file's first bytes are `---`), and the header is the first line directly after the closing `---`. Claude Code reads a rule's frontmatter only when it opens on line 1, so a header above the opener makes the whole block body text and the rule loads always-on in every consumer.
100
+ - **Rule without frontmatter** — the header stays on line 1.
101
+
102
+ The shape for a path-scoped vendored rule:
97
103
 
98
104
  ```markdown
99
- <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/foo.md) -->
100
105
  ---
101
106
  globs:
102
107
  - src/**/*.tsx
108
+ paths:
109
+ - src/**/*.tsx
110
+ tier: wave-only
103
111
  ---
112
+ <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/foo.md) -->
104
113
  # Foo Rules (Path-scoped)
105
114
  ```
106
115
 
107
- `rule-loader.mjs`'s frontmatter parser (`parseGlobsFrontmatter`) tolerates a leading run of blank lines and/or single-line HTML comments before the opening `---`, so a vendored rule's provenance header does not defeat its `globs:` scoping — the header line is skipped, then frontmatter parses exactly as it would without the header. This tolerance is header-agnostic (it accepts any single-line HTML comment, not only the plugin's own), so a hand-authored rule that happens to start with a one-line comment is unaffected.
116
+ A path-scoped vendored rule declares its patterns under **both** `globs:` and `paths:`, with identical lists. `paths:` is the only key Claude Code reads from a rule; `globs:` is what `rule-loader.mjs` reads first (#742/#795 — when both are present `globs:` wins silently), so one key alone, or two different lists, would scope the rule differently per loader.
117
+
118
+ `rule-loader.mjs`'s frontmatter parser (`parseGlobsFrontmatter`) counts a frontmatter block only when its opening `---` is line 1 — the same rule Claude Code applies. Until #1449 it skipped a leading run of blank lines and single-line HTML comments, which classified a header-first rule as path-scoped while Claude Code loaded it always-on. Now a block with anything before its opener (a comment, a blank line, any text) yields no globs and no meta: the rule is always-on, and its entry carries `defect: 'frontmatter-not-at-top'` so callers such as the instruction-budget guard can report the file instead of loading it always-on without a trace. The header after the closing `---` is ordinary body text and has no effect on loading.
119
+
120
+ Consumer copies written before #1449 (header on line 1, frontmatter after it) still count as plugin-owned: `isPluginOwnedContent()` (`validate-vendored-rules.mjs`, re-exported by `rules-sync.mjs`) accepts the header at either position, so the next `/bootstrap --sync-rules` rewrites such a copy into the current format rather than preserving it as a local override.
121
+
122
+ This convention binds only files that are actual sync SOURCES — the entries `syncRules()` resolves from `rules/_index.md` (`join(pluginRoot, 'rules', '_index.md')`, the manifest it reads before writing anything into a consumer's `.claude/rules/`). `rules/README.md` and `rules/_index.md` itself are never entries in that manifest, so they are never sync targets and carry no provenance header by construction — not an oversight to fix.
108
123
 
109
124
  ### Vendoring validation (issue #722)
110
125
 
111
- Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`). Five probes:
126
+ Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`). Six probes:
112
127
 
113
128
  | Probe | Severity | Rejects / flags |
114
129
  |-------|----------|------------------|
115
- | `paths-frontmatter` | error | A top-level `paths:` frontmatter key **in a `rules/` library source**. Since #795 `rule-loader.mjs` accepts `paths:` as an alias for `globs:`, so such a rule IS glob-scoped — this is a vendoring-CONVENTION gate (`globs:` is the canonical form for vendored rules, #742), not a loader-compatibility gate. Its population is what `syncRules()` reads, i.e. `<pluginRoot>/rules/**` as listed by `rules/_index.md`; the consolidated files under `.claude/rules/` are `paths:`-canonical (see § Consolidated rules point 3) and are never its input. |
116
- | `provenance-header` | error (opt-in via `requireProvenance`, default `true` in `syncRules()`) | Missing provenance header on a library source — without it, `rules-sync.mjs` mis-detects the file as a local override on the next re-sync and can never update it again. |
130
+ | `frontmatter-not-at-top` | error | A `---` frontmatter block preceded by anything — the provenance comment, a blank line, any text (#1449). Claude Code reads frontmatter only when it opens on line 1, so such a rule loads ALWAYS-ON in every consumer repo, whatever its scope keys say. Fix: move the block to line 1 and put the provenance header on the first line after the closing `---`. |
131
+ | `globs-paths-mismatch` | error | A library source whose line-1 frontmatter carries exactly one of `globs:` / `paths:`, or both with different lists (compared order-insensitively) (#1449). A path-scoped vendored rule carries both with the identical list: `paths:` is the only key Claude Code reads, `globs:` is what `rule-loader.mjs` reads first (#742/#795). Replaces the pre-#1449 `paths-frontmatter` probe, which rejected `paths:` in a library source outright. Its population is what `syncRules()` reads, i.e. `<pluginRoot>/rules/**` as listed by `rules/_index.md`; the consolidated files under `.claude/rules/` are `paths:`-only by design (see § Consolidated rules point 3) and are never its input. |
132
+ | `provenance-header` | error (opt-in via `requireProvenance`, default `true` in `syncRules()`) | Missing provenance header on a library source — without it, `rules-sync.mjs` mis-detects the file as a local override on the next re-sync and can never update it again. Two positions are accepted: line 1 (a rule without frontmatter) or the first non-blank line after the closing `---` of a frontmatter that opens on line 1. A header on line 1 ABOVE a frontmatter (the pre-#1449 format) still satisfies this probe — `frontmatter-not-at-top` reports that shape, once — and keeps an old consumer copy plugin-owned, so the next sync upgrades it to the current format instead of freezing it as a local override. |
117
133
  | `placeholder` | error | Unfilled placeholder tokens: `{{PROJECT_NAME}}`-style handlebars, a `## TODO: Customize` heading, or a `<!-- TODO:` comment — skeleton content, not a finished rule. |
118
134
  | `zero-match-globs` | warn | A `globs:` pattern matching 0 files in the target repo's tracked file list (`git ls-files`, falling back to a directory walk). Legitimately possible in a freshly-scaffolded repo. |
119
135
  | `foreign-glob` | warn | A glob segment carrying a PascalCase, product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope. |
@@ -285,6 +301,44 @@ them silently loses a learning or regenerates it:
285
301
  TTL past what its type registry granted it.) State the rule in the file
286
302
  itself, so the next editor does not "fix" it upward.
287
303
 
304
+ Since #1387 an expired file also stops DEDUPING: the `/reconcile` provenance
305
+ reader (`scripts/lib/reconcile/backlog.mjs`
306
+ `defaultReadMaterializedProvenance`) skips an expired file whole —
307
+ frontmatter key and body bullets alike — through the same `isRuleExpired`
308
+ predicate the loader's injection gate uses, fail-open on an absent or
309
+ unparseable date. Two KNOWN LIMITS, both measured 2026-09-18 and both left
310
+ unsolved on purpose:
311
+
312
+ - **Only the on-disk half.** `partitionMaterialized` marks a learning
313
+ materialized on `sidecarTerminal || onDisk`; this covers `onDisk` only. On
314
+ this repo 79 of 92 provenance keys are ALSO terminal in
315
+ `.orchestrator/runtime/reconcile-candidates.jsonl`, so only 13 keys
316
+ actually become re-proposable. The sidecar half stays untouched because it
317
+ carries operator DECLINES — expiring a rule must not re-ask a question the
318
+ operator already answered.
319
+ - **A file the sweep cannot split re-proposes forever.** A file the sweep
320
+ skips (`no-1to1-mapping`, `no-provenance-block`, `unreadable`,
321
+ `no-counter-sentence`) that then
322
+ expires legitimately becomes re-proposable on EVERY run, with no
323
+ mechanical exit: the sweep will not rewrite it, so nothing retires its
324
+ markers. The live tree is currently clear of this —
325
+ `node scripts/sweep-expired-rules.mjs --dry-run --json` reported
326
+ `skipped: []` over 7 generated files (2026-09-18) — but the class has no
327
+ guard, so a future unsplittable file lands in it silently.
328
+
329
+ The expiry sweep (`node scripts/sweep-expired-rules.mjs`) maintains the
330
+ earliest-date rule in
331
+ ONE direction: a header sitting EARLIER than the earliest absorbed date is
332
+ RAISED to it — the raise is its own rewrite trigger (`action: "rewrite"`,
333
+ `reason: "header-raise"`, visible in `--json` before `--apply`), and it
334
+ touches nothing but the frontmatter line and the sentence above. Without it
335
+ the file passes its header date with nothing expired, `rule-loader.mjs`
336
+ stops injecting it, and the surviving provenance markers keep `/reconcile`
337
+ treating its learnings as materialized — the substance goes dark and is
338
+ never re-proposed. The sweep never LOWERS a header: a header that outlives
339
+ its content is reported as an `advisory` only, because lowering on every run
340
+ would cut a healthy entry's TTL short.
341
+
288
342
  3. **`paths:` is the canonical scope key, and it carries the UNION of the
289
343
  parts.** The merged file loads for any path any of its parts covered, so its
290
344
  list is the union of theirs. `paths:` is the key Claude Code's OWN native
@@ -295,13 +349,18 @@ them silently loses a learning or regenerates it:
295
349
  (`parseGlobsFrontmatter`, issue #795) and `instruction-budget-guard.mjs`
296
350
  (`:960`) goes through that same parser, so a `paths:`-only file is
297
351
  glob-scoped for every reader in this repo and still counts under
298
- `bySurface.pathScoped`. Measured 2026-09-16 over the live tree
352
+ `bySurface.pathScoped`. Measured 2026-09-18 over this repo's own
353
+ `.claude/rules/` (25 files; NOT the vendored `rules/` library, and not the
354
+ private baseline's `rules/` population `docs/baseline.md` counts separately)
299
355
  (`for f in .claude/rules/*.md; do awk '/^---$/{n++;next} n==1 && /^(paths|globs):/{print FILENAME": "$1}' "$f"; done`):
300
- 10 path-scoped rule files — 9 `paths:`-only, 1 (`cli-design.md`) carrying
301
- both. `globs:` is canonical only for rules VENDORED OUT through the `rules/`
302
- fleet library, where `validate-vendored-rules.mjs`'s `paths-frontmatter`
303
- probe enforces it (issue #742); that probe judges `rules/` sources only and
304
- never sees a consolidated file under `.claude/rules/`. Carrying both keys is
356
+ 10 path-scoped rule files, ALL 10 `paths:`-only — 0 carry `globs:`, 0 carry
357
+ both (`grep -rn '^globs:' .claude/rules/` → no match, exit 1). Until this
358
+ session `cli-design.md` carried both; the duplicate `globs:` was removed
359
+ here. Only rules VENDORED OUT through the `rules/` fleet library carry
360
+ both keys, with identical lists — `paths:` for Claude Code, `globs:` for
361
+ `rule-loader.mjs` (issues #742, #1449) — and `validate-vendored-rules.mjs`'s
362
+ `globs-paths-mismatch` probe enforces it; that probe judges `rules/`
363
+ sources only and never sees a consolidated file under `.claude/rules/`. Carrying both keys is
305
364
  allowed, but NEVER with different values: `globs:` wins SILENTLY when both
306
365
  are present (#795), and `check-rules.mjs` fails a divergent pair outright.
307
366
 
@@ -354,7 +413,7 @@ under four contracts, all of them consequences of the four merge rules above:
354
413
  learning look unmaterialized and `/reconcile` would regenerate it. **A pair
355
414
  is never deleted while its file survives.**
356
415
 
357
- 2. **Two fail-open cases, both reported rather than guessed.** An entry carries
416
+ 2. **Three fail-open cases, all reported rather than guessed.** An entry carries
358
417
  no date of its own; its date is recoverable only via `learning-id` →
359
418
  `.orchestrator/metrics/learnings.jsonl` `expires_at` (measured 2026-09-17 @
360
419
  `9e8146b4`: 87 of 92 unique ids resolve, 5 do not). An **unresolvable id**
@@ -364,8 +423,16 @@ under four contracts, all of them consequences of the four merge rules above:
364
423
  plus a `skipped` record with reason `no-1to1-mapping`. Measured the same day,
365
424
  the 1:1 mapping held in 3 of the 7 live files
366
425
  (`measurement-discipline` 12/12, `process-contracts` 6/6,
367
- `toolchain-and-build` 10/10) and failed in the other 4. Malformed
368
- `learnings.jsonl` lines are COUNTED (`malformedLines`), never skipped.
426
+ `toolchain-and-build` 10/10) and failed in the other 4. The third case is an
427
+ **unrecognised counter sentence** (GH#70, 2026-09-20): when the body sentence
428
+ restating `expires-at` is in no spelling `COUNTER_FORMS` knows, the file gets
429
+ `action: 'keep'` plus a `skipped` record with reason `no-counter-sentence`
430
+ and NO write of any kind — not a rewrite, not a raise, and not a delete
431
+ either, because a shape the sweep cannot read is the last shape whose most
432
+ destructive action should run. Moving only the header would ship a
433
+ frontmatter date contradicting a body sentence that forbids exactly that
434
+ correction. Malformed `learnings.jsonl` lines are COUNTED
435
+ (`malformedLines`), never skipped.
369
436
 
370
437
  3. **Deleting a whole file obeys the stamp-before-delete rule above.** A file is
371
438
  deleted only when it has zero kept AND zero unresolved pairs, and EVERY pair
@@ -416,7 +483,7 @@ Core security principles that apply to ALL code.
416
483
 
417
484
  ### (b) Glob-scoped rule (block-style `globs:`)
418
485
 
419
- Loads only on waves whose `allowedPaths` intersect the patterns. Mirrors the real `frontend.md`:
486
+ Loads only on waves whose `allowedPaths` intersect the patterns. Mirrors the real `frontend.md` (the vendored `rules/opt-in-stack/frontend.md` additionally carries `paths:` with the same list, `tier: wave-only`, and its provenance header after the closing `---` — see [Vendored Rules](#vendored-rules-issue-722-epic-a)):
420
487
 
421
488
  ```markdown
422
489
  ---
@@ -506,4 +573,4 @@ timeout masks real perf regressions.
506
573
  - [`scripts/lib/reconcile/eligibility.mjs`](../scripts/lib/reconcile/eligibility.mjs) — `CONVERT_TYPES` (derived from `LEARNING_TYPE_REGISTRY`), rule-conversion eligibility gates
507
574
  - [`scripts/lib/memory-proposals/schema.mjs`](../scripts/lib/memory-proposals/schema.mjs) — `PROPOSAL_TYPES` (derived from `LEARNING_TYPE_REGISTRY`)
508
575
  - [`docs/session-config-reference.md`](session-config-reference.md#reconcile-693--696--697) § Reconcile — `reconcile.rule-expiry-days` / `min-rule-days` / `min-insight-chars` config keys that tune the emitted `expires-at` and eligibility gates
509
- - Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode)
576
+ - Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode), #1449 (frontmatter on line 1, `globs:` + `paths:` pair and provenance-header position for vendored rules)
@@ -127,11 +127,11 @@ The blast radius is asymmetric. A false positive on the dispatch path blocks eve
127
127
  | 7 | ledger unreadable / corrupt | WARN + ALLOW + **self-heal** (the verdict carries a fresh ledger) |
128
128
  | 8 | `git ls-files` failed | ALLOW, degraded (stage 3a loses witnesses; concrete collisions still found) |
129
129
  | 9 | `findScopeCollisions` not evaluable | WARN + ALLOW |
130
- | 10 | same agent id re-dispatched | ALLOW, ledger record replaced (a retry must not self-lock) |
130
+ | 10 | same agent id re-dispatched | ALLOW; the earlier record is replaced only once the probe shows it FINISHED (a retry after a failure must not self-lock). A still-running predecessor with a distinct `tool_use_id` stays a collision partner (overlap → row 11); without ids on both sides it stays in the ledger as a claim, with no self-collision (#1480 B4). Replaced without a probe only where no claim is lost: the same `tool_use_id`, or (no ids on either side) a predecessor wholly covered by the retry's scope |
131
131
  | 10a | collision, but every colliding prior agent has FINISHED | ALLOW + prune those records |
132
132
  | 11 | collision with a prior agent still IN FLIGHT | **DENY** |
133
133
  | 12 | unexpected throw in `main()` | ALLOW + stderr |
134
- | 13 | liveness probe throws / no evidence at all | treated as IN FLIGHT (bounded by the TTL, § 5) |
134
+ | 13 | liveness probe throws / no evidence at all / only a result form that proves nothing | treated as IN FLIGHT (bounded by the TTL, § 5) |
135
135
  | 14 | ledger lock not acquirable within its budget | run the cycle UNLOCKED (degraded), never deny |
136
136
 
137
137
  Two structural rules keep this matrix honest, both recorded in the hook header:
@@ -174,7 +174,11 @@ The discriminator is therefore neither time nor the agent's name, but whether th
174
174
  - **Synchronous dispatch** — the `tool_result` for the dispatch's `tool_use` id arrives when the agent is done (measured: five `Agent` rows within 0.44 s, their results 5–11 minutes later). At the fifth agent's `PreToolUse` none of the first four has a result → all in flight → a real same-batch overlap still denies.
175
175
  - **Asynchronous dispatch** — the `tool_result` arrives in ~0.2 s and reads `Async agent launched successfully`. **That text is a launch receipt, not a completion.** Treating it as one would let every real background-batch collision through. The completion arrives later as a `<task-notification>` record carrying `<tool-use-id>` and `<status>completed</status>` (measured: launch 14:14:26.768 → notification 14:24:39.360).
176
176
 
177
- `buildTranscriptIndex()` reads all three record shapes and counts a description as finished only when **every** one of its dispatch ids is finished. Cost containment: the transcript is read **only once a collision has already been found** — i.e. only on the path that is about to deny; the no-collision path pays nothing. Transcripts above 256 MiB are treated as *no evidence*, never as a completion.
177
+ **A completion needs positive evidence (#1480 B2).** Before 2026-09-30 every `tool_result` that did not contain the launch receipt counted as a completion, so any drift in the receipt's wording turned a launch into a finish. That was measured, not hypothetical: a forked dispatch answers `Fork started — processing in background`, which was read as done. A `tool_result` now finishes its dispatch only in a form the census found on real completions (`RESULT_COMPLETION_FORMS`): `is_error`, the sync report's `<usage>`/`total_tokens` trailer (690 of 690), or a text beginning `This agent's report was delivered` (35 of 35, none followed by any notification). Any other form is *no evidence* and falls to the TTL (row 13), never to "finished".
178
+
179
+ **Resume re-opens a dispatch (#1459 item 1).** A failed or finished agent can be resumed with `SendMessage` addressed to its task id (the `agentId` in its own result, or the `<task-id>` of its notification). The resumed run finishes under the SendMessage's `tool_use` id, not the original one (census 2026-09-30: 455 of 455 resumes with a later completion). The probe therefore treats every `SendMessage` to the task id as an activation, unless its result says `"success":false` or is an error, and counts the dispatch as finished only when a completion follows the LAST activation. The completion can be a terminal notification under the original id, a SendMessage id or the task id. Closing still comes only from harness-written notification records or the dispatch's own `tool_result`. A message cannot close anything; it can only re-open.
180
+
181
+ **Exact id first (#1480 B3).** Each ledger record carries the dispatch's own `useId` (the payload's `tool_use_id`) and is resolved by that id. The description is a fallback for records without one, used only when it is non-empty, and it excludes the current dispatch's own id. A record is never looked up by its ledger id, which for an empty description is just the agent type and matched any namesake. `buildTranscriptIndex()` stays the description view: a description counts as finished only when **every** one of its dispatch ids is. Cost containment: the transcript is read **only once a collision has already been found** — i.e. only on the path that is about to deny; the no-collision path pays nothing. Transcripts above 256 MiB are treated as *no evidence*, never as a completion.
178
182
 
179
183
  **Blind fallback and its named ceiling (BV-004).** With no transcript, or none carrying a record of that agent, liveness falls back to the ledger entry's own age with `IN_FLIGHT_TTL_MS = 30 min`. The ceiling is derived: the largest **measured** same-batch dispatch spread is 95.7 s, so 30 min is ~19× headroom against the false-ALLOW direction, while both measured sequential repair gaps (36 min, 49 min) sit above it. **Revisit trigger:** a same-batch spread above ~5 min appearing in `.orchestrator/metrics/`, or a harness change that stops writing `transcript_path` — either invalidates the headroom the number rests on.
180
184
 
@@ -196,7 +200,7 @@ Complete list of what this guard does **not** see, or sees only approximately:
196
200
 
197
201
  ## 7. Debugging
198
202
 
199
- **A dispatch was denied and you do not believe it.** Read `.orchestrator/wave-dispatch-scopes.json`: it carries `waveKey`, `updated`, and one `{id, desc, files, at}` record per already-dispatched agent. The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
203
+ **A dispatch was denied and you do not believe it.** Read `.orchestrator/wave-dispatch-scopes.json`: it carries `waveKey`, `updated`, and one `{id, desc, useId, files, at}` record per already-dispatched agent (`useId` absent when the harness sent no `tool_use_id`). The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
200
204
 
201
205
  - The other agent is genuinely running and the overlap is real → fix the ownership in the session plan (one file, one agent), rewrite the affected `filescopes/wave-<N>/*.json`, re-assert, re-dispatch.
202
206
  - The other agent has finished, but the ledger still binds it → the transcript carried no evidence (§ 5) and you are inside the TTL window. Delete `.orchestrator/wave-dispatch-scopes.json`; the next dispatch rebuilds it.
@@ -223,6 +227,24 @@ Every figure above is quoted from a measurement recorded next to the code that c
223
227
 
224
228
  Re-measure before citing any of these downstream. A count re-briefed later is a claim about the past (`.claude/rules/parallel-sessions.md` § PSA-006).
225
229
 
230
+ ## The two-shape trap (#1020)
231
+
232
+ > Moved from CLAUDE.md Critical Gotchas, 2026-09-25.
233
+
234
+ **`allowedPaths` is COMPUTED, and the scope declaration has TWO shapes — writing only one is the silent failure (#1020).**
235
+
236
+ (a) **Per agent**, `<state-dir>/filescopes/wave-<N>/<agent-id>.json`, a JSON array of path STRINGS. This is `$AGENT_FILESCOPE_JSON` — what the `FILE-SCOPE` prompt injection, the Learnings-Index and `--assert-subset` read.
237
+
238
+ (b) **Per wave**, ONE aggregate sidecar holding an array of `{id, files}` RECORDS, built from every file in (a) plus the coordinator's own. This is the only thing `--assert-disjoint` and `--union` accept: pass them the wave directory and you get `Cannot read --union file`; pass them a single (a)-file and you get `must be a JSON array of {id, files} records`.
239
+
240
+ `node scripts/materialize-wave-scope.mjs --state-dir <state-dir> --wave <N>` is the canonical writer: it receives that one record array on stdin, writes every (a) file first, then (b) last. Neither is a `$TMPDIR` copy, and neither substitutes for the other.
241
+
242
+ Skipping (a) degrades the chain to signal-free ALLOW: no `FILE-SCOPE` block reaches the prompt, so `hooks/pre-task-scope-disjoint.mjs` extracts nothing, permits the dispatch, and writes no ledger entry — the absence looks identical to a clean run.
243
+
244
+ **Measured 2026-08-19:** a coordinator that wrote only (b) ran SIX waves and ~27 dispatches with zero injection; `.orchestrator/wave-dispatch-scopes.json` still carried a `|w2|` entry from two days earlier. Only `wave-scope.json` `allowedPaths` (global Gate 7) was live, which is why blocks still fired and the gap stayed invisible.
245
+
246
+ The coordinator's OWN direct edits go in a `coordinator.json` under (a) **and must be folded into (b)** — a `coordinator.json` that is never folded in is read by nothing. 2 of the 5 recorded divergences were coordinator-direct edits, which no agent scope file covers by construction.
247
+
226
248
  ## See Also
227
249
 
228
250
  - `skills/wave-executor/wave-loop.md` § Scope Manifest — the coordinator runbook (steps 3.1–3.3) and § Pre-Dispatch: File-Scope Injection (the prompt block shape).
@@ -200,6 +200,15 @@ down session-start.
200
200
  | `stale-branch-days` | integer | `7` | Days of inactivity before a branch is flagged as stale. |
201
201
  | `stale-issue-days` | integer | `30` | Days without progress before an issue is flagged for triage. |
202
202
 
203
+ **`cross-repo.projects` block form (#469, #478).** Not to be confused with the flat `cross-repos` list above. The dotted name is only how `parseSessionConfig()` returns it (`scripts/lib/config.mjs:446`); in the config file it is a `projects:` sub-key under a top-level `cross-repo:` block:
204
+
205
+ ```yaml
206
+ cross-repo:
207
+ projects: [~/Projects/my-app, ~/Projects/another-app] # INLINE list only
208
+ ```
209
+
210
+ Parsed by `_parseCrossRepo()` (`scripts/lib/config/cross-repo.mjs:50-84`). Only the inline form on the `projects:` line is read — a block list of `- ` items under `projects:` yields `[]` without a warning (measured 2026-09-18). An entry containing anything outside `[A-Za-z0-9._~/-]` (a shell metacharacter, a space) is dropped with one stderr line `cross-repo: rejected project entry with shell metacharacter: …`. Absent, empty, `none` or `null` → `[]`. Readers additionally drop entries resolving outside `~/Projects/` (confinement root, `getConfinementRoot()`). Read by `scripts/run-migrate-v2-cross-repo.mjs`, `scripts/vault-integration-watcher.mjs`, `scripts/promote-vault-strict.mjs`, and — as a secondary repo source after the filesystem scan — `scripts/lib/dispatcher/enumerate.mjs:303`.
211
+
203
212
  ## Auto-Skill Dispatch (#337)
204
213
 
205
214
  Opt-in phrase-match meta-skill (`skills/using-orchestrator/SKILL.md`) that inspects the user's first message for implicit slash-command intent (e.g. "plane neues Projekt", "run discovery on the backlog") and dispatches to the highest-confidence matching entry-point skill via the `Skill` tool — before that skill's own Phase 1 — once the bootstrap gate is open. Off by default; when `false` (or absent) the meta-skill returns immediately with zero reads, zero logging, zero side effects, and every calling skill behaves exactly as if it was never invoked.
@@ -343,9 +352,15 @@ slopcheck:
343
352
  | `grounding-check` | boolean | `true` | Enable file-level grounding verification in session-end Phase 1.1a (planned vs touched files). When true, session-end compares each agent's declared file scope against `git diff --name-only $SESSION_START_REF..HEAD` and reports scope creep + incomplete coverage. Informational — does not block session close. |
344
353
  | `grounding-injection-max-files` | integer | `3` | Max files with recent `edit-format-friction` stagnation history to inject as line-numbered GROUNDING blocks into each agent's prompt before dispatch (wave-executor pre-dispatch step). Per-agent scope; selects top N by recency. `0` disables the feature. Gated on `persistence: true`. (#85) |
345
354
  | `isolation` | string | `auto` | Agent isolation mode: `worktree`, `none`, or `auto`. `auto` resolves per-wave via the graduated default (#194): ≤2 agents → `none`, 3–4 agents on feature/deep → `worktree`, ≥5 agents → `worktree`, housekeeping 3–4 → `none`. Explicit `worktree` or `none` overrides the graduation. The resolved value surfaces per wave as `waves[].isolation` in the session shape's JSON output (`scripts/session-shape.mjs`) — a coordinator-direct or read-only wave resolves `none` without consulting the graduation at all. See [isolation graduation](#isolation-graduation) below. |
355
+ | `worktree-exclude` | list | `[node_modules, dist, build, .next, .nuxt, coverage, .cache, .turbo, .vercel, out]` | Top-level directory names deleted (recursive `fs.rm`) from a freshly created worktree, to cut disk and RAM on constrained hosts (#192). Parsed as a flat list by `scripts/lib/config.mjs:237-241`; read by `createWorktree()` (`scripts/lib/worktree/lifecycle.mjs:155-181`) and applied by `applyWorktreeExcludes()` (`scripts/lib/worktree/listing.mjs`). Names only, no globs, no nested paths; a name absent from the worktree is skipped silently. `null` / `none` / `[]` disable all excludes. **Scope limit (measured 2026-09-18):** `createWorktree()` has no production caller — `grep -rn "createWorktree(" scripts hooks skills` finds only a comment in `scripts/lib/wave-executor/foreign-dispatch.mjs`, which creates its own detached worktree with `git worktree add`. Wave dispatch under `isolation: worktree` uses the harness's native Agent worktree and never reads this key, so setting it changes nothing in a wave today. |
346
356
  | `max-turns` | integer or string | `auto` | Maximum agent turns before PARTIAL. Auto expands PER SESSION TYPE inside the resolved shape (`scripts/lib/session-shape.mjs` `MAX_TURNS_DEFAULT`, § Session Shapes above): housekeeping=8, feature=15, deep=25 (`maxTurnsDefault` in the shape JSON, applied to every wave). The `ultradeep` profile does **not** use one flat number — it sets `max-turns` PER WAVE: 40 for the Research+Code-Discovery wave, 25 for Impl-Core/Impl-Polish/Quality, 15 for Release/Finalization (the Synthesis-Gate wave is coordinator-direct and carries no `max-turns` at all). |
347
357
  | `auto-commit-per-wave` | boolean | `false` | Automatically commit each wave's work after the Quality-Lite gate passes. Checkpoint commits per wave reduce the risk of data loss from `git stash` collisions in parallel sessions (V3.3 RESCUE incident — see GitLab #214). When `false`, all work is committed at session-end via `/close`. Requires `persistence: true`; the flag is silently ignored when `persistence: false`. Trade-off: each wave produces an additional commit; git log shows N+1 commits instead of 1. Use `/simplify` or `git rebase -i --autosquash` before final close to squash if a clean history is desired. **Implementation note:** the procedural commit sequence (`scripts/lib/auto-commit.mjs`) is deferred to V3.6. Until then, setting this flag to `true` triggers a session-start warning that auto-commits are not yet active — the flag is a no-op but is validated so projects can opt in early. <!-- path-check: historical --> |
348
358
 
359
+ **Guard coverage (moved from CLAUDE.md Critical Gotchas, 2026-09-25).** **Destructive-Command Guard is active in main + subagent waves on harnesses with a compatible command hook.** On Claude Code, and through the supported Cursor and Pi bridges, `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json`:
360
+ **12 of its 16 rules are `severity: block`, the other 4 only warn** (measured 2026-09-19: `jq '[.rules[].severity]|group_by(.)|map({(.[0]):length})|add' .orchestrator/policy/blocked-commands.json`).
361
+ The 16th is `ledger-delete-protected` (#1401): `rm`/`mv`/`unlink` on `.orchestrator/metrics/**` — added after a wave agent deleted a rotated 53.896-line ledger archive that no rule covered, because `rm` existed only as the pattern `rm -rf` and the redirect denylist's `*.jsonl` did not match `events.jsonl.1`.
362
+ On Codex, the destructive-command rules are instructions only because no compatible `PreToolUse` command handler is wired. Rule source of truth: [`.claude/rules/parallel-sessions.md`](../.claude/rules/parallel-sessions.md) (PSA-003). Per-session bypass via Session Config: `allow-destructive-ops: true` (intentional maintenance only).
363
+
349
364
  ### enforcement-gates: the five gate keys (#800/#915)
350
365
 
351
366
  `enforcement-gates` is surfaced to the hook layer as `gates` inside the wave's
@@ -765,7 +780,25 @@ vault-integration:
765
780
  vault-name: # optional (#660) — per-project vault namespace override
766
781
  ```
767
782
 
768
- > **Host-local override (#653; extended #819).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves with an extra per-context tier in between: `SO_BASELINE_PATH` env > `owner.yaml` `baselines:` directory-prefix match against cwd > `owner.yaml` `paths.baseline-path` (legacy scalar) > the committed default. This keeps maintainer-specific absolute paths out of version control. Resolvers: `scripts/lib/config/host-paths.mjs` (both keys) and `scripts/lib/named-baseline-resolver.mjs` (the `baselines:` match tier).
783
+ > **Host-local override (#653; extended #819, agents/vault#319, #1448).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `vault-dirs:` directory-prefix match against cwd > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves the same way: `SO_BASELINE_PATH` env > `owner.yaml` `baselines:` directory-prefix match against cwd > `owner.yaml` `paths.baseline-path` (legacy scalar) > the committed default. The match tier exists because one host-wide `paths.vault-dir` otherwise redirects EVERY repo on the host into one vault, including a repo whose own vault lives elsewhere on that host. First match wins, so list the more specific prefix first:
784
+ >
785
+ > ```yaml
786
+ > # ~/.config/session-orchestrator/owner.yaml
787
+ > paths:
788
+ > vault-dir: ~/Projects/work/vault # host-wide default
789
+ > vault-dirs:
790
+ > - path: ~/Projects/private-world/vault # used for any cwd under the prefix
791
+ > match: { path-prefix: ~/Projects/private-world/ }
792
+ > ```
793
+ >
794
+ > A leading `~` is expanded to the home directory: `path-prefix` when matching, and the `path` / `paths.vault-dir` value the two owner.yaml tiers return (`match`, `owner`). The `env` and `committed` tiers pass through unexpanded; `scripts/vault-mirror.mjs` expands its `--vault-dir` at entry, so a `~/…` from any tier reaches the vault rather than `<cwd>/~/…`.
795
+ >
796
+ > The parsed config reports which tier won as `vault-integration.vault-dir-source` (`env`, `match`, `owner`, `committed`). This keeps maintainer-specific absolute paths out of version control. Resolvers: `scripts/lib/config.mjs` (`resolveVaultDir`, module-private), `scripts/lib/config/host-paths.mjs` (`resolveHostPath`, used for `plan-baseline-path`) and `scripts/lib/named-baseline-resolver.mjs` (the `baselines:` / `vault-dirs:` match tier).
797
+ >
798
+ > **Fake-repoRoot trap (moved from CLAUDE.md Critical Gotchas, 2026-09-25).** A throwaway invocation of a vault writer (`mirrorNarrative()`, `vault-mirror.mjs`, the board writer) with a synthetic `repoRoot` does **not** land in a sandbox — the fake repo path only fakes the SOURCE, never the DESTINATION. A reviewer in this repo walked into exactly that.
799
+ > Export `SO_VAULT_DIR=<tmpdir>` before ANY manual invocation of vault-writing code. <!-- consistency:exempt:runtime-only -->
800
+ >
801
+ > **Host-local switch (#1448).** A host can switch the integration off or soften it without touching the committed file, e.g. a host with no vault clone, where `vault-mirror.mjs` would otherwise exit 2 (`missing-vault-dir`) and a `strict` repo would block `/close`. Precedence: env `SO_VAULT_INTEGRATION=off|warn|strict` > `owner.yaml` `vault-integration: { enabled: false }` or `vault-integration: { mode: off|warn|strict }` > the committed block. The host may only LOWER the committed level (`strict` → `warn` → `off`); `enabled: true` or a stronger mode is ignored, so a host can never mirror a repo that did not opt in. An invalid `owner.yaml` `vault-integration:` section is dropped on its own (the rest of `owner.yaml` stays in force). When the host lowered the level, `vault-integration.host-override` names the tier (`env:SO_VAULT_INTEGRATION` or `owner.yaml`), otherwise it is `null`; session-end then prints `vault-mirror: bewusst aus auf diesem Host (<tier>)` instead of failing.
769
802
 
770
803
  > **`SO_CONFIG_HOME` — the host-private config directory itself.** A sibling override, one layer below `owner.yaml`'s own contents rather than a key inside it: `scripts/lib/config/private-config-dir.mjs` (`resolvePrivateConfigDir()`) is THE resolver (#1223) for the directory holding `owner.yaml`, `host-private.json`, and the host-alias ledger (`SO_HOST_ALIASES_FILE`) — `scripts/lib/host-identity.mjs`'s `_privateDir()` delegates to it — with precedence env-var (`SO_CONFIG_HOME`, names the private dir ITSELF) > `XDG_CONFIG_HOME` (names its PARENT) > the homedir default `~/.config/session-orchestrator`. Both env vars are read with `.trim() || fallback`, not a bare `||` (`.claude/rules/development.md` § Error Handling env-var-fallback-whitespace trap).
771
804
 
@@ -774,7 +807,7 @@ vault-integration:
774
807
  | Field | Type | Default | Description |
775
808
  |-------|------|---------|-------------|
776
809
  | `vault-integration.enabled` | boolean | `false` | If true, session-end and evolve skills invoke `vault-mirror.mjs` to sync learnings and sessions into the vault. When false (or missing), mirroring is skipped silently. |
777
- | `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository. Falls back to `$VAULT_DIR` env variable if not set. Required when `enabled` is true. |
810
+ | `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository. Host-local tiers override it (see the note above). If it is still empty after them, only the bash snippet in session-end and evolve falls back to the `$VAULT_DIR` env variable. Required when `enabled` is true. |
778
811
  | `vault-integration.mode` | string | `warn` | Mirror error handling. `strict` blocks session close if the mirror exits non-zero. `warn` reports errors but does not block. `off` bypasses mirror invocation entirely (useful when transitioning). |
779
812
  | `vault-integration.vault-name` | string or null | `null` | Optional override for the per-project vault namespace segment (#660). When set (or via CLI `--vault-name`), vault writes go to `40-learnings/<vault-name>/` and `50-sessions/<vault-name>/`, sanitised to a kebab slug. When null/absent, the namespace is derived from the git origin via `deriveRepo()`. Owner-privacy leaks (personal home path / private slug / personal name) are redacted to `redacted-repo`. NOT a filesystem path → NOT host-path-resolved. **Coverage gap:** portfolio board rows are NOT yet namespace-aware — the override only reaches `40-learnings/`, `50-sessions/`, and the per-repo narrative mirror (`narrative-mirror.mjs`, #832 item 2); tracked separately in #832. Resolver: `scripts/lib/vault-mirror/namespace.mjs` (`resolveRepoNamespace`). |
780
813
  | `vault-integration.gitlab-groups` | string[] or null | `null` | List of GitLab group paths to scan for repos missing `.vault.yaml`. Consumed by `scripts/vault-backfill.mjs` (via `readVaultIntegrationConfig()`) and the `/plan retro` vault-backfill sub-mode (`skills/plan/mode-retro.md` Phase 1.6 Step 1). When null/unset, the backfill CLI exits with a "no groups configured" notice. |
@@ -1003,14 +1036,14 @@ All fields live under a top-level `dialectic` object in your Session Config (CLA
1003
1036
  dialectic:
1004
1037
  cadence: 5 # integer ≥ 0; 0 = kill-switch
1005
1038
  model: haiku # haiku | sonnet | opus
1006
- budget-tokens: 8000 # input token budget per call
1039
+ budget-tokens: 32000 # input token ceiling per call
1007
1040
  ```
1008
1041
 
1009
1042
  | Field | Type | Default | Description |
1010
1043
  |-------|------|---------|-------------|
1011
1044
  | `dialectic.cadence` | integer | `5` | Number of sessions between auto-dialectic dispatches. Set to `0` to disable all dispatches (kill-switch). Non-integer and negative values silently fall back to default. **The dispatch moment moved (2026-09-09):** `shouldDispatchAutoDialectic()` is still the decision function this key feeds, but session-end Phase 3.6.7's own auto-trigger nudge is gone — retired as a standalone close-time prompt, and its recording wrapper (with the `orchestrator.dialectic.nudge_decided` event) removed in #1288 — see § Persistence & Safety above "Nudge retirement". The session-start `maintenance-due` probe now calls it as one of seven signals, so a due dialectic surfaces where the operator can act on it (session start), not where they are closing down. |
1012
1045
  | `dialectic.model` | string | `haiku` | Model tier for the critique call. Must be one of `haiku`, `sonnet`, `opus`. **Fail-fast**: unknown values cause parse-config.mjs to exit 1 at startup — NOT silently ignored. |
1013
- | `dialectic.budget-tokens` | integer | `8000` | Input token budget per call. Output budget is fixed at 4000 (per #506). Non-integer and negative values fall back to default. |
1046
+ | `dialectic.budget-tokens` | integer | `32000` | Input token ceiling per call (`DEFAULT_BUDGET_TOKENS`, `scripts/lib/config/dialectic.mjs:24`). `32000` — card bodies + steering alone estimated ~12k tokens in this repo, a consumer repo measured 30,262 (2026-09-18); the value is a ceiling, the pre-dispatch estimate aborts above it (`checkBudget()`, `scripts/dialectic-deriver.mjs:125`, status `budget-exceeded` — never a truncation). Output budget is fixed at 4000 (per #506). Non-integer and negative values fall back to default. |
1014
1047
 
1015
1048
  **Used by:** `skills/evolve/SKILL.md` Phase 6, `scripts/dialectic-deriver.mjs`, `scripts/lib/auto-dialectic.mjs`, and the session-start `maintenance-due` probe (`scripts/lib/maintenance-due-banner.mjs`). Session-end Phase 3.6.7 no longer reads it (retired 2026-09-09).
1016
1049
 
@@ -1018,9 +1051,9 @@ dialectic:
1018
1051
 
1019
1052
  **Trigger behavior:** When `cadence > 0` AND sessions-since-last-dialectic ≥ cadence AND (≥1 new session OR ≥1 new learning since last run), `shouldDispatchAutoDialectic()` returns `trigger: true` and the session-start `maintenance-due` probe reports it; the housekeeping session then runs the deriver in dry-run mode (session-end Phase 3.6.7 is retired — it dispatches nothing). The diff sidecar lands at `.orchestrator/dialectic-pending.md` (gitignored, vault-mirror-excluded). When `cadence: 0`, the signal never fires; manual `/evolve --dialectic` always works. <!-- path-check: example -->
1020
1053
 
1021
- **Token cost:** With defaults (cadence: 5, budget-tokens: 8000, output 4000, model haiku), ~12k tokens every 5 sessions. At haiku pricing this is ~$0.02/run. Surfaced in Final Report.
1054
+ **Token cost:** `budget-tokens` is a ceiling, not a spend — a run costs its actual input plus at most 4000 output tokens, once every 5 sessions with the default cadence (model haiku). Surfaced in Final Report.
1022
1055
 
1023
- **Empirical note (2026-07-04 session-3) — the parser default of `8000` proved structurally unreachable for this repo's own peer-card/steering corpus.** Fixed overhead (Peer-Cards + Steering + scaffold) alone runs ≈13k tokens, and a full input set (top-50 learnings + last-10 sessions) runs ≈28.4k tokens — both already exceed the `8000` default before any call is made. This repo's own committed Session Config therefore sets `budget-tokens: 32000`, not the documented default. If your repo accumulates a similarly large peer-card/steering corpus over time, raise `budget-tokens` accordingly rather than leaving the low default in place — a too-low budget silently truncates the dialectic critique input rather than erroring.
1056
+ **Empirical note — why the default is `32000` (#1380).** The former parser default of `8000` proved structurally unreachable: in this repo (2026-07-04 session-3) the fixed overhead (Peer-Cards + Steering + scaffold) alone ran ≈13k tokens and a full input set (top-50 learnings + last-10 sessions) ≈28.4k tokens; a consumer repo measured 30,262 (2026-09-18). Because the budget gate aborts BEFORE dispatch instead of truncating (`scripts/dialectic-deriver.mjs:604-612`), a corpus of that size can never run under `8000` — it ends `budget-exceeded` every time. If your peer-card/steering corpus outgrows `32000`, raise `budget-tokens` — a too-low ceiling aborts the run, it never shortens the input.
1024
1057
 
1025
1058
  ## Eval (#803)
1026
1059
 
@@ -1049,6 +1082,10 @@ eval:
1049
1082
 
1050
1083
  **Parser gotcha (learning confidence 0.9 — mirrors `custom-phases:` and every other block-shaped key):** the `eval:` key-line itself MUST NOT carry an inline comment. The block-open scan uses the shared `matchBlockHeader(line, 'eval')` (`scripts/lib/config/block-header.mjs`) — it tolerates the bold-bullet `- **eval:**` rendering (#830) but a trailing `# comment` on the header line still fails the match, so the parser never enters the block and ALL fields silently fall back to their defaults — no error, no warning surfaces anywhere. Sub-key lines (`enabled:`, `mode:`, …) tolerate inline comments without issue. See § Parser Gotcha: No-Inline-Comment Block Headers (top of this file) for the general contract this key shares with 36 other block-shaped keys.
1051
1084
 
1085
+ **Rubric version is NOT a Session Config key (#1037).** No key here selects the check set. The engine pins it as a constant — `RUBRIC_VERSION` in `scripts/lib/eval/engine.mjs` — because the rubric is *pre-registered*: a changed check mints a new version and a new file, it is never a per-repo dial. Current value: **`rubric-v2`** (`skills/eval/rubric-v2.md`, 2026-09-19), which supersedes `rubric-v1` (2026-07-16) for every newly written record. The two changes: `process-safety` no longer fails on `destructive_guard.blocked` (measured 2026-09-19 @ `d92c2ca4`: 32 of 40 records `fail`, all 32 solely from `blocked`, spiral `0` in all 40 — `.claude/rules/host-resources.md` HR-101), and the guard counts moved to a new reported-only `guard-friction` dimension. Stored records keep the `rubric_version` they were scored under and stay readable: neither `scripts/lib/eval/schema.mjs` nor `scripts/lib/eval/report.mjs` enumerates a fixed dimension set. `--verify` in `scripts/eval-session.mjs` used to re-score every stored record with the current engine and therefore reported `DRIFT` on all 40 `rubric-v1` records (measured 2026-09-19); since #1400 it compares only at matching `rubric_version` and otherwise returns its own verdict `version-mismatch` with exit code **3** — a planned rubric change is no longer indistinguishable from a forged record.
1086
+
1087
+ Neither `eval.enabled` nor `eval.judge` changed in v2 — the harness runs exactly where it ran before.
1088
+
1052
1089
  **Used by:** `scripts/lib/config/eval.mjs` (`_parseEval`), `scripts/lib/config.mjs`. Skill consumer (`skills/eval/SKILL.md`) is a follow-up wave of Epic #803 — not yet implemented as of this parser.
1053
1090
 
1054
1091
  ## Vault Staleness
@@ -1205,9 +1242,38 @@ docs-orchestrator:
1205
1242
  - `skills/session-end/SKILL.md` — Phase 3.2 gap-reporting step (activated when `enabled: true`)
1206
1243
  - `agents/docs-writer.md` — agent dispatched for documentation generation
1207
1244
 
1245
+ ## GitLab Portfolio (GH #41)
1246
+
1247
+ Opt-in cross-repo health dashboard for the `gitlab-portfolio` skill (`/portfolio`): discovers repos from `<vault>/01-projects/*/_overview.md`, aggregates open issues, MRs, critical labels and stale signals, and writes `_PORTFOLIO.md` into the vault. It needs `vault-integration.vault-dir` — the CLI exits 2 without it (`scripts/lib/gitlab-portfolio/cli.mjs`).
1248
+
1249
+ All fields live under a top-level `gitlab-portfolio` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`):
1250
+
1251
+ ```yaml
1252
+ gitlab-portfolio:
1253
+ enabled: false # opt-in
1254
+ mode: warn # warn | strict | off
1255
+ stale-days: 30 # integer ≥ 1
1256
+ critical-labels: # BLOCK list only — see the row below
1257
+ - priority::critical
1258
+ - priority::high
1259
+ ```
1260
+
1261
+ | Field | Type | Default | Allowed values | Description |
1262
+ |-------|------|---------|----------------|-------------|
1263
+ | `gitlab-portfolio.enabled` | boolean | `false` | `true` / `false` | Only the literal `true` enables it. When false, `/portfolio` prints `disabled` and exits 0 (`cli.mjs:192-199`), and session-start Phase 2.7 skips its dry-run banner (`skills/session-start/SKILL.md` § Phase 2.7). |
1264
+ | `gitlab-portfolio.mode` | string | `warn` | `warn` / `strict` / `off` | `strict` makes the CLI exit 2 when any repo failed to fetch (`cli.mjs:375`). `warn` and `off` both exit 0 in the CLI; `off` additionally suppresses the session-start banner. An unknown value falls back to `warn`. |
1265
+ | `gitlab-portfolio.stale-days` | integer | `30` | integer ≥ 1 | An open issue whose `updated_at` is older than this counts as stale (`scripts/lib/gitlab-portfolio/aggregator.mjs`). An invalid value falls back to `30`. |
1266
+ | `gitlab-portfolio.critical-labels` | array of string | `[priority::critical, priority::high]` | non-empty strings | Labels that count an issue as critical; matching is scope-tolerant (`priority:critical` also matches). **Block list only:** items need their own `- ` lines indented at least 3 spaces. The inline form `critical-labels: [a, b]` is NOT read — the parser silently keeps the default (measured 2026-09-18 via `_parseGitlabPortfolio`). |
1267
+
1268
+ Parsing is tolerant: `_parseGitlabPortfolio()` (`scripts/lib/config/gitlab-portfolio.mjs`, called at `scripts/lib/config.mjs:443`) drops unknown sub-keys and replaces invalid values with the defaults above, never throwing. The strict check lives separately in `validateGitlabPortfolio()` (`scripts/lib/config-schema.mjs:369-405`, wired at `:104-106`), which reports wrong types and unknown sub-keys as schema errors under `enforcement`.
1269
+
1270
+ **Used by:** `scripts/lib/gitlab-portfolio/cli.mjs` (`/portfolio`, and session-start Phase 2.7 via `--dry-run`), `skills/gitlab-portfolio/SKILL.md`.
1271
+
1208
1272
  ## Events Rotation
1209
1273
 
1210
- Size-based rotation for `.orchestrator/metrics/events.jsonl` (#251). Rotation fires at **session-start only** — per-append overhead is rejected design given typical growth of ~6 KiB/day. When the active log exceeds `max-size-mb`, it is renamed to `events.jsonl.1`, older backups shift down (`.1` → `.2`, …, `.N-1` → `.N`), and the oldest backup (`events.jsonl.{max-backups}`) is deleted. Rotation failure never blocks session-start; errors are logged to stderr and swallowed.
1274
+ Size-based rotation for `.orchestrator/metrics/events.jsonl` (#251). Rotation fires at **session-start only** — per-append overhead is rejected design given typical growth of ~6 KiB/day. When the active log exceeds `max-size-mb`, it is renamed into `.orchestrator/metrics/_archive/events-<firstTs>_<lastTs>.jsonl` (each stamp `YYYYMMDDTHHMMSSZ`, derived from the archived content; `unknown` for the first when no timestamp parsed, plus a `-<n>` suffix on a name collision), archives beyond `max-backups` are pruned oldest-first, and an `orchestrator.events.rotated` record naming the archive, its byte size, line count, timestamp range and any pruned files is appended as the **first line of the new active file**. That record is what lets a reader tell a rotation from a deletion. Rotation failure never blocks session-start; errors are logged to stderr and swallowed.
1275
+
1276
+ **Reading across a rotation:** use `readEventsWithRotations(repoRoot, opts)` from `scripts/lib/events.mjs` — active file plus every archive in time order, `malformed_lines` counted, and a rotation whose `archived_as` target is missing reported in `gaps` with `complete: false`. It also still reads a legacy `events.jsonl.1`..`.N` ring left on disk by pre-#1401 versions; the writer never creates, shifts or prunes one any more. The ring was replaced because its shift step renamed every surviving backup on each rotation, which would have made the `archived_as` pointer stale one rotation after it was written.
1211
1277
 
1212
1278
  All fields live under a top-level `events-rotation` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
1213
1279
 
@@ -1222,9 +1288,9 @@ events-rotation:
1222
1288
  |-------|------|---------|-------------|
1223
1289
  | `events-rotation.enabled` | boolean | `true` | If false, rotation is skipped entirely and `events.jsonl` grows unbounded. |
1224
1290
  | `events-rotation.max-size-mb` | integer | `10` | Size threshold in MiB. When `events.jsonl` exceeds this, it is rotated at the next session-start. Bounds: `1..1024`; out-of-range values silently fall back to the default. |
1225
- | `events-rotation.max-backups` | integer | `5` | Number of retained backup files (`events.jsonl.1` … `events.jsonl.N`). The oldest is deleted before shifting. Bounds: `1..20`; out-of-range values silently fall back to the default. |
1291
+ | `events-rotation.max-backups` | integer | `5` | Number of retained archives in `.orchestrator/metrics/_archive/`. After each rotation the excess is deleted oldest-first (by the leading timestamp in the filename) and the deletions are listed in the rotation record's `pruned` field. Only files matching the archive naming contract are eligible, so a hand-placed file in `_archive/` is never pruned. Bounds: `1..20`; out-of-range values silently fall back to the default. |
1226
1292
 
1227
- **Rename safety (POSIX):** Atomic rename is safe with in-flight writers. Open file descriptors continue writing to the original inode (now `events.jsonl.1`); new writers open the new file on next append. Maximum observed line size is 220 bytes, well under the 4096-byte PIPE_BUF atomicity guarantee.
1293
+ **Rename safety (POSIX):** Atomic rename is safe with in-flight writers. Open file descriptors continue writing to the original inode (now the archive); new writers open the new file on next append. Maximum observed line size is 220 bytes, well under the 4096-byte PIPE_BUF atomicity guarantee.
1228
1294
 
1229
1295
  ## Test
1230
1296
 
@@ -1307,7 +1373,7 @@ remote-hosts:
1307
1373
 
1308
1374
  **Two enums, never conflated.** `roles-allowed` holds `agent-mapping` roles (`test`, `ui`, `perf`) — NOT wave roles (`Impl-Core`, `Quality`, …). The wave→role translation is `OFFLOADABLE_WAVE_ROLES` in `scripts/lib/wave-resource-gate.mjs`; a wave role absent from that map is local-only by default.
1309
1375
 
1310
- **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct`. It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
1376
+ **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct` — or, with `offload-first: true`, on `proceed` as well (see § `offload-first` below). It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
1311
1377
 
1312
1378
  **agent-mapping interaction.** A declared alias is what an `agent-mapping` value of the form `<role>: ssh:<alias>` validates against; naming an undeclared host throws at parse time, naming the `ssh` channel with no target throws as for any other channel.
1313
1379
 
@@ -1315,6 +1381,32 @@ Read by: `scripts/lib/config/remote-hosts.mjs` (parser), `scripts/lib/config.mjs
1315
1381
 
1316
1382
  See `skills/remote-offload/SKILL.md` for the wave-executor-side decision rule, the three offload channels, and how a declared alias here is what an `agent-mapping` `ssh:<alias>` value validates against.
1317
1383
 
1384
+ ### `offload-first` (#1465)
1385
+
1386
+ Opt-in switch that makes placement the FIRST choice for an offloadable wave role, not only the answer to local pressure. Scalar key, so a trailing `# comment` on its line is fine (the block-header gotcha above does not apply).
1387
+
1388
+ ```yaml
1389
+ offload-first: false # true = offloadable roles go to a witnessed-ready remote host even on `proceed`
1390
+ ```
1391
+
1392
+ | Field | Type | Default | Description |
1393
+ |-------|------|---------|-------------|
1394
+ | `offload-first` | boolean | `false` | When `true`, the gate's offload arm fires on a `proceed` verdict too, not only on `reduce` / `coordinator-direct`. `false` (or absent) changes nothing. |
1395
+
1396
+ **Behaviour with `offload-first: true`.**
1397
+
1398
+ - Only roles in `OFFLOADABLE_WAVE_ROLES` (`scripts/lib/wave-resource-gate.mjs`) move: wave roles `quality` / `test` → `test`, `ui` → `ui`, `perf` → `perf`. The table is NOT extended by this switch — build, lint and audit work is covered by the Quality wave's `test` role. `impl-core`, `security-review`, `migration`, `release`, `secrets`, `incident` and `refactor-crosscut` never leave the local host.
1399
+ - The target is the FIRST declared `remote-hosts` entry whose `roles-allowed` carries the mapped role AND that is witnessed ready (`remoteReady: { m5: true }` or an async `probeFn`). The gate still never probes the network itself, so the witness is mandatory for the switch to have any effect: without one, nothing offloads even with the switch on.
1400
+ - The HR-004 heavy-repo cap still binds the offloaded agent count.
1401
+ - No ready host for the role → the verdict is unchanged (`proceed`, or the `reduce`/`coordinator-direct` the resource rules computed) and `reasons` carries `offload-first: no ready host for '<role>' — staying local`.
1402
+ - Switch on but no `remote-hosts` declared → the verdict is unchanged and `reasons` carries `offload-first: true but no remote-hosts declared — staying local`. `validateSessionConfig()` (`scripts/lib/config-schema.mjs`) additionally emits a warn-level cross-field finding `offload-first-cross-field` for that combination; it never flips `ok`.
1403
+ - `resource-awareness: false` remains a FULL opt-out (no probe, no HR-004 cap, no placement), as documented in § Environment Awareness — the switch does not change that.
1404
+ - On the `probe failed (ignored)` path placement DOES apply when the switch is on, because placement needs no measurement; the `probe failed (ignored)` reason is kept.
1405
+
1406
+ **Committed-only.** There is no host-local precedence (env > `owner.yaml` > committed) for this key: the existing `resolveDispatcherAutonomy` resolver is enum-specific and `owner.yaml` freezes its allowed sections, so reusing it would need a schema change for a need nobody has raised yet (BV-001) — the value in the committed Session Config is the value that applies.
1407
+
1408
+ Read by: `scripts/lib/config.mjs` (parser), `scripts/lib/config-schema.mjs` (cross-field warning), `scripts/lib/wave-resource-gate.mjs` (placement).
1409
+
1318
1410
  ## Evolve Extra Sources (#638)
1319
1411
 
1320
1412
  Opt-in EXTRA learning sources for `/evolve`. A domain measurement (e.g. an eval-learn regression harness) runs OUT-OF-BAND and writes a sidecar JSON of regression flags; `/evolve` then READS each declared sidecar and emits a `domain-regression` learning candidate per flag that has persisted across ≥2 consecutive sessions. This is a strict **read-only consumption contract**: `/evolve` never runs the domain measurement — it only consumes the sidecar output. Absent/empty ⇒ `[]` ⇒ no extra sources are read; existing `/evolve` runs are unaffected.
@@ -1503,6 +1595,56 @@ loop-guard:
1503
1595
 
1504
1596
  **Used by:** `hooks/loop-guard.mjs`, `scripts/lib/config/loop-guard.mjs` (`_parseLoopGuard`). Issue #619.
1505
1597
 
1598
+ ## Orphan Reaper (#1425 / #1432)
1599
+
1600
+ Watchdog against orphaned descendants of quality-gate commands. On **2026-09-20** four orphaned `tsgo --noEmit` processes from another repo on the same host — two with PPID 1, 86–588 % CPU, up to 8.0 GB RSS each — drove the machine to 13 % free memory and a 67.4 load average. Part A of the PRD stops them at the source (process-group kill in the gate); this block configures part B, the net that catches what still escapes.
1601
+
1602
+ The scan itself lives in `scripts/lib/orphan-reaper.mjs`. Two hooks TRIGGER it — `hooks/post-tool-batch-wave-signal.mjs` (PostToolBatch, the only hook with measurably high frequency *during* a wave) and `hooks/on-stop.mjs` (Stop + SubagentStop, which lands exactly when gate children have just finished). Neither runs the scan inline: one `ps` round-trip out of Node was measured at ~47 ms, which alone would exceed `max-hook-latency-ms`. The hook does one config read plus one `stat` of the throttle marker (`.orchestrator/tmp/reaper-last-scan`, shared by both hooks and gitignored), then hands the work to a DETACHED, `unref()`-ed child that owns the kill, the audit and the event.
1603
+
1604
+ **Default OFF, and deliberately so.** Unlike `loop-guard`, this signal can send signals to processes, so it ships inert. `.claude/rules/host-resources.md` HR-101 binds it: a warning class above ~10 % is a broken instrument to be re-aimed, not obeyed — measure the firing rate over `false-alarm-window` decisions in the JSONL audit BEFORE arming `mode: kill`. HR-105 binds the other half: a rate nothing records is unfalsifiable, which is why every decision (including a withdrawn one) is appended to `.orchestrator/metrics/reaper-audit.jsonl`.
1605
+
1606
+ ```yaml
1607
+ reaper:
1608
+ enabled: false # default OFF; only an explicit `true` arms the watchdog
1609
+ mode: report # report (decide + audit only) | kill (send signals)
1610
+ min-age-seconds: 300
1611
+ min-scan-interval-seconds: 30
1612
+ kill-grace-ms: 10000
1613
+ verify-wait-ms: 500
1614
+ max-hook-latency-ms: 50
1615
+ false-alarm-window: 50
1616
+ ```
1617
+
1618
+ | Field | Type | Default | Provenance / Description |
1619
+ |-------|------|---------|--------------------------|
1620
+ | `reaper.enabled` | boolean | `false` | Master toggle. Only an explicit `enabled: true` arms it; `false` means the hooks do no `stat` and no spawn at all. |
1621
+ | `reaper.mode` | `report` \| `kill` | `report` | `report` runs the scan in dry-run (decide + audit, no signal); `kill` arms the signal path (PRD Stufe 2, after calibration). An unrecognised value falls back to `report` — never to `kill`. |
1622
+ | `reaper.min-age-seconds` | integer (≥ 0) | `300` | DevWatchdog's hard limit for `tsgo`; the 2026-09-20 orphans were 7–17 min old, well above it. Self-healing clamp: a value below `min-scan-interval-seconds` is widened to it, because a process younger than one scan period can be born and reaped between two scans. |
1623
+ | `reaper.min-scan-interval-seconds` | integer (≥ 1) | `30` | DevWatchdog's normal scan cadence. The throttle that keeps a `PostToolBatch` storm from taxing every tool call. |
1624
+ | `reaper.kill-grace-ms` | integer (≥ 0) | `10000` | `DEFAULT_KILL_GRACE_MS` from `process-group.mjs:52` — repo convention, not newly invented. SIGTERM → grace → SIGKILL. |
1625
+ | `reaper.verify-wait-ms` | integer (≥ 0) | `500` | Wait before reading the effect back. Without it the 2026-09-20 check falsely reported "still alive" measuring immediately after `kill -9` (PRD B6: a sent signal proves nothing). |
1626
+ | `reaper.max-hook-latency-ms` | integer (≥ 1) | `50` | Ceiling the TRIGGER may delay a hook by — the config read, the marker `stat` and the detached spawn, measured with `performance.now()` in `maybeTriggerOrphanScan()`. Overrunning it writes ONE stderr WARN line and nothing else: no event (a `PostToolBatch`-class hook emitting per fire is the always-on signal HR-101 calls a broken instrument) and no abort (the child is already detached by then). The scan itself is not covered — that is the point of spawning detached. |
1627
+ | `reaper.false-alarm-window` | integer (≥ 1) | `50` | Rolling window of audit decisions the HR-101 10 % false-alarm rate is judged over — a window in DECISIONS, not calendar time, so the rate has a population on quiet hosts too. Reaches the scan as `--false-alarm-window` on the detached child's argv and becomes `runOrphanScan`'s `falseAlarmWindow`; the CLI refuses `0`, which `falseAlarmRate()` would read as “every firing ever recorded” rather than as a window. |
1628
+
1629
+ **Used by:** `hooks/post-tool-batch-wave-signal.mjs`, `hooks/on-stop.mjs` (both via `maybeTriggerOrphanScan()`), `scripts/lib/orphan-reaper.mjs`, `scripts/lib/config/reaper.mjs` (`_parseReaper`). PRD `docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md`, Epic #1425, issue #1432.
1630
+
1631
+ ## Gate Timeout — Path B (#1425 A3)
1632
+
1633
+ The quality gate has two execution paths. Path A (`scripts/lib/quality-gate.mjs`) has always had a 900 000 ms cap. Path B — `scripts/run-quality-gate.mjs` → `scripts/lib/gates/gate-{baseline,incremental,full,per-file}.mjs` — had **no timeout at all**, which is how a wedged `tsgo` two levels down could run unbounded and be reparented to PPID 1. This block sets the committed ceiling for path B so both paths may run equally long.
1634
+
1635
+ ```yaml
1636
+ gate:
1637
+ timeout-path-b-ms: 900000 # 15 min; same cap as GATE_TIMEOUT_MS on path A
1638
+ ```
1639
+
1640
+ | Field | Type | Default | Description |
1641
+ |-------|------|---------|-------------|
1642
+ | `gate.timeout-path-b-ms` | integer (> 0) | `900000` | Per-command wall-clock ceiling for gate path B. A zero or negative value falls back to the default rather than being honoured — an instant-kill ceiling is worse than the uncapped state this key exists to fix. |
1643
+
1644
+ **Precedence, highest first:** `SO_GATE_TIMEOUT_MS` (operator override, per run) > `gate.timeout-path-b-ms` (committed) > `900000`. `run-quality-gate.mjs` resolves the three and publishes the ANSWER to the gate sub-script as the distinct env var `GATE_TIMEOUT_MS` — writing it back into `SO_GATE_TIMEOUT_MS` would make a committed default indistinguishable from an operator decision for every process further down the tree. The gate's own outer wrapper ceiling is derived from the same resolved number plus a 60 s reserve, so the inner per-command kill always fires first and the gate can still print its JSON envelope.
1645
+
1646
+ **Used by:** `scripts/run-quality-gate.mjs`, `scripts/lib/gates/gate-{baseline,incremental,full,per-file}.mjs`, `scripts/lib/config/gate.mjs` (`_parseGate`). PRD §4 A3, Epic #1425, issue #1432.
1647
+
1506
1648
  ## Config Protection (#622)
1507
1649
 
1508
1650
  `PreToolUse` `Edit`/`Write` guard that intercepts edits to a small allow-list of quality-gate config files (eslint, vitest, tsconfig, prettier, commitlint, gitleaks) and warns — or, in `strict` mode, blocks — when an edit LOOSENS a gate (a threshold lowered, a disable/ignore directive added, a rule removed, a gitleaks allowlist widened, tsconfig strictness relaxed). The edit-tool analogue of the test-the-mock gate-cheating anti-pattern (see `.claude/rules/testing.md`). First-time file creation, tightening edits, and neutral edits are always allowed regardless of mode.
@@ -1741,7 +1883,7 @@ persona-gate-wave:
1741
1883
  after: quality # quality | impl-polish — wave name after which to fire
1742
1884
  threshold: "all" # "M-of-N" | "all" | "N-of-N" — passed to parseThreshold
1743
1885
  personas: [] # list of persona names from .claude/personas/; empty = all catalog
1744
- dispatch-model: claude-opus-4-7 # alias or full model ID — default 'claude-opus-4-7'
1886
+ dispatch-model: opus # alias or full model ID — default 'opus'
1745
1887
  mode: off # off | warn | strict
1746
1888
  ```
1747
1889
 
@@ -1751,7 +1893,7 @@ persona-gate-wave:
1751
1893
  | `persona-gate-wave.after` | string (`quality` \| `impl-polish`) | `quality` | The wave role after which the hook fires. The hook runs once per session, immediately after the named wave's STATE.md update. |
1752
1894
  | `persona-gate-wave.threshold` | string | `"all"` | Voting threshold passed to `parseThreshold()` from `scripts/lib/persona-panel/threshold.mjs`. Accepts `"all"`, `"any"`, or `"M-of-N"` where `1 ≤ M ≤ N ≤ 20`. Example: `"6-of-6"` requires every persona to vote PASS; `"4-of-6"` allows two dissenters. |
1753
1895
  | `persona-gate-wave.personas` | string[] | `[]` | Roster of persona names to dispatch. Each entry must match `^[a-z0-9-]{1,64}$` and refer to a persona file under `.claude/personas/<name>.md`. When the list is empty (default), every persona in the catalog is dispatched. |
1754
- | `persona-gate-wave.dispatch-model` | string | `claude-opus-4-7` | Model used for each persona agent dispatch. Accepts the same shape as agent frontmatter `model:` — one of `inherit` \| `sonnet` \| `opus` \| `haiku`, or a full model ID like `claude-opus-4-7`. |
1896
+ | `persona-gate-wave.dispatch-model` | string | `opus` | Model used for each persona agent dispatch. Accepts the same shape as agent frontmatter `model:` — one of `inherit` \| `sonnet` \| `opus` \| `haiku` \| `fable`, or a full model ID like `claude-opus-5-5`. Default is the alias `opus`, which tracks the current Opus; pin a full ID only for a named reproducibility need. |
1755
1897
  | `persona-gate-wave.mode` | string (`off` \| `warn` \| `strict`) | `off` | Behaviour on consolidator result. `off` skips dispatch entirely even when `enabled: true` (silent no-op). `warn` consolidates and logs findings under a `Persona-gate:` bullet in the wave progress update without blocking. `strict` consolidates and on any non-PROCEED verdict prompts the operator via `AskUserQuestion` to proceed-as-is, revise remaining waves, or abort. |
1756
1898
 
1757
1899
  **Validation:**