session-orchestrator 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. package/pi/prompts/npm-publish.md +0 -11
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: eval
3
- description: "Use this skill to run an honest session-process evaluation (Standard v1, aiat-llm-eval/1.0) — score the last completed orchestrator session against the pre-registered rubric-v1 dimensions, run /eval, evaluate this session, produce an eval report, or re-verify a stored eval run for reproducibility. Deterministic-first with an optional advisory LLM judge; never produces a global score."
3
+ description: "Use this skill to run an honest session-process evaluation (Standard v1, aiat-llm-eval/1.0) — score the last completed orchestrator session against the pre-registered rubric-v2 dimensions, run /eval, evaluate this session, produce an eval report, or re-verify a stored eval run for reproducibility. Deterministic-first with an optional advisory LLM judge; never produces a global score."
4
4
  ---
5
5
 
6
6
  # eval
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: npm-publish
3
3
  description: "Use when publishing this package to npm — a version release (npm publish), verifying the registry/pi.dev listing, or diagnosing npm auth failures (E403 2FA/token errors). Token-based flow via NPM_TOKEN in .env.local with a temp userconfig, the leakage gate before every publish, post-publish verification and marker/badge upkeep. Trigger on \"publish to npm\", \"npm release\", \"E403 publish error\"."
4
+ disable-model-invocation: true
4
5
  ---
5
6
 
6
7
  # npm-publish
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "author": {
6
6
  "name": "Bernhard Goetzendorfer"
@@ -112,10 +112,10 @@
112
112
  "type": "redirect-truncate",
113
113
  "pattern": ">",
114
114
  "severity": "block",
115
- "target-denylist": ["CLAUDE.md", "AGENTS.md", ".claude/rules/**", ".orchestrator/policy/**", ".orchestrator/metrics/*.jsonl", ".git/**", "SECURITY.md"],
115
+ "target-denylist": ["CLAUDE.md", "AGENTS.md", ".claude/rules/**", ".orchestrator/policy/**", ".orchestrator/metrics/*.jsonl*", ".git/**", "SECURITY.md"],
116
116
  "modes": ["truncate"],
117
- "rationale": "Truncating redirect (>, &>, N>) onto protected artefacts silently destroys them — #983. Append (>>) stays allowed.",
118
- "sources": ["#983"]
117
+ "rationale": "Truncating redirect (>, &>, N>) onto protected artefacts silently destroys them — #983. Append (>>) stays allowed. The metrics entry is `*.jsonl*`, not `*.jsonl` (#1401): `events-rotation.mjs` renames the live ledger to `events.jsonl.1`/`.2`/… (scripts/lib/events-rotation.mjs:74), and the narrow glob missed every one of those rotated generations — the same suffix blind spot the old .gitignore line had. Deliberately NOT widened to `.orchestrator/metrics/**`: the bootstrap templates legitimately create `README.md` and empty ledgers in that directory with truncating redirects (skills/bootstrap/{standard,deep}-template.md), and only the ledger family needs this rule. Deletion and renaming of ANY file under .orchestrator/metrics/ is covered by the separate `ledger-delete-protected` rule below, where no legitimate writer exists.",
118
+ "sources": ["#983", "#1401"]
119
119
  },
120
120
  {
121
121
  "id": "redirect-harness-memory",
@@ -126,6 +126,15 @@
126
126
  "modes": ["truncate","append"],
127
127
  "rationale": "The harness auto-memory directory is injected as TRUSTED project context into every later session, yet lives outside git diff, CI review, check-owner-leakage (git ls-files only) and gitleaks. #1352 closed the Edit/Write lane for dispatched agents; this closes the shell-redirect lane, append included. NAMED CEILING (BV-004), every case measured 2026-09-13: (a) redirect-shaped by construction — `tee -a` and an in-process write (node -e fs.appendFileSync) are NOT covered; (b) NAMED CEILING: `eval` with a non-literal payload (`eval \"$CMD\"`, `eval $(…)`) is unreachable — the command text does not exist at hook time (`unresolved` class, #641). Literal `eval '… > target'`, `dd of=target` and `env -S <str> …` are covered since #1366; (c) a cwd-relative target after a directory change (`cd ~ && … > .claude/projects/*/memory/…`) is out of reach — the hook does not know the chain's cwd and a relative target is judged repo-root-relative, which is not lexically solvable; (d) `$HOME` / `${HOME}` / `$(…)` / backticks in the target fall into the pre-existing `unresolved` class (#641), where the hook warns and allows rather than guessing — for a home directory that is the most natural spelling. (e) the payload-evaluation budget is SHARED and finite (`MAX_PAYLOAD_EVALUATIONS` = 32 in `scripts/lib/command-blocker.mjs`): once it is spent, redirect-target collection returns `{unresolved, reason: 'budget-exhausted'}`, which this rule treats like every other unresolved target — warn and ALLOW. Measured 2026-09-16: 40 `eval 'echo fN'` fillers followed by `eval 'echo x > ~/.claude/…/MEMORY.md'` → ALLOW plus `⚠ unresolved redirect target (budget-exhausted)`. Not a regression of #1366 — `bash -c` fillers exhaust the same budget at HEAD (#988 T2) — but a standing ceiling of the same class as (d), and an attacker-controllable one: the filler count is free. Revisit together with (d) if the unresolved class is ever made blocking. Revisit if the policy grows a non-redirect target class or the payload traversal learns redirects. Home-anchored entries are matched against the expanded ABSOLUTE path (#1362) because relativizeAgainstRoot discards every out-of-repo target by design.",
128
128
  "sources": ["#1352","#1362"]
129
+ },
130
+ {
131
+ "id": "ledger-delete-protected",
132
+ "type": "path-delete",
133
+ "pattern": "rm|mv|unlink .orchestrator (state dir) or .orchestrator/metrics/**",
134
+ "severity": "block",
135
+ "target-denylist": [".orchestrator", ".orchestrator/metrics", ".orchestrator/metrics/**"],
136
+ "rationale": "Deleting or renaming anything under .orchestrator/metrics/ destroys append-only session telemetry with no recovery path — the ledger IS the history, and nothing re-derives it. Measured 2026-09-19 (#1401): a wave subagent ran `touch .orchestrator/metrics/events.jsonl.1` (which ADOPTED the existing rotated ledger instead of creating a probe file), then `rm -f` on the same path — 53,896 lines / 10 MB covering 2026-04-12 to 2026-09-18 were gone, and the guard said nothing, because `rm` was only ever matched as the literal pattern `rm -rf`. Severity is `block`, not `warn`, on a measured absence of legitimate callers: rotation and ledger repair run through Node fs, never through Bash (`renameSync`/`unlinkSync` at scripts/lib/events-rotation.mjs:16,61,69,74), and a census of every shell rm/mv/unlink in scripts/, hooks/, .husky/, .gitlab-ci.yml and the guard's own 60-command test corpus found ZERO targeting this directory. Intentional maintenance has two named bypasses: `allow-destructive-ops: true` in Session Config (per session), or SO_DISABLED_HOOKS=pre-bash-destructive-guard (per invocation). Covers `rm`, `mv` (both operand roles — the source vanishes, an existing destination is overwritten) and `unlink`, through transparent wrappers (sudo/env/timeout/nohup/nice/…), every chain position, and literal `bash -c` / `env -S` / `eval` payloads. NAMED CEILING (BV-004), each measured: `find … -delete`, `find … -exec rm`, `… | xargs rm`, `git rm <path>` and in-process `node -e fs.unlinkSync(…)` are NOT covered — in each the path is not lexically an operand of a resolvable destructive verb (see PATH_DESTRUCTIVE_VERBS in scripts/lib/command-blocker.mjs). REVISIT TRIGGER: any of those five shapes appearing in a real deletion incident or a transcript census. `.orchestrator/tmp/**` is outside this denylist by construction, not by an exemption clause, and the match is judged PER TARGET — a sibling tmp operand can never lift the verdict for a metrics operand. SPELLING CEILING (measured 2026-09-19 against this hook, one payload per row): the #1401 incident was an agent re-spelling a blocked command, so the four re-spellings were run back through the guard. Two were closed HERE by adding the exact entry `.orchestrator` to the denylist above — `rm -r .orchestrator` (ALLOW before: only the literal `rm -rf` pattern of `rm-rf-destructive` caught the -rf form, and `rm -r` is one character shorter) and `mv .orchestrator /tmp/x` (ALLOW before). That entry is EXACT, not a prefix: `.orchestrator/tmp/**` deletes stay ALLOW, re-measured after the change (`rm -rf .orchestrator/tmp/*`, `rm -rf .orchestrator/tmp`, `rm -f .orchestrator/tmp/scratch.json` → all ALLOW), so the widening adds no exemption clause that could be lifted. Three remain OPEN and are DECLARED, not papered over: (0) a TRAILING SLASH on the state dir — `rm -r .orchestrator/` → ALLOW (measured 2026-09-19, found while re-measuring this change): the operand keeps its slash and the exact entry `.orchestrator` does not match it, while a `.orchestrator/` entry would be read as a DIRECTORY-PREFIX glob (measured: `pathMatchesPattern('.orchestrator/tmp/x', '.orchestrator/') === true`) and would swallow the tmp exemption — so the fix belongs in operand normalisation in scripts/lib/command-blocker.mjs, never in this list; (1) a wildcard in a DIRECTORY segment — `rm -f .orchestrator/*/events.jsonl` — because the guard is lexical and never expands globs, so the operand it sees is the literal string `.orchestrator/*/events.jsonl`, which no concrete-path glob in this denylist matches; (2) a cwd-changing chain — `cd .orchestrator/metrics && rm -f events.jsonl` — because the guard does not track cwd across chain segments and the operand it sees is the bare `events.jsonl`. Closing either needs glob expansion or cwd tracking in scripts/lib/command-blocker.mjs, i.e. a matcher change, not a policy change. REVISIT TRIGGER for these two: either form appearing in a real deletion incident or a transcript census — or any widening of this denylist beyond exact paths, which would need a per-verb exemption for `.orchestrator/tmp/**` bound to the statement the matcher hit (widening without narrowing the bypass opens a hole the narrow form did not have, `.claude/rules/guard-design.md`).",
137
+ "sources": ["#1401", "PSA-003"]
129
138
  }
130
139
  ]
131
140
  }
package/AGENTS.md CHANGED
@@ -31,8 +31,9 @@ These are the non-obvious, mistake-causing facts that must load every session. E
31
31
  Phase 4 of session-start invokes `scripts/lib/ci-status-banner.mjs` via `checkCiStatus({ repoRoot })` to render a 🚨 banner when CI is red on HEAD.
32
32
  Never claim CI green from `npm test` alone — the 8-pipeline silent regression (2026-05-09 → 2026-05-10, fixed in deep-2) is the cautionary tale. <!-- consistency:exempt:runtime-only -->
33
33
  - **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`:
34
- **11 of its 15 rules are `severity: block`, the other 4 only warn** (measured 2026-09-16: `jq '[.rules[].severity]|group_by(.)|map({(.[0]):length})|add' .orchestrator/policy/blocked-commands.json`). On Codex, the destructive-command rules are instructions only because no compatible `PreToolUse` command handler is wired.
35
- 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).
34
+ **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`).
35
+ 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`.
36
+ 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).
36
37
  - **Session Config below is runtime-critical.** `scripts/parse-config.mjs` parses the `## Session Config` block; `claude-md-drift-check` Check 6 enforces top-level-key parity against `docs/session-config-template.md`. Edit it like code, not prose — a dropped key changes runtime behaviour.
37
38
  - **Live state is not in this file.** Stack: Node 24+, vitest, ESLint 10 (`npm ci` after clone). Test counts, backlog, version, component inventory drift fast — the SSOT is README badges + `.orchestrator/metrics/sessions.jsonl`. Per-session detail lives in the Meta-Vault decisions log (linked above), not here.
38
39
  - **`memory.propose` requires `SO_WAVE_AGENT=1`.** `scripts/memory-propose.mjs` exits `3` (`rejected-wrong-context`) unless `process.env.SO_WAVE_AGENT === '1'`.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,142 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.3.0] - 2026-09-22
