session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
package/CHANGELOG.md CHANGED
@@ -7,10 +7,1063 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.0.0] - 2026-09-06
11
+
12
+ > **A MAJOR, because public surfaces are REMOVED** (see *Removed (BREAKING)* below).
13
+ > This entry was folded out of `[Unreleased]` by hand at cut time. That is a manual
14
+ > editorial act and not an automated one: `scripts/release.mjs` carries no writer for it —
15
+ > `checkChangelogEntry()` (`release.mjs:283`) only REQUIRES a dated `## [4.0.0]` heading as
16
+ > the topmost release with `[Unreleased]` emptied (`release.mjs:296`), and `--check`
17
+ > separately requires HEAD to be present on BOTH remotes (`evaluateRemoteHeadParity`,
18
+ > `release.mjs:556`) before it will tag. An earlier draft of this note asserted that the
19
+ > script renames the heading; it does not, and a release trusting that would have failed
20
+ > its own preflight. The reason the heading is written no earlier than the cut is the
21
+ > `3.22.1` failure this repo already paid for once — a release section for a release that
22
+ > never completed, unnoticed for 12 days (`e4674109`).
23
+
24
+ **Predecessor commits (v3.24.0 → `e4674109`).** Three commits (`ee8ea425` Wave 2,
25
+ `e22a702e` Wave 3, `37169158` Wave 4; 71 files, +3,505/−678, measured
26
+ `git diff --stat 2cb8708b..37169158`), no `BREAKING CHANGE:` footer and no `!` subject.
27
+ Twelve Wave-2 follow-up issues land as mechanism: identity resolution folds onto the
28
+ process-local reader on a third surface, seven ledger readers move onto one canonical
29
+ `sessions.jsonl` collapse, and a `secret_masker.applied` guard closes its last two gaps.
30
+ Wave 3 arms the schema-drift CI gate; Wave 4 is a 3-Opus-reviewer panel plus fixpass that
31
+ closed 2 HIGH findings inside the panel's own Wave-2/3 diff.
32
+
33
+ **The 360° audit session (`main-2026-09-06-deep-1`) — what makes this a major.** Everything
34
+ below marked *4.0.0 — audit session* is that session's own change set, which lands as this
35
+ release's commit: 277 working-tree entries at fold time (`git status --short | wc -l`,
36
+ 2026-09-06), on top of 13 already-committed predecessors measuring 256 files, +13,537/−1,669
37
+ (`git diff --stat v3.24.0..HEAD | tail -1`, same day). Fourteen read-only Discovery agents measured the plugin against its own
38
+ telemetry, the 90-day fleet ledger and the vendor docs; the synthesis is
39
+ [`docs/audits/2026-09-06-360-audit.md`](docs/audits/2026-09-06-360-audit.md), per-agent
40
+ evidence under `docs/audits/2026-09-06-360-audit/w1/`. Nothing was removed on judgement: the
41
+ rule was 0 telemetry ∧ 0 fleet invocation in 90 days ∧ no runtime consumer, and
42
+ prose-invoked skills (0 by construction) were exempt. Upgrade guide:
43
+ [`docs/migration-v4.md`](docs/migration-v4.md).
44
+
45
+ ### Added
46
+
47
+ #### 4.0.0 — audit session
48
+
49
+ - **Root `AGENTS.md`, root `plugin.json`, and a portable `.agents/skills/` mirror — this repo
50
+ was previously invisible to Copilot CLI and to agent-plugins.org clients.** The root
51
+ `AGENTS.md` is byte-identical to `CLAUDE.md` (`diff -q AGENTS.md CLAUDE.md` → no
52
+ difference), so a harness that resolves project instructions from `AGENTS.md` now finds
53
+ this repo's own Session Config instead of nothing — before this there was no root
54
+ `AGENTS.md` at all, meaning the plugin's own repository could not read its own config on a
55
+ harness that looks for that filename. `plugin.json` declares
56
+ `$schema: https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`, the cross-vendor
57
+ manifest shape Cursor, Amazon, Microsoft, OpenAI and Vercel converged on.
58
+ `.agents/skills/<name>/SKILL.md` mirrors all 43 skills (`ls .agents/skills | wc -l` → 43)
59
+ for harnesses that read that convention rather than a plugin manifest.
60
+ - **`ultradeep` — a session PROFILE over `deep`, deliberately NOT a fourth session type.**
61
+ `/session ultradeep` resolves to `session-type: deep` + `session-profile: ultradeep` in
62
+ STATE.md frontmatter (`commands/session.md` § Argument alias). `session-type` never becomes
63
+ `ultradeep`: `VALID_SESSION_TYPES` is a closed set in
64
+ `scripts/lib/session-schema/constants.mjs`, and a fourth member would degrade silently in
65
+ two places (`telemetry/schema.mjs` maps an unknown type to `"other"`,
66
+ `session-close-backfill.mjs` labels it `housekeeping`). The profile changes only the wave
67
+ SHAPE — 7 waves with a coordinator-direct Synthesis-Gate at W2 (`agents: 0`, the one wave
68
+ whose whole job is to stop and ask before any code is written), per the new row at
69
+ `skills/session-plan/SKILL.md:336`. `SESSION_PROFILE_FIELD` in `scripts/lib/state-md.mjs`
70
+ is the reader; absent is not empty. Budgets (`ultradeep.max-*`) are deliberately NOT
71
+ implemented — the PRD defers thresholds until three runs have been measured. Spec:
72
+ [`docs/prd/2026-09-06-ultradeep-session-profile.md`](docs/prd/2026-09-06-ultradeep-session-profile.md).
73
+ - **A plugin-update banner at session-start — the first code in this repo that compares
74
+ INSTALLED against AVAILABLE.** `scripts/lib/plugin-update-banner.mjs` (`checkPluginUpdate`,
75
+ imported lazily from `hooks/on-session-start.mjs:976`) reads the RUNNING package's own
76
+ `package.json` — resolved as `../..` from `scripts/lib/`, i.e. the loaded bytes, never
77
+ `$CLAUDE_PLUGIN_ROOT`, which is precisely what resolved to the checkout while a stale cache
78
+ copy was executing — and compares it against the npm `dist-tags.latest`. The measured gap:
79
+ this host ran the marketplace cache at **3.19.0** (installed 2026-08-09) while repo and npm
80
+ were at **3.24.0** — five minors, four weeks, no warning, because the freshness probe
81
+ shells out to `git -C <plugin-dir> log -1` against a FILE COPY and
82
+ `classifyVersionMismatch()` downgrades anything below a major jump to `info`. Contract:
83
+ fail SILENT, never optimistic — offline, non-2xx, malformed JSON, timeout and unusable
84
+ cache each return `null` ("no statement"), never "you are up to date" (the #1031 collapse
85
+ class).
86
+ - **A telemetry sandbox guard — `detectSandbox()` in `scripts/lib/telemetry/sync.mjs`.**
87
+ This session's own read-only Wave-1 benches sent 6 production pings from agent sandboxes:
88
+ `hooks/on-session-end.mjs` ran from the repo checkout with `SO_CONFIG_HOME` redirected
89
+ while `owner.yaml` was unreachable inside the sandbox. The sender now refuses when
90
+ `SO_TELEMETRY_DISABLED=1` (`sandbox:telemetry-disabled`), `DO_NOT_TRACK` is set
91
+ (`sandbox:do-not-track`), the config home is split from the state dir
92
+ (`sandbox:config-home-split`), or the state dir resolves under a temp root
93
+ (`sandbox:temp-root` — matched after realpath, because macOS's `/var` → `/private/var`
94
+ symlink defeats a raw string prefix). Every refusal is reported with its `sandbox:*` reason
95
+ rather than as a silent no-op.
96
+ - **[`docs/baseline.md`](docs/baseline.md) — the `projects-baseline` relationship written
97
+ down.** One line: the baseline is a **private, optional** companion repository, read when
98
+ present, degraded to a documented fallback when absent, required by nothing in this plugin
99
+ and needed by no public consumer. The measurement behind it: of 49 baseline-touching files,
100
+ 17 are docs-only, 15 test-only, 13 soft, and only 4 hard-runtime — **2 of those 4 pointed
101
+ at a hardcoded `$HOME/Projects/projects-baseline` that does not exist**.
102
+
103
+ #### Predecessor commits (v3.24.0 → `e4674109`)
104
+
105
+ - **`scripts/lib/wave-executor/dispatch-common.mjs` (#1204).** Six symbols
106
+ (`NEVER_FOREIGN_ROLES`, `DEFAULT_TIMEOUT_SEC`, `DEFAULT_KILL_GRACE_MS`, `isSafeRunId`,
107
+ `isNeverForeignRole`, `runChild`) extracted out of `foreign-dispatch.mjs` into one shared
108
+ module, imported by both dispatch call sites (`foreign-dispatch.mjs`, `remote-dispatch.mjs`)
109
+ and their two test files. A redundant `isNeverForeignRole` check that had never lived in
110
+ `wave-resource-gate.mjs`'s own invariant is now a documented non-check there instead of a
111
+ silent gap.
112
+ - **`scripts/lib/learnings/evolve-telemetry.mjs` (#1206).** Mechanical
113
+ `orchestrator.evolve.completed` / `orchestrator.dialectic.completed` emitters replace
114
+ hand-written prose emit sites in `skills/evolve/SKILL.md`: `recordEvolveCompletion()` is
115
+ called from `scripts/sweep-expired-learnings.mjs`'s `--prune` exit path (the one store
116
+ write `/evolve analyze` already performs), and `recordDialecticRun()` from
117
+ `scripts/dialectic-deriver.mjs`. Both skip the emit with a stderr note when no `repoRoot`
118
+ is given, so test runs never write the fleet ledger. `docs/events-schema.md` gained both
119
+ event's producer lines.
120
+ - **`hooks/post-edit-import-probe.mjs` — a PostToolUse hook that catches a broken
121
+ hook-reachable module at save time, not at the next tool call (#1224).** On every
122
+ Edit/Write/MultiEdit to a `.mjs`/`.js`/`.cjs` file that is a member of the new committed
123
+ allowlist `hooks/_lib/hook-import-set.json` (149 modules at generation time — every module
124
+ transitively reachable from the four hook manifests' entry files), it runs ESLint's
125
+ `no-undef` on that single file (primary check) plus an `import()` probe scoped to
126
+ `scripts/lib/**` only (secondary — `hooks/*.mjs` is excluded because half of them run
127
+ `main()` at module bottom). Reports via `additionalContext` + the new
128
+ `orchestrator.hook.import_probe_failed` event (payload: `file`, `check`, `error`,
129
+ `reachable_from`, `duration_ms` — documented in `docs/events-schema.md`); never blocks
130
+ (exit 0 always); kill switch `SO_DISABLED_HOOKS=post-edit-import-probe`. The allowlist is
131
+ built by the new `scripts/generate-hook-import-set.mjs` (`--check` re-crawls and diffs) and
132
+ is guarded three ways: a new blocking `validate-plugin.mjs` check
133
+ (`runHookImportSetCheck`), a new `.husky/pre-commit` stage scoped to commits touching
134
+ `hooks/` or `scripts/lib/`, and — deliberately absent — no Codex wiring
135
+ (`hooks-codex.json`), documented as an intentional asymmetry in
136
+ `check-hooks-symmetry.mjs`'s `DOCUMENTED_ASYMMETRIES` (same missing Edit/Write-payload
137
+ adapter gap as `post-edit-validate.mjs`). Wired into `hooks.json`, `hooks-cursor.json`, and
138
+ `hooks-pi.json`. Rationale: the 2026-09-04 host-wide hook-block incident documented
139
+ elsewhere in this section (a `SyntaxError`/`ReferenceError` in an intermediate save of
140
+ `own-session.mjs` blocked every Bash/Edit call on the host for ~8 minutes) was a
141
+ **call-time** defect neither `node --check` (syntactically valid) nor a bare `await
142
+ import()` (module-level evaluation succeeds) catches — ESLint's `no-undef` does. The
143
+ allowlist has since been regenerated to include `scripts/lib/config/private-config-dir.mjs`
144
+ (new, #1223 below), which became hook-reachable via `host-identity.mjs`: measured
145
+ 2026-09-05, `node scripts/generate-hook-import-set.mjs --check` → `✓ hook-import-set: 150
146
+ modules, in sync`.
147
+
148
+ ### Changed
149
+
150
+ #### 4.0.0 — audit session
151
+
152
+ - **43 generated rule files consolidated into 8 thematic ones; `.claude/rules/` goes 61 → 26
153
+ files.** Measured now: `ls .claude/rules/*.md | wc -l` → 26, of which **zero** still match
154
+ the generated-rule naming (`anti-pattern-*`, `proven-pattern-*`, `recurring-issue-*`,
155
+ `convention-*`, `fragile-*`); at `HEAD` (`e4674109`) the same directory held 61 files
156
+ (`git ls-tree -r --name-only HEAD .claude/rules/ | grep -c '\.md$'`). The eight
157
+ replacements are `identity-and-locks`, `guard-design`, `measurement-discipline`,
158
+ `test-hygiene`, `process-contracts`, `toolchain-and-build`, `git-and-worktrees`,
159
+ `review-and-adapter-contracts`. Grounds: the 43 files were delivered TWICE per dispatched
160
+ agent (natively plus via rule injection) — 112 kB, 46 % overhead — and 10 of them restated
161
+ an always-on rule's insight verbatim.
162
+ - **The three largest instruction files are SPLIT into `references/`, not shortened.**
163
+ `skills/session-start/SKILL.md` 1,275 → 388 lines (9 reference files);
164
+ `skills/session-end/SKILL.md` 1,203 → 316 (6 reference files);
165
+ `skills/wave-executor/wave-loop.md` 1,337 → a 39-line index over 3 references
166
+ (`wave-loop-dispatch.md`, `wave-loop-review.md`, `wave-loop-scope-manifest.md`). Baselines
167
+ measured `git show HEAD:<path> | wc -l`, current `wc -l`. Content moved rather than
168
+ vanished: a phase you need is one hop away, a phase you do not need is no longer in every
169
+ agent's context.
170
+ - **`orchestrator.session.stopped` → `orchestrator.turn.stopped` (GitLab #1234).** The
171
+ emitter fires per assistant TURN, not per session: measured over the 90-day fleet window,
172
+ **15,538 records against 2,016 distinct `orchestrator.session.started` ids = 7.7 per
173
+ session**, with 184 for one id — so six consumers reading it as a session-lifecycle signal
174
+ were wrong by that factor. `hooks/on-stop.mjs:489`/`:493` emits BOTH names from the same
175
+ payload object for one deprecation generation (so they cannot disagree); the legacy name
176
+ additionally carries `deprecated: true`, letting a reader tell at a glance that it matched
177
+ the old name. **Removal of `orchestrator.session.stopped`: 2027-03-06.** Readers switch the
178
+ name they match on and change nothing else. NOT affected: the SubagentStop branch keeps
179
+ emitting `orchestrator.agent.stopped`, whose per-agent cardinality is correct. Contract:
180
+ `docs/events-schema.md:92-93`.
181
+ - **The telemetry `fleet` field becomes `fleet_self_declared`, and attribution moves
182
+ server-side.** The old name asserted something the client cannot know: 394 of 490 pings
183
+ were the operator's own second Mac, mis-attributed as "external", because the flag hung off
184
+ an `owner.yaml` line rather than off a person. The client field now says what it is —
185
+ self-declared, derived from the RESOLVED consent state (`enabled-fleet` / `enabled-env`)
186
+ rather than from a raw `owner.yaml` read (`scripts/lib/telemetry/schema.mjs:552-555`) — and
187
+ the authoritative classification is an anon-id allowlist in the ingest server
188
+ (`server/ingest/config.mjs`, `server/ingest/validate.mjs:190-196`). `fleet` remains in the
189
+ payload, identical in value for the whole generation so the server's existing column stays
190
+ comparable: **DEPRECATED 2026-09-06, removal 2027-03-06** (`docs/telemetry.md:42-43`).
191
+
192
+ - **`zod` and `yaml` hoisted from a nested skill's own `package.json` to this repo's
193
+ root `dependencies`.** `skills/vault-sync/package.json` resolved them as its own
194
+ transitive install; a fresh top-level `npm ci` without that nested install step
195
+ left `vault-sync`'s schema validation unable to resolve either package. Root
196
+ `package.json` now pins `yaml@^2.9.0` and `zod@^3.25.76` directly (the nested
197
+ `package.json` still carries its own copies — redundant, harmless, left as a
198
+ cleanup candidate).
199
+ - **`scripts/release.mjs`'s preflight gains a GitHub-mirror CI row and two
200
+ publish-time hardenings.** `evaluateGithubCiRow()` adds `ci-green-on-head-github`
201
+ as its own preflight check (26 rows total) via `checkCiStatus({ vcs: 'github' })`
202
+ — `--skip-ci` marks it skipped, and a repo with no configured GitHub remote also
203
+ skips it rather than failing; `publishInvocation()` now pins
204
+ `npm_config_loglevel=notice` on the publish spawn (an inherited silent level from
205
+ an outer gate run suppressed output the packlist gate depends on); and
206
+ `describeTagProgress()` is printed on a post-receipt tag/push failure, so a
207
+ publish that succeeds but fails to tag no longer leaves the operator guessing
208
+ what state the release is in. `commands/release.md` step 7 documents the ≥600s
209
+ Bash wrapper this needs; `skills/npm-publish/SKILL.md` is updated to match.
210
+ - **The release drift sweep moves off `git grep` onto `enumerateRepoFiles()`
211
+ (#1248) — the same gitignore/untracked blind spot the two checkers below were
212
+ fixed for, closed once.** `collectDriftHits()` in `scripts/release.mjs` now
213
+ enumerates via `git ls-files --cached --others --exclude-standard` instead of
214
+ shelling out to `git grep` for the previous release tag, so an untracked file is
215
+ swept for a leftover version literal exactly as a tracked one is.
216
+ - **The Full Gate now names which test FILES failed, not just the pass/fail
217
+ count.** `extractFailedTestFiles()` (`scripts/lib/gates/gate-helpers.mjs`) parses
218
+ vitest's own failure output; `gate-full.mjs` carries the result as a new
219
+ `failed_files[]` field alongside the existing counts, on both the returned
220
+ object and the `orchestrator.quality_gate.*` event payload. Separately,
221
+ `.husky/pre-push` now passes the real repo root to the gate as an **argv flag**,
222
+ `--ledger-root <path>` (`npm run --silent quality-gate -- --ledger-root
223
+ "$repo_root"`), so a gate run against a materialised temp worktree writes its
224
+ event into the REAL repo ledger instead of a tree an `EXIT` trap deletes seconds
225
+ later — the root cause of several `quality_gate.passed` records the test suite
226
+ itself had polluted into `.orchestrator/metrics/events.jsonl`.
227
+ `resolveLedgerRoot()` (`scripts/run-quality-gate.mjs`) validates the flag as an
228
+ existing directory that itself contains `.orchestrator/`; anything else is one
229
+ stderr WARN and a fallback to the pre-existing resolution, never a crash. **This
230
+ shipped first as an exported `SO_GATE_LEDGER_ROOT` env var and was replaced
231
+ before release, inside the same session**: the Wave-4 architect-panel review
232
+ measured that an exported env var is inherited by every descendant process,
233
+ including the gate's OWN vitest children three levels down (`npm run
234
+ quality-gate` → `gate-full.mjs` → `npm test`), where `run-quality-gate.test.mjs`'s
235
+ telemetry tests spread `...process.env` and so wrote their fixture events to the
236
+ pinned root instead of their own tmp fixture — measured **8 of 9 telemetry tests
237
+ red** under the hook's own environment. An argv flag reaches only the one
238
+ process meant to read it. `run-quality-gate.test.mjs` still sandboxes
239
+ `CLAUDE_PROJECT_DIR` per test and additionally scrubs any ambient
240
+ `SO_GATE_LEDGER_ROOT` before each run (belt-and-braces against a leftover from
241
+ the removed env-var form); a new `tests/husky/pre-push-gate.test.mjs` case
242
+ ("hands the real repo root to the gate as --ledger-root") pins the hook's own
243
+ argv, and 0 production readers of `SO_GATE_LEDGER_ROOT` remain repo-wide
244
+ (`grep -rn SO_GATE_LEDGER_ROOT` outside test files and history comments → no
245
+ match, measured 2026-09-06).
246
+ - **The session-start plugin-update banner is now English and platform-aware.**
247
+ `checkPluginUpdate()` (`scripts/lib/plugin-update-banner.mjs`) accepts an
248
+ optional `platform` and picks the matching upgrade command from
249
+ `PLATFORM_UPDATE_INSTRUCTIONS` (`claude`/`codex`/`cursor`/`pi`), falling back to
250
+ a generic `npm update`-shaped instruction when the platform is unset or
251
+ unknown — previously every consumer saw the same `claude plugin update` line
252
+ regardless of harness.
253
+ - **Six documentation corrections following the 4.0.0 removals above.** The
254
+ `autopilot` SKILL's Configuration section is now a 4-line pointer to
255
+ `docs/migration-v4.md` instead of describing the removed `bg-isolation` field in
256
+ full; `NOTICE`'s attribution table is repointed at
257
+ `skills/architecture/references/{domain-model,ADR-FORMAT,CONTEXT-FORMAT}.md` and
258
+ notes `ubiquitous-language`'s removal while keeping its MIT attribution history;
259
+ `check-agents.mjs`'s warn-string citations of `agents/AGENTS.md` now cite
260
+ `docs/agent-authoring.md`; `templates/_shared/journey-manifest.md` gained a
261
+ retirement notice (kept as an optional per-repo template — no command consumes
262
+ it anymore); `skills/architecture/SKILL.md` documents when to load
263
+ `references/domain-model.md`; and `README.md` picked up matching corrections
264
+ (Windows is untested rather than "runs natively"; the update banner is
265
+ patch-silent; nothing is sent without consent EXCEPT the update check's own
266
+ anonymous `GET`, opt-out via `SO_DISABLE_UPDATE_CHECK`/`DO_NOT_TRACK`).
267
+
268
+ #### Predecessor commits (v3.24.0 → `e4674109`)
269
+
270
+ - **Quality-gate ownership check now folds onto the process-local reader (#1205).**
271
+ `quality-gate.mjs`'s private copy of `readOwnSessionIds()` is gone; the ownership check now
272
+ calls `readProcessLocalSessionIds()` directly (removing the STATE.md-lock fallback), with
273
+ the first test of the ownership path itself.
274
+ - **Wave-scope manifest binding writes `attributionForRecord()`, not a hand-rolled call
275
+ (#1207).** `skills/wave-executor/wave-loop.md`'s manifest-binding step now sources
276
+ `session`/`wave` from the same `attributionForRecord()` used by `emitEvent()`'s own
277
+ correlation fill, instead of a raw `sessionAttribution()` call; the STATE.md prose
278
+ comparison the previous step asked for is removed as a redundant, sometimes-wrong check
279
+ (a peer-owned lock and STATE.md agree with each other, not with this process).
280
+ - **Seven readers now go through `readCanonicalSessions()` instead of their own duplicate
281
+ parse (#1209).** `session-id.mjs`, `auto-dream.mjs`, `dispatcher/rank.mjs`,
282
+ `eval/engine.mjs` (+ new `eval/session-resolve.mjs`), `harness-audit/categories/category4.mjs`
283
+ (via `canonicalizeSessions({ keepUnidentified: true })`, since a bare wrapper broke 3
284
+ id-less fixtures), `skill-health/join.mjs`, `sessions-staleness-banner.mjs`, and
285
+ `dialectic-deriver.mjs` each had a red-on-HEAD fake-regression proof before the fix
286
+ (auto-dream 3→2 sessions, rank 50→<6 candidates, eval 2→1 peers). `category9` is
287
+ deliberately untouched.
288
+ - **Four modules now distinguish silent `ENOENT` from a loud read failure (#1210).**
289
+ `session-close-backfill.mjs`, `reconcile/engine.mjs`, `memory-proposals/store.mjs` (via a
290
+ new shared helper — a decorative `err.code` branch there is now real), and
291
+ `agent-status.mjs` all warn on stderr for `EACCES`/`EISDIR`/other, and stay silent only on
292
+ a genuinely missing file.
293
+ - **German distributional-claim patterns join the English set (#1211).** Six German
294
+ quantifier-triggered patterns (`N von M`, `100% von/der/aller`, `alle N`, `keine
295
+ verbleibenden`, `jeder/jede/jedes`, `keiner/keine/keins von/der`) plus a narrow German
296
+ `CARDINAL_NOUN` extension (`zeilen?|dateien?|datei|aufrufer|eintr(?:ag|(?:ä|ae)ge)`) land
297
+ in `hooks/post-subagent-discovery-validator.mjs`. Measured against a 41-line German claim
298
+ corpus pulled from this repo's own `events.jsonl`: the quantifier patterns flag 7/41
299
+ (same order of magnitude as the English baseline); a wider bare-cardinal noun set
300
+ (`Sessions`/`Repos`/`Treffer`/`Stellen`/`Module`/`Tests`) was tried and rejected after it
301
+ flagged 39/41 (~57% false-positive rate) — those six nouns stay reachable only through the
302
+ lexically-anchored quantifier patterns. German `GATE_SUMMARY` recap forms are also
303
+ recognised and skipped, matching the existing English recap-skip.
304
+ - **`skills/vault-sync/validator.mjs`'s vendored schema catches up with upstream (#531).**
305
+ `peer-card` (#503), `board` (#738), and the optional `source-repo` field (#725) were
306
+ vendor-ahead of `projects-baseline` until this session; upstream-lifted in
307
+ `projects-baseline@cb9ec97`. `sync-vault-schema.mjs` now strips a bare `//` line (with a
308
+ regression test), and the provenance notes documenting the vendoring history moved outside
309
+ the generated-block sentinels so a `--write` regeneration can no longer drop them (measured:
310
+ it did, once).
311
+ - **The schema-drift CI gate is armed (#1175, follow-up to #531).** `SCHEMA_DRIFT_OPTIONAL`
312
+ is `"false"` in both `.gitlab-ci.yml` jobs that read it — `schema-drift-check` (line 636)
313
+ and `pipeline-gate` (line 723) — so a missing `SCHEMA_DRIFT_TOKEN` now hard-fails (exit 4)
314
+ instead of degrading to an amber "not verified" pass. A test pins the armed state (a half
315
+ revert of either flag turns 2 tests red). Proof: pipelines 8355–8357 @ `bca78dae` (bogus
316
+ enum) failed with `schema-drift-check` reporting "drift detected"; pipeline 8358 @
317
+ `dc9522dd` (branch `proof/1175-schema-drift-green`) passed with the check running WITH the
318
+ token, in sync. `docs/ci-setup.md` documents the Project-Access-Token recipe and the
319
+ now-current state (was previously the follow-up gap this same doc named).
320
+ - **`#1203` — the `enforce-scope.mjs` peer-manifest fix confirmed already SHIPPED via
321
+ #1194.** `tests/hooks/enforce-scope.test.mjs:1219/:1245/:1302` (56/56) prove the behaviour
322
+ landed with #1194; no further code change was needed.
323
+ - **`scripts/lib/platform.mjs`'s five module-constant exports become lazy, memoized
324
+ accessors; the five constants are REMOVED (#1153 P5).** They were internal accessor
325
+ constants of this plugin, never a documented public API, and every in-repo importer is
326
+ migrated — an external importer of these five names (none known) switches to the
327
+ getters; no `BREAKING CHANGE:` footer is carried for that reason. `SO_PLATFORM`, `SO_PLUGIN_ROOT`,
328
+ `SO_PROJECT_DIR`, `SO_STATE_DIR`, `SO_CONFIG_FILE` used to be `export const … =
329
+ detect…()` evaluated at module load, so every one of ~31 static importers — including
330
+ the hottest deny-capable hooks, which run on every tool call — paid a filesystem
331
+ walk-up (`statSync`/`existsSync` per ancestor directory) merely for importing the
332
+ module, whether or not the value was ever read. They are replaced by
333
+ `getPlatform()`/`getPluginRoot()`/`getProjectDir()`/`getStateDir()`/`getConfigFile()`,
334
+ computed on first call and memoized for the process (plus a test-only
335
+ `_resetPlatformCache()`); the deprecated names are fully REMOVED, not kept as
336
+ deprecated live bindings — a re-introduction is caught by the named-export assertion in
337
+ `tests/lib/platform.test.mjs`. 23 non-test call sites across `scripts/` and `hooks/` now
338
+ call a getter (re-measured 2026-09-05: `grep -rlE "getPlatform\(\)|getPluginRoot\(\)|getProjectDir\(\)|getStateDir\(\)|getConfigFile\(\)" scripts/ hooks/ --include="*.mjs" | grep -v /tests/ | grep -v platform.mjs | wc -l` → 23; it was 22 on 2026-09-04, before this session's own new modules landed).
339
+ - **Wave-scope manifest session keys renamed to `session_id`/`semantic_session_id`,
340
+ canonical since #1153 P2.** The pre-#1153 spellings `session`/`semantic_session` are
341
+ still ACCEPTED on the read side for one release (`MANIFEST_SESSION_KEYS` in
342
+ `scripts/lib/session-identity/own-session.mjs` is the shared SSOT list every writer and
343
+ reader now imports); `scripts/validate-wave-scope.mjs` flags a manifest that carries
344
+ BOTH spellings with conflicting values as an error rather than silently preferring one
345
+ and dropping the other. `scripts/wave-scope-binding.mjs` (new — #1153 P4) replaces the
346
+ inline `node --input-type=module -e` block `skills/wave-executor/wave-loop.md` § Scope
347
+ Manifest previously asked the coordinator to retype once per wave; an unbound `{}`
348
+ manifest now emits `orchestrator.scope.unbound_manifest` (0 hits repo-wide before this
349
+ file existed), making the previously-silent fail-closed case countable. `--merge` folds
350
+ the binding into an existing manifest in place.
351
+ - **`_privateDir()` (host-identity.mjs) now honours `SO_CONFIG_HOME` (#1153 P6).** Two
352
+ overrides, most specific first: `SO_CONFIG_HOME` names the private config directory
353
+ itself; `XDG_CONFIG_HOME` (same variable `owner-config-loader.mjs` already reads) names
354
+ its parent. Both read via `.trim() || fallback`, not a bare `||` (the whitespace-only
355
+ env-var trap). Without either, the homedir default `~/.config/session-orchestrator` is
356
+ unchanged.
357
+ - **`scripts/lib/session-identity/own-session.mjs`'s static import closure cut from 3,567
358
+ to 269 lines (#1153 P7).** It no longer imports `../session-lock.mjs` — which drags
359
+ `session-lock` → `exclusivity-matrix` → `file-lock` → `io` → `host-identity` behind it —
360
+ and instead shares the shape predicate `isLockShape()` via a new zero-import module,
361
+ `scripts/lib/session-lock-shape.mjs`, with `session-lock.mjs`'s own `parseLock()`. This
362
+ matters because `hooks/enforce-scope.mjs` loads `own-session.mjs` on every Edit/Write;
363
+ anything it imports joins that hook's static closure.
364
+ - **codex-cli 0.144.4 fixes the `plugin add` failure documented against 0.141.0
365
+ (#1163, FIXED-UPSTREAM — no code change here).** Re-verified end-to-end 2026-09-04
366
+ against this repo's unchanged flat layout: `codex plugin add session-orchestrator@kanevry
367
+ --json` now exits 0 where it previously failed with `plugin session-orchestrator was
368
+ not found in marketplace kanevry`. `docs/codex-setup.md`'s "Short-Form Marketplace Add"
369
+ section is promoted to the recommended install path and the prior failure kept as a
370
+ historical note; a new "Switching Marketplace Sources" section documents that
371
+ `marketplace add` silently REPLACES an already-registered marketplace of the same
372
+ declared name.
373
+ - **`#1152` closed — the reported defect was a false premise (two separate writers, not
374
+ one drifting one).** No code change; investigation confirmed the two paths write
375
+ independently and neither needed reconciling.
376
+ - **`#1214` — the suspected `maskerWouldChange` gap in the vault narrative-mirror sink was
377
+ a false premise, refuted by measurement.** Unlike the two `vault-mirror/process.mjs`
378
+ sinks fixed under #1028 (five-field comparisons that can match while a raw needle
379
+ survives on disk), `scripts/lib/vault-status/narrative-mirror.mjs` compares the WHOLE
380
+ rendered document, and the candidate always passes through the CURRENT `maskNarrative` —
381
+ so a value that masker would redact can never appear in it, and a false
382
+ `matchesModuloRedaction` match cannot occur. Measured 2026-09-04 at HEAD `cd785003` in
383
+ both directions (no marker on disk; marker on disk plus a second needle entering the
384
+ env): both returned `written` with the raw value gone. No probe added; two invariant
385
+ tests in `tests/lib/vault-status/narrative-mirror.test.mjs` pin it. Named ceiling: the
386
+ invariant depends on `maskNarrative` walking every rendered string, and `repo` is fed to
387
+ `renderNarrative` OUTSIDE that walk (a directory basename, not STATE.md content) — a
388
+ future rendered field added the same way would reopen this.
389
+ - **Three host-private-config-dir resolvers collapse into one (#1223).** New zero-import
390
+ leaf `scripts/lib/config/private-config-dir.mjs` (`node:os` + `node:path` only) exports
391
+ `resolvePrivateConfigDir({env}?)` — precedence `SO_CONFIG_HOME` (the dir itself) >
392
+ `XDG_CONFIG_HOME` (its parent) > `~/.config/session-orchestrator`, each `.trim()`ed. Before
393
+ this, `owner-yaml.mjs`'s import-time `OWNER_YAML_PATH` (homedir-only, no overrides at all),
394
+ `owner-config-loader.mjs`'s `resolveOwnerConfigPath()` (XDG-only, untrimmed), and
395
+ `host-identity.mjs`'s `_privateDir()` each had a different precedence — so
396
+ `SO_CONFIG_HOME=<sandbox>` moved the self-alias ledger but not `owner.yaml`, which kept
397
+ reading the operator's real home (the CLAUDE.md "vault-dir resolves HOST-LOCALLY" hazard
398
+ class). `host-identity.mjs` now delegates `_privateDir()` to the leaf directly (it is
399
+ reachable from live hooks via `session-lock.mjs`, so the leaf must stay import-free);
400
+ `owner-yaml.mjs` re-exports `resolvePrivateConfigDir` and adds a call-time
401
+ `resolveOwnerYamlPath(env?)`; the old `OWNER_YAML_PATH` constant is fully REMOVED (verified
402
+ 0 remaining consumers repo-wide), not kept as a deprecated back-compat export.
403
+ `owner-config-loader.mjs` and `owner-interview.mjs` now call through
404
+ `resolveOwnerYamlPath()` instead of their own copies. `tests/husky/pre-commit-owner-leakage.test.mjs`'s
405
+ file-by-file copied import chain (the CP11 scanner's dynamic-import fixture) gained the new
406
+ leaf as a required copy.
407
+ - **`skills/wave-executor/wave-loop.md` gains a "Shell variables used in this section"
408
+ preamble (#1225).** `$PLUGIN_ROOT`, `$WAVE`, `$ROLE`, `$STATE_DIR` are defined once ahead
409
+ of the Scope Manifest steps instead of being re-explained inline (the `$PLUGIN_ROOT`
410
+ per-platform expansion parenthetical is now a single cross-reference instead of a repeated
411
+ clause).
412
+ - **`scripts/lib/ci-status-banner.mjs` gains a three-state contract instead of two (#1031).**
413
+ A frozen `DEGRADED_REASONS` enum (`cli-missing`, `timeout`, `parse-error`, `query-failed`,
414
+ `git-error`) now distinguishes "state could not be read" from "no CI here" — before this,
415
+ 4 return sites collapsed every CLI-missing / timeout / unparseable-output / VCS-probe-error
416
+ case onto the same `null` an absent remote returns, which the banner reads as all-clear
417
+ (the same collapse class documented for `mirror-issues-banner.mjs` in
418
+ `skills/session-start/SKILL.md`, now fixed on this probe too). Consumers updated to the new
419
+ shape: the probe registry in `scripts/lib/session-start-probes.mjs` (`ci-status` entry's
420
+ `render`/`severityOf` now treat a `degraded` result as `warn`, not `ok`);
421
+ `scripts/lib/dispatcher/rank.mjs` (new exported `normalizeCiSignal()` reduces every shape —
422
+ bare string, `{status}`, `{degraded}`, `null` — to `{ciStatus, ciDegraded}`, mapping
423
+ `degraded` to `'unknown'` for scoring parity with the old behaviour while surfacing the
424
+ reason into `signals.readiness.ciDegraded` and a `warnings` entry); `scripts/release.mjs`'s
425
+ `evaluateCiRow()` treats `ci.degraded` as "CI status unknown (\<reason\>)", never as green.
426
+ `skills/session-start/SKILL.md`'s CI-status paragraph documents the new degraded banner
427
+ line. `scripts/lib/qg-command-drift-banner.mjs` is converted too (W3-P1): it mints its own
428
+ `degradedBanner(reason)` and passes a nested `detailed.degraded` straight through
429
+ (`:59`/`:99`/`:102-103`), so a config that could not be READ is reported as `warn` rather
430
+ than read as "no drift".
431
+ - **`hooks/post-tool-batch-wave-signal.mjs` starts emitting the `files_changed` measurement
432
+ `scripts/lib/convergence-monitor.mjs`'s `shrinking_diff` signal has always read but no
433
+ producer ever wrote (#980).** At an N→N+1 wave-open transition, the `orchestrator.wave.completed`
434
+ emitted for wave N now carries `files_changed` (the deduped union of
435
+ `git diff --name-only <wave_start_sha>` and `git ls-files --others --exclude-standard`,
436
+ measured against the working tree because the coordinator commits at session close, not per
437
+ wave) and `files_changed_source: 'worktree-vs-wave-start-sha'`; `wave_start_sha` (`git
438
+ rev-parse HEAD`) is persisted into `.orchestrator/current-session.json` under the same
439
+ ownership gate as the other wave keys when a wave opens. Both keys are optional and
440
+ absent-is-not-zero — any git failure, a 1.5s timeout, or a missing `wave_start_sha` omits
441
+ them, and the monitor reads the absence as `null` rather than firing on a fabricated 0.
442
+ `skills/convergence-monitoring/SIGNALS.md` gained a "Live monitor input" subsection under
443
+ all three signals (shrinking_diff, pass_rate_plateau, velocity_drop), each quoting a fresh
444
+ measurement of `.orchestrator/metrics/events.jsonl` (2026-09-05): 144 pre-existing
445
+ `wave.completed` records carried no `files_changed` key at all before this fix; 33
446
+ `orchestrator.quality_gate.*` records carry both `wave_number` and a well-formed `counts`
447
+ object (making `pass_rate_plateau` fireable, `_evaluateSignals` now a test-only export);
448
+ 11,754 `orchestrator.agent.stopped` records make `velocity_drop` fireable, versus 0 for the
449
+ `agent.dispatched` type the reader also accepts. `monitors/monitors.json`'s
450
+ `convergence-monitor` description corrected — it never opened `sessions.jsonl` (a stale
451
+ claim from an earlier draft of the monitor).
452
+ - **`scripts/validate-wave-scope.mjs`'s internal `validateSession()` renamed to
453
+ `validateSessionBinding()` (#1153 P11).** Rename only, no behaviour change — the old name
454
+ read as "validate the session" when it validates the wave-scope manifest's session-BINDING
455
+ fields specifically; a previously-considered `skipped[]` return addition was dropped as a
456
+ false premise (the function already reports via `errors`/`warnings`).
457
+ - **`docs/rule-authoring.md`'s `LEARNING_TYPE_REGISTRY` table gains its fourth axis,
458
+ `hostScoped` (#1153 P12, follow-up to #1090).** The table previously transcribed only
459
+ `ttlDays`/`agentProposable`/`ruleConvertible`, so a drifted `host_class`-gating cell was
460
+ structurally invisible to `tests/docs/rule-authoring-registry-parity.test.mjs`; the parser
461
+ regex and parity-diff logic there now check all four axes bijectively.
462
+ `hardware-pattern` is the only `hostScoped: true` type today.
463
+ - **`docs/README.md` gains a "Superseded design notes" section (#1153 P14).** Because
464
+ `docs/specs/` is gitignored, a correction written INTO a spec can never be committed — the
465
+ correction for `docs/specs/2026-05-26-parallel-aware-sessions-design.md` (which still
466
+ specifies PID-based lock liveness) lives here instead: liveness has been heartbeat-age
467
+ based since #1137, and the recorded PID has been consulted nowhere since #1151.
468
+ `skills/_shared/parallel-aware-preamble.md` cross-references it.
469
+
470
+ ### Removed (BREAKING)
471
+
472
+ Every entry under *4.0.0 — audit session* below is a REMOVED PUBLIC SURFACE and the reason
473
+ this release is a major. Each names its replacement or states explicitly that there is none.
474
+ Upgrade guide: [`docs/migration-v4.md`](docs/migration-v4.md).
475
+
476
+ **Deviation from the deprecate-then-remove norm** (`.claude/rules/development.md` § Package
477
+ Lifecycle: stub + first-call `console.warn` for ≥1 minor cycle). The 5 skills and 3 commands
478
+ below are HARD-deleted, not stubbed — an explicit operator decision made 2026-09-06. The
479
+ removal test was **0 telemetry ∧ 0 fleet invocation over 90 days ∧ no runtime consumer**, and
480
+ that 90-day window is honestly a *search* window, not 90 days of shipped exposure: `journey-audit`
481
+ itself had existed for only 9 days (added 2026-08-28) before this cut. Full rationale and the
482
+ manual cleanup steps: [`docs/migration-v4.md`](docs/migration-v4.md).
483
+
484
+ #### 4.0.0 — audit session
485
+
486
+ - **5 skills removed** — `find skills -name SKILL.md | wc -l` → 43, against
487
+ `git ls-tree -r --name-only HEAD skills/ | grep -c 'SKILL.md$'` → 49 (the sixth delta is
488
+ the `domain-model` merge below):
489
+ - **`skills/daily/`** (plus `generate.sh`, its template and 2 test files) — **no
490
+ replacement.** 0 telemetry, 0 fleet invocations in 90 days, no runtime consumer; only its
491
+ own test and documentation referenced it. Recurring daily notes are a Routine / scheduled
492
+ task, not a session skill (`.claude/rules/loop-and-monitor.md` § LM-004).
493
+ - **`skills/skill-creator/`** — **replaced by Anthropic's official `skill-creator` plugin**
494
+ plus `claude plugin init`. 0/0/0 with 0 in-repo references.
495
+ - **`skills/ubiquitous-language/`** — **no replacement.** 0/0/0.
496
+ - **`skills/contract-version-bump/`** — **no replacement.** 0/0/0; one comment referenced
497
+ it.
498
+ - **`skills/journey-audit/`** — **no replacement.** 0 journey manifests exist fleet-wide,
499
+ so the skill was never runnable in the first place.
500
+ - **3 commands removed** — `commands/*.md` 28 → 25, mirrored in `.cursor/commands/` and
501
+ `pi/prompts/` (both also 25): **`/contract-version-bump`**, **`/journey-audit`** (with
502
+ their skills above) and **`/autopilot-multi`**. **No replacement** for any of the three.
503
+ `/autopilot` is a different command, unaffected, and stays.
504
+ - **Session Config `autopilot.bg-isolation` removed** — the `autopilot:` block's only field,
505
+ retired together with `/autopilot-multi` above (`scripts/parse-config.mjs` never parsed an
506
+ `autopilot` key at all, so removing it changes no runtime behaviour). **No replacement** —
507
+ delete the block from your `CLAUDE.md`/`AGENTS.md`; single-story `/autopilot` takes no
508
+ Session Config block and is unaffected.
509
+ - **8 top-level scripts removed** (`scripts/*.mjs`, 0 runtime callers each, with their test
510
+ files): `autopilot-multi.mjs`, `backfill-learnings.mjs`, `backfill-learnings-expires.mjs`,
511
+ `fleet-instruction-scan.mjs`, `lifecycle-sim-v6.mjs`, `migrate-learnings-jsonl.mjs`,
512
+ `migrate-subagents-jsonl.mjs`, `upload-social-preview.mjs` — **no replacement.** The two
513
+ `migrate-*` scripts were one-shot format migrations whose target format has been canonical
514
+ for several releases; the two `backfill-learnings*` scripts were one-shot ledger repairs.
515
+ - **`skills/_shared/model-selection.md` removed — no replacement, 0 consumers.**
516
+ - **`skills/domain-model/` removed and MERGED into `skills/architecture/references/`** — its
517
+ three files now live as `references/domain-model.md`, `references/ADR-FORMAT.md` and
518
+ `references/CONTEXT-FORMAT.md`. It was reference-only material with 4 inbound links, never
519
+ a dispatchable skill; `/architecture` is the entry point.
520
+ - **Two non-agents moved OUT of `agents/`** — `agents/*.md` 16 → 14. The agent loader
521
+ registers every `.md` file in that directory as a dispatchable agent, and
522
+ `agents/AGENTS.md` — the authoring SPEC — was measurably dispatched as an agent 5 times. It
523
+ is now [`docs/agent-authoring.md`](docs/agent-authoring.md), and
524
+ `agents/memory-proposal-collector.md` is now
525
+ [`docs/memory-proposal-flow.md`](docs/memory-proposal-flow.md). Same content, no longer a
526
+ dispatch target. **Anything dispatching either by name must stop** — they are
527
+ documentation, and were only ever documentation.
528
+
529
+ - **`scripts/lib/autopilot/multi-killswitch.mjs` and
530
+ `scripts/lib/autopilot/dep-graph.mjs` deleted, with their test files** —
531
+ orphaned by the `/autopilot-multi` removal above, with only their own tests as
532
+ consumers. **No replacement**; single-story `/autopilot` never imported either
533
+ module.
534
+
535
+ #### Predecessor commits (v3.24.0 → `e4674109`)
536
+
537
+ - **`scripts/lib/webhook-url.mjs` deleted — zero callers repo-wide (#1168).** `resolveWebhookUrl`/
538
+ `WebhookConfigError` and their test file (`tests/lib/webhook-url.test.mjs`, 192 lines) are gone,
539
+ −364 lines total; the `## Webhooks (#228)` section in `docs/session-config-reference.md`
540
+ (the `webhooks.<kind>.url` Session Config surface it backed) is removed with it. The still-live
541
+ Clank Event Bus webhook (`scripts/lib/events.mjs`, `CLANK_EVENT_SECRET`/`CLANK_EVENT_URL`) is
542
+ unrelated and unaffected — its doc section is promoted from `### Clank Event Bus` to
543
+ `## Clank Event Bus` in the same edit, since it is no longer a subsection of the now-removed one.
544
+
545
+ ### Fixed
546
+
547
+ #### 4.0.0 — audit session
548
+ - **`tests/telemetry/sync.test.mjs`: the two `detectSandbox` "real operator shape" cases no longer use `process.cwd()`** — the husky pre-push gate runs the suite in a tracked-tree copy under `$TMPDIR`, where cwd IS a temp root and the guard correctly answers `sandbox:temp-root`; both cases were red on every push attempt while green in the checkout (measured 2026-09-06 — the first push of the 4.0.0 review session failed on exactly this file, unnamed until the gate learned to name `failed_files`). They now probe a path under `os.homedir()`.
549
+
550
+ - **`js-yaml` out of the eager hook import graph — 4 of 27 hooks crashed on every turn
551
+ without `node_modules`; now 0 (GH#62 / GH#63 / GitLab #1230).**
552
+ `scripts/lib/owner-yaml.mjs` carried a static `import yaml from 'js-yaml'` and sits on the
553
+ import graph of `on-session-start`, `on-session-end`, `post-edit-validate` and
554
+ `skill-invocation-telemetry` — so a skipped or interrupted `npm install`, a half-synced
555
+ plugin cache, or an EPERM sandbox killed all four at MODULE-LOAD time with
556
+ `ERR_MODULE_NOT_FOUND` and exit 1, on every single turn. `js-yaml` now resolves lazily
557
+ inside `loadOwnerConfig`/`writeOwnerConfig`. Measured 2026-09-06 @ `e4674109` with `hooks/`
558
+ and `scripts/` copied to a tmp dir carrying no `node_modules` anywhere up the tree:
559
+ **23/27 hooks exited 0 before the fix, 27/27 after.** Pinned by the new
560
+ `tests/hooks/hooks-run-without-node-modules.test.mjs`, which EXECUTES every hook instead of
561
+ reading source text — the pre-existing static-import guard would have accepted a lazy
562
+ `require()` of a package that does not exist. The `zx` half of #1230 is stale and now says
563
+ so: `scripts/lib/worktree/listing.mjs`'s `zx` imports are already lazy and caught, verified
564
+ by installing a `js-yaml` stub alone and watching all four hooks reach rc=0 with `zx` still
565
+ absent.
566
+ - **`picomatch` declared in `package.json` dependencies.** It is imported by four runtime
567
+ modules (`rule-loader.mjs`, `command-blocker.mjs`, `validate-vendored-rules.mjs`,
568
+ `reconcile/emitter.mjs`) and was absent from the manifest at `HEAD`
569
+ (`git show HEAD:package.json | grep picomatch` → no match) — reachable only as somebody
570
+ else's transitive dependency, the GH#62 class exactly.
571
+ - **The Cursor adapter generator wrote a malformed `argument-hint` into 24 of 28 command
572
+ files (GH#54).** `scripts/generate-cursor-adapter.mjs` now renders every frontmatter value
573
+ through `yamlQuote()`, which JSON-quotes anything failing `YAML_PLAIN_SAFE` (and anything
574
+ that looks like a YAML scalar, or starts with a digit). Measured after the fix:
575
+ `grep -c "argument-hint: \[" .cursor/commands/*.md` reports zero files carrying the array
576
+ shape. The regression survived for as long as it did because the generator was tested
577
+ against itself; it is now pinned against the spec.
578
+ - **`generateFrontmatterSnippet()` THREW when the baseline was absent.**
579
+ `scripts/lib/frontmatter-guard.mjs` resolves the schema through
580
+ `resolveHostPath('baseline-path')` and treats a missing schema as `null` — the honest
581
+ outcome, with the documented fallback enum set — instead of raising inside a pre-dispatch
582
+ guard. `scripts/lib/vault-backfill/template.mjs` uses the same resolver.
583
+ - **The session registry wrote into the operator's REAL host-private config from a
584
+ sandbox.** `scripts/lib/session-registry.mjs` now derives its `sessions/` directory from
585
+ `resolvePrivateConfigDir()` (`SO_CONFIG_HOME` > `XDG_CONFIG_HOME` >
586
+ `~/.config/session-orchestrator`) instead of its own homedir-only path, so a redirected
587
+ config home actually redirects it.
588
+ - **`node scripts/parse-config.mjs --json` failed with `File not found: --json`.** The flag
589
+ is now an explicit NO-OP alias for the default (stdout has always been JSON), because the
590
+ bootstrap documentation names that exact command as THE validation command and
591
+ `cli-design.md` § JSON-First Output requires every CLI to accept `--json`.
592
+ - **The vault-staleness probe measured against the clock instead of against the repo
593
+ (#1238).** `skills/discovery/probes/vault-staleness.mjs` now asks whether the upstream repo
594
+ advanced PAST the last sync, not what `now - lastSync` is — a mirror of a repo nobody has
595
+ committed to in three weeks is CURRENT, not three weeks stale. Both sides of the comparison
596
+ (`lastSync`, `lastCommit`) come from one read of the same `_overview.md` frontmatter.
597
+ Measured against the live vault before the fix (2026-09-05): **33 of 48 overviews reported
598
+ "stale", 26 of them >7d, with a demonstrably healthy sync chain.** An overview without
599
+ `lastCommit` still falls back to the wall-clock comparison, marked
600
+ `basis: 'probe-runtime'` and carried at lower confidence so a consumer can tell a measured
601
+ delta from a guessed one.
602
+ - **The discovery-validator scanned the WRONG transcript, so effectively every violation it
603
+ recorded was the coordinator's own prose (#1191).** `input.transcript_path` on
604
+ `SubagentStop` is the PARENT/main session transcript, not the subagent's — measured
605
+ 2026-09-02 on a seeded random sample of 60 violations: **100 % coordinator text,
606
+ scope-adjusted precision 0 %**, with `agent` reported as `"unknown"` in 90.8 % of 1,541
607
+ events. The hook now reads `<transcriptDir>/<session>/subagents/agent-<agent_id>.jsonl`
608
+ (the layout `subagent-telemetry.mjs` and `wave-transcript-tail.mjs` already use) and NEVER
609
+ falls back to the parent path: a scan of the wrong transcript is worse than no scan. Claims
610
+ are additionally collapsed per distinct normalised claim with an `occurrences` count
611
+ (`normalizeClaim()` / `dedupeViolations()` in `hooks/_lib/subagent-transcript.mjs`, #1198)
612
+ — the worst repo in the fleet held 3,360 records over 205 distinct claim texts, a ×16.4
613
+ duplication factor.
614
+ - **The `ecosystem-health` watcher could never start.** Its trigger in
615
+ `monitors/monitors.json` was `on-skill-invoke:ecosystem-health` — self-referential: the
616
+ watcher could only start when the `ecosystem-health` skill ran, and that skill has 0
617
+ recorded invocations fleet-wide, so it never started once. The trigger is now
618
+ `on-skill-invoke:session-start`.
619
+
620
+ - **CP11 (the confidential-names owner-leakage check) could fail OPEN and still
621
+ print `PASS` (#1244).** `getConfidentialNamePatterns()`
622
+ (`scripts/lib/validate/check-owner-leakage.mjs`) collapsed three different
623
+ outcomes onto the same `[]` degrade: (a) the standalone single-file copy, where
624
+ the helpers are genuinely unresolvable — the only case meant to go inert; (b)
625
+ CP11 simply unconfigured; and (c) CP11 configured but unreadable (missing
626
+ `js-yaml`, an unparseable `owner.yaml`, or a wholesale-discarded partial
627
+ config), which used to read identically to (b) and print the clean verdict it
628
+ never earned. Case (c) now returns a `disabledReason` and the scanner prints
629
+ `CP11 DISABLED: <reason>` plus a counted FAIL — with no path in the reason
630
+ string, since this scanner's output is mirrored to a public CI log.
631
+ `owner-yaml.mjs`'s whole-file discard on an invalid REQUIRED section (#820) now
632
+ merges a still-VALID optional object section (e.g. `paths:`) back onto its
633
+ default instead of discarding it too, so a correctly declared
634
+ `confidential-names-file` no longer vanishes because an unrelated section was
635
+ malformed; a genuinely invalid optional section is reported via
636
+ `droppedSections`, and an unparseable file gets the new `reason: 'unparseable'`.
637
+ A third, unrelated fail-open closed in the same file: `isMain` now compares
638
+ `realpathSync()` of both sides, so a symlinked invocation path (`/tmp` →
639
+ `/private/tmp` on macOS) can no longer make the scanner exit 0 having scanned
640
+ nothing.
641
+ - **CP11's own DISABLED path (above) was itself overbroad, and its degrade
642
+ path had two further defects — all three closed together in
643
+ `scripts/lib/validate/check-owner-leakage.mjs` and `.husky/pre-commit`
644
+ (Wave-4 Codex second-look).** First, DISABLED fired even when NO
645
+ confidential-names file was ever configured: a healthy `owner.yaml` with
646
+ only an unrelated invalid optional section (e.g. `paths: { vault-dir: 42
647
+ }`) and no `confidential-names-file` key reported `CP11 DISABLED` and a
648
+ counted FAIL, when the correct verdict is "inactive." A new
649
+ `rawConfidentialNamesKeyState()` re-reads the RAW (pre-validation) key
650
+ before classifying, so DISABLED now fires only when a names path was
651
+ actually configured; a healthy owner.yaml with `paths:` present but no
652
+ names-file key now stays `PASS` with no CP11 line at all, pinned by a new
653
+ case in `tests/lib/validate/check-owner-leakage.test.mjs`. Second, the
654
+ standalone-copy inert degrade (the ONE case meant to go silently
655
+ patterns-`[]`) was scoped by a bare `ERR_MODULE_NOT_FOUND` check, so a
656
+ missing TRANSITIVE module — not just the scanner's own three vendored
657
+ sibling imports — also went inert instead of failing closed.
658
+ `isMissingDirectSibling()` now inspects `err.url` (the URL Node's own
659
+ `ERR_MODULE_NOT_FOUND` carries) against the scanner's three known
660
+ direct-sibling import URLs; a miss anywhere deeper now THROWS. Third,
661
+ `.husky/pre-commit` was capturing but discarding the scanner's own stderr
662
+ on failure and printing the same "privacy leak detected" line regardless
663
+ of cause; it now prints the scanner's (redacted) diagnostic tail and
664
+ distinguishes "the scanner could not run" from "the scanner found a
665
+ leak." 259/259 tests pass; 4 fake-regression probes (each reverting one
666
+ of the three fixes above) still turn red; a live repo scan still PASSes.
667
+ - **`checkOwnerConfig()`'s whole-file-discard message claimed "the entire
668
+ file was discarded" even when a validly-configured OPTIONAL section
669
+ survived the discard onto its default (#1244 merge rule, Fixed above).**
670
+ `scripts/lib/owner-config-banner.mjs` now diffs each optional object
671
+ section (`paths`, `dispatcher`) in the merged config against
672
+ `getDefaults()`: a section that differs was real and survived, and the
673
+ banner names it — `"paths" kept, everything else defaulted` — instead of
674
+ claiming total loss; `droppedSections` renders on this branch too.
675
+ Reproduced with a tmp owner.yaml carrying an invalid `owner.name`
676
+ (required, triggers the whole-file path) alongside a valid
677
+ `paths.confidential-names-file` (a Wave-4 architect-panel finding); a new
678
+ case in `tests/lib/owner-config-banner.test.mjs` pins the "kept" wording.
679
+ 168/168 tests pass; 2 fake-regression probes still turn red; a real,
680
+ healthy host's banner still returns null.
681
+ - **`check-skill-script-paths` and `check-validator-registration` answered an
682
+ existence question from the git INDEX, so a brand-new untracked file's own
683
+ citation was invisible to them until `git add` (#1248).** Measured in a clone
684
+ (Wave-1 discovery): an untracked skill citing a non-existent script reported
685
+ `1 passed, 0 failed` before staging and `0 passed, 1 failed` after — same tree,
686
+ same defect. New shared `scripts/lib/validate/enumerate-repo-files.mjs`
687
+ (`enumerateRepoFiles()`) answers "does this exist under these roots right now"
688
+ via `git ls-files --cached --others --exclude-standard`, honouring
689
+ `.gitignore` without reopening #1143 (a bare `readdirSync` walk was tried and
690
+ REFUTED: it pulls a gitignored peer worktree under `.claude/worktrees/` into
691
+ the census — measured 287 vs 290 `.md` files with one peer worktree present).
692
+ Both checkers, and the release drift sweep (Changed, above), now share this
693
+ one enumerator instead of three different partial views.
694
+ - **`enumerateRepoFiles()` (above) silently dropped `stat` failures other than
695
+ `ENOENT`/`ENOTDIR`, so a permission-denied file read as "does not exist"
696
+ instead of "unknown" — the release drift sweep (Changed, above) could
697
+ report a clean sweep on 0 actual reads (Wave-4 Codex second-look).** It
698
+ now throws a typed `RepoEnumerationError`
699
+ (`scripts/lib/validate/enumerate-repo-files.mjs`) for
700
+ `EACCES`/`EPERM`/`ELOOP`/`EIO`/`ENAMETOOLONG`/anything else it cannot
701
+ classify as "provably absent," carrying the path and the underlying error
702
+ code; only `ENOENT` and `ENOTDIR` are still treated as absent.
703
+ `collectDriftHits()` (`scripts/release.mjs`) reports this as exit 128,
704
+ which the release preflight already reads as "inconclusive" — the same
705
+ reading a crashed `git grep` got, never a silent clean. 195/195 tests
706
+ pass across the 5 touched files; the Codex reproduction now shows status
707
+ 128 / `ok:false` instead of a clean sweep; `validate-plugin.mjs` still
708
+ 234/0. Two known sibling sites of the same swallow-class are filed as
709
+ follow-up issues rather than fixed here: `listRepoFiles()`
710
+ (`scripts/lib/repo-files.mjs`) has the identical catch-all, and 3 further
711
+ test files still hand-roll `git init` + a bare recursive `rmSync` in
712
+ template-string form (invisible to a literal-string census) rather than
713
+ routing through `tests/_helpers/tmp-fixture.mjs`.
714
+ - **15 more fixture-shaped test files are routed onto the
715
+ `tests/_helpers/tmp-fixture.mjs` helper (the detached-`git-maintenance` fix,
716
+ below) this session — 3 of them deleting a weaker inline copy of the same
717
+ helper that `enumerate-repo-files.test.mjs`, `check-validator-registration.test.mjs`
718
+ and `check-skill-script-paths.test.mjs` had each written for themselves
719
+ (above), plus 11 legacy files and a `release.test.mjs` git-grep-to-fixture
720
+ probe.** `tests/unit/quality-gate-session-config.test.mjs` — the one file
721
+ this pass left out of scope, needed here because it was also touching
722
+ `enumerate-repo-files.test.mjs` — is itself routed in this same session (23
723
+ insertions / 30 deletions: `execSync('git init', ...)` →
724
+ `fixtureGit(['init', ...])`, `rmSync(repoRoot, ...)` → `removeTree(repoRoot)`,
725
+ `mkdtempSync(...)` → `makeTmpDir(...)`). Net −28 LOC across the routed
726
+ files; 526/526 of their own tests unchanged per file; eslint clean. The
727
+ self-census this whole effort is measured against
728
+ (`tests/lib/validate/check-test-git-config-target.test.mjs`) cannot resolve
729
+ a fixture helper's own `...spread` token to a git subcommand, so its floor
730
+ was lowered from `>50` to `>20` with a Revisit-Trigger comment naming the
731
+ exact condition ("the census learning to resolve the helper's spread")
732
+ rather than silently loosened — the remaining git-fixture files are reduced
733
+ this session, not proven at zero: 3 further sites in template-string form
734
+ are filed as follow-up issues (above).
735
+ - **The full test suite failed 1–2 tests per run, never the same one twice — a
736
+ detached `git maintenance` child racing the fixture's own cleanup.**
737
+ `GIT_TRACE=1` on a bare fixture commit showed `git maintenance run --auto
738
+ --quiet --detach` still writing into a temp `.git` directory after
739
+ `execFileSync` returned, so the `afterEach` `rmSync` raced a live process and
740
+ failed with `ENOTEMPTY`. New `tests/_helpers/tmp-fixture.mjs` (`fixtureGit`,
741
+ `fixtureGitSpawn`, `removeTree`, `makeTmpDir`) sets `maintenance.auto=false`,
742
+ `gc.auto=0` and `core.fsmonitor=false` on every fixture repo, and retries
743
+ removal (`maxRetries: 5`) for what config alone cannot reach. 24 fixture files
744
+ (118 git call sites, 35 `rmSync` sites, 39 `mkdtemp` sites) now route through
745
+ it; the detached-maintenance trace line is gone from the routed files, and the
746
+ 494 tests across those 24 files still pass.
747
+ - **10 of the 43 learning-provenance markers the 43→8 generated-rule
748
+ consolidation (Changed, above) dropped are restored, across
749
+ `git-and-worktrees`, `guard-design`, `measurement-discipline`,
750
+ `process-contracts`, `review-and-adapter-contracts`, `test-hygiene` and
751
+ `toolchain-and-build.md` (+121/−19 across the 7 files).** That consolidation's
752
+ own claim — "each keeping its provenance markers" — was false for 10 of them
753
+ when written; a fresh census now counts 43 of 43 keys and 43 of 43 ids (a
754
+ previously reported "42 ids" was itself a UUID-filter miss, not a real 42nd
755
+ gap). One of the 10 was a markers-only restoration against an
756
+ already-present duplicate prose block; the other 9 restore both the prose and
757
+ its evidence line.
758
+ - **`docs/migration-v4.md` §6 Rollback and its "Cursor still shows the removed
759
+ commands" recipe both had defects that would have re-broken what they were
760
+ trying to fix (Wave-4 Codex second-look).** §6's `cursor-install.mjs` /
761
+ `pi-install.mjs` rollback lines were missing the target-project argument
762
+ both scripts require — they default to `process.cwd()`
763
+ (`scripts/cursor-install.mjs:24-51`), so an operator following the doc
764
+ literally would link the installer into the session-orchestrator checkout
765
+ itself, not their own project. Both lines now carry
766
+ `/path/to/your-project` explicitly, with a note that both installers are
767
+ ADD-ONLY: they skip any destination that already exists, so re-running the
768
+ 3.24.0-checkout installer never resyncs a `hooks.json` or Pi settings file
769
+ written under 4.0.0 — only adds what your project is missing. The
770
+ "Cursor still shows the removed commands" recipe used `rm -f` to delete
771
+ three named files, which deletes a REGULAR file with the same name exactly
772
+ as readily as a symlink — including a command an operator wrote themselves
773
+ under one of those three retired names; it is now a `[ -L "$p" ] && rm
774
+ "$p"` loop, symlink-only. Separately, the session-start plugin-update
775
+ banner's Pi instruction (Changed, above) led with the dev-fallback checkout
776
+ recipe though the documented PRIMARY Pi install path is `pi install
777
+ npm:session-orchestrator` (`docs/pi-setup.md:16`, §"Option 1") —
778
+ `PLATFORM_UPDATE_INSTRUCTIONS.pi` (`scripts/lib/plugin-update-banner.mjs`)
779
+ now leads with re-running that exact command, naming the checkout +
780
+ `--settings-only` recipe only as the fallback for a dev-fallback
781
+ registration. 25/25 tests pass; `check-skill-links.mjs` reports 86;
782
+ `validate-plugin.mjs` 234/0.
783
+ - **`#1229` closed — the session-lock heartbeat already refreshes on `Stop`,
784
+ `PostToolBatch` and `SessionStart`, not only once per wave; the reported gap
785
+ was a false premise, refuted by measurement.** Live call sites confirmed at
786
+ `hooks/on-stop.mjs:438` (fires every turn), `hooks/post-tool-batch-wave-signal.mjs:377`,
787
+ and `hooks/on-session-start.mjs:1078`; a heartbeat mid-wave measured 3 seconds
788
+ old. No `UserPromptSubmit` hook exists to add a fourth site.
789
+ - **`#1230` closed — `js-yaml` is already lazily required in `owner-yaml.mjs`
790
+ (`:120-127`, memoised, one WARN on failure) and all 27 hooks already exit 0
791
+ with both `zx` and `js-yaml` blocked; the reported gap was a false premise,
792
+ refuted by measurement.** The eager-import class the Predecessor-commits Fixed
793
+ entry above already closed was re-verified still holding at this session's
794
+ base commit — no regression, no further code change needed.
795
+
796
+ #### Predecessor commits (v3.24.0 → `e4674109`)
797
+
798
+ - **Two silent Session Config parsing bugs fixed across 39 block-shaped parsers
799
+ (#1162).** New shared module `scripts/lib/config/block-preprocess.mjs` fixes both: (a) a
800
+ block commented out with a multi-line `<!-- … -->` was read as LIVE config —
801
+ `stripHtmlCommentBlocks()` now strips it via the existing `htmlCommentSkipper()` state
802
+ machine; (b) the bold-bullet sub-key rendering (`- **enabled:** true`) matched no sub-key
803
+ regex and silently fell back to its default — `normalizeBoldSubkeys()` now normalises it
804
+ to `enabled: true` before parsing. `preprocessBlockLines()` (35 standard parsers) and
805
+ `preprocessBlockLinesNoDash()` (4 dash-RECORD parsers — `custom-phases`, `remote-hosts`,
806
+ `evolve`'s `_parseEvolve`, `health-endpoints`) are the two drop-ins for
807
+ `content.split(/\r?\n/)`; NoDash skips bold-normalisation because de-dashing a record's
808
+ first key would silently merge it into the previous record.
809
+ `config-protection.mjs`'s `_isConfigWeakeningAllowed()` and
810
+ `hooks/pre-bash-destructive-guard.mjs`'s bypass scan deliberately use HTML-comment
811
+ stripping ONLY, never bold-normalisation — the bold form
812
+ (`- **allow-config-weakening:** true` / `- **allow-destructive-ops:** true`) must not arm
813
+ a bypass. An UNTERMINATED `<!--` fails closed in both directions: a block parser gets its
814
+ lines back UNFILTERED, while both bypass scanners treat it as NOT ARMED (said out loud on
815
+ stderr, since a silently-ignored bypass would look identical to an operator typo);
816
+ `scripts/parse-config.mjs` prints one stderr WARN per session, not one per parser. Two
817
+ new parity test files pin the contract per parser: `tests/lib/config/preprocess-parity-a-l.test.mjs`
818
+ and `-m-z.test.mjs`.
819
+ - **PSA-007's git-write detector is argument-aware, closing a false-positive AND a
820
+ false-negative class (#1172, #1215).** `isGitWrite()` in
821
+ `scripts/lib/wave-transcript-tail.mjs` used to match the subcommand literal alone; it now
822
+ parses the arguments too. `git stash list`/`show`, `--version`, `--help`/`-h`,
823
+ `--dry-run`, and `-n` on every subcommand except `commit` (where `-n` means
824
+ `--no-verify`, still a real write — measured 2026-09-04 that `git commit -h` prints
825
+ `-n, --no-verify` while `add`/`rm`/`push -h` all print `-n, --[no-]dry-run`) now read as
826
+ reads, not writes. The value-taking global flags `-C`/`-c`/`--git-dir`/`--work-tree`/
827
+ `--namespace`/`--exec-path`/`--config-env`, and `command`/`env VAR=x` prefixes, are now
828
+ absorbed so `git -C /tmp stash` and `env FOO=x git commit` are still recognised as git
829
+ writes at all (measured false negative before the fix: `isGitWrite('git -C /tmp stash')`
830
+ returned `false`). `FIXTURE_CONTEXT_RE`'s `cd /tmp` match now also fires on the BARE path
831
+ with no trailing `/` — the reported false alarm was
832
+ `cd /tmp && git init && git commit -m x`. 32 table rows added (70 cases in the file
833
+ after the change) in `tests/lib/wave-transcript-tail.test.mjs`.
834
+ - **`buildLiveSignals()`'s injectable `_scanBacklog` test seam was unused at 5 call sites,
835
+ so the affected test suite shelled out to a live `glab issue list` on every run
836
+ (#1169).** `tests/lib/autopilot.test.mjs` now passes a null-returning `_scanBacklog` stub
837
+ at every call site that does not assert on `signals.backlog` (`selectMode`/`computeDelta`
838
+ never read that field, so the stub is behaviour-preserving). `scripts/lib/autopilot/loop.mjs`
839
+ also migrated off the removed `SO_STATE_DIR` constant onto `getStateDir()` (#1153 P5) in
840
+ the same pass. Test-phase wall time for the affected suite dropped from ~750ms to ~25ms,
841
+ with no live network calls remaining.
842
+ - **One flaky spawn-timeout test pinned with margin instead of disabled (#1217).**
843
+ `tests/unit/plugin-manifests-exit-codes.test.mjs`'s "exits 1 (not 2) when plugin.json
844
+ contains broken JSON" case now passes `{ timeout: 30_000 }` explicitly — measured
845
+ 887–965ms unloaded (W1-D4, 2026-09-04), with one observed 10,095ms outlier attributed to
846
+ host contention, not spawn cost. Same discriminating-margin pattern as the three tests
847
+ hardened earlier in this file; no repo-wide pinned-slow list exists (#976 never
848
+ implemented).
849
+ - **Legacy `.bak.`-delimited backup files were invisible to both rotation and restore
850
+ (#1173).** `scripts/lib/learnings/io.mjs` exports `backupSuffixOf()`/`isBackupOf()`,
851
+ accepting both the canonical `.bak-<ISO>` delimiter this module writes and the legacy
852
+ `.bak.<label>-<ts>` delimiter pre-#721 writers left behind (e.g.
853
+ `learnings.jsonl.bak.evolve-<ts>`), shared with `backfill-learnings-from-vault`'s restore
854
+ sweep so the two predicates cannot drift apart again. `rotateBackups()` now sorts on the
855
+ SUFFIX (a leading non-digit label stripped first) rather than the whole filename —
856
+ sorting on the whole name grouped every `.`-delimited legacy file after every
857
+ `-`-delimited one regardless of age (`-` is 0x2D, `.` is 0x2E), so rotation pruned only
858
+ hyphen-form backups. `scripts/backfill-learnings-expires.mjs` itself was still emitting
859
+ the dot form on `--apply` — its own backups were the ones going unrotated and unrestored
860
+ — and now emits the canonical hyphen form (`tests/scripts/backfill-learnings-expires.test.mjs`
861
+ pins both the new emission and the absence of the old one).
862
+ - **A peer session's `wave-scope.json` drove this session's Bash gates on two hooks, and a
863
+ rebind of the manifest could suppress its own tamper notice (#1153 P1).**
864
+ `hooks/enforce-commands.mjs` and `hooks/post-bash-write-verify.mjs` both gain a Gate 3b
865
+ ownership check — `classifyManifestSession()` against `readProcessLocalSessionIds()` (hook
866
+ payload + `CLAUDE_CODE_SESSION_ID`, deliberately not the lock-file tier that made a
867
+ peer's manifest classify as `'own'` under #1194) — and stand down silently when the
868
+ manifest provably names another session, emitting `orchestrator.scope.foreign_session_ignored`
869
+ for observability. In `post-bash-write-verify.mjs` the ordering is the fix itself: the
870
+ control-file hash/enforcement snapshot is now computed BEFORE the Gate 3b stand-down, so
871
+ a `cat >` rebind of `wave-scope.json` to a fabricated `session_id` can no longer disarm
872
+ this session's gates AND suppress the #938 control-file notice in the same stroke — the
873
+ notice now fires once, on the call where the SESSION BINDING itself changed.
874
+ - **`processSession`'s two skipped-noop returns had no `maskerWouldChange` re-probe
875
+ (#1028, found by the Wave-4 security-reviewer as MED, confirmed HIGH by qa-strategist).**
876
+ The session-note generator's skip paths (`process.mjs:953`/`:991`) now carry the same
877
+ `maskerWouldChange` guard the learning-note generator already had at 3 sites
878
+ (`:703`/`:773`/`:797`); a missing `!force` check in the disambiguation-collision branch is
879
+ also fixed. 4 new tests, red on HEAD before the fix.
880
+ - **The legacy-flat heal wrote the masked copy to the namespaced path while the plaintext
881
+ legacy original stayed in place and became unreachable (#1028, qa-strategist HIGH-2).**
882
+ Both generators (learning-note and session-note) now re-render the leaking legacy note
883
+ masked IN PLACE (same fix shape at both `#660 IDEMPOTENCY DUAL-PROBE` sites), marking the
884
+ result `action: 'created'` + `meta.healed_legacy_flat: true` instead of leaving a second,
885
+ unreachable plaintext copy on disk.
886
+ - **`orchestrator.secret_masker.applied` now emits from all three channels that mask vault
887
+ content, not two.** `scripts/lib/vault-mirror/process.mjs`,
888
+ `scripts/lib/vault-status/narrative-mirror.mjs`, and `scripts/export-hw-learnings.mjs`
889
+ (documented in its own comment as "the third") each carry the emit at the end of their run.
890
+ - **`quality-gate.mjs`'s fail-open corrective-context path was silent (#1205, Wave-4
891
+ security-reviewer LOW).** Without `CLAUDE_CODE_SESSION_ID` (Codex/Cursor with no
892
+ process-local witness), a peer's `corrective_context` was kept without any signal. It now
893
+ prints a stderr WARN naming the UNVERIFIED session id before keeping the context; behaviour
894
+ is unchanged, only the visibility.
895
+ - **`check-skill-script-paths` had 24 dead script citations sitting unannotated inside
896
+ `docs/` (#1208).** `docs/` is now in `SCAN_DIRS`; 24 dead citations were annotated with the
897
+ existing `<!-- path-check: planned #<iid> | historical | example -->` marker convention
898
+ (measured: 1,087 citations across 259 files, 53 total annotated after this pass). A new
899
+ `--strict-sh` mode extends the same dead-path scan to `.sh` citations, reported as an
900
+ advisory count line in `validate-plugin` output (0 findings currently).
901
+ - **A block commented out with `<!-- … -->` still armed the dispatcher-autonomy one-time
902
+ capture AUQ (#1222).** `isDispatcherAutonomyBlockPresent()` called the shared
903
+ `hasBlockHeader()` matcher directly on raw content, while `scripts/parse-config.mjs`
904
+ preprocesses with comment-stripping first — so a `dispatcher-autonomy:` header sitting
905
+ inside a comment parsed to defaults (correctly) but was reported PRESENT (incorrectly),
906
+ meaning the operator was never asked. The guard now preprocesses with the same
907
+ `preprocessBlockLinesNoDash()` the parser uses before testing for the header, so an
908
+ unterminated `<!--` still fails toward PRESENT (one un-asked question, not a silently
909
+ disarmed bypass) matching the module's own documented one-time-capture contract.
910
+ `block-header.mjs`'s docblock now states explicitly that comment-stripping is the caller's
911
+ job. The three further planned call sites in `skill-evolution.mjs`, `persona-gate-wave.mjs`,
912
+ and `wave-reviewers.mjs` are NOT part of this change — none of the three currently calls
913
+ `hasBlockHeader()`/`isDispatcherAutonomyBlockPresent()` at all, so there is nothing to sweep
914
+ there yet.
915
+ - **`reapStaleBudgetFiles()`'s exact-boundary comparison had no falsifying test (#1153
916
+ P8).** A file aged exactly `maxAgeDays` is meant to be KEPT (`mtimeMs >= now -
917
+ maxAgeDays*86400000`); the existing 30-vs-1-day test stayed green even if `>=` were
918
+ weakened to `>`, which would silently reap a peer session's issue-budget file the instant
919
+ it turned `maxAgeDays` old. A new test in `tests/unit/hook-issue-budget.test.mjs` seeds one
920
+ file exactly on the cutoff and one a second past it and asserts the former survives and the
921
+ latter does not.
922
+ - **`sessionFromLock()`'s `host_id` fallback for a pre-#1072 lock had no test (#1153 P9).**
923
+ Two new cases in `tests/lib/session-discovery.test.mjs` pin both branches of
924
+ `lock.host_id ?? stableHostname(...)`: a lock that already carries `host_id` passes it
925
+ through unchanged, and a lock written before #1072 (no `host_id` field) derives it from the
926
+ raw `host` — without the fallback pin, a lock-sourced session could silently regress to the
927
+ raw, non-normalised host for comparison against the registry path's normalised twin.
928
+ - **`markCandidateProcessed()`'s mint branch stamped a self-contradictory terminal record
929
+ (#1153 P10, follow-up to #1042).** A freshly-minted candidate (no prior sidecar record) was
930
+ always given `status: 'proposed'` even while being stamped `processed_at` +
931
+ `outcome: 'rejected'` in the same write — a terminal, declined candidate that still read as
932
+ a live proposal to anything rendering `status`. `status` now agrees with `outcome`
933
+ (`'rejected'` when the outcome is `'rejected'`, `'proposed'` otherwise, the only two
934
+ ReconcileCandidate status values). Separately, the function used to return the
935
+ freshly-built stamp even when `mergeCandidates`'s dedupe rule kept an OLDER terminal record
936
+ on disk instead — caller and store then disagreed about the verdict, silently. It now reads
937
+ the record back out of the merge result and returns that, plus a new `alreadyProcessed`
938
+ flag (`written` stays `true` in that case: the store IS in the intended terminal state, and
939
+ `written: false` is reserved for a genuine write failure). Four new tests in
940
+ `tests/lib/reconcile/idempotency.test.mjs`, red on HEAD before the fix.
941
+ - **A learning whose `file_paths` were entirely unusable produced the same generic
942
+ "no activation axis" rejection as a learning with no `file_paths` at all (#1153 P13).**
943
+ `globsFromFilePaths()` now records each skipped entry (glob-metachar / control-char / quote)
944
+ into an out-parameter sink, rendered by the new `formatDroppedFilePaths()` into a
945
+ `— dropped file_paths: N (…)` suffix on the never-always-on rejection reason — the message
946
+ engine.mjs forwards verbatim into the operator-visible candidates ledger. Each dropped value
947
+ is `JSON.stringify`-escaped (so a raw newline or ANSI control char in a hostile `file_paths`
948
+ entry cannot inject into the reason string) and hard-truncated to 60 chars with no
949
+ mid-escape cut. Two new tests in `tests/lib/reconcile/emitter.test.mjs` pin both the naming
950
+ and the escaping.
951
+ - **`writer.mjs`'s `isOperatorRejection()` inferred an operator rejection from rendered
952
+ `content` being non-empty, which an operator-declined proposal with an empty rendered body
953
+ could fail (#1153 P15, closes the inference gap left open by #1042's own documented
954
+ ceiling).** `skills/session-end/phase-3-6-tail.md` step 6 now stamps every proposal the
955
+ operator left unselected with an explicit `operatorRejected: true` flag before it joins the
956
+ engine's `rejected` array; `isOperatorRejection()` keys on that flag first, falling back to
957
+ the old content-presence heuristic (marked `@deprecated`, kept only for a consumer repo
958
+ pinning a pre-P15 skill body) when the flag is absent. Two new tests in
959
+ `tests/lib/reconcile/writer.test.mjs` cover the flag-wins-over-empty-content case and the
960
+ legacy flagless fallback.
961
+
962
+ ### Security
963
+
964
+ #### 4.0.0 — audit session
965
+
966
+ - **The published npm tarball shipped 0 of the 6 files under
967
+ `.orchestrator/policy/` — including the destructive-command guard's own floor
968
+ policy (`blocked-commands.json`) — so an npm-installed consumer without a host
969
+ overlay ran `hooks/pre-bash-destructive-guard.mjs` against `rules: null` and it
970
+ silently ALLOWED every command the floor exists to block (found by the Wave-1
971
+ Codex external review, P1: `npm pack --dry-run | grep -c orchestrator/policy`
972
+ → 0).** `package.json`'s `files[]` now lists `.orchestrator/policy/`;
973
+ `npm pack --dry-run --json | grep -c "orchestrator/policy"` reports **6**
974
+ policy files today (`blocked-commands.json`, `ecosystem.schema.json`,
975
+ `quality-gates.example.json`, `quality-gates.schema.json`,
976
+ `templates-policy.json`, `test-profiles.json`) where it reported 0 before,
977
+ pinned by the new `tests/scripts/pack-policy-floor.test.mjs` (a real
978
+ `npm pack --dry-run`, not a mock). The release leakage gate's `.orchestrator/`
979
+ exclusion is narrowed to carve THIS directory back IN (`scripts/release.mjs`
980
+ around line 319) — the operator's own `metrics/`, `debug/` artefacts and live
981
+ `*.lock` files stay excluded; only the tracked policy floor ships. **The
982
+ Wave-4 review panel found this test asserted only a FLOOR (≥ 6 files) while
983
+ `package.json`'s `files[]` admits the WHOLE `.orchestrator/policy/` directory
984
+ and the carve-out above matched the path anywhere in the string, not just at
985
+ its start** — an untracked private file dropped into that directory would
986
+ ship, invisible to a floor check. `pack-policy-floor.test.mjs` now asserts SET
987
+ EQUALITY between the packed policy entries and `git ls-files
988
+ .orchestrator/policy` (tracked-only, sorted); the carve-out regex is anchored
989
+ to the path start (`/^\.orchestrator\/policy\//`, previously unanchored),
990
+ with the VCS-trust rationale — everything under this one directory ships
991
+ because it is tracked, not because of a per-file allowlist — recorded in a
992
+ comment beside it.
993
+ - **`session_profile` was the one free-form string field on the telemetry
994
+ wire — both an internal Wave-1 review and the Codex review reproduced sending
995
+ an arbitrary string through it.** `VALID_SESSION_PROFILES` (`['ultradeep']`
996
+ today) is now enforced on BOTH ends: the client
997
+ (`scripts/lib/telemetry/schema.mjs`) omits the field when it is not a known
998
+ profile rather than passing it through, and the ingest server
999
+ (`server/ingest/validate.mjs`) rejects a record whose `session_profile` fails
1000
+ the same enum plus a 32-char length ceiling. Reproduced before the fix: a
1001
+ crafted `session_profile` string reached the wire; after, the client sends
1002
+ nothing and the server rejects a forged one directly. `detectSandbox()`
1003
+ (`scripts/lib/telemetry/sync.mjs`) now also fails CLOSED — a probe that
1004
+ itself throws returns `{ sandbox: true, reason: 'sandbox:probe-failed' }`
1005
+ instead of falling through to "not a sandbox". `docs/telemetry.md` gained the
1006
+ field table, the no-free-text-fields claim, and a new section documenting the
1007
+ npm update-check's own egress (`SO_DISABLE_UPDATE_CHECK`, `DO_NOT_TRACK`,
1008
+ `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`).
1009
+ - **The `session_profile` whitelist above covered only the LIVE send path — a
1010
+ record already sitting in the offline queue when that whitelist landed still
1011
+ carried whatever it was enqueued with, and `flush()`
1012
+ (`scripts/lib/telemetry/sync.mjs`) forwarded every queued batch to the sender
1013
+ UNCHANGED.** Reproduced by the Wave-4 Codex second-look: a record the ingest
1014
+ server rejects is retried, still rejected, still requeued — the offline queue
1015
+ grew 1 → 2 → 3 and never drained. `sanitizeQueuedRecord()` now re-projects
1016
+ (`projectUsagePing`) and re-whitelists `session_profile` on every queued
1017
+ record before it joins a send batch, closing the same gap the live path was
1018
+ already closed for; the Codex reproduction now shows the queue back at 0
1019
+ after 3 consecutive rejections. A batch the server rejects with HTTP 400/422
1020
+ is now EVICTED (`reason: 'rejected-evicted'`) instead of re-queued: the
1021
+ ingest API validates a batch all-or-nothing with no per-record index, so the
1022
+ only bounded response to "the server will never accept this payload" is to
1023
+ drop the whole batch (named ceiling, BV-004: revisit if the server ever
1024
+ reports which record failed). `defaultSender` now attaches the HTTP status to
1025
+ the thrown error (`err.status`) so `flush()` can tell a schema rejection
1026
+ (evict) from a transport failure (re-queue, unchanged behaviour) — an
1027
+ injected sender that throws a bare `Error` still falls through to the
1028
+ pre-existing re-queue path. `docs/telemetry.md` documents `rejected-evicted`
1029
+ under "When a ping is sent".
1030
+
1031
+ ### Notes
1032
+
1033
+ - **Full Gate (Wave 4, `37169158`):** typecheck 432 OK, lint 0, 642 files / 15,926 passed /
1034
+ 0 failed / 11 skipped, `validate-plugin` 249/0, owner-leakage PASS (macOS). Quoted from
1035
+ the Wave-4 commit trailer, not re-run for this entry.
1036
+ - **The Wave-4 review panel (3 Opus reviewers: security-reviewer, session-reviewer,
1037
+ qa-strategist) found both HIGH findings above inside its own Wave-2/Wave-3 diff** — the
1038
+ `#1028` masker-guard residue the panel itself was reviewing. Both were closed same-wave by
1039
+ a dedicated fixpass (F-A/F-B/F-C) with fresh red-on-HEAD proofs before the green fix,
1040
+ plus test-hygiene cleanup (`dispatch-common.test.mjs` lost 12 structure-only assertions;
1041
+ `evolve-telemetry.test.mjs`'s regression tests no longer stat the real repo ledger) and
1042
+ three LOW fixes (`evolve-telemetry.mjs`'s `catch {}` now WARNs on stderr;
1043
+ `post-subagent-discovery-validator.mjs`'s German pattern now matches singular `Eintrag`;
1044
+ `.gitlab-ci.yml`/`docs/ci-setup.md` prose brought in line with the now-armed drift gate).
1045
+ - **A use-before-define in an intermediate save of `own-session.mjs` blocked every
1046
+ Bash/Edit call of every session on this host for roughly 8 minutes.** `hooks/enforce-commands.mjs`
1047
+ and `hooks/enforce-scope.mjs` both import the module live on every tool call (the #1153
1048
+ P1 Gate 3b wiring), so a `SyntaxError`/`ReferenceError` mid-refactor link-crashed both
1049
+ hooks host-wide rather than degrading to one repo's GUARD INACTIVE banner. Follow-up
1050
+ issue pending to make an intermediate, uncommitted save of a hot-path hook dependency
1051
+ fail more locally.
1052
+ - **This session's `session.lock` heartbeat was only refreshed once per wave, not
1053
+ continuously.** A long session-start/plan phase between heartbeats let a foreign
1054
+ `SessionEnd` reap the lock as stale before this session's own wave loop had a chance to
1055
+ refresh it. Deviation logged in this session's narrative; a follow-up issue for a
1056
+ time-based (not wave-based) heartbeat refresh is to be filed at close, not yet opened as of
1057
+ this diff.
1058
+ - **Full Gate (session close, uncommitted vs `4b451303`):** 651 files / 16,261 passed /
1059
+ 0 failed / 16 skipped, typecheck 435 OK, lint 0, `validate-plugin` 250/0, Semgrep 0 new
1060
+ findings, hook-import-set-check in sync (150 modules) — supersedes the Wave-4 note above.
1061
+
10
1062
  ## [3.24.0] - 2026-09-02
11
1063
 
12
- Five commits since v3.23.0 (4 `feat`, 1 `docs`; 152 files, +14,596/−942), no
13
- `BREAKING CHANGE:` footer and no `!` subject.
1064
+ Six commits since v3.23.0 (4 `feat`, 1 `fix`, 1 `docs`; 160 files,
1065
+ +15,586/−981 before the release commit), no `BREAKING CHANGE:` footer and no
1066
+ `!` subject.
14
1067
 
15
1068
  One `docs` commit (`c3ab4801`) adds ADR-0013, naming the ownership-check-before-deregister
16
1069
  ordering that closed Wave 4 of the previous session. One `feat` commit (`a019d5a4`, deep
@@ -303,6 +1356,19 @@ mint (UUIDv7, semantic session ids) were rejected or dropped by readers built fo
303
1356
  only — every resume looked foreign — and call sites that needed a repo root passed
304
1357
  `undefined` instead, so fail-open paths ran on every invocation.
305
1358
 
1359
+ This tag is also the first to carry the thirty-three commits of the 2026-08-22 → 2026-08-25
1360
+ line (`141d418..0626387`: 20 `fix`, 11 `feat`, 2 `docs`; 297 files, +24,535/−1,318 — measured
1361
+ 2026-09-06 @ `befdda47`, `git log --format=%s v3.22.0..0626387` → 33 subjects). A `3.22.1` cut
1362
+ dated 2026-08-25 had changelogged that line, but the release commit never left its working
1363
+ copy — no push, no tag, no registry tarball — and the next day's work resumed from `0626387`
1364
+ without it. Its entries are carried below under *From the 2026-08-22 → 2026-08-25 line*. One
1365
+ fact sits under all of them: **a surface that said it was live, and a measurement that never
1366
+ had a caller.** Cursor advertised as a first-class platform and loaded only `.cursor/rules` —
1367
+ no slash commands, no skills, no hook bridge; `/session` was chat text. The same class shows up
1368
+ in the rest of the line: 18 probes with zero mechanical callers, three vault writers that could
1369
+ not report their own failure, gates that wrote and never read, and `detectPlatform()` walking
1370
+ to `$HOME` until 63 of 84 telemetry records said `platform=pi`.
1371
+
306
1372
  ### Added
307
1373
 
308
1374
  - **Eight core rules ship as sanitized copies, with a report-only leak scanner (#1098).**
@@ -330,6 +1396,27 @@ only — every resume looked foreign — and call sites that needed a repo root
330
1396
  session-start Phase 0.5, Phase 1.2.1, and both `parallel-aware-*` docs. A real
331
1397
  `git worktree` two-root integration test goes phantom-peer red → green.
332
1398
 
1399
+ #### From the 2026-08-22 → 2026-08-25 line
1400
+
1401
+ - **Cursor is a native surface, not a rules overlay.** `.cursor/commands/*.md`
1402
+ (27 slash-command wrappers), `.cursor/skills/<name>/SKILL.md` (47 wrappers),
1403
+ `.cursor/hooks.json` → `scripts/lib/cursor-hook-bridge.mjs`. Same pattern as
1404
+ Pi: one maintained body in `commands/` and `skills/`, generated adapters,
1405
+ `validate-plugin` check (`check-cursor-adapter.mjs --check`). `cursor-install.mjs`
1406
+ now links commands and skills and writes `hooks.json` for other projects.
1407
+ Cursor has no Skill tool — commands Read `skills/<name>/SKILL.md`.
1408
+ - **The coordinator can see its agents, and a foreign model can build (#1113, #1150).**
1409
+ Wave supervision plus `cursor-agent` CLI foreign dispatch (detached worktree,
1410
+ filesystem-measured result, mandatory Claude review, `NEVER_FOREIGN_ROLES`).
1411
+ - **`/reconcile` ran for the first time in seven weeks** — 10 learnings became
1412
+ conditional rules (`feat(rules)`).
1413
+ - **Express-path** gained a mechanical caller and a single documented reading.
1414
+ - **18 measurement probes** now have a mechanical caller (`feat(probes)`);
1415
+ session-start no longer depends on prose to run them.
1416
+ - **Vault writers report their own failure**; the vault mirror reports that it
1417
+ is healthy (`feat(vault)`, `feat(telemetry)`).
1418
+ - **Ingest server** counts answers; the weekly digest has a caller.
1419
+
333
1420
  ### Fixed
334
1421
 
335
1422
  - **Six onboarding contradictions + a `cursor-install` TARGET guard (#1078).** First
@@ -424,6 +1511,36 @@ only — every resume looked foreign — and call sites that needed a repo root
424
1511
  one) plus a matching `hookTimeout` bump, and two further spawn-timeout assertions gained
425
1512
  the same discriminating margin between contention and a genuine failure.
426
1513
 
1514
+ #### From the 2026-08-22 → 2026-08-25 line
1515
+
1516
+ - **Cursor hooks were a reference mapping that silent-no-op'd (#919).**
1517
+ `hooks/hooks-cursor.json` is the live handler map. The bridge projects Cursor
1518
+ stdin onto `tool_name` / `tool_input` and maps deny envelopes onto Cursor
1519
+ `{ permission }`. `afterFileEdit` remains post-hoc (cannot unwrite).
1520
+ - **`detectPlatform()` walked to `$HOME` and found `~/.pi`** — 63 of 84
1521
+ telemetry records carried `platform=pi` (`fix(platform)`).
1522
+ - **Identity compared the machine name, then the working copy, then DHCP**
1523
+ (#1072, #1085 follow-through). Budget and lock judge the session. One machine,
1524
+ one name.
1525
+ - **A vault probe wrote into the operator's real vault**; the mirror now writes
1526
+ on the second run too (#1033).
1527
+ - **Guards that saw the wrong thing or discarded the right one** (#1029, #1130,
1528
+ #1077, #1125, #979, and a second trio that measured nothing).
1529
+ - **Reconcile: a No stays a No**; three paths that turned a rule into silent
1530
+ reject; gates that wrote and never read (EventDrop #484, #386, #385, #429);
1531
+ a type registry is enough (#1042, #1151).
1532
+ - **STATE.md: a broken list item costs the item, never the document (#1111).**
1533
+ - **Telemetry: noop-silent mirror, recorded aborts, budget reaper (#1151, #1127).**
1534
+ Consent question and flush no longer hang on prose.
1535
+ - **Privacy: the operator's machine name leaves the examples (#1151 Review).**
1536
+ - **Archive: a citation is not a declaration; foreign files are not mine
1537
+ (#1112, #1123).**
1538
+ - **Scope manifest knows whom it belongs to, and the hook asks (#1123, #1082).**
1539
+ - **Security copy claimed SAST/Semgrep levels that never existed**; the private
1540
+ npm scope no longer rides along with a copied baseline.
1541
+ - **A dying suite at import is counted as what it is**; a blocking guard now
1542
+ says why.
1543
+
427
1544
  ### Changed
428
1545
 
429
1546
  - **Host-registry census counts `semantic_session_id` (#1066).** `hooks/on-session-start.mjs`
@@ -453,6 +1570,12 @@ Two of this session's diffs were authored by foreign models under the #1150 adap
453
1570
  #1078 by composer-2.5, #1093 by grok-4.6 — each passing mandatory Claude review before
454
1571
  landing. First production use of the foreign-dispatch path.
455
1572
 
1573
+ npm 3.22.0 never contained the 2026-08-22 → 2026-08-25 line; 3.23.0 is the first registry
1574
+ tarball that does (`npm view session-orchestrator versions` @ 2026-09-06: 3.22.0, 3.23.0,
1575
+ 3.24.0 — no 3.22.1). The 3.22.1 cut that first documented the line was a release commit that
1576
+ was never pushed, which is why those entries surface here, one release late, re-measured at
1577
+ `befdda47` rather than quoted.
1578
+
456
1579
  ## [3.22.0] - 2026-08-22
457
1580
 
458
1581
  Twenty commits (12 `fix`, 5 `docs`, 2 `feat`, 1 `chore`; 150 files, +17,312/−2,575), no