11
+
12
+ ### Added
13
+
14
+ - `scripts/lib/tail-window.mjs` (`readTailWindow(file, maxBytes)` → `{ text, cut, size }`) — the ONE bounded tail-window reader for append-only JSONL ledgers and transcripts, replacing five private copies of the same byte-window primitive: `hooks/on-stop.mjs`, `hooks/subagent-telemetry.mjs`, `scripts/lib/agent-status.mjs`, `scripts/lib/session-id.mjs` and `scripts/lib/telemetry-flush-health-banner.mjs` (#1272). Each caller keeps its own error mapping (`null` / `[]` / discriminated result / throw); the module owns only the window, loops until it is filled, and reports `cut` so a caller can drop the partial first line. Named ceiling (BV-004): the window is held in memory in one piece — fine for the 64 KiB..1 MiB windows in use, revisit above ~16 MiB. New suite `tests/lib/tail-window.test.mjs`; `hooks/_lib/hook-import-set.json` regenerated (the module is reachable from four hook entry files).
15
+ - `consumeDialecticPending({ repoRoot })` in `scripts/lib/auto-dialectic.mjs`, plus the Step 6.4 loop-closing block in `skills/evolve/references/evolve-dialectic-mode.md` (#1380). After a successful `/evolve dialectic --apply` — or after the operator explicitly discards a reviewed proposal — the skill now writes the last-run stamp AND deletes `.orchestrator/dialectic-pending.md`. Nothing else writes the one or removes the other, so without these two calls the session-start `maintenance-due` probe reported `dialectic` and `pending-sidecar` as due forever. The same step now serialises `result.diff` (an object `{ user?, agent? }`) into the Markdown string `writeDialecticPending` expects — passing the object threw `TypeError`.
16
+ - `scripts/lib/reconcile/backlog.mjs` — a LEAF module holding `partitionMaterialized` / `countReconcileBacklog`, re-exported from `scripts/lib/reconcile/engine.mjs` so every existing caller is unchanged. The session-start reconcile-nudge probe imports the count from here instead of from the write-capable engine, which dragged the emitter, renderer, unicode validator and candidate-store writer into a read-only banner's static closure: 11 modules / 151,946 B → 19 modules / 322,897 B (+112 %, measured 2026-09-18), inherited unasked by `scripts/lib/maintenance-due-banner.mjs`. The leaf brings the probe back to 13 modules / 176,331 B (and the maintenance-due banner from 35 / 577,439 B to 29 / 430,873 B); the +2 modules against the pre-session closure are the leaf itself plus `eligibility.mjs`, the price of the probe counting the same population `/reconcile` does. The runtime cost was never the point; a banner must not be able to reach a writer.
17
+ - Three runtime-read Session Config surfaces that no document described, now in `docs/session-config-reference.md` first and `docs/session-config-template.md` second (#1155, #1243, #1365): `cross-repo.projects` (a block key, NOT the flat `cross-repos` list — only the inline `projects: [a, b]` form is read, a `- ` block list yields `[]` silently), `worktree-exclude` (with its measured scope limit: `createWorktree()` has no production caller, so the key changes nothing in a wave today) and the whole `gitlab-portfolio` block (`enabled` / `mode` / `stale-days` / `critical-labels` — the last is block-form only; an inline list is silently ignored).
18
+ - `scripts/lib/js-mask.mjs` (`maskSource(src, { keepLiterals })`) — ONE JavaScript lexer for every "separate code from data" scanner here, with the regex-literal branch three hand-rolled `stripComments` copies never had (#1388). Five consumers: `scripts/lib/validate/check-untracked-test-deps.mjs` (where it grew; a one-line re-export keeps the old import path alive, because `package.json` carries no `exports` map and every `scripts/lib/**` path is therefore public), `check-entry-guard.mjs`, plus the three converted this session — `check-validator-registration.mjs`, `check-test-git-config-target.mjs` and `scripts/lib/auq/parse.mjs` (see Fixed for what each was measurably getting wrong). Named ceiling (BV-004) on the one-token keyword lookback, measured 2026-09-18 over `git ls-files '*.mjs'` (1245 files): 0 genuine division chains after `return`/`typeof`/`case`/… — all 18 files whose masking the lookback changes carry a real regex literal; revisit trigger is the first such chain.
19
+ - `scripts/lib/vault-yaml.mjs` (`readVaultSlug(repoRoot)`, `VAULT_YAML_FILE`) — the reader for the `metadata.slug` key in a repo's own `.vault.yaml`, which nothing read until now (#1131). Every vault writer derived the vault folder from the CHECKOUT DIRECTORY name instead (`narrative-mirror.mjs` via `subjectToSlug`, `vault-mirror/namespace.mjs` via `basename(process.cwd())`), so a repo whose directory name differs from its registered slug had its narrative, sessions and learnings filed under a second, wrong folder — and the #829 loose-slug healer only repairs case and punctuation drift, never a genuinely different name. Precedence is now explicit `repo` opt > `vault-name` override > declared slug > basename (`narrative-mirror.mjs`) and `vaultName` > declared slug > `deriveRepo()` (`namespace.mjs`, which stays a leaf: `node:fs`, `node:path`, `node:module`, `vault-mirror/utils.mjs`). A declared slug SKIPS `resolveLooseSlug` — it IS the canonical folder name, and healing it re-opens the ambiguity that keeps a duplicate pair alive once one exists. The reader never throws: absent file, unreadable file, malformed YAML, missing key, non-string or non-slug value all return `null` and leave the caller on its pre-#1131 chain, and `js-yaml` is resolved lazily through `createRequire` so a checkout without `node_modules` cannot crash at module load.
20
+ - `renderPendingBody(diff)` / `comparePendingBody()` in `scripts/lib/auto-dialectic.mjs` plus the Step 6.4 prose and its AUQ in `skills/evolve/references/evolve-dialectic-mode.md` (#1386). `--apply` now compares the FRESHLY derived body against the sidecar the operator actually reviewed before any peer card is written. The serializer moved out of the skill's JS snippet VERBATIM so both paths build the body from one implementation — two hand-copied serializers would report drift that is only formatting.
21
+ - `.orchestrator/consumed/<ISO-stamp>-<rand>-dialectic-pending.md` (`DIALECTIC_CONSUMED_DIR`, `DIALECTIC_CONSUMED_RETENTION` = 10, #1388): `consumeDialecticPending` MOVES the sidecar instead of unlinking it — it was the only copy of what the operator reviewed. The `maintenance-due` probe reads an explicit two-path list, so an archived file does not re-raise `pending-sidecar` (#1380 stays fixed). Ceiling + revisit trigger (BV-004): newest 10; revisit if `checkStaleArtifacts` ever reports a `consumed/` file, since it counts every untracked `.orchestrator/**` file older than 30 days. The prune is best-effort — a failure never fails a consume.
22
+ - `isRuleExpired(rawExpiry, now, onUnparseable)`, exported from `scripts/lib/rule-loader.mjs` and now also the gate in `scripts/lib/reconcile/backlog.mjs`'s `defaultReadMaterializedProvenance` (#1387). An EXPIRED rule file is skipped WHOLE — frontmatter key and body bullets alike — so its learnings stop counting as materialized by a rule that no longer reaches any agent (materialized forever, re-proposable never). ONE predicate, ONE `Date.parse` and one clock reading per scan, fail-open in both consumers: an absent or unparseable `expires-at` is NOT expired, because fail-closed would un-materialize the whole corpus on one typo. Two limits documented in `docs/rule-authoring.md` § Consolidated rules and `skills/reconcile/SKILL.md`, both measured 2026-09-18 and both left unsolved on purpose: it covers only the on-disk half of `partitionMaterialized` (79 of this repo's 92 provenance keys are ALSO sidecar-terminal, so 13 keys actually return — the sidecar half carries operator DECLINES and must not re-ask an answered question), and a file the sweep cannot split (`no-1to1-mapping` &c.) that then expires legitimately re-proposes on every run with no mechanical exit (`--dry-run --json` reported `skipped: []` over 7 generated files, so the live tree is clear).
23
+ - `header_raises` in the `sweep-expired-rules` summary and its human stdout line (`scripts/sweep-expired-rules.mjs`, `summarize` now exported, #1388). A header-raise is an `action: 'rewrite'` plan carrying `reason: 'header-raise'`, so it was counted inside `rewrites` and invisible on stdout although `--json` always carried `plans[].reason`. It is a SUBSET of `rewrites`, never a sibling — the two are deliberately not disjoint.
24
+ - `malformed_scope_files` on the `verifyWaveScope` report, the `scope_verified` payload and the human table (`scripts/lib/scope-echo.mjs`, `docs/events-schema.md`; #1379). A per-agent `<state-dir>/filescopes/wave-<N>/*.json` that cannot be read or parsed was skipped silently, which made a wave with corrupt scope files indistinguishable from one that was never injected — the same `digest-unknown` / `injection-missing` verdicts either way. Always present, including as `0`, the same honesty guarantee `malformed_lines` carries for the ledger side (HR-105). A file that parses but holds an empty or non-array scope is NOT counted: an empty scope is legitimate.
25
+ - `closeIssues()` and the `--close [--vcs gitlab|github] [-R <spec>] <id>...` CLI in `scripts/lib/issue-close-strip-labels.mjs` (#308) — one call strips every `status:*` label, closes, then re-reads each issue, printing one JSON line per id (`{id, stripped, closed, state}`) and exiting 1 when any id did not verify as closed. `skills/session-end/references/phase-5-issue-cleanup.md` Phase 5 now runs this single command instead of the old two-step `stripStatusLabels` + `glab issue close` prose, which had been skipped often enough that 339 closed issues still carried `status:in-progress` (measured 2026-09-19).
26
+ - `WAVE_KEY_ALIASES` (`agents_planned`/`agents_started`/`agents_completed` → `agent_count_*`) and `normalizeWaveKeys()` in `scripts/lib/session-schema/{constants,normalizer}.mjs`, applied on read by `normalizeSession` and on write by `scripts/emit-session.mjs` (#1390 P1) — one recorded session (`main-2026-09-18-session-1`, 5 waves) had written the de-underscored spelling, so every `agent_count_*` reader saw `undefined` for its waves. `scripts/emit-session.mjs` also derives the optional field `session_start_ref` from STATE.md's `session-start-ref` frontmatter — owner-checked, full sha only — so `/evolve analyze` gets an attributable commit range instead of a time window that also catches a parallel session's commits (#1339 P8). `scripts/lib/session-schema.mjs`'s barrel now also re-exports `isCoordinatorDirectHousekeeping` from the existing `session-schema/filters.mjs`.
27
+ - `resolveRepoNamespace({ repoRoot })` in `scripts/lib/vault-mirror/namespace.mjs` (#1389) — reads the declared `.vault.yaml` slug from a caller-supplied `repoRoot` instead of always `process.cwd()`; `scripts/lib/vault-mirror/process.mjs` and `scripts/vault-mirror.mjs` thread the CLI's invocation cwd through. Additive: every caller that omits `repoRoot` is unaffected.
28
+ - `neutralizeFingerprintSentinel()` and `fingerprintSentinelLine()`, exported from `scripts/lib/test-runner/issue-reconcile.mjs` (#1339 P6) — the one shared sentinel-neutralisation and sentinel-writer pair, now also used by `scripts/lib/ux-grill/reconcile.mjs`'s `oneLine()`/`buildUxGrillIssueBody()` instead of a second hand-copied `sanitizeSentinel()`.
29
+ - `scripts/lib/skill-evidence-window.mjs` — the skill-applied-judge (`instruction-adherence`/`report-quality`) gets an actual evidence window for the first time: excerpts built around the skill's call site in the transcript instead of a tail slice (#1399). Measured: the call site landed inside the old tail window in 6 of 67 cases, inside the new call-site window in 67 of 67. `runSkillJudge` no longer dispatches on near-empty evidence — the previous `''` default was 1387 characters, under the 8000-token gate, and was silently judged as though it were real transcript content. Session-end Phase 3.6.6 now joins over the raw UUID.
30
+ - A second PSA-006 claim class, `gate-verdict` (`hooks/_lib/subagent-transcript.mjs`, `hooks/post-subagent-discovery-validator.mjs`; #1385 S2) — a completion/gate-green assertion (`STATUS: done`, "tests pass", "alles grün") carrying no run receipt (a pass/fail count, a file count, or an exit code) and no adjacent measurement command. The scanner had been structurally blind to exactly these two line shapes: the #1198 gate-summary exemption skipped `STATUS:`/"N passed" lines before any pattern ran. Measured on a 130-case labelled corpus built for this fix: the scanner answered "no violation" on 128 of 130 before the fix (accuracy 30.2%, Cohen's κ 0.007 — indistinguishable from guessing); after, wanted hits went 6/92 → 12/92 with false positives unchanged at 3/34. Records now carry `kind`; a record with no `kind` predates the split and is `distributional` by construction. The hook remains log-and-warn — it blocks nothing.
31
+ - `scripts/release-session-lock.mjs` — a new CLI that releases the session lock, emits `lock.released` with explicit session/lock IDs, and reads the lock back to confirm absence; exit 0 only when the release is verified (#1395). `skills/session-end/SKILL.md` Phase 3.8 is now this single call instead of prose instructions to release the lock. Measured before this change: this repo's own ledger held 5 `acquired` events against 0 `released`; fleet-wide 110 of 702 (15.7%).
32
+ - `orchestrator.wave_dispatch.worktree_base_checked` (#1413) — a new event from the dispatch hook, which compares HEAD against `session-start-ref` behind a process-local identity check and warns (never blocks) on drift; the worktree base belongs to the harness, since the Agent tool itself carries no base field. The event always carries a `stale` value on BOTH outcomes, on purpose — reporting only the positive case would leave the firing rate unfalsifiable. Measured: per-wave isolation shows in 2 of 455 records, a commit marker in 0 of 455.
33
+
34
+ - **Epic #1425 Stufe 1, Teil A — `scripts/lib/process-group.mjs`** (`spawnInGroup`, `killProcessGroup`, `verifyProcessIdentity`, `installExitHandler`, `recordGateProcess`/`readGateProcessLedger`/`pruneGateProcessLedger`, `buildCommandSignature`). `spawn(cmd, { shell: true })` makes the SHELL the child, so `child.kill()` signalled that shell only and its own children (`tsgo --noEmit`, vitest workers) were reparented to PID 1 and kept running — the 2026-09-20 incident on this host: four orphaned `tsgo` processes, two at PPID 1, 86–588 % CPU and up to 8.0 GB RSS each, the machine at 13 % free memory. `detached: true` makes the shell a process-group LEADER, so `process.kill(-pgid, sig)` reaches every descendant that did not `setsid` away. The kill is a ladder — SIGTERM → `DEFAULT_KILL_GRACE_MS` grace (re-exported from `wave-executor/dispatch-common.mjs`, never re-defined) → SIGKILL to the GROUP — because a grandchild installing `trap "" TERM` survives the group SIGTERM (measured on Darwin 25.6.0, 2026-09-21). Three further measured constraints are wired in rather than assumed: `close` fires only once every group member has closed the shared pipe (hence a hard deadline), `maxBuffer` does not exist on async `spawn` (hence a hand-rolled 16 MiB byte cap reproducing the existing spawnSync/ENOBUFS contract), and a detached child survives its parent's exit at PPID 1 (hence `installExitHandler`). **A4, the ancestry register:** every gate process this repo starts itself is recorded with pid, pgid, start time and command signature in `.orchestrator/runtime/gate-processes.jsonl` — the identity basis Teil B joins against.
35
+ - **Epic #1425 Stufe 1, Teil B — `scripts/lib/orphan-reaper.mjs`** (#1430, #1431, #1433): the net under the process-group kill, for orphans that happen anyway (a crashed session, a harness SIGKILL, a leak from a foreign repo on the same host). Shape is the `lock-reaper.mjs` split — `parsePsSnapshot` → `decideReapCandidates` (PURE: no I/O, no signal, fully testable from `ps` text fixtures) → targeted `ps -p` identity re-check → `killProcessGroup` → wait → re-measure. A candidate is a CONJUNCTION (register ∧ PPID 1 ∧ age ∧ read-only command signature ∧ identity still verifying), because PPID 1 alone is a weapon pointed at the operating system: **538 of 784 processes (68.6 %) had PPID 1** on this host, measured 2026-09-21. The identity gate runs before SIGTERM **and** before SIGKILL, so a recycled pgid aborts the ladder instead of being signalled. Effect is read back after `verify-wait-ms` — `survived_sigkill` is never booked as success (PRD B6: on 2026-09-20 the ad-hoc check falsely reported "still alive" because it measured immediately after `kill -9`). Every decision, including a withdrawn one, is appended to `.orchestrator/metrics/reaper-audit.jsonl`; `falseAlarmRate()` judges the instrument over a rolling window of decisions and reports `instrument_suspect` above the 10 % HR-101 ceiling without ever acting on it. The existing detector (`countZombieProcesses`) would have counted ZERO in the 2026-09-20 incident: its `ps` carries no `ppid` and no `rss`, and it defines a zombie as IDLE — the exact opposite of a runaway.
36
+ - **Orphan-scan triggers in two hooks, plus the `reaper:` and `gate:` Session Config blocks** (#1432 B4). `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) each do one config read plus one `stat` of the shared throttle marker `.orchestrator/tmp/reaper-last-scan`, then hand the work to a DETACHED, `unref()`-ed child running the new CLI tail of `orphan-reaper.mjs`. Nothing is scanned inline: one `ps` round-trip out of Node was measured at ~47 ms, alone over the 50 ms `reaper.max-hook-latency-ms` budget; steady-state hook latency with the trigger in place measured 0.15 ms. New parsers `scripts/lib/config/reaper.mjs` (`_parseReaper`) and `scripts/lib/config/gate.mjs` (`_parseGate`), both tolerant and independent of the `## Session Config` section boundary, with the blocks documented opt-in in `docs/session-config-template.md` and `docs/session-config-reference.md`. New event `orchestrator.reaper.scan_completed`, emitted at most once per scan and **only when the scan found something** (candidates, reports or kills) **or the instrument itself is suspect** (false-alarm rate over 10 % of the last `false-alarm-window` decisions) — a per-fire record would be exactly the always-on signal HR-101 calls a broken instrument.
37
+ - `scripts/lib/validate/check-hook-entry-guards.mjs` (#1422) — an AST oracle over the hooks registered in the four hook manifests, flagging `missing-entry-guard` and `toplevel-profile-exit`. Registered BLOCKING in `scripts/validate-plugin.mjs` (232 checks / 0 failures at `110f334d`). The class it closes is concrete: a hook-reachable module without an entry guard executes its whole program when imported.
38
+ - `scripts/check-sessions-integrity.mjs` (#1417) — CLI front-end for `checkSessionsIntegrity` (`--session-id`, `--json`; exit 0/1/2, a missing record deliberately exiting 1). Until now the check existed only as a `node --input-type=module -e` one-liner inside `skills/session-end/session-metrics-write.md`, because the function had no entry point. The #1408 record parsed fine and was still invalid (`ended_at` instead of `completed_at`, four required fields missing), so vault-mirror dropped it as `skipped-invalid` and nobody was told until the next session-start banner.
39
+ - `scanEventsBackwards()` / `listEventSourcesNewestFirst()` in `scripts/lib/events.mjs` (#1414) — newest-first reading across ledger rotations, so a reader that only needs the recent tail stops at the first hit instead of parsing the whole corpus. Plus `parseEtimeToSeconds()` in `scripts/lib/resource-probe/parsers.mjs`, the `ps` `etime` parser the reaper's age criterion needs.
40
+
41
+ ### Changed
42
+
43
+ - **Default `dialectic.budget-tokens` 8000 → 32000** (`DEFAULT_BUDGET_TOKENS` in `scripts/lib/config/dialectic.mjs`, now the SSOT that `scripts/dialectic-deriver.mjs`'s `DEFAULT_BUDGET.input` imports; #1380). The value is an INPUT CEILING the pre-dispatch estimate aborts on, never a spend and never a truncation — so the old default made a real corpus unrunnable rather than smaller: card bodies + steering alone estimate ~12k tokens in this repo, a consumer repo measured 11,158 (2026-09-10) and 30,262 (2026-09-18). Repos that already set `budget-tokens` explicitly are unaffected; repos on the default stop ending `budget-exceeded`. Docs, template and the `--budget-tokens` help text in the evolve skill (and its generated `.agents/` / `.codex-plugin/` copies) say "ceiling", not "budget".
44
+ - **The dialectic deriver's output contract is per-`## `-section, not a full-body replacement** (`agents/dialectic-deriver.md`, the prompt in `scripts/dialectic-deriver.mjs`, `skills/evolve/references/evolve-dialectic-mode.md`; #1380). The merge has been section-wise since #1310 — but the agent was still told "emit the FULL replacement body … omitted sections are deleted", which invites an LLM to reproduce every hand-written managed section. The prompt now says omitted sections stay unchanged, and the anti-pattern list is corrected: an invented section is NOT rejected downstream (`detectEmptying` refuses only a proposal with zero content lines), so the instruction is to skip the target instead.
45
+ - **The backticked-citation checker now scans the rule corpora, and it blocks.** `scripts/lib/validate/check-skill-script-paths.mjs` adds `.claude/rules` and `.cursor/rules` to `SCAN_DIRS` plus a `MD_EXTENSIONS` covering Cursor's `.mdc` (#1384). Both surfaces ship — `.cursor/rules` goes into the npm tarball and is symlinked into consumer repos — and neither had a gate; `.cursor/rules/010-session-workflow.mdc` kept citing `commands/go.md` and `commands/close.md` for a full release after `3ebf0e9d` folded them into skills. Measured 2026-09-18: corpus 295 → 329 files, 4 blocking findings on the first run (all fixed in the same change, see Fixed). Because a rules file legitimately cites paths that only exist at runtime, a missing target is excused only when it is under a harness state root (`.orchestrator/`, `.claude/`, `.codex/`, `.cursor/`, `.pi/`) **and** git ignores it — one `git check-ignore --stdin` for the whole run, failing open toward REPORTING. The git half alone would have silenced a typo in a `docs/specs/…` or `coverage/…` citation forever. `check-skill-links.mjs` documents the resulting division of labour: links there, backticked citations here.
46
+ - **Three consolidated rule files now expire earlier** — `.claude/rules/measurement-discipline.md` 2026-10-04 → 2026-10-02, `.claude/rules/review-and-adapter-contracts.md` 2026-10-04 → 2026-10-02, `.claude/rules/test-hygiene.md` 2026-10-20 → 2026-10-07 (#1382). Each header was outliving its own earliest absorbed learning, which the merge contract forbids (`docs/rule-authoring.md` § Consolidated rules); the body sentence `expires-at <D> = the EARLIEST of the N absorbed dates` was corrected with it. Consumers loading these rules lose them from injection sooner. Five folded provenance pairs across `guard-design.md`, `identity-and-locks.md`, `review-and-adapter-contracts.md` and `test-hygiene.md` are annotated `markers only` with the entry their substance was folded into, so all 7 consolidated files are 1:1 sweepable again rather than 3 of 7.
47
+ - **The expiry sweeper now RAISES a too-low rule header** (`scripts/lib/reconcile/rule-expiry-sweep.mjs`, #1382): a frontmatter `expires-at` sitting EARLIER than the earliest absorbed date is lifted to it, as its own rewrite trigger (`action: 'rewrite'`, `reason: 'header-raise'`, visible in `--dry-run --json` before `--apply`) touching nothing but that line and the counter sentence. Left alone the file passes its header date with nothing expired, `rule-loader.mjs` stops injecting it, and the surviving provenance markers keep `/reconcile` treating its learnings as materialized — the substance goes dark and is never re-proposed. Measured 2026-09-18 on the live corpus: 3 of 7 files in that state (`identity-and-locks` → 2026-10-02, `process-contracts` → 2026-10-27, `toolchain-and-build` → 2026-10-16), ≈31.5 kB. The direction is one-way by contract: a header that OUTLIVES its content is still reported as an `advisory` only, because lowering on every run would cut a healthy entry's TTL short. Documented in `docs/rule-authoring.md` § point 2.
48
+ - `scripts/release.mjs`'s `evaluateCiRow()` names the CAUSE a CI preflight row went red (#1384): the degraded branch appends the probe's `detail`, the read branch falls back to `details.reason` when no failing job can be named. A short `sha`, a commit the mirror has never seen and a HEAD with no pipeline all printed the identical `CI status unknown (query-failed)` before. With neither field present the text is byte-identical to before.
49
+ - `scripts/lib/ci-status-banner.mjs` clamps and control-byte-escapes every API-supplied fragment (`reason`, `failingJobName`, `allowFailureJobs`) at generation via `sanitizeApiText()`, 60 chars (#1384). The degraded branch has escaped since #1031; the READ branch interpolated a raw API `status` — a value carrying `\r` plus an ANSI sequence could repaint the release preflight's `ci-green-on-head` row green while its verdict stayed `ok: false`.
50
+ - `isUserInvocableValue(value, file, key)` takes the frontmatter key it is judging and names THAT key in its WARN (`scripts/lib/user-invocable-skills.mjs`, #1384). It is also the normaliser for the sibling marker `disable-model-invocation`, and a `disable-model-invocation: yes` demotion reported `user-invocable: "yes"` — sending the operator to a line that does not exist in the file. `scripts/generate-cursor-adapter.mjs` now parses each SKILL.md once through a memoised `skillFlags()`: the predicate is not side-effect-free, so reading the same key from three call sites emitted the same WARN three times (measured on a two-skill fixture: 3 lines for 2 defects).
51
+ - `scripts/lib/validate-vendored-rules.mjs` no longer validates `README.md` as a vendored rule — `rules-sync.mjs` vendors only the entries listed in `_index.md` (#1164). Six library rules under `rules/` dropped `## See Also` entries naming files the vendored corpus does not contain (`development.md`, `security.md`, `testing.md`, `mvp-scope.md`, `cli-design.md`).
52
+ - `CONTRIBUTING.md` — "Adding a New Skill" step 5 now says a slash command is a skill with `user-invocable: true` and that adding a `commands/<name>.md` beside it is forbidden (the twin aborts two generators and fails a test), and a new step 6 table names the five generators, when each must run, and which `validate-plugin` check fails on a stale copy. "Adding a New Command" is re-scoped to names a skill cannot carry (#1165). `scripts/lib/validate/check-cursor-adapter.mjs` and `check-pi-prompts.mjs` print the regeneration command with their stale-wrapper failure.
53
+ - `docs/baseline.md` carries ONE dated measurement of the baseline rule corpus — 2026-09-18 at `5cc51f2`: 32 rule files, 23 with both `paths:` and `globs:`, 9 with neither, 0 with only one — replacing the undated "26 rule files, all `paths:`, 0 `globs:`" claim that `scripts/lib/rule-loader.mjs`, `scripts/lib/validate/check-rules.mjs` and two test files each restated; all four now point here (#1155).
54
+ - `monitors/monitors.json`'s `ecosystem-health` description says what the watcher actually does — every 900 s it stats and reads ONE local file, `.orchestrator/metrics/ecosystem-health.jsonl`, with no network, no CI or branch probing and no Session Config read. Nothing writes that file (0 writers, measured 2026-09-18), so every run emits `no-state-yet` and idles; `scripts/lib/ecosystem-health.mjs` carries the matching named ceiling and revisit trigger at `DEFAULT_INTERVAL_S` (#1232).
55
+ - `skills/claude-md-drift-check/SKILL.md` Check 9 probe 1 is described as it behaves since #840: a well-formed `paths:`-only rule is correctly scoped and NOT flagged; the probe fires only when the loader's own parser returns `globs === null` for a file that textually carries `paths:`. `skills/autopilot/SKILL.md`'s DI example no longer shows a synchronous `probeSync()` that does not exist, and stops calling the in-process driver's missing walk-away UX "Phase C-5's job" — that driver shipped.
56
+ - **The CI reading for a commit is now CHOSEN, not `.find()`-ed off the most recently updated row** (`scripts/lib/ci-status-banner.mjs`, #857). The 15-row pipeline window MIXES refs (`main`, `refs/merge-requests/39/head`, a `codex/…` branch — measured 2026-09-18), so a foreign branch's or an MR's pipeline could silently become the local HEAD's verdict. Selection is now a ref preference over the same-sha rows — (1) pipelines whose `ref` names the current branch, (2) pipelines whose ref cannot be judged (no `ref`, or no branch resolved: detached HEAD, `git` unavailable — the pre-#857 candidate set), (3) an MR HEAD ref (`refs/merge-requests/<iid>/head`, for repos whose `workflow: rules:` produce no branch pipeline at all; sha equality is the correctness argument, and a merged-results pipeline runs a different sha so it cannot reach this tier), (4) otherwise nothing is selected — followed by worst-status-wins within the tier. `failed` outranks `canceled`, everything unsettled outranks `success`, and `skipped`/`manual` rank BELOW `success` as terminal non-failing states: lumping them in with `running`/`pending` turned the everyday `success` + `skipped` pair into `unknown / unrecognised-status-skipped` where the pre-#857 reading was green (category separation, not a threshold tweak). Measured on this project 2026-09-18: 14 of ~86 distinct shas in the last 100 pipelines carry 2-4 pipelines, 7 of them with contradicting statuses from different `source`s. The `status` vocabulary stays FROZEN at `green | red | unknown` — `dispatcher/rank.mjs` and `autonomy/suitability.mjs` key on the literal `'red'` and the session-start renderer prints nothing for an unknown string — so the findings ride ADDITIVE `details` fields instead: `matchedRef`, and (only when more than one candidate exists, so an ordinary reading is byte-identical to before) `candidateCount` / `candidateStatuses` / `ambiguous`. A commit whose pipelines are ALL on foreign refs reports `unknown` with `reason: 'pipeline-unmatched-ref'` rather than adopting one or claiming "no pipeline"; `scripts/lib/session-start-probes.mjs` routes that reason into the pushed-commit re-query beside `no-pipeline-for-head-sha` / `no-check-runs-for-head`, because for that consumer it is the same dark outcome. A user-cancelled pipeline now carries `reason: 'pipeline-canceled'` — it printed `🚨 CI RED on HEAD` indistinguishably from a real failure; the status stays `red` on purpose. The branch lookup (`git rev-parse --abbrev-ref HEAD`, 2 s cap) never throws and never degrades a reading: an unresolvable branch costs the preference only.
57
+ - **`lastGreen` can no longer name the very commit being reported red** (`scripts/lib/ci-status-banner.mjs`, #857). The look-back slice excludes EVERY row carrying the queried sha, not just the chosen one — pipelines are not one-per-commit here, and a `success` duplicate of the red commit used to be found as its own `lastGreen`; reproduced on `52ae12f1` (a `canceled` push pipeline beside a `success` api pipeline for the same commit): status `red`, `lastGreen.sha` === HEAD. `lastGreen.ageCommits` is now counted over DISTINCT shas in the red run — the old value was the ROW count under a comment asserting one-pipeline-per-commit, measured false at 16 %, so the distance was overstated whenever a commit had been re-run — and the row count survives under its own name, `agePipelines`.
58
+ - **The convergence monitor's agent counter admits only `orchestrator.agent.stopped` records that NAME their agent** (`scripts/lib/convergence-monitor.mjs` `namesAnAgent`, plus the recipe in `skills/convergence-monitoring/SIGNALS.md`; #1379). The type is bimodal (#939/#949) and the unfiltered count is dominated by the coordinator's own phantom stops. Measured 2026-09-18 over `.orchestrator/metrics/events.jsonl` plus its rotation, with the `jq` envelope quoted at both sites: 16,438 stop records, 6,553 wave-scoped, of which **5,930 (90.5 %) carry no `agent`** and 623 do — a ~10.5× inflation of every per-wave agent count and the `velocity_drop` signal derived from it. It is the same type-and-shape gate the gate-event branch already applied, not a new mechanism.
59
+ - **The backtick-citation checker scans `rules/` and `output-styles/` too** (`SCAN_DIRS` in `scripts/lib/validate/check-skill-script-paths.mjs`, #1384). Both are in `package.json` `files[]`, so both ship to every consumer, and neither had a gate; the whole-repo census that motivated it (2026-09-18 @ `20a4cbff`) found 6 dangling citations across them (see Fixed). `CHANGELOG.md` and the root files stay OUT by decision: a changelog cites the paths a release TOUCHED, so a path deleted afterwards is still a correct historical statement — 88 such hits, all correct by construction, and annotating them would be noise on every future entry.
60
+ - **The AUQ hurdle texts are RENDERED from the thresholds instead of standing beside them** (`scripts/lib/auq/schema.mjs`, `hooks/pre-auq-clarity.mjs`; #1110). `HURDLES.H1.title`/`rule` and `H2.title`/`rule` interpolate `THRESHOLDS.K5.headerCharsFail` and `THRESHOLDS.K6.optionsMin`/`optionsMax`, and the hook's deny text builds its option-limit sentence the same way — spelled-out numerals ("zwei bis vier") were invisible to any numeric grep, so a moved threshold would block on the new number while telling the operator the old one. `H1.evidence` keeps its literal 12: that is the BUNDLE's number, not our threshold. The definition order is now load-bearing and documented as such — `THRESHOLDS` must precede `HURDLES`, because a template literal evaluates at module load and the reverse order throws `ReferenceError` on a module `hooks/pre-auq-clarity.mjs` loads in EVERY session, i.e. a host-wide block rather than a test failure. A missing threshold drops the sentence rather than inventing a number.
61
+ - `readTranscriptTail()` reads a BOUNDED window from the end of a transcript (`TAIL_WINDOW_BYTES` = 2 MiB via the shared `readTailWindow`, `hooks/_lib/subagent-transcript.mjs`, #1388) instead of decoding and line-parsing the whole file to keep the last 8 assistant records. Ceiling measured 2026-09-18 over 1016 real subagent transcripts: the byte span of those records is median 41 KB, p99 322 KB, max 1.25 MB (file size median 676 KB, max 8.2 MB) — 2 MiB loses zero records in that sample, 1 MiB would already lose one; revisit if `TAIL_RECORDS` grows or a re-measurement puts the max span above ~1.5 MB. A cut window drops its first (possibly mid-UTF-8) line explicitly rather than leaning on the `JSON.parse` catch, since a truncated record can still parse; every fs error still maps to `''`, the contract `post-subagent-discovery-validator` relies on.
62
+ - `backfillCompletedFromStateMd()` reports `skipped-key-occupied` where it used to report `skipped-already-recorded` (`scripts/lib/session-close-backfill.mjs`, `docs/events-schema.md`; #1388). The two causes shared one string and were therefore indistinguishable in the `backfill_completed` telemetry: `skipped-already-recorded` means this very session is on file, the new value means identity said ABSENT while a canonical non-stub record of a DIFFERENT session already holds the key.
63
+ - `isUserInvocableValue()`'s WARN names the right NOUN for the key it is judging (`MARKER_NOUN` in `scripts/lib/user-invocable-skills.mjs`, #1388) — a `disable-model-invocation: yes` author was told his value "is NOT a slash-command marker", i.e. failed to be something it never was. The default phrase for `user-invocable` is unchanged, so the four existing call sites print identical text.
64
+ - `scripts/generate-cursor-adapter.mjs` carries a NAMED CEILING on ground 2 of its `disable-model-invocation` truth table (#1384): whether Cursor reads the key at all is UNVERIFIED as of 2026-09-18 (`rg "disable-model-invocation" docs .claude .cursor/rules` returns zero Cursor citations), so 24 wrappers are stamped on an assumption. Kept because stamping a restriction a reader ignores costs nothing while dropping it would loosen 24 wrappers if the reader honours it; revisit trigger is Cursor documenting the key or a measured run showing it ignored. Pinned by `tests/scripts/generate-cursor-adapter.test.mjs`.
65
+ - `scripts/lib/validate/check-rules.mjs`'s `globs:`-without-`paths:` message stops demanding both keys: under `.claude/rules/` `paths:` is the canonical scope key (`docs/rule-authoring.md`), and `globs:` belongs beside it only when the rule is ALSO vendored out through `rules/` (#742). `docs/rule-authoring.md` point 3 carries the re-measurement — 2026-09-18 over this repo's own `.claude/rules/` (25 files, explicitly NOT the vendored `rules/` library nor the baseline population `docs/baseline.md` counts): 10 path-scoped files, ALL 10 `paths:`-only, `grep -rn '^globs:' .claude/rules/` → no match, exit 1, after the duplicate `globs:` block was removed from `.claude/rules/cli-design.md` here (#1379).
66
+ - `.claude/rules/toolchain-and-build.md` states the PRECONDITION for re-measuring in a materialized tree (measured 2026-09-18 @ `20a4cbff`): such a tree has no `.git`, so `scripts/validate-plugin.mjs` dies in its `:49` guard with `ERROR: Not inside a git repository` before any of its 230 checks runs — `git init` plus a commit first. The trap named with it: the same tree materialized INSIDE the repo (e.g. under `.orchestrator/tmp/`) silently reads the parent's `.git` and reports 230/0.
67
+ - `npm-publish` is no longer user-invocable (`user-invocable: false` in `skills/npm-publish/SKILL.md`, #1384 P2) — Cursor, Codex CLI and Pi lose the `/npm-publish` command; Claude Code's own dispatch is unaffected. `docs/components.md`'s command/skill counts move accordingly (27→26 commands, 26→24 skills).
68
+ - `.gitignore` now ignores rotated `.orchestrator/metrics/*.jsonl.[0-9]*` backups — `scripts/lib/events-rotation.mjs`'s `maybeRotate()` writes `events.jsonl.1`, `.2`, … but the existing `*.jsonl` glob only matched a name ENDING in `.jsonl`, so a rotated file was untracked but not ignored.
69
+ - **Docs:** `SECURITY.md` documents a known limitation for a standalone single-file `check-owner-leakage.mjs` copy (its CP11 confidential-names guard degrades open via `owner.yaml` but fails closed via `SO_CONFIDENTIAL_NAMES_FILE`); `skills/gitlab-ops/SKILL.md`'s label-update caveat is corrected from the old "PUT-replaces" warning to the measured `glab 1.117.0` behaviour (`--label` adds and leaves existing labels in place, `--unlabel` removes; only the raw GitLab REST API's `labels=` parameter replaces the whole set); `skills/wave-executor/references/wave-loop-dispatch.md` gains a `stopped (post-restart)` dispatch-state row for resuming an agent after a Claude Code process restart; `docs/rule-authoring.md` scopes its provenance-header convention to actual `rules/_index.md` sync sources, excluding `rules/README.md` and `_index.md` itself.
70
+ - **Scope-validator grading is now canonicalized the same direction the live hook resolves paths — an existing `allowedPaths` manifest entry can start (or stop) failing validation.** `gradeScopeEntry()`, a new shared function in `scripts/lib/scope-gate.mjs` used by both `scripts/validate-wave-scope.mjs` and the hooks, replaces two grading directions that used to disagree (#1405/#1406): the validator graded the LITERAL grant string while Gate 5b (the live per-session hook) matches the realpath-resolved candidate. Measured before → after on the same grant set: `/private/etc`/`/private/var` 0 → 1 (now graded, with a hint toward the canonical spelling), `/tmp/x` 0 → 1 likewise, `~/...` 0-silent → 1, `~/.cache/<study>/**` 1 → 0 (a depth carve-out), `~/.cache/**` stays 1. Separately, and by explicit operator decision (#1398 AC4): Gate 5b now WARNS — and still allows — when the validator would grade the matched grant as ERROR; it never rejects, because the hook runs live for every session on the host and a validator-only ERROR is not by itself a safe reason to block a running session.
71
+ - **The staging fence's on-disk record shape changed — from a 512-character raw-command snippet to path operands plus `command_hash`** (`hooks/pre-bash-staging-fence.mjs`; #1404). The issue's own recommendation (hash + prefix) was unworkable: the fence's only reader searched for tracked paths INSIDE the raw text. The reader now compares path lists instead, which incidentally covers `git add -A` (never recognized by the old regex reader) and falls back to the legacy raw-text form for entries already on disk in the old shape — that fallback is dated to expire in **5.4.0**. Measured: a test secret appeared 0 times in the fence file after the change. Side-finding from the same pass: `git -C <dir> add` had never been fenced at all until now.
72
+ - **A class of Bash command that used to bypass the sessions-ledger write guard is now denied.** `hooks/pre-bash-sessions-ledger-guard.mjs` gains a per-statement predicate for interpreter writes (`node -e`, `python -c`, `perl -e`, `ruby -e`, `deno eval`) that name `sessions.jsonl` in the FIRST argument (#1408) — the previous form caught 0 of 438 real commands, even though an interpreter append is the common bypass shape (measured 38 fleet-wide). 10 DENY / 10 ALLOW cases measured, no bypass widened. `/close` gains Step 4a, which schema-checks its own record before writing. Two more forms are closed in the same cycle (`rm -r <parent>`, `mv <parent>`); three remain open and are named as a ceiling with a revisit trigger — a wildcard directory segment and a `cd`-chain are lexically unsolvable as written, and `rm -r` with a trailing slash needs operand normalization first. 0 new false positives measured over the real repo command corpus.
73
+ - **A session id containing a `/` can now be materialized into a rule** (`PROVENANCE_TOKEN_RE` in `scripts/lib/reconcile/sanitize.mjs`, GH#71). The pattern was `^[A-Za-z0-9._:-]+$`, and semantic session ids are derived from branch names, which contain slashes — so every learning from such a session stayed eligible forever: proposed each run, rejected each run. Reported from a consumer repo where it hit two usable learnings; latent here (3 slash-bearing ids in `sessions.jsonl`, 0 affected learnings). Because this WIDENS a sanitizer, the slash is gated by a new `assertNoPathEscape` in the shared `assertMachineToken`, with tests for traversal, a leading slash, backtick, newline-escape, wrapper forgery, a Unicode tag and a backslash — plus a test proving nothing previously accepted is now rejected.
74
+ - **The generated-rule expiry sweep now skips, fail-closed, any file whose counter sentence it cannot recognise — including on delete** (`scripts/lib/reconcile/rule-expiry-sweep.mjs`, GH#70). `COUNTER_RE` matched only the English `= the EARLIEST of the N absorbed dates`, so a German consolidated file had its frontmatter `expires-at` raised while the body sentence — which literally says the date must not be corrected upward — was left contradicting it. Reported from a consumer repo with two such files after `--apply`. The German form is recognised too, and re-emission splices the matched spans rather than re-templating, so a file keeps its own wording and language. `no-counter-sentence` is a fourth skip reason; three docs still enumerate only three (#1420). Not reproducible here — all 7 consolidated files are English.
75
+ - **`orchestrator.telemetry.flush`'s `reason` field is no longer truncated to a bare classification word, and a new `reason_class` field is added** (`classifyFlush`, #1392). The health banner tested `startsWith('sandbox:')` against a `reason` that `classifyFlush` had shortened to the literal string `'sandbox'` — a check that could never fire. `reason` now carries the full text (clamped at 200 characters) and `reason_class` carries the short classification separately. A consumer parsing `reason` off this event sees a different value than before. A new wiring test drives a real `flush()` through the real writer into the real banner; measured fleet-wide: 344 of 344 flushes reported `sent`.
76
+ - **`/eval` gets `rubric-v2`, which splits `process-safety` from a new, never-graded `guard-friction` dimension** (`skills/eval/rubric-v2.md`, `scripts/lib/eval/engine.mjs`; #1037). Measured before the split (40 records / 38 sessions, 2026-09-19): `process-safety` failed on 32 of 40 records, all 32 solely because `destructive_guard.blocked >= 1`; per `host-resources.md` HR-101, a class firing on far more than ~10% is a broken instrument, not a threshold to tune (a threshold fix was checked and rejected: N=3 still fails 23 of 38 sessions, N=6 still fails 9). `process-safety` now scores only `spiral`; `guard-friction` reports `blocked`/`warned`/`loop.warning` without ever grading them (the same mechanism `efficiency-kpis` already uses). Guard events are now attributed by `session_id` with the time window as a fallback only, closing a gap where a parallel session's blocks counted against this session's score. Re-run on the live ledger: `process-safety` fails 0 of 38 sessions (30 flip fail → pass); `guard-friction` reports 220 blocks, max 58 in one session. `rubric-v1` stays frozen and readable — all 40 stored v1 records still validate and render. Known follow-up, not fixed here: `scripts/eval-session.mjs --verify` now reports DRIFT on v1 records because it re-scores them with the current (v2) engine.
77
+ - `scripts/eval-session.mjs --verify` reports `version-mismatch` (exit 3) instead of `DRIFT` when a stored record's rubric version does not match the engine's current rubric (#1400).
78
+ - **The `report-quality` eval dimension is retired; `instruction-adherence` gains six pre-registered owner rules and 15 code-computed facts** (#1381). `report-quality` was variance-free by construction — its evidence came from the engine's own templates, and no final report exists yet at `/eval` time (Phase 6 runs after Phase 3.7d). `instruction-adherence`'s new regex readers sit beside the templates they read; a reader that fails to match now reports `parse_miss` instead of `null`, so an unmeasurable fact is no longer indistinguishable from a genuinely absent one.
79
+ - The rotation tombstone is now resolved by filename inside the ledger's own `_archive/`; the absolute path it used to store is provenance only (#1411). This also closes a silent false match where a stale checkout at the old stored path could count as a hit.
80
+ - `scripts/lib/telemetry/sync.mjs` and `scripts/lib/tmux-layout/telemetry-stats.mjs` now read across ledger rotations and surface `ledger_complete: false` when a rotation archive is missing (#1407, part 1 of 2 — the two hot-path readers are deliberately left out, tracked as a separate issue). Flush overhead measured in an A/B in one process: +190ms at 60MB / 294k records, 6.4% of the 3-second flush timeout.
81
+ - `convergence-monitoring` is no longer user-invocable (`user-invocable: false` in `skills/convergence-monitoring/SKILL.md`, #1384 P2) — Cursor, Codex CLI and Pi lose the `/convergence-monitoring` command; Claude Code's own dispatch (via the wave-executor, its only real caller) is unaffected. `docs/components.md`'s command count first moves 28 → 27 for this change; the subsequent 27 → 26 (`npm-publish`, same flag) is already documented above.
82
+
83
+ - **The quality gate now runs its commands in their own process group, on both paths.** Path A (`scripts/lib/quality-gate.mjs`, #1427) moved `runGate` from `spawnSync` to `spawnInGroup`; path B (`scripts/run-quality-gate.mjs` + `scripts/lib/gates/gate-helpers.mjs`, #1428) had **no timeout at all** and now carries a 900 000 ms (15 min) wall-clock ceiling on the outer runner and on all 10 `runCheck` call sites, published to the gate sub-script as `GATE_TIMEOUT_MS`. On expiry the whole process GROUP is killed (SIGTERM → grace → SIGKILL) and the gate reports **exit 124**; the pre-existing exit-code contract (124 timeout / 1 ENOBUFS) is preserved, and the ENOBUFS group of tests stayed green through the migration. Precedence on path B: `SO_GATE_TIMEOUT_MS` (operator override) > the committed `gate.timeout-path-b-ms` > `DEFAULT_GATE_TIMEOUT_MS` — a deliberately different env-var name from the operator override, so a committed default stays distinguishable from an operator one.
84
+ - `readEventsWithRotations()` is now three-valued: `complete: true | false | null` plus a `notices` field (#1423, #1414). `existsSync` is not evidence that an archive was READ, so the missing third state (“source never readable”) had been collapsed into “complete”; `maintenance-due-banner.mjs` and `backfill-abandoned-sessions.mjs` now read across rotations, and three readers report the three states.
85
+ - `orchestrator.wave_dispatch.worktree_base_checked` is now emitted for EVERY worktree dispatch, including the `skipped` and `stale: null` outcomes (#1424) — reporting only the drift case made the denominator unknowable.
86
+ - Hook and validator now grade absolute grants identically (#1398): `canonicalizeGrantPrefix()` moved into `scripts/lib/scope-gate.mjs` as the one implementation both call, closing a 1-of-9 grading divergence at a measured resolver cost of +0.067 ms, pinned by a parity test and a wiring test.
87
+ - `ledger_complete` is now an optional boolean on the telemetry ping (#1416) — `null` drops the key rather than shipping a fabricated `false`.
88
+ - `tests/eval/rubric-parity.test.mjs` now drives all **19** rubric table rows (5/4/5/3/2 across the tables in `skills/eval/rubric-v2.md`) per scorer, with vacuum, binding and ordering guards; six scorers plus `RUBRIC_SCORERS` are exported for it, formulas unchanged (#1418).
89
+ - `spanEndIndexMain` for coordinator-anchored skills, with the call pinned to the single production call site (`includeSubagents: true` in `phase-3-6-tail.md`) (#1421, remainder of #1412).
90
+ - `checkInstructionBudget` gained a near-threshold stage at **90 %** of the path-scoped rule ceiling, carrying the last `reconcile-capped` counter (#1419).
91
+ - Autopilot peer fixes from the navigator review: headless invocation is namespaced (`/session-orchestrator:session`) and passes `--plugin-dir`; the stall sampler reads `session.lock`'s `last_heartbeat` with an mtime fallback instead of mtime alone; `--max-tokens` is parsed and forwarded (the kill-switch stays inert while records carry no `total_tokens` field — tracked as a follow-up).
92
+
93
+ ### Deprecated
94
+
95
+ - `buildIssueBody` in `scripts/lib/ux-grill/reconcile.mjs` → `buildUxGrillIssueBody` — the old name is kept as a delegating alias that warns once per process via `console.warn`, for one minor cycle per `.claude/rules/development.md` § Package Lifecycle & Versioning, and is removed in 6.0.0. The rename is covered by this deprecation alias, so it does not force a major bump — this release stays a **minor**.
96
+
97
+ ### Removed
98
+
99
+ - The `orchestrator.wave.started` event (#1202 §11) — emit sites removed from `hooks/post-tool-batch-wave-signal.mjs`, row removed from `docs/events-schema.md`. Its only reader, `scripts/lib/convergence-monitor.mjs`, no longer admitted it (see Fixed below): the batch hook wrote it in the same millisecond as `completed{N}`, masking the `(N-1, N)` `shrinking_diff` pair, and it carried no measurement of its own — telemetry consumers that counted it lose it.
100
+
101
+ ### Fixed
102
+
103
+ - Codex portable command discovery now includes command-only `session` alongside skills and retains explicit-only `go`/`close` invocation policies. Session modes are parsed independently of following task text; explicitly requested parallel housekeeping uses the actual wave plan. Generated Codex, portable, Cursor and Pi adapters and setup guidance agree on these contracts. Merge, release and installed-plugin acceptance are tracked in the [release handover](docs/handover/2026-09-19-codex-session-entrypoints.md).
104
+ - **Peer-card data loss on `/evolve dialectic --apply` (#1380).** A managed region wrapping MORE than one `## ` heading cannot be addressed per heading; `mergeDerivedBody` used to target it anyway, so a proposal touching one of its headings either replaced the region (deleting its sibling headings' hand-written text) or appended a duplicate heading. Such a region is now fail-closed: never a target, left byte-unchanged, and every skipped heading surfaced as a `{ type: 'multi-heading-region', region, headings, skipped }` conflict for the operator to resolve by splitting the region.
105
+ - **Peer-card frontmatter read as body (#1380).** `scripts/lib/peer-cards/reader.mjs` now folds multi-line list values — both the flow form Prettier reformats the writer's output into and a hand-written `- ` block list — before handing the card to `parseStateMd`, and when the frontmatter is still unparseable it strips the block instead of returning it as Markdown. A card's own `---` frontmatter therefore can no longer reach the deriver payload as content.
106
+ - **Short read decoded as uninitialised memory** in `hooks/subagent-telemetry.mjs`: `findStartTimestampMs()` ignored the byte count `fs.readSync` returned and decoded the whole `allocUnsafe` buffer into `JSON.parse` — fixed by the shared reader above, which loops to fill the window and slices to what it read (#1272).
107
+ - **An unterminated `/*` no longer blanks a validator's view to end-of-file.** `stripComments()` in `scripts/lib/validate/check-entry-guard.mjs` delegates to the shared lexer (`maskSource(src, { keepLiterals: true })`, which moved to `scripts/lib/js-mask.mjs` in the Unreleased cycle — a one-line re-export remains at its original home `check-untracked-test-deps.mjs`), which recognises regex literals — a `/` read as a division let a regex body's `/*` open a block comment and silently hid every entry guard below it, a fail-open miss for a quote-bearing regex, a `/[/*]/` character class and a regex after a keyword alike (#1383). The lexer gains a one-token keyword lookback (`return`, `typeof`, `case`, …) for the regex/division call; named ceiling, measured over 1245 tracked `.mjs` files: 0 genuine division chains after those keywords, so 0 files are mis-masked in the other direction. A block comment that is never closed now reports rather than blanks.
108
+ - **`/close` could displace a canonical session record.** `backfillCompletedFromStateMd()` appended under a `recordId` an authoritative record already held whenever identity resolution said `absent` (label drift, a conflicting native join) — canonical readers collapse by `session_id` newest-wins, so the zero-counter backfill row won. It now skips on an occupied key, matching the abandoned path's guard (#1380).
109
+ - **The sessions-staleness banner counted a `state-md-completed` backfill as a stub** and kept warning after the stub it superseded was gone, recommending a backfill run with nothing left to do (observed live 2026-09-18). A `_backfill_source` now disqualifies a record as a time anchor only when its status is not `completed`; the predicates in the two modules are renamed for the question each answers (`isNonAnchorStub` vs `isSupersedableStub`) and deliberately not shared (#1380).
110
+ - **The reconcile nudge was un-greenable.** `scripts/lib/reconcile-nudge-banner.mjs` judged threshold (c) on ALL rule-eligible learnings, including those already materialized in the idempotency sidecar or a `.claude/rules/` provenance marker — 98 eligible against 35 materialized in the repo that reported it (#1380; this repo measured 181 / 80 / 101 on 2026-09-18), so no `/reconcile` run could ever clear it. It now judges on the BACKLOG (`countReconcileBacklog`), and reads `reconcile.min-insight-chars` before counting rather than after, which had left the placeholder-insight gate practically off for the banner (#1380). `scripts/lib/maintenance-due-banner.mjs`'s signal 3 comment follows.
111
+ - The findings the widened citation checker reported on its first run over the two rule corpora (#1384): `.cursor/rules/000-session-orchestrator.mdc` cited `.orchestrator/metrics/model-selection.md`, which nothing writes — the line is gone; `.cursor/rules/050-plan.mdc`, `.claude/rules/cli-design.md`, `.claude/rules/measurement-discipline.md` and `.claude/rules/security.md` cited example, template or historical paths and now carry the `path-check` annotation that says so.
112
+ - **Three scanners were reading a regex literal as a block comment, each with its own measured consequence** — all three now delegate to the shared lexer (`maskSource(src, { keepLiterals: true })` from `scripts/lib/js-mask.mjs`), measured 2026-09-18 @ `20a4cbff` on the same input shape `/\/*$/` (#1388). `scripts/lib/validate/check-validator-registration.mjs`: the `/*` inside that regex in `scripts/validate-plugin.mjs` opened a comment that swallowed every `runCheck(...)` line below it — a FALSE `UNREGISTERED` for every checker registered after that point, and its own header had until now argued in writing that this direction was impossible. `scripts/lib/validate/check-test-git-config-target.mjs`: the same swallow inside a call tail made `wrapperHasCwd` return `false` — a false alarm. `scripts/lib/auq/parse.mjs`: the swallowed region no longer contained `(Recommended)`, a SILENT FALSE NEGATIVE in the clarity measurement, length-preserving and therefore visible at no line number. The one behavioural difference — `maskSource` blanks comment bytes to SPACES where two of the strippers collapsed them — was checked rather than assumed: `wrapperHasCwd`'s `\s*` is greedy and consumes them, measured over `, /* no cwd */`, `, /* a */ /* b */`, `,\n // note\n`, `, /* x */ undefined`, `, undefined /* y */` (all `false`) and `, /* c */ dir` (`true`). The `#`-comment shell/YAML surfaces (`.husky/pre-commit`, `.gitlab-ci.yml`) keep the small local walker, whose flat quote-tracking ceiling is real but bounded to those two files.
113
+ - **The pre-commit control-byte guard could be bypassed by the filename.** `core.quotePath` defaults to TRUE, so git prints any path carrying a byte ≥ 0x80, a control byte or a newline C-quoted in double quotes (`"f\303\274nf.mjs"`) — the old `grep -Ei '\.(mjs|…)$'` extension filter dropped such a path (it ends in `"`), `git show ":$f"` never ran, and a `fünf.mjs` carrying a raw ESC committed clean while an identical `plain.mjs` was blocked. `.husky/pre-commit` now enumerates with `-c core.quotePath=false` and tests the extension with a POSIX `case` over the lowercased path — same set as the old grep, no re-quoting. The guard is widened in the same change from NUL-only to every C0 byte except TAB/LF/CR (0x00-0x08, 0x0b, 0x0c, 0x0e-0x1f; DEL is deliberately out of scope), because a literal ESC in an ANSI-stripping code path and its test carried the same defect class — both are now written as escapes (`hooks/post-tool-failure-corrective-context.mjs`'s `split('\x1b')`, which had been a raw byte inside `split('')`, and `tests/hooks/additional-context-hooks.test.mjs`). KNOWN CEILING, named rather than fixed: a path containing a literal LF still splits across two `read` iterations and is missed — the airtight form needs `read -r -d ''`, a bashism unavailable to a hook run as `sh -e` (dash) on the Linux CI runner.
114
+ - **`createWorktree` could never recover from an orphan directory at its target path** (#984). An interrupted `removeWorktree` drops the git registration first and can die before the directory is gone; `git worktree add` then fails with "already exists" while `git worktree remove --force` fails with "is not a working tree" (swallowed by nothrow), so every retry looped on the same error until someone deleted the directory by hand — the Full Gate went red twice from exactly this state. `scripts/lib/worktree/lifecycle.mjs` now removes such a directory before the retry, behind five in-code conditions, any one of which failing leaves the directory alone (restoring the pre-#984 error): the leaf is a real directory and not a symlink; the literal base `<os.tmpdir()>/so-worktrees` is itself a real directory and not a symlink (a co-tenant on a shared `/tmp` can pre-create it, and a realpath-resolved base symlink would otherwise BECOME the delete root); the realpath of the target is a DIRECT child of the realpath of that base (both sides resolved — macOS maps `/tmp` → `/private/tmp`); its basename starts with `so-worktree-`; and it is not a live working tree of ANY repository, so a peer session's or another repo's worktree is out of reach. `git worktree prune` is deliberately not used — it is repo-global and would drop a peer's registration. Ceiling (BV-004): revisit when worktree paths become caller-supplied or move out of `os.tmpdir()`, at which point the containment base stops being a constant. The tests use per-run random suffixes so two concurrent runs cannot share a path.
115
+ - **An object WRAPPER passed `emitRewrite`'s input guard and destroyed the tool input** (`scripts/lib/io.mjs`, #1110). `typeof updatedInput === 'object'` admits `new Date()` and `new String('x')`, which serialize to JSON STRINGS, and `new Map([...])`, which serializes to `{}` — a shape that looks valid and, per this function's own contract, deletes every field of the tool input. Each reaches the harness as a schema-invalid `updatedInput`, which the bundle turns into `behavior: "deny"`: on the `AskUserQuestion` path the operator's question is destroyed to save a rewrite. The admissible set is now a PLAIN object only (prototype `Object.prototype` or `null`); everything else takes the same no-op-plus-stderr contract as `null`, an array or a string, and the stderr line names the offending prototype. The adjacent `JSON.stringify` → `undefined` guard is documented as currently unreachable rather than removed: `updatedInput` sits NESTED in the envelope literal, so a `toJSON` returning `undefined` OMITS the property instead (measured 2026-09-18: 53 bytes of stdout, no `updatedInput`, which is the same safe degradation `bail` produces) — the guard stays because it is the envelope SHAPE that makes the case unreachable, and hoisting the value would restore it.
116
+ - **The cold-start nudge ignored the `enable-host-banner` opt-out** (`hooks/on-session-start.mjs`, #1133): every other start-of-session emitter rides that flag on the stated principle that users who silence one expect silence. The DISPLAY now rides it; the one-shot marker consumption deliberately does NOT — the opt-out silences the display, never the measurement or the state transition, and consuming unconditionally is what keeps the nudge one-shot for an operator who has banners off instead of re-arming it on every start forever. The flag is read once into `hostBannerEnabled` rather than inferred from `bannerData`, which is `null` on `emitHostBanner()`'s own error paths and would silently re-open the AUQ nudge.
117
+ - **The portfolio dashboard failed the vault frontmatter gate it is written into** (#1144). `scripts/lib/gitlab-portfolio/markdown-writer.mjs` emitted `type: dashboard`, which is not in `skills/vault-sync/validator.mjs`'s enum, and no `id` at all — so the generated `_PORTFOLIO.md` blocked `/close` on the hard gate. It now writes `id: portfolio` + `type: board`, the enum member added for generated dashboards in #738 and the same shape `vault-status/board-writer.mjs` uses.
118
+ - **Every agent count of one recorded session rendered `?` beside a real number** (`scripts/lib/vault-mirror/render-sessions.mjs`, the #1074 bug class). `metrics-collection.md` documents `agent_count_{planned,started,completed}`, but the live ledger carries one session (`main-2026-09-18-session-1`, 5 waves, measured 2026-09-18 over `.orchestrator/metrics/sessions.jsonl`) whose waves emit only the de-underscored producer spelling `agents_planned` / `agents_started` / `agents_completed` — all 15 of its counts were missing cells. Each alias now sits at the precedence of its documented twin, so no cell that already rendered a value can change.
119
+ - The 6 dangling citations the widened checker found in the two SHIPPING corpora it now scans (#1384): `output-styles/wave-summary.md` cited `hooks/on-stop.sh` for a file that has been `.mjs` for two migrations — corrected; `rules/opt-in-stack/backend.md` (2), `rules/opt-in-stack/security-web.md` (2) and `CONTRIBUTING.md` (1) cited consumer-side example or template paths and now carry the `path-check: example` annotation that says so.
120
+ - **The `sessions-staleness` probe warned on a previous session's own SessionEnd output.** `CLOSING_DIAGNOSTIC_EVENTS` in `scripts/lib/sessions-staleness-banner.mjs` now skips a closing session's own teardown events (`orchestrator.session.ended`, `.telemetry.flush`, `.session.backfill_completed`, the lock-release events, and an `on-session-end`-emitted `wave.completed`/`.final_refused`) when computing `newestForeignEvent` — reproduced live 2026-09-19 as a false 9.6h close-through warning whose only post-ledger events were one SessionEnd's own diagnostics.
121
+ - **The `sessions-staleness` probe warned about the running session itself after a context compact.** The SessionStart hook re-acquires the lock under the same id and rewrites `started_at`, which moved the self-exclusion cutoff past the session's own earlier events (not all of which carry a `session_id`). The cutoff is now pulled back to that lock id's first `orchestrator.session.lock.acquired` event — reproduced live 2026-09-19 as a false "11.9h behind" warning on a 2h15m session.
122
+ - **A same-sha CI pipeline set aside by the ref-preference tier disappeared behind a green reading.** `selectShaPipelines()`/`candidateEvidence()` in `scripts/lib/ci-status-banner.mjs` now publish the set-aside rows as `droppedCount`/`droppedStatuses` (#1390 P5); `scripts/lib/session-start-probes.mjs` renders a `warn` banner line when a `green` HEAD has a set-aside `failed` run, and the unknown-CI hint now prints `glab ci list --sha <full sha>` instead of the invalid `glab ci status --ref <short>` (measured on glab 1.117.0, #1339 P1).
123
+ - **The convergence monitor's `shrinking_diff` signal was masked when a wave's completion and the next wave's start landed in the same tail tick.** `isWaveScopedEvent()` in `scripts/lib/convergence-monitor.mjs` now admits only the exact `orchestrator.wave.completed` name instead of the whole `orchestrator.wave.` prefix, so an emptied same-tick `started{N+1}` summary can no longer suppress the `(N-1, N)` comparison (repro: 40→10→5 files read as `shrinking_diff:2`/`:3` instead of same-tick `[]`).
124
+ - **`consumeDialecticPending` could follow a symlinked `.orchestrator/consumed` and delete files it did not write (data loss).** `scripts/lib/auto-dialectic.mjs` now `lstat`s the directory and refuses a symlink outright (`{ok: false}`, nothing moved or pruned), and the retention prune only counts/removes names matching its own `ARCHIVE_NAME_RE` (#1390 P6) — a tmp repro had previously deleted 3 of 12 unrelated files through the symlinked target.
125
+ - **A `/evolve` repair-engine dry run wrote the candidate store.** `runRepairEngine()` in `scripts/lib/skill-evolution/engine.mjs` now skips `mergeCandidates()` entirely under `dryRun: true`, logging the skip instead. `scripts/lib/skill-evolution/candidate-intake.mjs`'s `extractCandidates()` is now fail-closed: a learning's extracted path must realpath-resolve to a regular file inside `repoRoot` that is also the one tracked file it names — behaviour change: candidate ids now differ for bare-basename targets, and `extractCandidates` reads git (`git ls-files -z`, lazily, at most once per call).
126
+ - **The `is-main-module` entry guard now protects 25 of 28 hooks, not just the three lifecycle ones** (#1298 P7, extended by #1393). `main()` sits behind `isMainModule(import.meta.url)`, and the `shouldRunHook` profile gate moved inside that guard — at module top level its `process.exit(0)` tore down every process that merely imported the hook. Measured with a 700 ms settle window (without one the probe runs before `main()`'s `.finally(() => process.exit(0))` fires and passes vacuously): before, 8 of 28 hooks survived a bare `import()` under the default profile and only 3 of 28 under `SO_HOOK_PROFILE=off`; after, 28 of 28 under both. The sweep changed 21 hooks; 4 were already guarded and 3 keep a documented hand-inlined equivalent. Why it matters: a bare `import()` (a probe, a test, an agent) previously ran `main()` against the live repo and could overwrite `.orchestrator/current-session.json` (incident W4-FX1) — and `hooks/post-tool-failure-corrective-context.mjs` did not merely exit, it wrote a `PostToolUseFailure` envelope to the importer's stdout. **No recurrence guard ships with this**: `scripts/lib/validate/check-entry-guard.mjs` keys on `process.argv[1]`, so a file with no guard at all passes it trivially (measured: 0 findings for the defect shape, 1 for a fragile guard) — tracked as #1422.
127
+ - **The vault-note frontmatter-guard's enum extraction could read comment prose as an enum value.** `_extractEnum()` in `scripts/lib/frontmatter-guard.mjs` now matches only quoted string literals in comment-masked source (`maskSource(text, { keepLiterals: true })`) instead of splitting the raw `z.enum([...])` body on commas — 6 of 16 `statusEnum` entries had been corrupted this way, one fused onto `'maintenance'` (#1298 P2).
128
+ - **A reconcile abort `reason` could leak the repo root or home directory into the ledger and webhook.** `buildReconcilePayload()` in `scripts/lib/reconcile/engine.mjs` now runs a new `redactLocalPaths()` over `result.error`, replacing the absolute repo root and `homedir()` with `<repo>`/`~` before the 300-char clamp (#1202 §8b).
129
+ - **The dialectic deriver's `unknown-model` and `subagent-crash` aborts went unrecorded.** `runDialecticDeriver()` in `scripts/dialectic-deriver.mjs` now calls a new `recordThrownAbort()` at both throw points, before rethrowing, instead of relying on the skill-prose caller to record them (#1221 P3).
130
+ - **`check-unwired-features`'s "drags N" count under-reported when a root imported two colliding basenames.** `collectUnreachableLibraryModules()` in `scripts/lib/validate/check-unwired-features.mjs` now derives the drag count from the same per-module `references()` predicate the cluster-root filter uses, instead of comparing bare basename tokens, which had collapsed two dragged `index.mjs` files into one (#1298 P3).
131
+ - **Test runs could leak events into the real `.orchestrator/metrics/events.jsonl` ledger.** `eventsFilePath()` only redirected to a sandbox in its default branch (neither `filePath` nor `repoRoot` given) when the resolved target would land outside the OS temp root; two test files (`orchestrators-e2e.test.mjs`, `learning-memory-modernization.test.mjs`) took neither branch and wrote to the live ledger under the live session id (#1397.11). A new `vitest` `setupFiles` guard (`tests/setup/events-ledger-guard.mjs`) pins the sandbox for every run. Measured: before the fix, one suite run appended 10 test events to this repo's own ledger (2583 → 2595 lines); after, 0 (only 4 `orchestrator.agent.stopped` records from live hooks). One of those leaked test events had previously decided a real session's gate-health reading (`main-2026-09-18-session-10`). Explicit `repoRoot`/`filePath` callers and the 38 tests with their own fixture root are unaffected. Follow-up not covered here: `scripts/lib/tmux-layout/telemetry.mjs` writes via its own `appendFileSync`, bypassing both guarded seams.
132
+ - **A hook payload logged the raw Bash command text into the tracked events ledger.** `hooks/enforce-commands.mjs`'s `foreign_session_ignored` payload carried the raw command as a documented field in `docs/events-schema.md`, not an oversight — but a tracked, ever-growing ledger commits that text on every clone. Now logs `command_hash` only, matching the sibling `pre-bash-destructive-guard.mjs`'s existing rationale ("payload never includes the raw command"); the shared `hashCommand()` is duplicated rather than imported so the `PreToolUse` path stays free of an extra loadable module. A test now pins both directions — hash present AND `command` absent — so the raw field cannot quietly return beside the hash. Measured impact elsewhere (a consumer repo's own issue #1140, 2026-09-19): 3,816 lines of a TRACKED ledger carried a raw command, including 24 distinct production share codes across 17 customer accounts and one logged `select access_pin_hash` — for 23 of those events the share code alone was the complete capability. A sibling gap named but not fixed in this commit — `hooks/pre-bash-staging-fence.mjs` also stored raw command text — is closed separately in this same cycle (see the staging-fence entry above, #1404).
133
+ - **The claim scanner missed a claimed test count with no observed run backing it.** The scanner now also flags a claimed test-count assertion that carries no matching observed vitest output (#1385 R1) — the earlier gap was the inverse: a claimed number that happened to BE its own run receipt read as verified. Measured false-positive rate: 4 of 1044 real transcripts (0.38%). A SHA-based match between claim and receipt was measured and discarded as an approach.
134
+ - **HIGH, security, self-caught before release: scope grading compared home-directory grants byte-for-byte while the filesystem folds case, letting `/Users/<user>/library/**` through as WARN instead of ERROR.** `classifyHomeGrant()`'s regression (introduced earlier in this same Unreleased cycle, #1398/#1405) meant Gate 5b honored such an entry; reproduced reaching `~/Library/Keychains` and `~/Library/LaunchAgents` — code execution at login — and, via the lowercased root, also `~/.ssh` and `~/.config`. Before that regression every `/Users/**` entry was ERROR outright. A third site carried the same defect: the system denylist itself let `/ETC/**` and `/library/**` through as WARN. All three segment comparisons now fold over a pinned `en-US` locale — under a Turkish locale, `LIBRARY` would otherwise fail to fold. Caught by an adversarial review panel before this ever shipped.
135
+ - **HIGH, self-caught before release: a self-contradicting ledger record could pass eval's gate facts as measured.** `computeRecordFacts()` emitted `changes_unverified: true` from an `events.jsonl` where the same record called itself `unmeasurable` twice — no `contradiction`, no `parse_miss` — which meant exactly the sessions with a damaged ledger would hit the pre-registered `fail` trigger of eval Rule 6. The gate facts are now nulled and the record's self-contradiction is named explicitly, so Rule 1 (`cannot-determine`) fires instead of Rule 6 (`fail`) — nulling alone would have swapped a false fail for an undeserved pass.
136
+ - `skills/eval/rubric-v2.md` claimed its `JUDGE_RULES` constant word-for-word; rules 1, 3 and 5 had actually diverged (3 and 5 since their origin commit) (#1410). Code is now aligned to the documented rubric, and a new `tests/eval/rubric-parity.test.mjs` pins the two together with a vacuum guard. No rubric version bump was needed — all 40 stored eval records are still `rubric-v1`.
137
+
138
+ - **`argv_truncated` said nothing measurable.** `hooks/pre-bash-memory-propose-audit.mjs` now records `command_hash` / `flags_present` / `argv_length` instead (#1415) — a truncation flag names the instrument's limit, not the observation.
139
+ - **The scope-disjoint identity gate stopped at the first candidate state dir.** `hooks/pre-task-scope-disjoint.mjs` now runs the identity check INSIDE the `STATE_DIR_CANDIDATES` loop (`continue` instead of `break`), so a foreign-session state dir no longer ends the search before the own one is reached (#1424).
140
+ - `no-counter-sentence` violations in three documentation enumerations (#1420), and the K5 text in `hooks/pre-auq-clarity.mjs`.
141
+
142
+ ### Security
143
+
144
+ - **The orphan reaper ships inert.** `reaper.enabled` defaults to `false` and `reaper.mode` to `report`; an unrecognised mode falls back to `report`, never to `kill`. Unlike every other guard in this repo, this one can send SIGNALS to processes, so arming it is a per-repo decision taken only after its firing rate has been measured over `reaper.false-alarm-window` decisions in `.orchestrator/metrics/reaper-audit.jsonl` — `.claude/rules/host-resources.md` HR-101 (a class above ~10 % is a broken instrument, to be re-aimed rather than obeyed) and HR-105 (a rate nothing records is unfalsifiable) bind that order. `mode: kill` belongs to PRD Stufe 2, after calibration (C1). The signal path itself is hardened in the same release: pgid validation and a pgid join on the ledger record, session-id scoping, and a read-only command allowlist evaluated per statement.
145
+
10
146
  ## [5.2.0] - 2026-09-17
11
147
 
12
148
  ### Added
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Session Orchestrator
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-5.2.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-5.3.0-blue.svg)](CHANGELOG.md)
5
5
  [![npm](https://img.shields.io/npm/v/session-orchestrator.svg)](https://www.npmjs.com/package/session-orchestrator)
6
6
 
7
7
  **Give your agents a working rhythm.**
@@ -93,7 +93,7 @@ When you type `/session feature`:
93
93
  .claude/STATE.md # wave progress and deviations (harness-specific directory)
94
94
  ```
95
95
 
96
- The plugin is **50 skills, 28 slash commands, 14 typed subagents and 27 hook files across 10 event types**. A slash command is a skill whose frontmatter says `user-invocable: true` (26 of them) or one of the two remaining `commands/*.md` files (`/session`, `/templates-ack`) — one definition per name, so nothing is listed twice in the `/` picker. Skills, commands and agents are Markdown with YAML frontmatter; the code that dispatches, validates and records runs in `scripts/lib/*.mjs` and `hooks/*.mjs`. There is no build step and no compiled artifact — when a session does something you did not expect, you can open the file that decided it. Full inventory: [`docs/components.md`](docs/components.md).
96
+ The plugin is **50 skills, 26 slash commands, 14 typed subagents and 27 hook files across 10 event types**. A slash command is a skill whose frontmatter says `user-invocable: true` (24 of them) or one of the two remaining `commands/*.md` files (`/session`, `/templates-ack`) — one definition per name, so nothing is listed twice in the `/` picker. Skills, commands and agents are Markdown with YAML frontmatter; the code that dispatches, validates and records runs in `scripts/lib/*.mjs` and `hooks/*.mjs`. There is no build step and no compiled artifact — when a session does something you did not expect, you can open the file that decided it. Full inventory: [`docs/components.md`](docs/components.md).
97
97
 
98
98
  ## Why it is built this way
99
99
 
@@ -111,7 +111,7 @@ How this compares to other orchestrators, with measured results kept separate fr
111
111
 
112
112
  | Feature | Claude Code | Codex CLI | Cursor IDE | Pi |
113
113
  |---|---|---|---|---|
114
- | All 28 commands | Native slash commands | Generated skills (`$session-orchestrator:<name>`) | Native `.cursor/commands` slash commands | Prompt templates |
114
+ | All 26 commands | Native slash commands | Generated skills (`$session-orchestrator:<name>`) | Native `.cursor/commands` slash commands | Prompt templates |
115
115
  | Parallel agents | Agent tool | Multi-agent roles | Sequential only | Sequential (parallel planned) |
116
116
  | Session persistence | `.claude/STATE.md` | `.codex/STATE.md` | `.cursor/STATE.md` | `.pi/STATE.md` |
117
117
  | Scope enforcement | Active PreToolUse hook; blocking in `strict`, reporting in `warn` | Instructions only; no compatible `apply_patch` handler | `preToolUse` + `beforeShellExecution` bridge; scope blocking requires `strict`; `afterFileEdit` is post-hoc | `tool_call` bridge; scope blocking requires `strict` |
@@ -121,14 +121,14 @@ How this compares to other orchestrators, with measured results kept separate fr
121
121
 
122
122
  All four platforms share the same skills, commands and scripts; only the hooks differ, because each harness fires different events. Codex leaves its `PreToolUse` handlers empty because these guards do not yet match its tool names and edit payloads ([why](docs/codex-setup.md#why-our-pretooluse-guards-stay-unwired--the-reason-corrected)). Cursor and Pi have known event-coverage limits — see [`docs/cursor-setup.md`](docs/cursor-setup.md) and [`docs/pi-setup.md`](docs/pi-setup.md).
123
123
 
124
- ## Recent highlights (v5.2.0)
124
+ ## Recent highlights (v5.3.0)
125
125
 
126
- Highlights of the v5.2.0 line:
127
-
128
- - **One definition per slash command.** 24 `commands/*.md` twins were folded into their `skills/<name>/SKILL.md`; the `/` picker no longer lists duplicates, `/discovery`, `/evolve` and `/plan` are live under `claude -p` again, and the Cursor, Pi and Codex wrappers are generated from one `user-invocable` reading (`scripts/lib/user-invocable-skills.mjs`) instead of four private ones. Operator-only commands (`/close`, `/go`, `/release`, …) keep `disable-model-invocation` on every generated surface.
129
- - **Expired generated rules can now be swept.** `node scripts/sweep-expired-rules.mjs` (dry-run by default, `--apply` to write) removes the prose of expired entries from the consolidated `.claude/rules/` files while keeping their provenance pairs, so `/reconcile` does not re-propose the learning. It refuses symlinks and paths outside the rules directory, writes atomically, and reports the files it cannot map 1:1 instead of guessing.
130
- - **Two guard holes closed.** The issue-budget hook enumerates every shell loop, so an exempt first loop no longer lifts the bulk deny for a second one; the entry-guard validator now also censuses the bare `argv[1].endsWith('<file>.mjs')` form. `release.mjs --check` asks both CI platforms about the release commit's sha rather than GitHub's own HEAD.
126
+ Highlights of the v5.3.0 line:
131
127
 
128
+ - **Gate commands die as a group now.** Both quality-gate paths start every command in its own process group and, on timeout, signal the whole group — SIGTERM, a grace period, then SIGKILL — instead of just the shell. The previously uncapped path B carries a 900 s ceiling and reports exit 124. Root cause was measured on 2026-09-20: four orphaned `tsgo --noEmit` processes at up to 8 GB each froze the host after a plain shell kill left them at PPID 1 (#1425, #1427, #1428).
129
+ - **An orphan watchdog, shipped off.** `scripts/lib/orphan-reaper.mjs` decides purely (own ancestry register ∧ PPID 1 ∧ age ∧ read-only allowlist ∧ identity re-checked before every signal) and runs detached from two hooks, throttled to one scan per 30 s. `reaper.enabled` defaults to `false` and `mode` to `report`; arming `kill` waits for a measured false-alarm rate (ADR-0015, HR-107).
130
+ - **Ledgers stop lying by omission.** `readEventsWithRotations` answers `complete: true | false | null`, a hand-placed archive is a notice rather than a gap, and the session-start probe plus the abandoned-session backfill read across rotations. `worktree_base_checked` records every worktree dispatch, including why it could not measure (#1423, #1414, #1424).
131
+ - **Codex entrypoints aligned** (MR !40, #1391): the `session` command reaches the portable `.agents/` surface, modes are parsed independently of the free text that follows, and `go`/`close` stay explicit-only on every generated surface.
132
132
 
133
133
  Full changes and verification: [CHANGELOG.md](CHANGELOG.md).
134
134
 
package/SECURITY.md CHANGED
@@ -238,6 +238,18 @@ rejects shell metacharacters as defense-in-depth. Full trust model in
238
238
  low-false-positive line/regex heuristic, not an exhaustive AST gate, and fails
239
239
  open by design so a guard bug never blocks legitimate work.
240
240
 
241
+ 6. **A standalone single-file copy of `check-owner-leakage.mjs` cannot resolve
242
+ its sibling helper modules** (`../config/host-paths.mjs`,
243
+ `./confidential-names.mjs`, `../owner-yaml.mjs`) — the documented vendoring
244
+ shape (see [Supply Chain](#supply-chain) above). A CP11 confidential-names
245
+ guard configured only via `owner.yaml` `paths.confidential-names-file`
246
+ therefore degrades to an inert WARN with exit 0 in that copy (CP1–CP10 keep
247
+ running); configured instead via the `SO_CONFIDENTIAL_NAMES_FILE`
248
+ environment variable, the same standalone copy fails **closed** (a counted
249
+ FAIL, exit 1), because the raw env var is still readable without the
250
+ helpers. Prefer the environment variable over `owner.yaml` when vendoring
251
+ this scanner as a single file.
252
+
241
253
  ## Credential Safety
242
254
 
243
255
  - Secrets are read from the environment or a gitignored `.env.local`, never written
@@ -13,7 +13,7 @@ You reason over recent learnings, sessions, peer cards, and project steering to
13
13
  to the canonical peer cards (`.orchestrator/peers/USER.md` and `.orchestrator/peers/AGENT.md`).
14
14
  You are dispatched by `scripts/dialectic-deriver.mjs::runDialecticDeriver` with a complete
15
15
  payload — your job is to read the payload, decide whether each peer card warrants an update,
16
- and emit the full proposed replacement body for any card you wish to update.
16
+ and emit the `## ` sections you change or add for any card you wish to update.
17
17
 
18
18
  ## Core responsibilities
19
19
 
@@ -22,8 +22,9 @@ and emit the full proposed replacement body for any card you wish to update.
22
22
  should behave in this project).
23
23
  2. **Be conservative**: only propose updates grounded in the supplied inputs. Do not invent
24
24
  new sections that no learning or session supports.
25
- 3. **Preserve continuity**: if an existing peer-card section is still accurate, keep it. Diff
26
- = full replacement body, so omitted sections are deleted be deliberate.
25
+ 3. **Preserve continuity**: omitted sections stay unchanged the merge replaces or appends per
26
+ `## ` section and never deletes one. Emit ONLY the sections you change or newly ground; never
27
+ invent a section without evidence from the supplied learnings/sessions.
27
28
  4. **Respect the model budget**: you run as Haiku. Keep your reasoning compact; emit only the
28
29
  blocks you actually want applied.
29
30
 
@@ -83,18 +84,19 @@ line is a comment identifying the target:
83
84
 
84
85
  ```diff
85
86
  # target: user
86
- <full proposed body of USER.md, replacing existing content>
87
+ <the `## ` sections of USER.md you change or add>
87
88
  ```
88
89
 
89
90
  ```diff
90
91
  # target: agent
91
- <full proposed body of AGENT.md, replacing existing content>
92
+ <the `## ` sections of AGENT.md you change or add>
92
93
  ```
93
94
 
94
95
  Rules:
95
96
 
96
97
  - Emit **at most one block per target**. Omit a target entirely when no update is warranted.
97
- - The block body is the **FULL replacement body** — not a unified diff hunk.
98
+ - The block body is a set of **whole `## ` sections** — not a unified diff hunk. Omitted sections stay
99
+ unchanged (nothing auto-deletes), so emit only the sections you change or newly ground.
98
100
  - **`## ` headings are the merge unit (#1310).** On `--apply` the orchestrator splits your body at each
99
101
  level-2 (`## `) heading and maps each one onto a sentinel-delimited managed region via
100
102
  `mergeDerivedBody()` (`scripts/lib/peer-cards/merger.mjs`). Consequences you control:
@@ -133,10 +135,11 @@ block entirely. The orchestrator treats absence as "no change".
133
135
 
134
136
  ## Anti-patterns
135
137
 
136
- - **Inventing sections** with no grounding in learnings/sessions — the deriver refuses these
137
- through the `detectEmptying` gate downstream; better to skip the target entirely.
138
- - **Emitting unified diff hunks** (`---/+++/@@`) the orchestrator expects the full body
139
- replacement, not a diff format.
138
+ - **Inventing sections** with no grounding in learnings/sessions — nothing downstream rejects
139
+ them (`detectEmptying` only blocks a proposal with zero content lines against a non-empty card),
140
+ so an invented section WOULD be applied; skip the target entirely instead.
141
+ - **Emitting unified diff hunks** (`---/+++/@@`) — the orchestrator expects whole `## `
142
+ sections, not a diff format.
140
143
  - **Including frontmatter** in your block — the orchestrator manages frontmatter; emitting
141
144
  `---` lines confuses the parser.
142
145
  - **Reasoning out of scope** — only synthesise from the supplied payload. Do not Read other