# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [4.2.0] - 2026-09-09

### Added

- `scripts/session-shape.mjs` (+ `scripts/lib/session-shape.mjs`) — the one place a `/session` mode + optional `--profile` resolves into an execution shape (wave count, roles, per-wave agent caps, Discovery on/off, per-wave `max-turns`). Replaces prose that answered "how many waves does a `deep` session have" in 27 places and disagreed with itself in 8 of them (measured 2026-09-09). Emits `orchestrator.session.shape_resolved`. Housekeeping is now always 1 coordinator-direct maintenance-loop wave; feature is 3 waves; deep is 5 (4 with `--known-scope true`); the `ultradeep` profile is a fixed 7 waves and ignores `waves` outright (`wavesConfigHonored: false` — PRD AC-9's "`waves < 7` is an error" is dropped). Documented in `docs/session-config-reference.md` § Session Shapes and `docs/session-config-template.md`.
- `scripts/lib/maintenance-due-banner.mjs` — one session-start probe (`maintenance-due`) replacing the standalone `reconcile-nudge` probe and the session-end Phase 3.6.5/3.6.7 auto-dream/auto-dialectic nudges. Six ANDed signals (evolve, sweep, reconcile, dialectic, memory-cleanup, pending-sidecar), a 7-day housekeeping cooldown, banner `⚠ maintenance due: N of 6 (…) — run /session housekeeping.`. Motivated by measurement: `orchestrator.evolve.completed` fired zero times across consumer repos while 628 learnings sat unprocessed, and the auto-dialectic nudge recorded `decided: true` while nobody ever ran it.
- `scripts/lib/session-end/tail-runner.mjs` (`runTailPhases`) — the mechanical APPLY half of session-end Phase 3.6.4's Expired-Learnings Sweep, closing a gap where the dry-run decision ran every close but nothing ever applied it (census: 0 sweeps applied, 628 learnings resident, across three consumer repos). Emits `orchestrator.learnings.sweep_applied`.
- `scripts/lib/issue-budget-reconcile.mjs` — a close-time cross-check between a session's recorded issue creations and what the issue-budget ledger actually charged, emitting `orchestrator.issue_budget.reconciled` with verdict `match` / `escaped` / `no-ledger` / `stale-record`. Motivated by a session record with 26 recorded issue creations and no ledger file under either accounting key.
- `issue-budget.max-per-session` now accepts the same per-session-type override syntax as `agents-per-wave` (`12 (feature: 6)`), resolved against the current session's `session-type:` (read from STATE.md) via the new `resolveMaxPerSession()` / `readSessionTypeFromStateMd()` in `scripts/lib/issue-budget.mjs`. The parser (`scripts/lib/config/issue-budget.mjs`) now returns two keys — `max-per-session` (always numeric) and `max-per-session-raw` (the unresolved value) — plus `session-type-resolved` on the loaded config.
- `scripts/lib/telemetry/pricing.mjs` — a per-model USD-per-million-token price table (`PRICING_TABLE_DATE: 2026-09-09`) covering uncached input, cache-read, cache-creation and output rates, used by `scripts/lib/session-token-rollup.mjs` to compute `total_cost_usd` per session (gated on `schema_version >= 2`; older records are reported as `legacy_v1_records` rather than silently summed in).

### Security

- **`js-yaml` `^4.3.1` → `^4.3.2`** (GHSA-2883-xcg3-v3hh, high) — pipeline #512 on `main` had failed in `npm-audit` on the old range; `npm audit --omit=dev --audit-level=high` now exits 0.
- **`vitest` + `@vitest/coverage-v8` `^4.1.5` → `^4.1.11`** (GHSA: path traversal / arbitrary file read via `@vitest/mocker` redirect mock, moderate; dev-only) — `npm audit` now reports 0 vulnerabilities at any level; the two scaffolding templates (`templates/node-minimal`, `templates/nextjs-minimal`) move from `^3.1.0` to `^4.1.11` so Dependabot stops failing on them (`dependency_file_not_supported`, no lockfile in a template).

### Changed

- **CI (#1294)** — GitLab `test` job: 3 shards (was 2), inner vitest cap 840 s (was 600 s), job timeout 15 min (was 10), per-shard floor 2500 tests. Measured 2026-09-09: a degraded autoscaler VM ran every test file 3–4× slower and killed a green shard at the 600 s cap twice while its sibling shard finished in 206 s.
- `hooks/subagent-telemetry.mjs` schema_version 2 (#1244) — `token_input` is redefined from raw `usage.input_tokens` to BILLABLE PROMPT VOLUME (uncached + cache_read + cache_creation). Under prompt caching the old value understated real prompt volume by up to ~65,646× on one measured agent (56 vs 3,676,179 tokens). New additive fields `token_input_uncached`, `token_cache_read`, `token_cache_creation`, `model`. This is a SERIES BREAK — v1 and v2 `token_input` values are not comparable, and `scripts/lib/session-token-rollup.mjs` sums only `schema_version >= 2` records.
- `hooks/pre-bash-issue-budget.mjs` now charges per STATEMENT rather than per whole command; `hooks/_lib/vcs-create-matcher.mjs` additionally matches `gh|glab api … POST …/issues`.
- `docs/USER-GUIDE.md` § 4 Session Types and § 6 The Wave Pattern rewritten around the four resolved shapes (housekeeping/feature/deep/ultradeep) and a pointer to `scripts/session-shape.mjs --help`; the retired 3/4/5/6+ role-combination mapping table and the stale `housekeeping=2/feature=6/deep=6-10` agent-count table are replaced with the shape's actual per-type ceilings.
- `docs/components.md` — the Scripts exemplar list extended with `session-shape`, `maintenance-due-banner`, `tail-runner`, `issue-budget-reconcile`, `telemetry/pricing`.
- **Dead-code sweep (`check-unwired-features.mjs`)** — deleted `scripts/lib/soul-resolve.mjs`, `scripts/lib/owner-config-loader.mjs`, `scripts/lib/owner-config.mjs` + `scripts/lib/owner-config/` (7 files), `scripts/lib/multi-provider-build/{templating,providers}.mjs`, and the `scripts/lib/vault-mirror/render.mjs` barrel, plus their 12 test files — unreachable-library-module findings 5 → 2 (the 2 remaining are #1293). `.claude/rules/owner-persona.md`, `docs/owner-config-schema.md`, and `scripts/lib/owner-config.example.yaml` now describe the live `owner-yaml.mjs` surface instead of the removed loader.
- **#1288** — `decideAndRecordAutoDialectic` wrapper removed from `scripts/lib/auto-dialectic.mjs` (its only caller, session-end Phase 3.6.7, was already retired); `orchestrator.dialectic.nudge_decided` retired to a historical catalog row in `docs/events-schema.md`.
- **#1290 (items 1+2)** — `resolveSessionShape` now rejects out-of-set `configIsolation`/`configEnforcement` up front instead of only where `buildWave` happens to reach the resolver (previously the housekeeping shape could swallow `configIsolation: 'worktre'` unnoticed); `maintenance-due-banner.mjs` tail-scans `events.jsonl` backwards in 256 KiB chunks instead of reading the whole file (probe 31→17 ms on a 7.9 MB ledger). Item 3 deferred.
- **Rules** — 10 `/reconcile`-generated learnings absorbed into the 4 thematic files `toolchain-and-build.md`, `guard-design.md`, `identity-and-locks.md`, `review-and-adapter-contracts.md` per the merge contract (`docs/rule-authoring.md` § "Consolidated rules"); generated surface 128,999 → 120,906 B / 11 files (ceiling 124,000).
- **Peer card** — `.orchestrator/peers/AGENT.md` frontmatter restored to the top of the file (a managed block had been hand-prepended above it in `1be450a0`); the 2026-09-07 dialectic sidecar merged (9 sections replaced, 1 added).
- **Docs/citations** — `skills/wave-executor/SKILL.md` + `references/wave-loop-review.md` now cite `scripts/lib/workspace.mjs` for `restoreCoordinatorCwd()` (was `worktree.mjs`, which does not export it); `.claude/rules/security.md` dropped a citation to a heading that no longer exists; `.claude/rules/development.md` § Documentation gained a rule requiring an `rg` sweep for the old path whenever an exported symbol or module moves, in the SAME change (#1221 item 7).

### Notes

- **Housekeeping/VCS** — 21 stale `status:in-progress` labels cleared; #1213 closed (→ #1291); 4 merged `codex/*` branches + 2 sibling worktrees removed; 5 merged `feat/*` branches deleted on the GitHub mirror.

## [4.1.0] - 2026-09-08

### Added

- The owner-privacy scanner accepts `--include-untracked` to check new files before staging while respecting Git ignore rules. Tracked-only remains the default (#1267).

### Fixed

- Privacy scanning includes MDX and preserves filenames containing Unicode or newlines. Failure output includes the scanned-file count, and the pre-commit regression verifies that an unavailable confidential-name policy blocks the commit (#1267, #1269).
- Native Codex `CODEX_THREAD_ID` now participates in process-confirmed session ownership and event attribution. Explicit harness selection prevents inherited IDs from proving ownership; ambiguous IDs remain unconfirmed. Test workers isolate the invoking harness's identity (#1274).
- GitHub release recovery prints commands with the exact repository and tag, retains failed-create notes for retry, and inspects uncertain release state before any creation (#1095).
- CI audit detection examines executable GitHub steps and GitLab jobs, recognizes package-manager options and supported wrappers, and excludes metadata, comments, unused templates and help-only invocations. Local GitLab references, inheritance and `spec:inputs` headers are supported; external includes and dynamic conditions remain outside this heuristic (#1040).
- Vault session notes render lifecycle agent counts, preserve measured zero, and label counts known only as completed or planned (#1276).
- Guide copy buttons stay beside their command blocks, including multiline snippets and narrow screens (#1275).
- Full-repository validation runs once before Vitest workers start, and its result is shared by the existing smoke assertions. This avoids four competing scans during coverage while retaining mandatory validation and failure propagation (#1278).
- Coverage verification now requires generated reports and compares structured measurements with the canonical Vitest thresholds before writing its verified marker. Missing artifacts and low coverage previously passed through shell conditions that did not stop the CI job (#1279).

### Changed

- README, English/German landing pages, guide and machine-readable summaries now agree on installation prerequisites, active plugin-cache selection, Codex command skills, Pi package management, platform-specific enforcement and update-cache behavior (#1275).
- Refreshed distribution evidence and submission drafts, corrected stable repository steering, and recorded the product-quality review with Chrome and backlog evidence (#824, #1080).
- Release instructions now place the fully green preflight after commit, both remote pushes and exact-commit CI, matching the executable gate's requirements (#1277).

## [4.0.1] - 2026-09-07

A PATCH release, two work streams. Codex CLI command entrypoints (closing Epic #1263 and
sub-issues #1264/#1265/#1266) make every `commands/*.md` and `skills/*/SKILL.md` entry
discoverable and selectable inside Codex. A 5-wave mechanical-fix session (7 Discovery + 12
Impl-Core + 5 Impl-Polish + a 4-reviewer/Codex-gpt-6-astra Quality panel with 5 fixers) closes
16 follow-up issues surfaced by the `4.0.0` 360° audit. The site redesign already staged in
`[Unreleased]` (session-orchestrator.com relaunch, closing GitLab #1237 and #1080 points
1/3/5/6/7) folds in unchanged below as `### Changed`.

### Added

- **Codex command workflows are now discoverable and selectable skills — 51 unique generated
  entries (25 command-backed, 26 skill-backed), closing an 8-command discovery gap (`close`,
  `go`, `harness-audit`, `portfolio`, `release`, `session`, `templates-ack`, `test`) and
  resolving 17 same-name command/skill overlaps in the command's favor (`cf0876c3`, Refs
  #1263/#1264/#1265/#1266).** `scripts/generate-codex-skills.mjs` builds `.codex-plugin/skills/`
  as the union of `commands/` and `skills/` names; each generated entry links via a
  package-relative path to its canonical `commands/<name>.md` or `skills/<name>/SKILL.md`, so it
  resolves from the installed bundle, not only the source checkout. Command adapters read the
  full canonical command before resolving any internal skill call directly, avoiding recursive
  dispatch back to the public entry. Trailing prompt text passes through as `$ARGUMENTS` — no
  shell expansion, no global substitution.
- **`disable-model-invocation: true` now maps to `agents/openai.yaml`
  `policy.allow_implicit_invocation: false`, so `go`, `close`, `bootstrap`, `brainstorm`, `plan`,
  and `release` keep requiring explicit selection under Codex too (`cf0876c3`, Refs
  #1263/#1264).** All other commands retain their source setting.
- **`scripts/lib/validate/check-codex-skills.mjs` independently re-derives the expected Codex
  surface from the emitted artifacts, not from the generator's own expected text, and is wired
  into `scripts/validate-plugin.mjs` (`cf0876c3`, Refs #1263/#1265).** It checks source-name
  coverage, manifest registration, frontmatter/metadata types, invocation-policy booleans, and
  canonical-link targets; its CLI also re-runs `generate-codex-skills.mjs --check`.
- **Native Codex `commands: []` now suppresses the installer's automatic `commands/` migration
  (`ae16dfcb`, Refs #1263/#1265/#1266).** Without it, a real install on Codex 0.153.3 added nine
  `source-command-*` aliases (including `source-command-close`) beside the generated entries,
  with no invocation policy attached. `scripts/lib/codex/plugin-contract.mjs` now requires
  `commands` to be an empty array when present.
- **The root `plugin.json` (added in 4.0.0) is replaced by `.cursor-plugin/plugin.json`,
  Cursor's native manifest format (`cf0876c3`, Refs #1263/#1264).** Read-only `plugin/read`
  probes on Codex CLI 0.153.3 and desktop 0.153.4 found the root Agent Plugins manifest
  overrides Codex's own declared skill path (fixing it to `./skills`) and root version —
  silently defeating the generated `.codex-plugin/skills/` surface even though every adapter
  unit test passed. `package.json` `files[]` and `scripts/release.mjs`'s version-surface table
  are updated accordingly (`.cursor-plugin/` added, root `plugin.json` removed).
- **Installed acceptance verified end to end (`3862505c`, `a244b870`, Refs #1263/#1266).**
  Public `codex plugin add session-orchestrator@local --json` produced enabled version
  `4.0.0+codex.20260907174300`; Codex desktop 0.153.4 returns exactly 51 unique enabled skills
  via both `plugin/read` and `skills/list`, no migration aliases, and all six explicit-only
  commands retain `allow_implicit_invocation: false`. Visible-picker selection is **not yet
  confirmed** — Computer Use cannot drive the Codex desktop app — and remains open under #1266
  (see Notes).
- **Documented the Codex command entrypoints across `README.md`, `docs/codex-setup.md`,
  `docs/components.md`, `docs/migration-v4.md`, and `docs/instruction-delivery.md` (`cf0876c3`,
  Refs #1263/#1266).**

### Changed

- Public website redesigned (session-orchestrator.com): light-first design in the family look of the author's personal site (Bricolage Grotesque, Source Sans 3, IBM Plex Mono, all self-hosted; Archivo removed).
- Two reading layers on the landing page: a plain-words layer for non-developers above a developer layer with install commands, the wave schedule, and the measured-claims table.
- German landing page at `/de` with `hreflang` alternates (`en`, `de`, `x-default`), same section ids and census spans as the English page.
- three.js hero (vendored `three@0.185.1`, two files: `three.module.min.js` + `three.core.min.js`), loaded via a lazy dynamic import skipped under `prefers-reduced-motion`, which renders the static `hero-fallback.svg` instead.
- Six AI-generated illustrations (gpt-image-2), each captioned "AI-generated illustration (gpt-image-2)" in EN/DE.
- FAQ section with `FAQPage` JSON-LD on both the EN and DE landing pages.
- Author block linking to the personal site (root + `utm_source=session-orchestrator`), closing GitLab #1237.
- `scripts/site-numbers.mjs` gained `npm-downloads-30d` and `github-stars`: fetched only under `--write` (5 s timeout, snapshot kept on failure), answered from the `site/_census.json` snapshot under `--check`; `SO_SITE_NUMBERS_OFFLINE=1` skips both metrics. Marker-bounded census blocks (`<!-- census:start -->` / `<!-- census:end -->`) added to `site/llms.txt` and `site/llms-full.txt` (GitLab #1080, points 1/3/5/6/7).
- New tests: `tests/site/voice-gate.test.mjs` (em-dash, superlative, "we", emoji gate) and `tests/site/structure.test.mjs` (EN/DE parity, hreflang, asset presence, AI-illustration disclosure).
- `vercel.json` cache headers for `/assets`, `/img`, `/vendor`; ESLint now ignores `site/vendor/**` (vendored three.js) and declares browser globals for `site/assets/**`.

### Fixed

- **`CHANGELOG.md`'s `[4.0.0]` entry and `docs/migration-v4.md` corrected two overclaims found
  by post-tag review (`a4315993`).** `package.json` `files[]` admits the whole
  `.orchestrator/policy/` directory, not a curated file list — the packlist test, not npm, is
  what pins the shipped set to the tracked set. `pi-install.mjs --settings-only` actually
  **rewrites** an existing Pi settings file (upsert), unlike `cursor-install.mjs`, which skips
  existing destinations. Note for readers of the published `v4.0.0` tag: this correction lands
  after that tag, so the tagged CHANGELOG text still carries the two overclaims — only `main`
  and `4.0.1` have the corrected wording.
- **`tests/scripts/site-numbers.test.mjs` fixtures run offline
  (`SO_SITE_NUMBERS_OFFLINE=1`) (`4ea84d3e`).** Without it, the census-block tests fetched live
  npm download counts and went red whenever the live count moved past the fixture snapshot
  (measured in CI 2026-09-07: 1237 vs. 1165).
- **`tests/lib/validate/check-auq-clarity.test.mjs` gets a 120 s hook timeout for its
  `validate-plugin.mjs` spawn in `beforeAll` (`19b66e6e`).** GitLab pipelines 8790 and 8791
  (2026-09-07) failed only here: the shared runner exceeded vitest's default 30 s `hookTimeout`
  running the full validator (~12 s locally). Aligns with the sibling
  `tests/scripts/validate-plugin.test.mjs`.

#### 4.0.1 — mechanical-fix session (16 follow-up issues from the 4.0.0 audit)

- **Reconcile-idempotency now has a contract and a regression test, not just a claim (#1242).**
  Discovery found the acceptance criterion already held — tracked Provenance markers already
  dedupe re-runs, a fresh-clone simulation regenerated 0 of 30 keys — so the delivered fix is a
  contract doc (the scan is authoritative, the sidecar is a cache) plus a mutation-proven
  fresh-clone consolidated-shape test, not new production code.
- **`check-unwired-features.mjs`'s Category S4 no longer conflates "genuinely unreachable" with
  "reachable only via coordinator dispatch" (#1239).** Discovery measured 52 raw S4 findings, 46
  of them (88.5%) false positives — modules invoked only through a coordinator `Task`/skill
  dispatch, never a static import. S4 now splits into 5 truly-unreachable findings and 46
  coordinator-invoked findings reported as advisory rather than blocking, with `skills/`
  recognised as a first-class reachability edge source; a same-wave fix-pass tightened the
  coordinator-invoked match to token boundaries (a Codex-repro case where a bare substring match
  over-matched) and dropped the advisory line's WARN severity per `host-resources.md` HR-101.
  The check's exit code is unchanged.
- **`inspectConfidentialNames()` is a new function; `loadConfidentialNames()` keeps its 4.0.0
  `string[] | null` public contract (#1250).** This session's own Wave-2 refactor had changed
  `loadConfidentialNames()`'s return shape to an object — a semver-breaking change the Wave-4
  review panel and Codex gpt-6-astra both caught before the cut, not after. The fix reinstates
  `loadConfidentialNames()` as a thin wrapper over the richer `inspectConfidentialNames()`. A new
  `'all-dropped'` status (every entry in the confidential-names file invalid) now fails closed
  instead of silently returning an empty list, and the loader's WARN on a missing/malformed file
  is redacted to the file's basename — the same fix-pass had regressed the full path into
  stderr, reopening the class of leak `check-owner-leakage.mjs` exists to prevent; found and
  fixed inside this session's own review cycle, before release.
- **`loadHostPaths()` no longer loads `owner.yaml` twice (#1251).** Health-check state now
  passes through from the caller's single load instead of triggering a second read.
- **Four small fixes under #1262: an env-configured standalone confidential-names copy now fails
  closed instead of passing through silently (point 2); `docs/migration-v4.md` gained rollback
  prose (point 3); `OPTIONAL_OBJECT_SECTIONS` is now an exported constant with a parity test,
  replacing a duplicated mirror list (point 4); a stale comment on the full quality-gate path was
  corrected (point 5).** Point 1 (a Codex-surface regeneration check) was dropped — Discovery
  measured the existing set-equality pack test as the stronger pin, so no code change was needed.
- **`VALID_SESSION_PROFILES` moved to a single source of truth in
  `scripts/lib/session-schema/constants.mjs`, with `SHARED_LIST_BOUNDS`/`INGEST_LIST_BOUNDS`
  parity enforced (#1252).** A re-export shim stayed at the old location for compatibility,
  pinned by an identity test.
- **Four git-fixture files and the two largest `rmSync` call sites now route through a shared
  tmp-fixture helper instead of ad hoc cleanup; `listRepoFiles()` now swallows only
  `ENOENT`/`ENOTDIR`, not every error (#1253).** This is a partial pass, not a sweep: 501 bare
  **recursive** `rmSync` call sites remain unrouted (574 claimed → 524 measured → 501 after this
  session) and are carried forward as #1268 with a Revisit-Trigger, not silently left
  unmentioned.
- **`check-skill-links.mjs` now scans `docs/` (previously skill/command files only), takes a
  `dirs` option, and right-anchors its GitLab `-/issues` carve-out (#1258).** The carve-out was
  previously unanchored and could match a URL fragment elsewhere in a line. Two dangling
  documentation links were repaired, both pointing at content that had moved to the vault
  archive.
- **An unknown or valueless CLI flag on `generate-hook-import-set.mjs` now exits 2 with a usage
  message instead of triggering the write (#1249).** `--out --check` no longer writes a file
  literally named `--check` — the argv loop had read `--check` as `--out`'s value.
- **A platform allowlist check now uses `Object.hasOwn()` (#1256).** Discovery's companion claim
  in the same issue — that a related export was dead code — was checked against real call sites
  and refuted; that half closes as "working as designed".
- **Desktop notifications now carry a platform-aware label (#1254).** `detectPlatform()` drives
  the OSC notify text so a Codex CLI session's notification reads differently from a Claude Code
  session's.
- **A new session-start probe, `telemetry-flush-health`, surfaces telemetry-sender problems that
  were previously silent (#1255).** It tail-reads the last `orchestrator.telemetry.flush` event:
  a `sandbox:*` refusal reason now renders as a WARN banner instead of nothing, and an
  unreadable ledger renders explicitly as `ledger-unreadable` rather than falling through to
  silence. The reason string is bounded to 120 characters with control characters stripped.
- **The tests:src ratio census now resolves calls made through `fixtureGit`/`fixtureGitSpawn`
  wrappers, including the `NO_BACKGROUND_WRITER` spread pattern, and strips comments before
  reading a wrapped call's second positional argument (#1259).** Applicable call sites rose from
  17 to 200 (measured 2026-09-07 on the release tree with
  `node scripts/lib/validate/check-test-git-config-target.mjs .`) — the tokenizer previously
  couldn't see wrapper calls at all — and the reporting floor was raised from 20 to 100 to match.
- **Four tests now pin Node 24's `err.url` behaviour using a real spawned `node` child process,
  not an in-process `vitest` failure (#1260).** An in-process `ERR_MODULE_NOT_FOUND` never
  carries a `url` field, so the previous test could not observe the behaviour it claimed to pin.
  The standalone single-file copy path is confirmed inert / fails closed for the same case.
- **A Discovery premise was refuted before any code changed (#1257).** `vault-sync`'s
  sub-package dependency pins looked accidental; they are intentional, because CI already
  installs the sub-package separately. Documented in `skills/vault-sync/SKILL.md` §
  Dependencies instead of "fixed".
- **`release.mjs`'s drift sweep now judges version literals as whole tokens and skips comment
  prose and lockfile dependency rows; its history allowlist admits nine files whose only literal
  is prose history of the 4.0.0 major (`CLAUDE.md`, its generated twin `AGENTS.md`,
  `CONTRIBUTING.md`, `NOTICE`, `.husky/pre-push`, `site/llms-full.txt`,
  `skills/architecture/references/domain-model.md`, `skills/autopilot/SKILL.md`,
  `templates/_shared/journey-manifest.md`).** Measured 2026-09-07 mid-release: `4.0.0` matched
  inside `>=24.0.0` in package.json, 72 third-party rows in package-lock.json, and `// pre-4.0.0`
  comments — none a version surface. The sweep's detail line now carries the total hit count
  instead of truncating at five, which is how the last four files had stayed hidden.

### Notes

- **4.0.1 is a PATCH: no public export changed shape.** `loadConfidentialNames()` keeps its
  `string[] | null` contract; the new exports (`inspectConfidentialNames`, `SHARED_LIST_BOUNDS`,
  `INGEST_LIST_BOUNDS`, `OPTIONAL_OBJECT_SECTIONS`, `stripComments`, `wrapperHasCwd`,
  `checkTelemetryFlushHealth`, `renderBanner`, `TAIL_BYTES`, and `VALID_SESSION_PROFILES` in
  `constants.mjs`) are additive. Reviewed by a 4-reviewer panel plus Codex gpt-6-astra on the
  packed tarball; 2 P1 + 3 P2 Codex findings were fixed before the cut, not after.
- **Visible Codex picker confirmation (#1266) remains an operator step; not a release gate.**
- **The telemetry ingest server was redeployed for this release (#1261).** Operational step, not
  a code change — rollback tag `telemetry-ingest:1.0.0-pre401` kept in place; health confirmed
  post-restart.

## [4.0.0] - 2026-09-06

> **A MAJOR, because public surfaces are REMOVED** (see *Removed (BREAKING)* below).
> This entry was folded out of `[Unreleased]` by hand at cut time. That is a manual
> editorial act and not an automated one: `scripts/release.mjs` carries no writer for it —
> `checkChangelogEntry()` (`release.mjs:283`) only REQUIRES a dated `## [4.0.0]` heading as
> the topmost release with `[Unreleased]` emptied (`release.mjs:296`), and `--check`
> separately requires HEAD to be present on BOTH remotes (`evaluateRemoteHeadParity`,
> `release.mjs:556`) before it will tag. An earlier draft of this note asserted that the
> script renames the heading; it does not, and a release trusting that would have failed
> its own preflight. The reason the heading is written no earlier than the cut is the
> `3.22.1` failure this repo already paid for once — a release section for a release that
> never completed, unnoticed for 12 days (`e4674109`).

**Predecessor commits (v3.24.0 → `e4674109`).** Three commits (`ee8ea425` Wave 2,
`e22a702e` Wave 3, `37169158` Wave 4; 71 files, +3,505/−678, measured
`git diff --stat 2cb8708b..37169158`), no `BREAKING CHANGE:` footer and no `!` subject.
Twelve Wave-2 follow-up issues land as mechanism: identity resolution folds onto the
process-local reader on a third surface, seven ledger readers move onto one canonical
`sessions.jsonl` collapse, and a `secret_masker.applied` guard closes its last two gaps.
Wave 3 arms the schema-drift CI gate; Wave 4 is a 3-Opus-reviewer panel plus fixpass that
closed 2 HIGH findings inside the panel's own Wave-2/3 diff.

**The 360° audit session (`main-2026-09-06-deep-1`) — what makes this a major.** Everything
below marked *4.0.0 — audit session* is that session's own change set, which lands as this
release's commit: 277 working-tree entries at fold time (`git status --short | wc -l`,
2026-09-06), on top of 13 already-committed predecessors measuring 256 files, +13,537/−1,669
(`git diff --stat v3.24.0..HEAD | tail -1`, same day). Fourteen read-only Discovery agents measured the plugin against its own
telemetry, the 90-day fleet ledger and the vendor docs; the synthesis is
[`docs/audits/2026-09-06-360-audit.md`](docs/audits/2026-09-06-360-audit.md), per-agent
evidence under `docs/audits/2026-09-06-360-audit/w1/`. Nothing was removed on judgement: the
rule was 0 telemetry ∧ 0 fleet invocation in 90 days ∧ no runtime consumer, and
prose-invoked skills (0 by construction) were exempt. Upgrade guide:
[`docs/migration-v4.md`](docs/migration-v4.md).

### Added

#### 4.0.0 — audit session

- **Root `AGENTS.md`, root `plugin.json`, and a portable `.agents/skills/` mirror — this repo
  was previously invisible to Copilot CLI and to agent-plugins.org clients.** The root
  `AGENTS.md` is byte-identical to `CLAUDE.md` (`diff -q AGENTS.md CLAUDE.md` → no
  difference), so a harness that resolves project instructions from `AGENTS.md` now finds
  this repo's own Session Config instead of nothing — before this there was no root
  `AGENTS.md` at all, meaning the plugin's own repository could not read its own config on a
  harness that looks for that filename. `plugin.json` declares
  `$schema: https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`, the cross-vendor
  manifest shape Cursor, Amazon, Microsoft, OpenAI and Vercel converged on.
  `.agents/skills/<name>/SKILL.md` mirrors all 43 skills (`ls .agents/skills | wc -l` → 43)
  for harnesses that read that convention rather than a plugin manifest.
- **`ultradeep` — a session PROFILE over `deep`, deliberately NOT a fourth session type.**
  `/session ultradeep` resolves to `session-type: deep` + `session-profile: ultradeep` in
  STATE.md frontmatter (`commands/session.md` § Argument alias). `session-type` never becomes
  `ultradeep`: `VALID_SESSION_TYPES` is a closed set in
  `scripts/lib/session-schema/constants.mjs`, and a fourth member would degrade silently in
  two places (`telemetry/schema.mjs` maps an unknown type to `"other"`,
  `session-close-backfill.mjs` labels it `housekeeping`). The profile changes only the wave
  SHAPE — 7 waves with a coordinator-direct Synthesis-Gate at W2 (`agents: 0`, the one wave
  whose whole job is to stop and ask before any code is written), per the new row at
  `skills/session-plan/SKILL.md:336`. `SESSION_PROFILE_FIELD` in `scripts/lib/state-md.mjs`
  is the reader; absent is not empty. Budgets (`ultradeep.max-*`) are deliberately NOT
  implemented — the PRD defers thresholds until three runs have been measured. Spec:
  [`docs/prd/2026-09-06-ultradeep-session-profile.md`](docs/prd/2026-09-06-ultradeep-session-profile.md).
- **A plugin-update banner at session-start — the first code in this repo that compares
  INSTALLED against AVAILABLE.** `scripts/lib/plugin-update-banner.mjs` (`checkPluginUpdate`,
  imported lazily from `hooks/on-session-start.mjs:976`) reads the RUNNING package's own
  `package.json` — resolved as `../..` from `scripts/lib/`, i.e. the loaded bytes, never
  `$CLAUDE_PLUGIN_ROOT`, which is precisely what resolved to the checkout while a stale cache
  copy was executing — and compares it against the npm `dist-tags.latest`. The measured gap:
  this host ran the marketplace cache at **3.19.0** (installed 2026-08-09) while repo and npm
  were at **3.24.0** — five minors, four weeks, no warning, because the freshness probe
  shells out to `git -C <plugin-dir> log -1` against a FILE COPY and
  `classifyVersionMismatch()` downgrades anything below a major jump to `info`. Contract:
  fail SILENT, never optimistic — offline, non-2xx, malformed JSON, timeout and unusable
  cache each return `null` ("no statement"), never "you are up to date" (the #1031 collapse
  class).
- **A telemetry sandbox guard — `detectSandbox()` in `scripts/lib/telemetry/sync.mjs`.**
  This session's own read-only Wave-1 benches sent 6 production pings from agent sandboxes:
  `hooks/on-session-end.mjs` ran from the repo checkout with `SO_CONFIG_HOME` redirected
  while `owner.yaml` was unreachable inside the sandbox. The sender now refuses when
  `SO_TELEMETRY_DISABLED=1` (`sandbox:telemetry-disabled`), `DO_NOT_TRACK` is set
  (`sandbox:do-not-track`), the config home is split from the state dir
  (`sandbox:config-home-split`), or the state dir resolves under a temp root
  (`sandbox:temp-root` — matched after realpath, because macOS's `/var` → `/private/var`
  symlink defeats a raw string prefix). Every refusal is reported with its `sandbox:*` reason
  rather than as a silent no-op.
- **[`docs/baseline.md`](docs/baseline.md) — the `projects-baseline` relationship written
  down.** One line: the baseline is a **private, optional** companion repository, read when
  present, degraded to a documented fallback when absent, required by nothing in this plugin
  and needed by no public consumer. The measurement behind it: of 49 baseline-touching files,
  17 are docs-only, 15 test-only, 13 soft, and only 4 hard-runtime — **2 of those 4 pointed
  at a hardcoded `$HOME/Projects/projects-baseline` that does not exist**.

#### Predecessor commits (v3.24.0 → `e4674109`)

- **`scripts/lib/wave-executor/dispatch-common.mjs` (#1204).** Six symbols
  (`NEVER_FOREIGN_ROLES`, `DEFAULT_TIMEOUT_SEC`, `DEFAULT_KILL_GRACE_MS`, `isSafeRunId`,
  `isNeverForeignRole`, `runChild`) extracted out of `foreign-dispatch.mjs` into one shared
  module, imported by both dispatch call sites (`foreign-dispatch.mjs`, `remote-dispatch.mjs`)
  and their two test files. A redundant `isNeverForeignRole` check that had never lived in
  `wave-resource-gate.mjs`'s own invariant is now a documented non-check there instead of a
  silent gap.
- **`scripts/lib/learnings/evolve-telemetry.mjs` (#1206).** Mechanical
  `orchestrator.evolve.completed` / `orchestrator.dialectic.completed` emitters replace
  hand-written prose emit sites in `skills/evolve/SKILL.md`: `recordEvolveCompletion()` is
  called from `scripts/sweep-expired-learnings.mjs`'s `--prune` exit path (the one store
  write `/evolve analyze` already performs), and `recordDialecticRun()` from
  `scripts/dialectic-deriver.mjs`. Both skip the emit with a stderr note when no `repoRoot`
  is given, so test runs never write the fleet ledger. `docs/events-schema.md` gained both
  event's producer lines.
- **`hooks/post-edit-import-probe.mjs` — a PostToolUse hook that catches a broken
  hook-reachable module at save time, not at the next tool call (#1224).** On every
  Edit/Write/MultiEdit to a `.mjs`/`.js`/`.cjs` file that is a member of the new committed
  allowlist `hooks/_lib/hook-import-set.json` (149 modules at generation time — every module
  transitively reachable from the four hook manifests' entry files), it runs ESLint's
  `no-undef` on that single file (primary check) plus an `import()` probe scoped to
  `scripts/lib/**` only (secondary — `hooks/*.mjs` is excluded because half of them run
  `main()` at module bottom). Reports via `additionalContext` + the new
  `orchestrator.hook.import_probe_failed` event (payload: `file`, `check`, `error`,
  `reachable_from`, `duration_ms` — documented in `docs/events-schema.md`); never blocks
  (exit 0 always); kill switch `SO_DISABLED_HOOKS=post-edit-import-probe`. The allowlist is
  built by the new `scripts/generate-hook-import-set.mjs` (`--check` re-crawls and diffs) and
  is guarded three ways: a new blocking `validate-plugin.mjs` check
  (`runHookImportSetCheck`), a new `.husky/pre-commit` stage scoped to commits touching
  `hooks/` or `scripts/lib/`, and — deliberately absent — no Codex wiring
  (`hooks-codex.json`), documented as an intentional asymmetry in
  `check-hooks-symmetry.mjs`'s `DOCUMENTED_ASYMMETRIES` (same missing Edit/Write-payload
  adapter gap as `post-edit-validate.mjs`). Wired into `hooks.json`, `hooks-cursor.json`, and
  `hooks-pi.json`. Rationale: the 2026-09-04 host-wide hook-block incident documented
  elsewhere in this section (a `SyntaxError`/`ReferenceError` in an intermediate save of
  `own-session.mjs` blocked every Bash/Edit call on the host for ~8 minutes) was a
  **call-time** defect neither `node --check` (syntactically valid) nor a bare `await
  import()` (module-level evaluation succeeds) catches — ESLint's `no-undef` does. The
  allowlist has since been regenerated to include `scripts/lib/config/private-config-dir.mjs`
  (new, #1223 below), which became hook-reachable via `host-identity.mjs`: measured
  2026-09-05, `node scripts/generate-hook-import-set.mjs --check` → `✓ hook-import-set: 150
  modules, in sync`.

### Changed

#### 4.0.0 — audit session

- **43 generated rule files consolidated into 8 thematic ones; `.claude/rules/` goes 61 → 26
  files.** Measured now: `ls .claude/rules/*.md | wc -l` → 26, of which **zero** still match
  the generated-rule naming (`anti-pattern-*`, `proven-pattern-*`, `recurring-issue-*`,
  `convention-*`, `fragile-*`); at `HEAD` (`e4674109`) the same directory held 61 files
  (`git ls-tree -r --name-only HEAD .claude/rules/ | grep -c '\.md$'`). The eight
  replacements are `identity-and-locks`, `guard-design`, `measurement-discipline`,
  `test-hygiene`, `process-contracts`, `toolchain-and-build`, `git-and-worktrees`,
  `review-and-adapter-contracts`. Grounds: the 43 files were delivered TWICE per dispatched
  agent (natively plus via rule injection) — 112 kB, 46 % overhead — and 10 of them restated
  an always-on rule's insight verbatim.
- **The three largest instruction files are SPLIT into `references/`, not shortened.**
  `skills/session-start/SKILL.md` 1,275 → 388 lines (9 reference files);
  `skills/session-end/SKILL.md` 1,203 → 316 (6 reference files);
  `skills/wave-executor/wave-loop.md` 1,337 → a 39-line index over 3 references
  (`wave-loop-dispatch.md`, `wave-loop-review.md`, `wave-loop-scope-manifest.md`). Baselines
  measured `git show HEAD:<path> | wc -l`, current `wc -l`. Content moved rather than
  vanished: a phase you need is one hop away, a phase you do not need is no longer in every
  agent's context.
- **`orchestrator.session.stopped` → `orchestrator.turn.stopped` (GitLab #1234).** The
  emitter fires per assistant TURN, not per session: measured over the 90-day fleet window,
  **15,538 records against 2,016 distinct `orchestrator.session.started` ids = 7.7 per
  session**, with 184 for one id — so six consumers reading it as a session-lifecycle signal
  were wrong by that factor. `hooks/on-stop.mjs:489`/`:493` emits BOTH names from the same
  payload object for one deprecation generation (so they cannot disagree); the legacy name
  additionally carries `deprecated: true`, letting a reader tell at a glance that it matched
  the old name. **Removal of `orchestrator.session.stopped`: 2027-03-06.** Readers switch the
  name they match on and change nothing else. NOT affected: the SubagentStop branch keeps
  emitting `orchestrator.agent.stopped`, whose per-agent cardinality is correct. Contract:
  `docs/events-schema.md:92-93`.
- **The telemetry `fleet` field becomes `fleet_self_declared`, and attribution moves
  server-side.** The old name asserted something the client cannot know: 394 of 490 pings
  were the operator's own second Mac, mis-attributed as "external", because the flag hung off
  an `owner.yaml` line rather than off a person. The client field now says what it is —
  self-declared, derived from the RESOLVED consent state (`enabled-fleet` / `enabled-env`)
  rather than from a raw `owner.yaml` read (`scripts/lib/telemetry/schema.mjs:552-555`) — and
  the authoritative classification is an anon-id allowlist in the ingest server
  (`server/ingest/config.mjs`, `server/ingest/validate.mjs:190-196`). `fleet` remains in the
  payload, identical in value for the whole generation so the server's existing column stays
  comparable: **DEPRECATED 2026-09-06, removal 2027-03-06** (`docs/telemetry.md:42-43`).

- **`zod` and `yaml` hoisted from a nested skill's own `package.json` to this repo's
  root `dependencies`.** `skills/vault-sync/package.json` resolved them as its own
  transitive install; a fresh top-level `npm ci` without that nested install step
  left `vault-sync`'s schema validation unable to resolve either package. Root
  `package.json` now pins `yaml@^2.9.0` and `zod@^3.25.76` directly (the nested
  `package.json` still carries its own copies — redundant, harmless, left as a
  cleanup candidate).
- **`scripts/release.mjs`'s preflight gains a GitHub-mirror CI row and two
  publish-time hardenings.** `evaluateGithubCiRow()` adds `ci-green-on-head-github`
  as its own preflight check (26 rows total) via `checkCiStatus({ vcs: 'github' })`
  — `--skip-ci` marks it skipped, and a repo with no configured GitHub remote also
  skips it rather than failing; `publishInvocation()` now pins
  `npm_config_loglevel=notice` on the publish spawn (an inherited silent level from
  an outer gate run suppressed output the packlist gate depends on); and
  `describeTagProgress()` is printed on a post-receipt tag/push failure, so a
  publish that succeeds but fails to tag no longer leaves the operator guessing
  what state the release is in. `commands/release.md` step 7 documents the ≥600s
  Bash wrapper this needs; `skills/npm-publish/SKILL.md` is updated to match.
- **The release drift sweep moves off `git grep` onto `enumerateRepoFiles()`
  (#1248) — the same gitignore/untracked blind spot the two checkers below were
  fixed for, closed once.** `collectDriftHits()` in `scripts/release.mjs` now
  enumerates via `git ls-files --cached --others --exclude-standard` instead of
  shelling out to `git grep` for the previous release tag, so an untracked file is
  swept for a leftover version literal exactly as a tracked one is.
- **The Full Gate now names which test FILES failed, not just the pass/fail
  count.** `extractFailedTestFiles()` (`scripts/lib/gates/gate-helpers.mjs`) parses
  vitest's own failure output; `gate-full.mjs` carries the result as a new
  `failed_files[]` field alongside the existing counts, on both the returned
  object and the `orchestrator.quality_gate.*` event payload. Separately,
  `.husky/pre-push` now passes the real repo root to the gate as an **argv flag**,
  `--ledger-root <path>` (`npm run --silent quality-gate -- --ledger-root
  "$repo_root"`), so a gate run against a materialised temp worktree writes its
  event into the REAL repo ledger instead of a tree an `EXIT` trap deletes seconds
  later — the root cause of several `quality_gate.passed` records the test suite
  itself had polluted into `.orchestrator/metrics/events.jsonl`.
  `resolveLedgerRoot()` (`scripts/run-quality-gate.mjs`) validates the flag as an
  existing directory that itself contains `.orchestrator/`; anything else is one
  stderr WARN and a fallback to the pre-existing resolution, never a crash. **This
  shipped first as an exported `SO_GATE_LEDGER_ROOT` env var and was replaced
  before release, inside the same session**: the Wave-4 architect-panel review
  measured that an exported env var is inherited by every descendant process,
  including the gate's OWN vitest children three levels down (`npm run
  quality-gate` → `gate-full.mjs` → `npm test`), where `run-quality-gate.test.mjs`'s
  telemetry tests spread `...process.env` and so wrote their fixture events to the
  pinned root instead of their own tmp fixture — measured **8 of 9 telemetry tests
  red** under the hook's own environment. An argv flag reaches only the one
  process meant to read it. `run-quality-gate.test.mjs` still sandboxes
  `CLAUDE_PROJECT_DIR` per test and additionally scrubs any ambient
  `SO_GATE_LEDGER_ROOT` before each run (belt-and-braces against a leftover from
  the removed env-var form); a new `tests/husky/pre-push-gate.test.mjs` case
  ("hands the real repo root to the gate as --ledger-root") pins the hook's own
  argv, and 0 production readers of `SO_GATE_LEDGER_ROOT` remain repo-wide
  (`grep -rn SO_GATE_LEDGER_ROOT` outside test files and history comments → no
  match, measured 2026-09-06).
- **The session-start plugin-update banner is now English and platform-aware.**
  `checkPluginUpdate()` (`scripts/lib/plugin-update-banner.mjs`) accepts an
  optional `platform` and picks the matching upgrade command from
  `PLATFORM_UPDATE_INSTRUCTIONS` (`claude`/`codex`/`cursor`/`pi`), falling back to
  a generic `npm update`-shaped instruction when the platform is unset or
  unknown — previously every consumer saw the same `claude plugin update` line
  regardless of harness.
- **Six documentation corrections following the 4.0.0 removals above.** The
  `autopilot` SKILL's Configuration section is now a 4-line pointer to
  `docs/migration-v4.md` instead of describing the removed `bg-isolation` field in
  full; `NOTICE`'s attribution table is repointed at
  `skills/architecture/references/{domain-model,ADR-FORMAT,CONTEXT-FORMAT}.md` and
  notes `ubiquitous-language`'s removal while keeping its MIT attribution history;
  `check-agents.mjs`'s warn-string citations of `agents/AGENTS.md` now cite
  `docs/agent-authoring.md`; `templates/_shared/journey-manifest.md` gained a
  retirement notice (kept as an optional per-repo template — no command consumes
  it anymore); `skills/architecture/SKILL.md` documents when to load
  `references/domain-model.md`; and `README.md` picked up matching corrections
  (Windows is untested rather than "runs natively"; the update banner is
  patch-silent; nothing is sent without consent EXCEPT the update check's own
  anonymous `GET`, opt-out via `SO_DISABLE_UPDATE_CHECK`/`DO_NOT_TRACK`).

#### Predecessor commits (v3.24.0 → `e4674109`)

- **Quality-gate ownership check now folds onto the process-local reader (#1205).**
  `quality-gate.mjs`'s private copy of `readOwnSessionIds()` is gone; the ownership check now
  calls `readProcessLocalSessionIds()` directly (removing the STATE.md-lock fallback), with
  the first test of the ownership path itself.
- **Wave-scope manifest binding writes `attributionForRecord()`, not a hand-rolled call
  (#1207).** `skills/wave-executor/wave-loop.md`'s manifest-binding step now sources
  `session`/`wave` from the same `attributionForRecord()` used by `emitEvent()`'s own
  correlation fill, instead of a raw `sessionAttribution()` call; the STATE.md prose
  comparison the previous step asked for is removed as a redundant, sometimes-wrong check
  (a peer-owned lock and STATE.md agree with each other, not with this process).
- **Seven readers now go through `readCanonicalSessions()` instead of their own duplicate
  parse (#1209).** `session-id.mjs`, `auto-dream.mjs`, `dispatcher/rank.mjs`,
  `eval/engine.mjs` (+ new `eval/session-resolve.mjs`), `harness-audit/categories/category4.mjs`
  (via `canonicalizeSessions({ keepUnidentified: true })`, since a bare wrapper broke 3
  id-less fixtures), `skill-health/join.mjs`, `sessions-staleness-banner.mjs`, and
  `dialectic-deriver.mjs` each had a red-on-HEAD fake-regression proof before the fix
  (auto-dream 3→2 sessions, rank 50→<6 candidates, eval 2→1 peers). `category9` is
  deliberately untouched.
- **Four modules now distinguish silent `ENOENT` from a loud read failure (#1210).**
  `session-close-backfill.mjs`, `reconcile/engine.mjs`, `memory-proposals/store.mjs` (via a
  new shared helper — a decorative `err.code` branch there is now real), and
  `agent-status.mjs` all warn on stderr for `EACCES`/`EISDIR`/other, and stay silent only on
  a genuinely missing file.
- **German distributional-claim patterns join the English set (#1211).** Six German
  quantifier-triggered patterns (`N von M`, `100% von/der/aller`, `alle N`, `keine
  verbleibenden`, `jeder/jede/jedes`, `keiner/keine/keins von/der`) plus a narrow German
  `CARDINAL_NOUN` extension (`zeilen?|dateien?|datei|aufrufer|eintr(?:ag|(?:ä|ae)ge)`) land
  in `hooks/post-subagent-discovery-validator.mjs`. Measured against a 41-line German claim
  corpus pulled from this repo's own `events.jsonl`: the quantifier patterns flag 7/41
  (same order of magnitude as the English baseline); a wider bare-cardinal noun set
  (`Sessions`/`Repos`/`Treffer`/`Stellen`/`Module`/`Tests`) was tried and rejected after it
  flagged 39/41 (~57% false-positive rate) — those six nouns stay reachable only through the
  lexically-anchored quantifier patterns. German `GATE_SUMMARY` recap forms are also
  recognised and skipped, matching the existing English recap-skip.
- **`skills/vault-sync/validator.mjs`'s vendored schema catches up with upstream (#531).**
  `peer-card` (#503), `board` (#738), and the optional `source-repo` field (#725) were
  vendor-ahead of `projects-baseline` until this session; upstream-lifted in
  `projects-baseline@cb9ec97`. `sync-vault-schema.mjs` now strips a bare `//` line (with a
  regression test), and the provenance notes documenting the vendoring history moved outside
  the generated-block sentinels so a `--write` regeneration can no longer drop them (measured:
  it did, once).
- **The schema-drift CI gate is armed (#1175, follow-up to #531).** `SCHEMA_DRIFT_OPTIONAL`
  is `"false"` in both `.gitlab-ci.yml` jobs that read it — `schema-drift-check` (line 636)
  and `pipeline-gate` (line 723) — so a missing `SCHEMA_DRIFT_TOKEN` now hard-fails (exit 4)
  instead of degrading to an amber "not verified" pass. A test pins the armed state (a half
  revert of either flag turns 2 tests red). Proof: pipelines 8355–8357 @ `bca78dae` (bogus
  enum) failed with `schema-drift-check` reporting "drift detected"; pipeline 8358 @
  `dc9522dd` (branch `proof/1175-schema-drift-green`) passed with the check running WITH the
  token, in sync. `docs/ci-setup.md` documents the Project-Access-Token recipe and the
  now-current state (was previously the follow-up gap this same doc named).
- **`#1203` — the `enforce-scope.mjs` peer-manifest fix confirmed already SHIPPED via
  #1194.** `tests/hooks/enforce-scope.test.mjs:1219/:1245/:1302` (56/56) prove the behaviour
  landed with #1194; no further code change was needed.
- **`scripts/lib/platform.mjs`'s five module-constant exports become lazy, memoized
  accessors; the five constants are REMOVED (#1153 P5).** They were internal accessor
  constants of this plugin, never a documented public API, and every in-repo importer is
  migrated — an external importer of these five names (none known) switches to the
  getters; no `BREAKING CHANGE:` footer is carried for that reason. `SO_PLATFORM`, `SO_PLUGIN_ROOT`,
  `SO_PROJECT_DIR`, `SO_STATE_DIR`, `SO_CONFIG_FILE` used to be `export const … =
  detect…()` evaluated at module load, so every one of ~31 static importers — including
  the hottest deny-capable hooks, which run on every tool call — paid a filesystem
  walk-up (`statSync`/`existsSync` per ancestor directory) merely for importing the
  module, whether or not the value was ever read. They are replaced by
  `getPlatform()`/`getPluginRoot()`/`getProjectDir()`/`getStateDir()`/`getConfigFile()`,
  computed on first call and memoized for the process (plus a test-only
  `_resetPlatformCache()`); the deprecated names are fully REMOVED, not kept as
  deprecated live bindings — a re-introduction is caught by the named-export assertion in
  `tests/lib/platform.test.mjs`. 23 non-test call sites across `scripts/` and `hooks/` now
  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).
- **Wave-scope manifest session keys renamed to `session_id`/`semantic_session_id`,
  canonical since #1153 P2.** The pre-#1153 spellings `session`/`semantic_session` are
  still ACCEPTED on the read side for one release (`MANIFEST_SESSION_KEYS` in
  `scripts/lib/session-identity/own-session.mjs` is the shared SSOT list every writer and
  reader now imports); `scripts/validate-wave-scope.mjs` flags a manifest that carries
  BOTH spellings with conflicting values as an error rather than silently preferring one
  and dropping the other. `scripts/wave-scope-binding.mjs` (new — #1153 P4) replaces the
  inline `node --input-type=module -e` block `skills/wave-executor/wave-loop.md` § Scope
  Manifest previously asked the coordinator to retype once per wave; an unbound `{}`
  manifest now emits `orchestrator.scope.unbound_manifest` (0 hits repo-wide before this
  file existed), making the previously-silent fail-closed case countable. `--merge` folds
  the binding into an existing manifest in place.
- **`_privateDir()` (host-identity.mjs) now honours `SO_CONFIG_HOME` (#1153 P6).** Two
  overrides, most specific first: `SO_CONFIG_HOME` names the private config directory
  itself; `XDG_CONFIG_HOME` (same variable `owner-config-loader.mjs` already reads) names
  its parent. Both read via `.trim() || fallback`, not a bare `||` (the whitespace-only
  env-var trap). Without either, the homedir default `~/.config/session-orchestrator` is
  unchanged.
- **`scripts/lib/session-identity/own-session.mjs`'s static import closure cut from 3,567
  to 269 lines (#1153 P7).** It no longer imports `../session-lock.mjs` — which drags
  `session-lock` → `exclusivity-matrix` → `file-lock` → `io` → `host-identity` behind it —
  and instead shares the shape predicate `isLockShape()` via a new zero-import module,
  `scripts/lib/session-lock-shape.mjs`, with `session-lock.mjs`'s own `parseLock()`. This
  matters because `hooks/enforce-scope.mjs` loads `own-session.mjs` on every Edit/Write;
  anything it imports joins that hook's static closure.
- **codex-cli 0.144.4 fixes the `plugin add` failure documented against 0.141.0
  (#1163, FIXED-UPSTREAM — no code change here).** Re-verified end-to-end 2026-09-04
  against this repo's unchanged flat layout: `codex plugin add session-orchestrator@kanevry
  --json` now exits 0 where it previously failed with `plugin session-orchestrator was
  not found in marketplace kanevry`. `docs/codex-setup.md`'s "Short-Form Marketplace Add"
  section is promoted to the recommended install path and the prior failure kept as a
  historical note; a new "Switching Marketplace Sources" section documents that
  `marketplace add` silently REPLACES an already-registered marketplace of the same
  declared name.
- **`#1152` closed — the reported defect was a false premise (two separate writers, not
  one drifting one).** No code change; investigation confirmed the two paths write
  independently and neither needed reconciling.
- **`#1214` — the suspected `maskerWouldChange` gap in the vault narrative-mirror sink was
  a false premise, refuted by measurement.** Unlike the two `vault-mirror/process.mjs`
  sinks fixed under #1028 (five-field comparisons that can match while a raw needle
  survives on disk), `scripts/lib/vault-status/narrative-mirror.mjs` compares the WHOLE
  rendered document, and the candidate always passes through the CURRENT `maskNarrative` —
  so a value that masker would redact can never appear in it, and a false
  `matchesModuloRedaction` match cannot occur. Measured 2026-09-04 at HEAD `cd785003` in
  both directions (no marker on disk; marker on disk plus a second needle entering the
  env): both returned `written` with the raw value gone. No probe added; two invariant
  tests in `tests/lib/vault-status/narrative-mirror.test.mjs` pin it. Named ceiling: the
  invariant depends on `maskNarrative` walking every rendered string, and `repo` is fed to
  `renderNarrative` OUTSIDE that walk (a directory basename, not STATE.md content) — a
  future rendered field added the same way would reopen this.
- **Three host-private-config-dir resolvers collapse into one (#1223).** New zero-import
  leaf `scripts/lib/config/private-config-dir.mjs` (`node:os` + `node:path` only) exports
  `resolvePrivateConfigDir({env}?)` — precedence `SO_CONFIG_HOME` (the dir itself) >
  `XDG_CONFIG_HOME` (its parent) > `~/.config/session-orchestrator`, each `.trim()`ed. Before
  this, `owner-yaml.mjs`'s import-time `OWNER_YAML_PATH` (homedir-only, no overrides at all),
  `owner-config-loader.mjs`'s `resolveOwnerConfigPath()` (XDG-only, untrimmed), and
  `host-identity.mjs`'s `_privateDir()` each had a different precedence — so
  `SO_CONFIG_HOME=<sandbox>` moved the self-alias ledger but not `owner.yaml`, which kept
  reading the operator's real home (the CLAUDE.md "vault-dir resolves HOST-LOCALLY" hazard
  class). `host-identity.mjs` now delegates `_privateDir()` to the leaf directly (it is
  reachable from live hooks via `session-lock.mjs`, so the leaf must stay import-free);
  `owner-yaml.mjs` re-exports `resolvePrivateConfigDir` and adds a call-time
  `resolveOwnerYamlPath(env?)`; the old `OWNER_YAML_PATH` constant is fully REMOVED (verified
  0 remaining consumers repo-wide), not kept as a deprecated back-compat export.
  `owner-config-loader.mjs` and `owner-interview.mjs` now call through
  `resolveOwnerYamlPath()` instead of their own copies. `tests/husky/pre-commit-owner-leakage.test.mjs`'s
  file-by-file copied import chain (the CP11 scanner's dynamic-import fixture) gained the new
  leaf as a required copy.
- **`skills/wave-executor/wave-loop.md` gains a "Shell variables used in this section"
  preamble (#1225).** `$PLUGIN_ROOT`, `$WAVE`, `$ROLE`, `$STATE_DIR` are defined once ahead
  of the Scope Manifest steps instead of being re-explained inline (the `$PLUGIN_ROOT`
  per-platform expansion parenthetical is now a single cross-reference instead of a repeated
  clause).
- **`scripts/lib/ci-status-banner.mjs` gains a three-state contract instead of two (#1031).**
  A frozen `DEGRADED_REASONS` enum (`cli-missing`, `timeout`, `parse-error`, `query-failed`,
  `git-error`) now distinguishes "state could not be read" from "no CI here" — before this,
  4 return sites collapsed every CLI-missing / timeout / unparseable-output / VCS-probe-error
  case onto the same `null` an absent remote returns, which the banner reads as all-clear
  (the same collapse class documented for `mirror-issues-banner.mjs` in
  `skills/session-start/SKILL.md`, now fixed on this probe too). Consumers updated to the new
  shape: the probe registry in `scripts/lib/session-start-probes.mjs` (`ci-status` entry's
  `render`/`severityOf` now treat a `degraded` result as `warn`, not `ok`);
  `scripts/lib/dispatcher/rank.mjs` (new exported `normalizeCiSignal()` reduces every shape —
  bare string, `{status}`, `{degraded}`, `null` — to `{ciStatus, ciDegraded}`, mapping
  `degraded` to `'unknown'` for scoring parity with the old behaviour while surfacing the
  reason into `signals.readiness.ciDegraded` and a `warnings` entry); `scripts/release.mjs`'s
  `evaluateCiRow()` treats `ci.degraded` as "CI status unknown (\<reason\>)", never as green.
  `skills/session-start/SKILL.md`'s CI-status paragraph documents the new degraded banner
  line. `scripts/lib/qg-command-drift-banner.mjs` is converted too (W3-P1): it mints its own
  `degradedBanner(reason)` and passes a nested `detailed.degraded` straight through
  (`:59`/`:99`/`:102-103`), so a config that could not be READ is reported as `warn` rather
  than read as "no drift".
- **`hooks/post-tool-batch-wave-signal.mjs` starts emitting the `files_changed` measurement
  `scripts/lib/convergence-monitor.mjs`'s `shrinking_diff` signal has always read but no
  producer ever wrote (#980).** At an N→N+1 wave-open transition, the `orchestrator.wave.completed`
  emitted for wave N now carries `files_changed` (the deduped union of
  `git diff --name-only <wave_start_sha>` and `git ls-files --others --exclude-standard`,
  measured against the working tree because the coordinator commits at session close, not per
  wave) and `files_changed_source: 'worktree-vs-wave-start-sha'`; `wave_start_sha` (`git
  rev-parse HEAD`) is persisted into `.orchestrator/current-session.json` under the same
  ownership gate as the other wave keys when a wave opens. Both keys are optional and
  absent-is-not-zero — any git failure, a 1.5s timeout, or a missing `wave_start_sha` omits
  them, and the monitor reads the absence as `null` rather than firing on a fabricated 0.
  `skills/convergence-monitoring/SIGNALS.md` gained a "Live monitor input" subsection under
  all three signals (shrinking_diff, pass_rate_plateau, velocity_drop), each quoting a fresh
  measurement of `.orchestrator/metrics/events.jsonl` (2026-09-05): 144 pre-existing
  `wave.completed` records carried no `files_changed` key at all before this fix; 33
  `orchestrator.quality_gate.*` records carry both `wave_number` and a well-formed `counts`
  object (making `pass_rate_plateau` fireable, `_evaluateSignals` now a test-only export);
  11,754 `orchestrator.agent.stopped` records make `velocity_drop` fireable, versus 0 for the
  `agent.dispatched` type the reader also accepts. `monitors/monitors.json`'s
  `convergence-monitor` description corrected — it never opened `sessions.jsonl` (a stale
  claim from an earlier draft of the monitor).
- **`scripts/validate-wave-scope.mjs`'s internal `validateSession()` renamed to
  `validateSessionBinding()` (#1153 P11).** Rename only, no behaviour change — the old name
  read as "validate the session" when it validates the wave-scope manifest's session-BINDING
  fields specifically; a previously-considered `skipped[]` return addition was dropped as a
  false premise (the function already reports via `errors`/`warnings`).
- **`docs/rule-authoring.md`'s `LEARNING_TYPE_REGISTRY` table gains its fourth axis,
  `hostScoped` (#1153 P12, follow-up to #1090).** The table previously transcribed only
  `ttlDays`/`agentProposable`/`ruleConvertible`, so a drifted `host_class`-gating cell was
  structurally invisible to `tests/docs/rule-authoring-registry-parity.test.mjs`; the parser
  regex and parity-diff logic there now check all four axes bijectively.
  `hardware-pattern` is the only `hostScoped: true` type today.
- **`docs/README.md` gains a "Superseded design notes" section (#1153 P14).** Because
  `docs/specs/` is gitignored, a correction written INTO a spec can never be committed — the
  correction for `docs/specs/2026-05-26-parallel-aware-sessions-design.md` (which still
  specifies PID-based lock liveness) lives here instead: liveness has been heartbeat-age
  based since #1137, and the recorded PID has been consulted nowhere since #1151.
  `skills/_shared/parallel-aware-preamble.md` cross-references it.

### Removed (BREAKING)

Every entry under *4.0.0 — audit session* below is a REMOVED PUBLIC SURFACE and the reason
this release is a major. Each names its replacement or states explicitly that there is none.
Upgrade guide: [`docs/migration-v4.md`](docs/migration-v4.md).

**Deviation from the deprecate-then-remove norm** (`.claude/rules/development.md` § Package
Lifecycle: stub + first-call `console.warn` for ≥1 minor cycle). The 5 skills and 3 commands
below are HARD-deleted, not stubbed — an explicit operator decision made 2026-09-06. The
removal test was **0 telemetry ∧ 0 fleet invocation over 90 days ∧ no runtime consumer**, and
that 90-day window is honestly a *search* window, not 90 days of shipped exposure: `journey-audit`
itself had existed for only 9 days (added 2026-08-28) before this cut. Full rationale and the
manual cleanup steps: [`docs/migration-v4.md`](docs/migration-v4.md).

#### 4.0.0 — audit session

- **5 skills removed** — `find skills -name SKILL.md | wc -l` → 43, against
  `git ls-tree -r --name-only HEAD skills/ | grep -c 'SKILL.md$'` → 49 (the sixth delta is
  the `domain-model` merge below):
  - **`skills/daily/`** (plus `generate.sh`, its template and 2 test files) — **no
    replacement.** 0 telemetry, 0 fleet invocations in 90 days, no runtime consumer; only its
    own test and documentation referenced it. Recurring daily notes are a Routine / scheduled
    task, not a session skill (`.claude/rules/loop-and-monitor.md` § LM-004).
  - **`skills/skill-creator/`** — **replaced by Anthropic's official `skill-creator` plugin**
    plus `claude plugin init`. 0/0/0 with 0 in-repo references.
  - **`skills/ubiquitous-language/`** — **no replacement.** 0/0/0.
  - **`skills/contract-version-bump/`** — **no replacement.** 0/0/0; one comment referenced
    it.
  - **`skills/journey-audit/`** — **no replacement.** 0 journey manifests exist fleet-wide,
    so the skill was never runnable in the first place.
- **3 commands removed** — `commands/*.md` 28 → 25, mirrored in `.cursor/commands/` and
  `pi/prompts/` (both also 25): **`/contract-version-bump`**, **`/journey-audit`** (with
  their skills above) and **`/autopilot-multi`**. **No replacement** for any of the three.
  `/autopilot` is a different command, unaffected, and stays.
- **Session Config `autopilot.bg-isolation` removed** — the `autopilot:` block's only field,
  retired together with `/autopilot-multi` above (`scripts/parse-config.mjs` never parsed an
  `autopilot` key at all, so removing it changes no runtime behaviour). **No replacement** —
  delete the block from your `CLAUDE.md`/`AGENTS.md`; single-story `/autopilot` takes no
  Session Config block and is unaffected.
- **8 top-level scripts removed** (`scripts/*.mjs`, 0 runtime callers each, with their test
  files): `autopilot-multi.mjs`, `backfill-learnings.mjs`, `backfill-learnings-expires.mjs`,
  `fleet-instruction-scan.mjs`, `lifecycle-sim-v6.mjs`, `migrate-learnings-jsonl.mjs`,
  `migrate-subagents-jsonl.mjs`, `upload-social-preview.mjs` — **no replacement.** The two
  `migrate-*` scripts were one-shot format migrations whose target format has been canonical
  for several releases; the two `backfill-learnings*` scripts were one-shot ledger repairs.
- **`skills/_shared/model-selection.md` removed — no replacement, 0 consumers.**
- **`skills/domain-model/` removed and MERGED into `skills/architecture/references/`** — its
  three files now live as `references/domain-model.md`, `references/ADR-FORMAT.md` and
  `references/CONTEXT-FORMAT.md`. It was reference-only material with 4 inbound links, never
  a dispatchable skill; `/architecture` is the entry point.
- **Two non-agents moved OUT of `agents/`** — `agents/*.md` 16 → 14. The agent loader
  registers every `.md` file in that directory as a dispatchable agent, and
  `agents/AGENTS.md` — the authoring SPEC — was measurably dispatched as an agent 5 times. It
  is now [`docs/agent-authoring.md`](docs/agent-authoring.md), and
  `agents/memory-proposal-collector.md` is now
  [`docs/memory-proposal-flow.md`](docs/memory-proposal-flow.md). Same content, no longer a
  dispatch target. **Anything dispatching either by name must stop** — they are
  documentation, and were only ever documentation.

- **`scripts/lib/autopilot/multi-killswitch.mjs` and
  `scripts/lib/autopilot/dep-graph.mjs` deleted, with their test files** —
  orphaned by the `/autopilot-multi` removal above, with only their own tests as
  consumers. **No replacement**; single-story `/autopilot` never imported either
  module.

#### Predecessor commits (v3.24.0 → `e4674109`)

- **`scripts/lib/webhook-url.mjs` deleted — zero callers repo-wide (#1168).** `resolveWebhookUrl`/
  `WebhookConfigError` and their test file (`tests/lib/webhook-url.test.mjs`, 192 lines) are gone,
  −364 lines total; the `## Webhooks (#228)` section in `docs/session-config-reference.md`
  (the `webhooks.<kind>.url` Session Config surface it backed) is removed with it. The still-live
  Clank Event Bus webhook (`scripts/lib/events.mjs`, `CLANK_EVENT_SECRET`/`CLANK_EVENT_URL`) is
  unrelated and unaffected — its doc section is promoted from `### Clank Event Bus` to
  `## Clank Event Bus` in the same edit, since it is no longer a subsection of the now-removed one.

### Fixed

#### 4.0.0 — audit session
- **`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()`.

- **`js-yaml` out of the eager hook import graph — 4 of 27 hooks crashed on every turn
  without `node_modules`; now 0 (GH#62 / GH#63 / GitLab #1230).**
  `scripts/lib/owner-yaml.mjs` carried a static `import yaml from 'js-yaml'` and sits on the
  import graph of `on-session-start`, `on-session-end`, `post-edit-validate` and
  `skill-invocation-telemetry` — so a skipped or interrupted `npm install`, a half-synced
  plugin cache, or an EPERM sandbox killed all four at MODULE-LOAD time with
  `ERR_MODULE_NOT_FOUND` and exit 1, on every single turn. `js-yaml` now resolves lazily
  inside `loadOwnerConfig`/`writeOwnerConfig`. Measured 2026-09-06 @ `e4674109` with `hooks/`
  and `scripts/` copied to a tmp dir carrying no `node_modules` anywhere up the tree:
  **23/27 hooks exited 0 before the fix, 27/27 after.** Pinned by the new
  `tests/hooks/hooks-run-without-node-modules.test.mjs`, which EXECUTES every hook instead of
  reading source text — the pre-existing static-import guard would have accepted a lazy
  `require()` of a package that does not exist. The `zx` half of #1230 is stale and now says
  so: `scripts/lib/worktree/listing.mjs`'s `zx` imports are already lazy and caught, verified
  by installing a `js-yaml` stub alone and watching all four hooks reach rc=0 with `zx` still
  absent.
- **`picomatch` declared in `package.json` dependencies.** It is imported by four runtime
  modules (`rule-loader.mjs`, `command-blocker.mjs`, `validate-vendored-rules.mjs`,
  `reconcile/emitter.mjs`) and was absent from the manifest at `HEAD`
  (`git show HEAD:package.json | grep picomatch` → no match) — reachable only as somebody
  else's transitive dependency, the GH#62 class exactly.
- **The Cursor adapter generator wrote a malformed `argument-hint` into 24 of 28 command
  files (GH#54).** `scripts/generate-cursor-adapter.mjs` now renders every frontmatter value
  through `yamlQuote()`, which JSON-quotes anything failing `YAML_PLAIN_SAFE` (and anything
  that looks like a YAML scalar, or starts with a digit). Measured after the fix:
  `grep -c "argument-hint: \[" .cursor/commands/*.md` reports zero files carrying the array
  shape. The regression survived for as long as it did because the generator was tested
  against itself; it is now pinned against the spec.
- **`generateFrontmatterSnippet()` THREW when the baseline was absent.**
  `scripts/lib/frontmatter-guard.mjs` resolves the schema through
  `resolveHostPath('baseline-path')` and treats a missing schema as `null` — the honest
  outcome, with the documented fallback enum set — instead of raising inside a pre-dispatch
  guard. `scripts/lib/vault-backfill/template.mjs` uses the same resolver.
- **The session registry wrote into the operator's REAL host-private config from a
  sandbox.** `scripts/lib/session-registry.mjs` now derives its `sessions/` directory from
  `resolvePrivateConfigDir()` (`SO_CONFIG_HOME` > `XDG_CONFIG_HOME` >
  `~/.config/session-orchestrator`) instead of its own homedir-only path, so a redirected
  config home actually redirects it.
- **`node scripts/parse-config.mjs --json` failed with `File not found: --json`.** The flag
  is now an explicit NO-OP alias for the default (stdout has always been JSON), because the
  bootstrap documentation names that exact command as THE validation command and
  `cli-design.md` § JSON-First Output requires every CLI to accept `--json`.
- **The vault-staleness probe measured against the clock instead of against the repo
  (#1238).** `skills/discovery/probes/vault-staleness.mjs` now asks whether the upstream repo
  advanced PAST the last sync, not what `now - lastSync` is — a mirror of a repo nobody has
  committed to in three weeks is CURRENT, not three weeks stale. Both sides of the comparison
  (`lastSync`, `lastCommit`) come from one read of the same `_overview.md` frontmatter.
  Measured against the live vault before the fix (2026-09-05): **33 of 48 overviews reported
  "stale", 26 of them >7d, with a demonstrably healthy sync chain.** An overview without
  `lastCommit` still falls back to the wall-clock comparison, marked
  `basis: 'probe-runtime'` and carried at lower confidence so a consumer can tell a measured
  delta from a guessed one.
- **The discovery-validator scanned the WRONG transcript, so effectively every violation it
  recorded was the coordinator's own prose (#1191).** `input.transcript_path` on
  `SubagentStop` is the PARENT/main session transcript, not the subagent's — measured
  2026-09-02 on a seeded random sample of 60 violations: **100 % coordinator text,
  scope-adjusted precision 0 %**, with `agent` reported as `"unknown"` in 90.8 % of 1,541
  events. The hook now reads `<transcriptDir>/<session>/subagents/agent-<agent_id>.jsonl`
  (the layout `subagent-telemetry.mjs` and `wave-transcript-tail.mjs` already use) and NEVER
  falls back to the parent path: a scan of the wrong transcript is worse than no scan. Claims
  are additionally collapsed per distinct normalised claim with an `occurrences` count
  (`normalizeClaim()` / `dedupeViolations()` in `hooks/_lib/subagent-transcript.mjs`, #1198)
  — the worst repo in the fleet held 3,360 records over 205 distinct claim texts, a ×16.4
  duplication factor.
- **The `ecosystem-health` watcher could never start.** Its trigger in
  `monitors/monitors.json` was `on-skill-invoke:ecosystem-health` — self-referential: the
  watcher could only start when the `ecosystem-health` skill ran, and that skill has 0
  recorded invocations fleet-wide, so it never started once. The trigger is now
  `on-skill-invoke:session-start`.

- **CP11 (the confidential-names owner-leakage check) could fail OPEN and still
  print `PASS` (#1244).** `getConfidentialNamePatterns()`
  (`scripts/lib/validate/check-owner-leakage.mjs`) collapsed three different
  outcomes onto the same `[]` degrade: (a) the standalone single-file copy, where
  the helpers are genuinely unresolvable — the only case meant to go inert; (b)
  CP11 simply unconfigured; and (c) CP11 configured but unreadable (missing
  `js-yaml`, an unparseable `owner.yaml`, or a wholesale-discarded partial
  config), which used to read identically to (b) and print the clean verdict it
  never earned. Case (c) now returns a `disabledReason` and the scanner prints
  `CP11 DISABLED: <reason>` plus a counted FAIL — with no path in the reason
  string, since this scanner's output is mirrored to a public CI log.
  `owner-yaml.mjs`'s whole-file discard on an invalid REQUIRED section (#820) now
  merges a still-VALID optional object section (e.g. `paths:`) back onto its
  default instead of discarding it too, so a correctly declared
  `confidential-names-file` no longer vanishes because an unrelated section was
  malformed; a genuinely invalid optional section is reported via
  `droppedSections`, and an unparseable file gets the new `reason: 'unparseable'`.
  A third, unrelated fail-open closed in the same file: `isMain` now compares
  `realpathSync()` of both sides, so a symlinked invocation path (`/tmp` →
  `/private/tmp` on macOS) can no longer make the scanner exit 0 having scanned
  nothing.
- **CP11's own DISABLED path (above) was itself overbroad, and its degrade
  path had two further defects — all three closed together in
  `scripts/lib/validate/check-owner-leakage.mjs` and `.husky/pre-commit`
  (Wave-4 Codex second-look).** First, DISABLED fired even when NO
  confidential-names file was ever configured: a healthy `owner.yaml` with
  only an unrelated invalid optional section (e.g. `paths: { vault-dir: 42
  }`) and no `confidential-names-file` key reported `CP11 DISABLED` and a
  counted FAIL, when the correct verdict is "inactive." A new
  `rawConfidentialNamesKeyState()` re-reads the RAW (pre-validation) key
  before classifying, so DISABLED now fires only when a names path was
  actually configured; a healthy owner.yaml with `paths:` present but no
  names-file key now stays `PASS` with no CP11 line at all, pinned by a new
  case in `tests/lib/validate/check-owner-leakage.test.mjs`. Second, the
  standalone-copy inert degrade (the ONE case meant to go silently
  patterns-`[]`) was scoped by a bare `ERR_MODULE_NOT_FOUND` check, so a
  missing TRANSITIVE module — not just the scanner's own three vendored
  sibling imports — also went inert instead of failing closed.
  `isMissingDirectSibling()` now inspects `err.url` (the URL Node's own
  `ERR_MODULE_NOT_FOUND` carries) against the scanner's three known
  direct-sibling import URLs; a miss anywhere deeper now THROWS. Third,
  `.husky/pre-commit` was capturing but discarding the scanner's own stderr
  on failure and printing the same "privacy leak detected" line regardless
  of cause; it now prints the scanner's (redacted) diagnostic tail and
  distinguishes "the scanner could not run" from "the scanner found a
  leak." 259/259 tests pass; 4 fake-regression probes (each reverting one
  of the three fixes above) still turn red; a live repo scan still PASSes.
- **`checkOwnerConfig()`'s whole-file-discard message claimed "the entire
  file was discarded" even when a validly-configured OPTIONAL section
  survived the discard onto its default (#1244 merge rule, Fixed above).**
  `scripts/lib/owner-config-banner.mjs` now diffs each optional object
  section (`paths`, `dispatcher`) in the merged config against
  `getDefaults()`: a section that differs was real and survived, and the
  banner names it — `"paths" kept, everything else defaulted` — instead of
  claiming total loss; `droppedSections` renders on this branch too.
  Reproduced with a tmp owner.yaml carrying an invalid `owner.name`
  (required, triggers the whole-file path) alongside a valid
  `paths.confidential-names-file` (a Wave-4 architect-panel finding); a new
  case in `tests/lib/owner-config-banner.test.mjs` pins the "kept" wording.
  168/168 tests pass; 2 fake-regression probes still turn red; a real,
  healthy host's banner still returns null.
- **`check-skill-script-paths` and `check-validator-registration` answered an
  existence question from the git INDEX, so a brand-new untracked file's own
  citation was invisible to them until `git add` (#1248).** Measured in a clone
  (Wave-1 discovery): an untracked skill citing a non-existent script reported
  `1 passed, 0 failed` before staging and `0 passed, 1 failed` after — same tree,
  same defect. New shared `scripts/lib/validate/enumerate-repo-files.mjs`
  (`enumerateRepoFiles()`) answers "does this exist under these roots right now"
  via `git ls-files --cached --others --exclude-standard`, honouring
  `.gitignore` without reopening #1143 (a bare `readdirSync` walk was tried and
  REFUTED: it pulls a gitignored peer worktree under `.claude/worktrees/` into
  the census — measured 287 vs 290 `.md` files with one peer worktree present).
  Both checkers, and the release drift sweep (Changed, above), now share this
  one enumerator instead of three different partial views.
- **`enumerateRepoFiles()` (above) silently dropped `stat` failures other than
  `ENOENT`/`ENOTDIR`, so a permission-denied file read as "does not exist"
  instead of "unknown" — the release drift sweep (Changed, above) could
  report a clean sweep on 0 actual reads (Wave-4 Codex second-look).** It
  now throws a typed `RepoEnumerationError`
  (`scripts/lib/validate/enumerate-repo-files.mjs`) for
  `EACCES`/`EPERM`/`ELOOP`/`EIO`/`ENAMETOOLONG`/anything else it cannot
  classify as "provably absent," carrying the path and the underlying error
  code; only `ENOENT` and `ENOTDIR` are still treated as absent.
  `collectDriftHits()` (`scripts/release.mjs`) reports this as exit 128,
  which the release preflight already reads as "inconclusive" — the same
  reading a crashed `git grep` got, never a silent clean. 195/195 tests
  pass across the 5 touched files; the Codex reproduction now shows status
  128 / `ok:false` instead of a clean sweep; `validate-plugin.mjs` still
  234/0. Two known sibling sites of the same swallow-class are filed as
  follow-up issues rather than fixed here: `listRepoFiles()`
  (`scripts/lib/repo-files.mjs`) has the identical catch-all, and 3 further
  test files still hand-roll `git init` + a bare recursive `rmSync` in
  template-string form (invisible to a literal-string census) rather than
  routing through `tests/_helpers/tmp-fixture.mjs`.
- **15 more fixture-shaped test files are routed onto the
  `tests/_helpers/tmp-fixture.mjs` helper (the detached-`git-maintenance` fix,
  below) this session — 3 of them deleting a weaker inline copy of the same
  helper that `enumerate-repo-files.test.mjs`, `check-validator-registration.test.mjs`
  and `check-skill-script-paths.test.mjs` had each written for themselves
  (above), plus 11 legacy files and a `release.test.mjs` git-grep-to-fixture
  probe.** `tests/unit/quality-gate-session-config.test.mjs` — the one file
  this pass left out of scope, needed here because it was also touching
  `enumerate-repo-files.test.mjs` — is itself routed in this same session (23
  insertions / 30 deletions: `execSync('git init', ...)` →
  `fixtureGit(['init', ...])`, `rmSync(repoRoot, ...)` → `removeTree(repoRoot)`,
  `mkdtempSync(...)` → `makeTmpDir(...)`). Net −28 LOC across the routed
  files; 526/526 of their own tests unchanged per file; eslint clean. The
  self-census this whole effort is measured against
  (`tests/lib/validate/check-test-git-config-target.test.mjs`) cannot resolve
  a fixture helper's own `...spread` token to a git subcommand, so its floor
  was lowered from `>50` to `>20` with a Revisit-Trigger comment naming the
  exact condition ("the census learning to resolve the helper's spread")
  rather than silently loosened — the remaining git-fixture files are reduced
  this session, not proven at zero: 3 further sites in template-string form
  are filed as follow-up issues (above).
- **The full test suite failed 1–2 tests per run, never the same one twice — a
  detached `git maintenance` child racing the fixture's own cleanup.**
  `GIT_TRACE=1` on a bare fixture commit showed `git maintenance run --auto
  --quiet --detach` still writing into a temp `.git` directory after
  `execFileSync` returned, so the `afterEach` `rmSync` raced a live process and
  failed with `ENOTEMPTY`. New `tests/_helpers/tmp-fixture.mjs` (`fixtureGit`,
  `fixtureGitSpawn`, `removeTree`, `makeTmpDir`) sets `maintenance.auto=false`,
  `gc.auto=0` and `core.fsmonitor=false` on every fixture repo, and retries
  removal (`maxRetries: 5`) for what config alone cannot reach. 24 fixture files
  (118 git call sites, 35 `rmSync` sites, 39 `mkdtemp` sites) now route through
  it; the detached-maintenance trace line is gone from the routed files, and the
  494 tests across those 24 files still pass.
- **10 of the 43 learning-provenance markers the 43→8 generated-rule
  consolidation (Changed, above) dropped are restored, across
  `git-and-worktrees`, `guard-design`, `measurement-discipline`,
  `process-contracts`, `review-and-adapter-contracts`, `test-hygiene` and
  `toolchain-and-build.md` (+121/−19 across the 7 files).** That consolidation's
  own claim — "each keeping its provenance markers" — was false for 10 of them
  when written; a fresh census now counts 43 of 43 keys and 43 of 43 ids (a
  previously reported "42 ids" was itself a UUID-filter miss, not a real 42nd
  gap). One of the 10 was a markers-only restoration against an
  already-present duplicate prose block; the other 9 restore both the prose and
  its evidence line.
- **`docs/migration-v4.md` §6 Rollback and its "Cursor still shows the removed
  commands" recipe both had defects that would have re-broken what they were
  trying to fix (Wave-4 Codex second-look).** §6's `cursor-install.mjs` /
  `pi-install.mjs` rollback lines were missing the target-project argument
  both scripts require — they default to `process.cwd()`
  (`scripts/cursor-install.mjs:24-51`), so an operator following the doc
  literally would link the installer into the session-orchestrator checkout
  itself, not their own project. Both lines now carry
  `/path/to/your-project` explicitly, with a note that both installers are
  ADD-ONLY: they skip any destination that already exists, so re-running the
  3.24.0-checkout installer never resyncs a `hooks.json` or Pi settings file
  written under 4.0.0 — only adds what your project is missing. The
  "Cursor still shows the removed commands" recipe used `rm -f` to delete
  three named files, which deletes a REGULAR file with the same name exactly
  as readily as a symlink — including a command an operator wrote themselves
  under one of those three retired names; it is now a `[ -L "$p" ] && rm
  "$p"` loop, symlink-only. Separately, the session-start plugin-update
  banner's Pi instruction (Changed, above) led with the dev-fallback checkout
  recipe though the documented PRIMARY Pi install path is `pi install
  npm:session-orchestrator` (`docs/pi-setup.md:16`, §"Option 1") —
  `PLATFORM_UPDATE_INSTRUCTIONS.pi` (`scripts/lib/plugin-update-banner.mjs`)
  now leads with re-running that exact command, naming the checkout +
  `--settings-only` recipe only as the fallback for a dev-fallback
  registration. 25/25 tests pass; `check-skill-links.mjs` reports 86;
  `validate-plugin.mjs` 234/0.
- **`#1229` closed — the session-lock heartbeat already refreshes on `Stop`,
  `PostToolBatch` and `SessionStart`, not only once per wave; the reported gap
  was a false premise, refuted by measurement.** Live call sites confirmed at
  `hooks/on-stop.mjs:438` (fires every turn), `hooks/post-tool-batch-wave-signal.mjs:377`,
  and `hooks/on-session-start.mjs:1078`; a heartbeat mid-wave measured 3 seconds
  old. No `UserPromptSubmit` hook exists to add a fourth site.
- **`#1230` closed — `js-yaml` is already lazily required in `owner-yaml.mjs`
  (`:120-127`, memoised, one WARN on failure) and all 27 hooks already exit 0
  with both `zx` and `js-yaml` blocked; the reported gap was a false premise,
  refuted by measurement.** The eager-import class the Predecessor-commits Fixed
  entry above already closed was re-verified still holding at this session's
  base commit — no regression, no further code change needed.

#### Predecessor commits (v3.24.0 → `e4674109`)

- **Two silent Session Config parsing bugs fixed across 39 block-shaped parsers
  (#1162).** New shared module `scripts/lib/config/block-preprocess.mjs` fixes both: (a) a
  block commented out with a multi-line `<!-- … -->` was read as LIVE config —
  `stripHtmlCommentBlocks()` now strips it via the existing `htmlCommentSkipper()` state
  machine; (b) the bold-bullet sub-key rendering (`- **enabled:** true`) matched no sub-key
  regex and silently fell back to its default — `normalizeBoldSubkeys()` now normalises it
  to `enabled: true` before parsing. `preprocessBlockLines()` (35 standard parsers) and
  `preprocessBlockLinesNoDash()` (4 dash-RECORD parsers — `custom-phases`, `remote-hosts`,
  `evolve`'s `_parseEvolve`, `health-endpoints`) are the two drop-ins for
  `content.split(/\r?\n/)`; NoDash skips bold-normalisation because de-dashing a record's
  first key would silently merge it into the previous record.
  `config-protection.mjs`'s `_isConfigWeakeningAllowed()` and
  `hooks/pre-bash-destructive-guard.mjs`'s bypass scan deliberately use HTML-comment
  stripping ONLY, never bold-normalisation — the bold form
  (`- **allow-config-weakening:** true` / `- **allow-destructive-ops:** true`) must not arm
  a bypass. An UNTERMINATED `<!--` fails closed in both directions: a block parser gets its
  lines back UNFILTERED, while both bypass scanners treat it as NOT ARMED (said out loud on
  stderr, since a silently-ignored bypass would look identical to an operator typo);
  `scripts/parse-config.mjs` prints one stderr WARN per session, not one per parser. Two
  new parity test files pin the contract per parser: `tests/lib/config/preprocess-parity-a-l.test.mjs`
  and `-m-z.test.mjs`.
- **PSA-007's git-write detector is argument-aware, closing a false-positive AND a
  false-negative class (#1172, #1215).** `isGitWrite()` in
  `scripts/lib/wave-transcript-tail.mjs` used to match the subcommand literal alone; it now
  parses the arguments too. `git stash list`/`show`, `--version`, `--help`/`-h`,
  `--dry-run`, and `-n` on every subcommand except `commit` (where `-n` means
  `--no-verify`, still a real write — measured 2026-09-04 that `git commit -h` prints
  `-n, --no-verify` while `add`/`rm`/`push -h` all print `-n, --[no-]dry-run`) now read as
  reads, not writes. The value-taking global flags `-C`/`-c`/`--git-dir`/`--work-tree`/
  `--namespace`/`--exec-path`/`--config-env`, and `command`/`env VAR=x` prefixes, are now
  absorbed so `git -C /tmp stash` and `env FOO=x git commit` are still recognised as git
  writes at all (measured false negative before the fix: `isGitWrite('git -C /tmp stash')`
  returned `false`). `FIXTURE_CONTEXT_RE`'s `cd /tmp` match now also fires on the BARE path
  with no trailing `/` — the reported false alarm was
  `cd /tmp && git init && git commit -m x`. 32 table rows added (70 cases in the file
  after the change) in `tests/lib/wave-transcript-tail.test.mjs`.
- **`buildLiveSignals()`'s injectable `_scanBacklog` test seam was unused at 5 call sites,
  so the affected test suite shelled out to a live `glab issue list` on every run
  (#1169).** `tests/lib/autopilot.test.mjs` now passes a null-returning `_scanBacklog` stub
  at every call site that does not assert on `signals.backlog` (`selectMode`/`computeDelta`
  never read that field, so the stub is behaviour-preserving). `scripts/lib/autopilot/loop.mjs`
  also migrated off the removed `SO_STATE_DIR` constant onto `getStateDir()` (#1153 P5) in
  the same pass. Test-phase wall time for the affected suite dropped from ~750ms to ~25ms,
  with no live network calls remaining.
- **One flaky spawn-timeout test pinned with margin instead of disabled (#1217).**
  `tests/unit/plugin-manifests-exit-codes.test.mjs`'s "exits 1 (not 2) when plugin.json
  contains broken JSON" case now passes `{ timeout: 30_000 }` explicitly — measured
  887–965ms unloaded (W1-D4, 2026-09-04), with one observed 10,095ms outlier attributed to
  host contention, not spawn cost. Same discriminating-margin pattern as the three tests
  hardened earlier in this file; no repo-wide pinned-slow list exists (#976 never
  implemented).
- **Legacy `.bak.`-delimited backup files were invisible to both rotation and restore
  (#1173).** `scripts/lib/learnings/io.mjs` exports `backupSuffixOf()`/`isBackupOf()`,
  accepting both the canonical `.bak-<ISO>` delimiter this module writes and the legacy
  `.bak.<label>-<ts>` delimiter pre-#721 writers left behind (e.g.
  `learnings.jsonl.bak.evolve-<ts>`), shared with `backfill-learnings-from-vault`'s restore
  sweep so the two predicates cannot drift apart again. `rotateBackups()` now sorts on the
  SUFFIX (a leading non-digit label stripped first) rather than the whole filename —
  sorting on the whole name grouped every `.`-delimited legacy file after every
  `-`-delimited one regardless of age (`-` is 0x2D, `.` is 0x2E), so rotation pruned only
  hyphen-form backups. `scripts/backfill-learnings-expires.mjs` itself was still emitting
  the dot form on `--apply` — its own backups were the ones going unrotated and unrestored
  — and now emits the canonical hyphen form (`tests/scripts/backfill-learnings-expires.test.mjs`
  pins both the new emission and the absence of the old one).
- **A peer session's `wave-scope.json` drove this session's Bash gates on two hooks, and a
  rebind of the manifest could suppress its own tamper notice (#1153 P1).**
  `hooks/enforce-commands.mjs` and `hooks/post-bash-write-verify.mjs` both gain a Gate 3b
  ownership check — `classifyManifestSession()` against `readProcessLocalSessionIds()` (hook
  payload + `CLAUDE_CODE_SESSION_ID`, deliberately not the lock-file tier that made a
  peer's manifest classify as `'own'` under #1194) — and stand down silently when the
  manifest provably names another session, emitting `orchestrator.scope.foreign_session_ignored`
  for observability. In `post-bash-write-verify.mjs` the ordering is the fix itself: the
  control-file hash/enforcement snapshot is now computed BEFORE the Gate 3b stand-down, so
  a `cat >` rebind of `wave-scope.json` to a fabricated `session_id` can no longer disarm
  this session's gates AND suppress the #938 control-file notice in the same stroke — the
  notice now fires once, on the call where the SESSION BINDING itself changed.
- **`processSession`'s two skipped-noop returns had no `maskerWouldChange` re-probe
  (#1028, found by the Wave-4 security-reviewer as MED, confirmed HIGH by qa-strategist).**
  The session-note generator's skip paths (`process.mjs:953`/`:991`) now carry the same
  `maskerWouldChange` guard the learning-note generator already had at 3 sites
  (`:703`/`:773`/`:797`); a missing `!force` check in the disambiguation-collision branch is
  also fixed. 4 new tests, red on HEAD before the fix.
- **The legacy-flat heal wrote the masked copy to the namespaced path while the plaintext
  legacy original stayed in place and became unreachable (#1028, qa-strategist HIGH-2).**
  Both generators (learning-note and session-note) now re-render the leaking legacy note
  masked IN PLACE (same fix shape at both `#660 IDEMPOTENCY DUAL-PROBE` sites), marking the
  result `action: 'created'` + `meta.healed_legacy_flat: true` instead of leaving a second,
  unreachable plaintext copy on disk.
- **`orchestrator.secret_masker.applied` now emits from all three channels that mask vault
  content, not two.** `scripts/lib/vault-mirror/process.mjs`,
  `scripts/lib/vault-status/narrative-mirror.mjs`, and `scripts/export-hw-learnings.mjs`
  (documented in its own comment as "the third") each carry the emit at the end of their run.
- **`quality-gate.mjs`'s fail-open corrective-context path was silent (#1205, Wave-4
  security-reviewer LOW).** Without `CLAUDE_CODE_SESSION_ID` (Codex/Cursor with no
  process-local witness), a peer's `corrective_context` was kept without any signal. It now
  prints a stderr WARN naming the UNVERIFIED session id before keeping the context; behaviour
  is unchanged, only the visibility.
- **`check-skill-script-paths` had 24 dead script citations sitting unannotated inside
  `docs/` (#1208).** `docs/` is now in `SCAN_DIRS`; 24 dead citations were annotated with the
  existing `<!-- path-check: planned #<iid> | historical | example -->` marker convention
  (measured: 1,087 citations across 259 files, 53 total annotated after this pass). A new
  `--strict-sh` mode extends the same dead-path scan to `.sh` citations, reported as an
  advisory count line in `validate-plugin` output (0 findings currently).
- **A block commented out with `<!-- … -->` still armed the dispatcher-autonomy one-time
  capture AUQ (#1222).** `isDispatcherAutonomyBlockPresent()` called the shared
  `hasBlockHeader()` matcher directly on raw content, while `scripts/parse-config.mjs`
  preprocesses with comment-stripping first — so a `dispatcher-autonomy:` header sitting
  inside a comment parsed to defaults (correctly) but was reported PRESENT (incorrectly),
  meaning the operator was never asked. The guard now preprocesses with the same
  `preprocessBlockLinesNoDash()` the parser uses before testing for the header, so an
  unterminated `<!--` still fails toward PRESENT (one un-asked question, not a silently
  disarmed bypass) matching the module's own documented one-time-capture contract.
  `block-header.mjs`'s docblock now states explicitly that comment-stripping is the caller's
  job. The three further planned call sites in `skill-evolution.mjs`, `persona-gate-wave.mjs`,
  and `wave-reviewers.mjs` are NOT part of this change — none of the three currently calls
  `hasBlockHeader()`/`isDispatcherAutonomyBlockPresent()` at all, so there is nothing to sweep
  there yet.
- **`reapStaleBudgetFiles()`'s exact-boundary comparison had no falsifying test (#1153
  P8).** A file aged exactly `maxAgeDays` is meant to be KEPT (`mtimeMs >= now -
  maxAgeDays*86400000`); the existing 30-vs-1-day test stayed green even if `>=` were
  weakened to `>`, which would silently reap a peer session's issue-budget file the instant
  it turned `maxAgeDays` old. A new test in `tests/unit/hook-issue-budget.test.mjs` seeds one
  file exactly on the cutoff and one a second past it and asserts the former survives and the
  latter does not.
- **`sessionFromLock()`'s `host_id` fallback for a pre-#1072 lock had no test (#1153 P9).**
  Two new cases in `tests/lib/session-discovery.test.mjs` pin both branches of
  `lock.host_id ?? stableHostname(...)`: a lock that already carries `host_id` passes it
  through unchanged, and a lock written before #1072 (no `host_id` field) derives it from the
  raw `host` — without the fallback pin, a lock-sourced session could silently regress to the
  raw, non-normalised host for comparison against the registry path's normalised twin.
- **`markCandidateProcessed()`'s mint branch stamped a self-contradictory terminal record
  (#1153 P10, follow-up to #1042).** A freshly-minted candidate (no prior sidecar record) was
  always given `status: 'proposed'` even while being stamped `processed_at` +
  `outcome: 'rejected'` in the same write — a terminal, declined candidate that still read as
  a live proposal to anything rendering `status`. `status` now agrees with `outcome`
  (`'rejected'` when the outcome is `'rejected'`, `'proposed'` otherwise, the only two
  ReconcileCandidate status values). Separately, the function used to return the
  freshly-built stamp even when `mergeCandidates`'s dedupe rule kept an OLDER terminal record
  on disk instead — caller and store then disagreed about the verdict, silently. It now reads
  the record back out of the merge result and returns that, plus a new `alreadyProcessed`
  flag (`written` stays `true` in that case: the store IS in the intended terminal state, and
  `written: false` is reserved for a genuine write failure). Four new tests in
  `tests/lib/reconcile/idempotency.test.mjs`, red on HEAD before the fix.
- **A learning whose `file_paths` were entirely unusable produced the same generic
  "no activation axis" rejection as a learning with no `file_paths` at all (#1153 P13).**
  `globsFromFilePaths()` now records each skipped entry (glob-metachar / control-char / quote)
  into an out-parameter sink, rendered by the new `formatDroppedFilePaths()` into a
  `— dropped file_paths: N (…)` suffix on the never-always-on rejection reason — the message
  engine.mjs forwards verbatim into the operator-visible candidates ledger. Each dropped value
  is `JSON.stringify`-escaped (so a raw newline or ANSI control char in a hostile `file_paths`
  entry cannot inject into the reason string) and hard-truncated to 60 chars with no
  mid-escape cut. Two new tests in `tests/lib/reconcile/emitter.test.mjs` pin both the naming
  and the escaping.
- **`writer.mjs`'s `isOperatorRejection()` inferred an operator rejection from rendered
  `content` being non-empty, which an operator-declined proposal with an empty rendered body
  could fail (#1153 P15, closes the inference gap left open by #1042's own documented
  ceiling).** `skills/session-end/phase-3-6-tail.md` step 6 now stamps every proposal the
  operator left unselected with an explicit `operatorRejected: true` flag before it joins the
  engine's `rejected` array; `isOperatorRejection()` keys on that flag first, falling back to
  the old content-presence heuristic (marked `@deprecated`, kept only for a consumer repo
  pinning a pre-P15 skill body) when the flag is absent. Two new tests in
  `tests/lib/reconcile/writer.test.mjs` cover the flag-wins-over-empty-content case and the
  legacy flagless fallback.

### Security

#### 4.0.0 — audit session

- **The published npm tarball shipped 0 of the 6 files under
  `.orchestrator/policy/` — including the destructive-command guard's own floor
  policy (`blocked-commands.json`) — so an npm-installed consumer without a host
  overlay ran `hooks/pre-bash-destructive-guard.mjs` against `rules: null` and it
  silently ALLOWED every command the floor exists to block (found by the Wave-1
  Codex external review, P1: `npm pack --dry-run | grep -c orchestrator/policy`
  → 0).** `package.json`'s `files[]` now lists `.orchestrator/policy/`;
  `npm pack --dry-run --json | grep -c "orchestrator/policy"` reports **6**
  policy files today (`blocked-commands.json`, `ecosystem.schema.json`,
  `quality-gates.example.json`, `quality-gates.schema.json`,
  `templates-policy.json`, `test-profiles.json`) where it reported 0 before,
  pinned by the new `tests/scripts/pack-policy-floor.test.mjs` (a real
  `npm pack --dry-run`, not a mock). The release leakage gate's `.orchestrator/`
  exclusion is narrowed to carve THIS directory back IN (`scripts/release.mjs`
  around line 319) — the operator's own `metrics/`, `debug/` artefacts and live
  `*.lock` files stay excluded. `files[]` admits the directory, not a file list — what
  keeps the shipped set equal to the TRACKED policy set is the packlist test below, not npm. **The
  Wave-4 review panel found this test asserted only a FLOOR (≥ 6 files) while
  `package.json`'s `files[]` admits the WHOLE `.orchestrator/policy/` directory
  and the carve-out above matched the path anywhere in the string, not just at
  its start** — an untracked private file dropped into that directory would
  ship, invisible to a floor check. `pack-policy-floor.test.mjs` now asserts SET
  EQUALITY between the packed policy entries and `git ls-files
  .orchestrator/policy` (tracked-only, sorted); the carve-out regex is anchored
  to the path start (`/^\.orchestrator\/policy\//`, previously unanchored),
  with the VCS-trust rationale — everything under this one directory ships
  because it is tracked, not because of a per-file allowlist — recorded in a
  comment beside it.
- **`session_profile` was the one free-form string field on the telemetry
  wire — both an internal Wave-1 review and the Codex review reproduced sending
  an arbitrary string through it.** `VALID_SESSION_PROFILES` (`['ultradeep']`
  today) is now enforced on BOTH ends: the client
  (`scripts/lib/telemetry/schema.mjs`) omits the field when it is not a known
  profile rather than passing it through, and the ingest server
  (`server/ingest/validate.mjs`) rejects a record whose `session_profile` fails
  the same enum plus a 32-char length ceiling. Reproduced before the fix: a
  crafted `session_profile` string reached the wire; after, the client sends
  nothing and the server rejects a forged one directly. `detectSandbox()`
  (`scripts/lib/telemetry/sync.mjs`) now also fails CLOSED — a probe that
  itself throws returns `{ sandbox: true, reason: 'sandbox:probe-failed' }`
  instead of falling through to "not a sandbox". `docs/telemetry.md` gained the
  field table, the no-free-text-fields claim, and a new section documenting the
  npm update-check's own egress (`SO_DISABLE_UPDATE_CHECK`, `DO_NOT_TRACK`,
  `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`).
- **The `session_profile` whitelist above covered only the LIVE send path — a
  record already sitting in the offline queue when that whitelist landed still
  carried whatever it was enqueued with, and `flush()`
  (`scripts/lib/telemetry/sync.mjs`) forwarded every queued batch to the sender
  UNCHANGED.** Reproduced by the Wave-4 Codex second-look: a record the ingest
  server rejects is retried, still rejected, still requeued — the offline queue
  grew 1 → 2 → 3 and never drained. `sanitizeQueuedRecord()` now re-projects
  (`projectUsagePing`) and re-whitelists `session_profile` on every queued
  record before it joins a send batch, closing the same gap the live path was
  already closed for; the Codex reproduction now shows the queue back at 0
  after 3 consecutive rejections. A batch the server rejects with HTTP 400/422
  is now EVICTED (`reason: 'rejected-evicted'`) instead of re-queued: the
  ingest API validates a batch all-or-nothing with no per-record index, so the
  only bounded response to "the server will never accept this payload" is to
  drop the whole batch (named ceiling, BV-004: revisit if the server ever
  reports which record failed). `defaultSender` now attaches the HTTP status to
  the thrown error (`err.status`) so `flush()` can tell a schema rejection
  (evict) from a transport failure (re-queue, unchanged behaviour) — an
  injected sender that throws a bare `Error` still falls through to the
  pre-existing re-queue path. `docs/telemetry.md` documents `rejected-evicted`
  under "When a ping is sent".

### Notes

- **Full Gate (Wave 4, `37169158`):** typecheck 432 OK, lint 0, 642 files / 15,926 passed /
  0 failed / 11 skipped, `validate-plugin` 249/0, owner-leakage PASS (macOS). Quoted from
  the Wave-4 commit trailer, not re-run for this entry.
- **The Wave-4 review panel (3 Opus reviewers: security-reviewer, session-reviewer,
  qa-strategist) found both HIGH findings above inside its own Wave-2/Wave-3 diff** — the
  `#1028` masker-guard residue the panel itself was reviewing. Both were closed same-wave by
  a dedicated fixpass (F-A/F-B/F-C) with fresh red-on-HEAD proofs before the green fix,
  plus test-hygiene cleanup (`dispatch-common.test.mjs` lost 12 structure-only assertions;
  `evolve-telemetry.test.mjs`'s regression tests no longer stat the real repo ledger) and
  three LOW fixes (`evolve-telemetry.mjs`'s `catch {}` now WARNs on stderr;
  `post-subagent-discovery-validator.mjs`'s German pattern now matches singular `Eintrag`;
  `.gitlab-ci.yml`/`docs/ci-setup.md` prose brought in line with the now-armed drift gate).
- **A use-before-define in an intermediate save of `own-session.mjs` blocked every
  Bash/Edit call of every session on this host for roughly 8 minutes.** `hooks/enforce-commands.mjs`
  and `hooks/enforce-scope.mjs` both import the module live on every tool call (the #1153
  P1 Gate 3b wiring), so a `SyntaxError`/`ReferenceError` mid-refactor link-crashed both
  hooks host-wide rather than degrading to one repo's GUARD INACTIVE banner. Follow-up
  issue pending to make an intermediate, uncommitted save of a hot-path hook dependency
  fail more locally.
- **This session's `session.lock` heartbeat was only refreshed once per wave, not
  continuously.** A long session-start/plan phase between heartbeats let a foreign
  `SessionEnd` reap the lock as stale before this session's own wave loop had a chance to
  refresh it. Deviation logged in this session's narrative; a follow-up issue for a
  time-based (not wave-based) heartbeat refresh is to be filed at close, not yet opened as of
  this diff.
- **Full Gate (session close, uncommitted vs `4b451303`):** 651 files / 16,261 passed /
  0 failed / 16 skipped, typecheck 435 OK, lint 0, `validate-plugin` 250/0, Semgrep 0 new
  findings, hook-import-set-check in sync (150 modules) — supersedes the Wave-4 note above.

## [3.24.0] - 2026-09-02

Six commits since v3.23.0 (4 `feat`, 1 `fix`, 1 `docs`; 160 files,
+15,586/−981 before the release commit), no `BREAKING CHANGE:` footer and no
`!` subject.

One `docs` commit (`c3ab4801`) adds ADR-0013, naming the ownership-check-before-deregister
ordering that closed Wave 4 of the previous session. One `feat` commit (`a019d5a4`, deep
session, 5 waves, 37 agents + 3 reviewers + 3 panel) closes the GH#67 peer-filter class
across every affected site, plus #1170, #1176, #1177, #1166, #1167, #1174, #1180 and #1175.
One `feat` commit (`936dae8a`) closes a Fleet-Mining v2 instrument audit across 18 repos
(1,587 learnings, 144k events, 347 telemetry records) with five repairs to the fleet's own
measurement instruments (#1189, #1190, #1191, #1192, #1193).

Two further `feat` commits (`2ccea0f2`, `3b352d78`) are Waves 2 and 3 of a new session (11
agents each; Opus for guards/identity/protocol, Sonnet for consolidation/polish). The
through-line: identity and scope boundaries are now drawn at the PROCESS, not the working
copy — a peer session's manifest in the same checkout is `foreign`, a dispatched subagent's
raw session id belongs to its coordinator, and four duplicated helper implementations
collapse to one canonical each.

### Added

- **`orchestrator.reconcile.completed` (#1192).** Wrapper around `runReconcile` covering all
  three return paths (empty corpus, normal, engine-error) with `trigger`
  (`skill|session-end|phase-skip|unknown`), `targets` (allowlisted `repo-local|baseline`),
  counters including measured zeros, `duration_ms`, and `aborted`+`reason` on the catch path.
  Skipped with a stderr WARN when no `repoRoot` is passed, so test runs never write the fleet
  ledger. First live record: `eligible 102 / proposals 10 / rejected 154 / capped 72 /
  already_materialized 20`.
- **`docs/audits/2026-09-02-fleet-instruments.md`** — the public scrubbed extract of the
  Fleet-Mining v2 instrument audit: 18 repos, 1,587 learnings, 143,967 events, 347 telemetry
  records (6 `anon_id`), 13 instrument verdicts and 12 orchestrator candidates (K1–K12).
- **A new blocking validator catches dead script paths cited in skill/command/agent prose
  (#1176).** `scripts/lib/validate/check-skill-script-paths.mjs` scans `skills/`, `commands/`
  and `agents/` for `scripts/**.mjs` citations that neither exist nor carry a same-line/
  line-above `<!-- path-check: planned #<iid> | historical | example -->` marker; fenced code
  blocks are skipped as illustrative shape, not a claim. Measured 2026-09-02 @ `c3ab480`: 237
  distinct citations, 7 dead, 4 of the 7 inside fences.
- **`events.jsonl` records are now schema-versioned and validated before they are written
  (#1177).** `emitEvent()` stamps every record `schema_version: 1` (never overwriting a
  caller-supplied value) and runs `validateEventRecord()` BEFORE the append and BEFORE any
  webhook POST — an invalid record throws `EventValidationError` and produces neither.
  `scripts/emit-event.mjs` maps that to exit `1` (I/O failures keep exit `2`);
  `scripts/lib/tmux-layout/telemetry.mjs` stamps + validates synchronously and drops invalid
  lines. The read path stays lenient — the 33k+ pre-#1177 records without the field remain
  valid.
- **A canonical reader collapses `sessions.jsonl`'s append-only duplication (#1167).**
  `scripts/lib/sessions-canonical.mjs` applies newest-wins per `session_id`, drops any record a
  later `supersedes` pointer refutes, and narrowly collapses the systemic double-stub class
  (two `abandoned` records sharing an exact `started_at`/`completed_at` pair, one synthetic).
  The startup/CLI backfill path (`scripts/backfill-abandoned-sessions.mjs`) now also emits
  `orchestrator.session.backfill_completed` per record — previously only the SessionEnd hook
  did — and stamps `raw_session_id`; `hooks/on-session-end.mjs`'s backfill gained a second
  UUID→semantic bridge via the `session.ended` event for sessions that lost the
  lock-acquire race.
- **A vault-scoped lock protects the shared live-status board's read-modify-write (#1180).**
  `scripts/lib/vault-status/board-lock.mjs` wraps `sweepBoard()`'s merge in a cross-repo mutex
  (mtime-based staleness — the vault dir can be synced cross-host, so a recorded pid isn't
  probeable); fail-open on acquire timeout or fs-error (one stderr WARN, then runs unlocked)
  since a board update is best-effort telemetry. `.gitignore` gained `state.lock` and
  `rules.lock`.
- **A GitLab Project Access Token recipe for the schema-drift CI job (#1175).**
  `docs/ci-setup.md` documents Option A (Project Access Token, recommended) and Option B
  (personal PAT) for `SCHEMA_DRIFT_TOKEN`. Activation is blocked on the documented
  vendor-ahead drift tracked in #531: the token is provisioned but the CI/CD variable is not
  yet set upstream.
- **A `remote-hosts:` config block routes wave work to another machine over SSH (#1160).**
  `scripts/lib/config/remote-hosts.mjs` adds the block (opt-in template + reference doc) and
  an `ssh:<alias>` channel in `agent-mapping`; the wave-resource gate adds an `offload`
  decision AFTER the HR-004 heavy-repo cap, gated on an injected readiness witness
  (`remoteReady` map or async `probeFn`) — the gate never probes the network itself, so with
  no witness the decision stays local. `dispatchRemote()`/`remoteDoctor()`/
  `remoteReadyProbe()` wrap the host-side `offload` CLI (prompt travels on stdin, never argv;
  exit codes 1–8 mapped to typed refusal reasons; an empty returned patch counts as failure,
  not success). Emits `orchestrator.remote_dispatch.completed` on every attempt AND every
  refusal (`ok:false`, `exit_code: null`) — the payload carries the configured alias, never a
  hostname or IP. `skills/remote-offload/SKILL.md` documents the channel generically for any
  consuming repo (skill count measured 48 → 49); wave-3 added the fourth wave-executor
  dispatch branch (`ssh:<alias>` via `dispatchRemote()`) to `wave-loop.md` alongside the
  existing three.
- **A Peer-Scope-Union protocol lets two coordinator sessions in the same working copy share
  scope without racing (#1195).** `skills/_shared/parallel-aware-auq.md` documents the
  four-step handshake plus a message template; `skills/wave-executor/wave-loop.md` §
  Scope Manifest 3.1a adds a `peer-session-<id>` record (rolled over each wave, cleared at
  session-end) that is included in the disjoint check but excluded from the coordinator's own
  file-scope union; a new branch in the `parallel-sessions.md` decision tree routes to it; and
  `hooks/post-bash-write-verify.mjs` now partitions a peer's writes from genuine scope
  violations instead of flagging both alike.
- **Session-start now writes its own identity before anything can read it wrong (#1199).**
  Phase 1.05 adds a self-report block (`session_id: null` before Phase 1.2 resolves it) so
  downstream consumers see an explicit unresolved state instead of inferring one.
- **`/evolve` and the auto-dialectic nudge now emit their own completion events (#1200).**
  `orchestrator.evolve.completed` and `dialectic.completed` are emitted via `emit-event.mjs`
  from inside the skill; `decideAndRecordAutoDialectic()` emits `dialectic.nudge_decided`
  mechanically rather than leaving the decision only in skill prose, consumed by
  `phase-3-6-tail.md`.

### Changed

- **BREAKING (behavior): `discovery-validator.enabled` defaults to `false` again (#1191).**
  Reverts the #690 flip of 2026-06-25. Fleet measurement (2026-09-02, 18 repos): 6,946
  `discovery_validator_violation` events had accumulated in repos that never declared the
  block, and a scope-adjusted n=60 sample put real precision at 0/60 — the hook was scanning
  the MAIN transcript on `SubagentStop`, not the subagent's own. It now reads the subagent
  transcript (`agent_transcript_path` when the harness sends it, else
  `<dir>/<base>/subagents/agent-<agent_id>.jsonl` — no fallback to the main transcript), and
  the violation event now carries `agent_id`. Repos that relied on the silent default lose the
  signal until they set `enabled: true` explicitly.
- **Scope enforcement now treats every identity as process-local, including your own
  (#1194).** `enforce-scope.mjs` reads session ids via a new `readProcessLocalSessionIds()`
  instead of `readOwnSessionIds()` — a peer's manifest sitting in the SAME working copy now
  classifies as `foreign`, not `own`. Two existing tests had asserted the inverted behaviour
  and were rewritten with a named ceiling (a harness with no session id, e.g. bare
  Codex/Cursor, resolves to `unknown` → enforce, never skip); `scope-collision-guard.md` and
  `state-ownership.md` were updated to match.
- **`memory.propose`'s lock check is now a raw→semantic lookup, not a trust boundary
  (#1188).** The lock is consulted only to translate the caller's raw id into its semantic
  session id, authorised by a process-local match against the RAW id — measured: a dispatched
  subagent carries the COORDINATOR's raw UUID in `CLAUDE_CODE_SESSION_ID`, never its own.
  `sessions-canonical.mjs` and the backfill path now distinguish silent `ENOENT` from loud
  `EACCES`/`EISDIR` in two separate ledger readers instead of treating every read failure the
  same.
- **Four duplicated helper implementations collapse to one each.**
  `scripts/lib/validate/markdown-fences.mjs` (#1181) replaces 4 line-start-only fence
  scanners with one line-start-AND-end-anchored automaton shared by all 4 callers
  (`validate-plugin` output byte-identical before/after, 203/0). `expandTilde` (#1182)
  replaces 8 copies of home-directory expansion with the existing `common.mjs`
  implementation — one of the deleted copies (`gitlab-portfolio/cli.mjs`) mis-expanded
  `~user` (a different user's home, not the caller's). `hooks/_lib/subagent-paths.mjs`
  (#1196) replaces 4 divergent sidecar-path derivations with one carrying the strictest rules
  from any of them (agent-id length `{1,64}`, `'unknown'` rejected,
  `agent_transcript_path` confined inside the transcript directory).
  `hooks/_lib/atomic-json.mjs` (#1197) replaces 4 byte-identical copies with one whose
  read-modify-write default only replaces a genuinely MISSING file (`ENOENT`) — an
  unparsable or unreadable file is no longer silently overwritten with the default.
- **A block-header matcher gained an indent+inline-value form, additively (#1185).**
  `matchBlockHeaderDetailed` is now shared so `health-endpoints.mjs` no longer maintains its
  own `HEADER_RE`.
- **Five readers now go through the canonical `sessions.jsonl` collapse instead of counting
  raw lines (#1186).** `telemetry/sync.mjs` and `build-live-signals.mjs` (wave 2), plus
  `site-numbers.mjs` and `vault-mirror.mjs --kind session` (wave 3), now read
  newest-per-`session_id`; on this repo the session count measured 289 lines → 278 sessions
  (11 duplicate lines collapsed). Two tests pinning the old raw-line count were removed as no
  longer testing real behaviour.
- **Wave-completion refusals are now named events, not silent skips (#1201).**
  `KNOWN_TRIGGERS` plus three explicit wrapper functions (`runReconcileFromSkill`/
  `AtSessionEnd`/`FromPhaseSkip`) replace an implicit trigger string — an unrecognised
  trigger now throws instead of silently defaulting. `emitFinalWaveCompleted` emits
  `orchestrator.wave.final_refused` for all six refusal paths instead of leaving them
  unrecorded — closing the same class of instrument gap named in
  `.claude/rules/host-resources.md` § HR-105.

### Fixed

- **`orchestrator.agent.stopped` carried `agent: ""` in 86.7% of fleet records (#1190).**
  89,991 of 103,763 historical records carried an empty `agent` because
  `input?.agent_type ?? 'unknown'` never fires on an empty string — the harness's actual
  shape. `agent` is now omitted when empty (never `'unknown'`, never a fabricated `0`/`false`).
  Seven optional sidecar-derived fields were added — `agent_id`, `transcript_found`,
  `tool_use_id`, `agent_type_meta`, `duration_ms` + `duration_source` (`meta-birthtime`), and
  `status` (`done|partial|blocked|failed|no-tests-needed`, from the last line-anchored
  `STATUS:` marker in the final 64 KiB of the agent transcript) — each independently omitted
  when unmeasured, each wrapped in its own try/catch, with a charset guard on `agent_id`
  before path interpolation. `CURRENT_SCHEMA_VERSION` is unchanged (additive). Measured
  `status` coverage: 61.7% (71 done / 3 partial / 46 absent) over 120 completed sidecars.
- **`commands[]` was empty in every usage ping (#1189).** `classifyInvocationName` routes
  plugin-prefixed command names (`session-orchestrator:session`) into `commands[]` under the
  bare name — the prefix is required, so a bare foreign name never becomes one of our
  commands — and `.command`-field names are prefixed before classification, since that field
  is itself the "this is one of ours" provenance signal a bare `.skill` arrival lacks.
- **The final `orchestrator.wave.completed` of every session was never emitted (#1193).**
  Fleet-wide: 1,018 `started` vs. 722 `completed`, a gap of exactly 296 — one per wave run.
  `hooks/on-session-end.mjs` now emits it at SessionEnd, idempotent via a
  `last_wave_completed` high-water mark written monotonically by both emitters, gated on
  ownership of `current-session.json` via the RAW stdin session id (not the resolved one), and
  skipped on `reason: clear`. `hooks/post-tool-batch-wave-signal.mjs` gained the same
  raw-stdin-id ownership check, so it can no longer write wave keys into a peer session's
  record either.
- **`orchestrator.session.stopped.duration_ms` was a literal `0` in 8,127 of 8,127 fleet
  records.** The harness never sends `start_ms`, so the old expression fell back to a
  fabricated zero — indistinguishable from a real instant turn. It is now derived from an
  OWNED `.orchestrator/session.lock` (`duration_source: 'session-lock'`, session-elapsed
  measured at turn end) or omitted entirely, never `0`.
- **`resolveSession()`'s ownership predicate was self-fulfilling** when the stdin `session_id`
  was absent or non-UUID: it fell back to the recorded id and then compared the fallback
  against itself. Fixed at the root (`rawStdinId !== null && rawStdinId === recordedId`) in
  `hooks/on-session-end.mjs` for `duration_ms`, `semantic_session_id` and the new wave-completed
  emit, plus the sibling site `resolveSessionId()` in `hooks/on-stop.mjs`.
- **A live local `session.lock` no longer hides a same-working-copy registry peer (GH#67).**
  Registry-sourced entries in `scripts/lib/session-discovery.mjs` / `peer-discovery.mjs` now
  carry additive `registryOnly` / `lockSuperseded` / `lockOwnerId` annotations instead of
  being filtered out — `lockSuperseded: true` is a HINT (the lock is advisory, so the entry
  may still be a live session that lost the acquire race), never a verdict. Only
  PROMOTION_OFFER decisions downgrade such a peer to advisory; peer-count and display
  consumers keep it. Lock-sourced sessions carry none of the three fields, so their shape
  stays byte-identical to pre-GH#67.
- **Worktree-promotion teardown is now mechanical, not skill prose (#1170).**
  `enterWorktree()` takes an optional `rawSessionId`; when given, it calls `leaveSourceRoot()`
  on the OLD root after the destination provably exists and records the outcome on
  `result.left`. Before #1170, four promotion call sites relied on coordinator prose to
  release the source root — measured zero `deregisterSelf`/`release(` call sites across the
  affected skill docs, so the abandoned registry entry advertised a phantom peer for up to an
  hour.
- **`memory.propose` filed wave-N+1 proposals into the wave-N quota bucket (#1166).**
  `scripts/memory-propose.mjs` now resolves the RUNNING wave from `<state-dir>/wave-scope.json`
  first (when present and unbound or bound to this session), falling back to `STATE.md
  current-wave + 1` only when no usable manifest exists — `current-wave` itself always
  records the JUST-COMPLETED wave.
- **`health-endpoints` silently dropped the wizard's own nested-block output (#1174).** The
  key was read off the flat KV map via `_coerceList`, which bails to `null` on any `{` and
  cannot see a nested YAML block at all. `scripts/lib/config/health-endpoints.mjs` now parses
  it content-scoped, accepting inline object arrays, the nested block form (top-level or
  under `ecosystem-health:`), and a bare URL list; `ecosystem-health`'s own valueless
  block-header form is now read as a fallback when the scalar key is absent.
- **`emitEvent()`'s automatic session/semantic-session fill now requires a process-local
  witness (#1177).** `CLAUDE_CODE_SESSION_ID` (or the hook-input session id) must exactly
  equal the lock's raw `session_id` before `session_id`/`semantic_session_id` are
  auto-filled — STATE.md is never treated as a witness. `wave` is filled the same way, only
  from a `wave-scope.json` manifest bound to this same session, and is coerced to an integer.
  See `docs/events-schema.md` § Correlation keys.
- **The memory banner's "sessions ever" stat now counts distinct sessions, not JSONL lines
  (#1167).** `sessions.jsonl` is append-only, so one physical session can occupy two lines;
  on this repo the stat drops from 286 lines to 275 sessions. Five readers now go through
  the canonical collapse: cold-start detection, the memory banner, harness-audit Category 1,
  evolve's autopilot-effectiveness/verdict, and autopilot's id-set guard.
- **`check-skill-script-paths` no longer goes blind past an unbalanced code fence (#1176).**
  An unclosed/unbalanced fence now reports as its own `unbalanced-fence` finding instead of
  silently absorbing the rest of the file as "inside a fence"; a fence opened inside a
  blockquote is now recognised as a fence too.
- **`sessions-canonical.mjs`'s `supersedes` collapse now requires an attestable join key
  (#1167).** A `supersedes` pointer on an abandoned record is honoured only when it can be
  matched to the record it claims to supersede; rule ordering was fixed so a superseded
  record's synthetic double-stub twin is collapsed too. A new `keepUnidentified` option lets
  callers retain records the canonicalizer could not confidently classify instead of
  dropping them.
- **`memory.propose` no longer trusts an unbound `wave-scope.json` (#1177).** A manifest
  without a `semantic_session` binding is a peer's or a stale artefact, not this session's
  own — it is now ignored with a stderr note instead of adopted; an unreadable/malformed
  manifest also WARNs on stderr before falling back to `current-wave + 1`, rather than
  failing silently.
- **The vault-status board lock's stale-override is now surfaced, not silent (#1180).**
  `onLockOutcome` reports a `staleOverride` reason when the lock was force-acquired past a
  stale holder, and `sweepBoard()`'s board event now records it too — distinguishing "the
  mutex worked as designed" from "a slow host looked like a crash".
- **`check-hooks-emit-event-guard` and `check-validator-registration` both went live in
  `validate-plugin` (#1183, #1184).** `check-hooks-emit-event-guard.mjs` (#1183) parses hooks
  via `@babel/parser` (AST, not regex) to catch a hook that mutates state without emitting a
  corresponding event; 5 pre-existing sites are grandfathered into an explicit WARN baseline
  with a reason and stale-detection instead of silently passing. `check-validator-registration.mjs`
  (#1184) confirms every validator module is actually wired into `validate-plugin.mjs`
  (measured 33 of 33 wired, using comment-stripped source so a commented-out registration
  doesn't count as live).
- **`check-skill-script-paths`'s new `--strict-sh` mode found its own premise refuted
  (#1187).** Extending the dead-path scan to shell-script citations, only 1 of 27 `.sh`
  references sits inside the tool's own scan directories (`skills/`, `commands/`, `agents/`)
  — the other 26 are in `docs/`, alongside 7 dead `.mjs` citations inside `docs/adr/` the same
  run surfaced. Shipped as a WARN-only opt-in flag; the `docs/` sweep is a follow-up.
- **The discovery-validator's dedupe key, gate-summary skip, and masking order all landed
  together (#1198).** A dedupe key (`session_id`, `agent_id`, `sha256(claim)`) collapses
  repeated flags of the identical claim; gate-summary recap lines (the coordinator's own
  restatement of a subagent's findings, not a fresh claim) are now skipped entirely; claim
  masking now runs before every pattern is checked, not a subset. Measured against the
  flagged corpus: 186 of 400 flagged claims were false positives. Two claims from this
  session's own discovery briefing were checked against the fix and refuted.
- **A pre-push scratch-vs-publish check now reads the two arguments git already passes it.**
  `.husky/pre-push` had never read `$1` (remote name/URL) or `$2` (remote URL) — confirmed by
  `rg -n '\$1|\$2' .husky/pre-push` finding only a local function's own arguments — so every
  push ran the full quality gate regardless of destination. It now skips the gate for a raw,
  unconfigured scratch URL (e.g. the remote-offload sync target) while continuing to gate
  real publish remotes; a named ceiling covers a real but unlisted third publish host, which
  still reads as scratch.
- **`check-untracked-test-deps` misclassified `.git` as an untracked file inside a linked
  worktree.** In a normal checkout `.git` is a directory and was already excluded; in a
  `git worktree add` checkout `.git` is a FILE containing `gitdir: <path>`, which satisfied
  the same `isFile()` check the untracked-candidate branch uses. `.git` is now explicitly
  exempted regardless of which shape it takes — measured 62 of 62 tests passing across the 5
  affected test files inside a linked worktree checkout.

## [3.23.0] - 2026-08-28

Two commits today (1 `feat`, 1 `fix`; 64 files, +4,971/−184) close Waves 3 and 4 of the
resumed session, plus eight items carried forward from Wave 2 (`36aa605`, 2026-08-26,
never previously changelogged). No `BREAKING CHANGE:` footer and no `!` subject. The
pattern repeats across both waves: a guard, sanitizer, or census that already existed but
checked the wrong population or stopped short of the surface it needed to reach — a parity
guard sampling five rule files by name instead of reading the directory, a sanitizer whose
findings never reached stderr, a byte-count drifting 338 lines from the line it cited, a
tracked directory hiding an untracked hook beside it.

One `feat` commit (session-2, `30940cb`, 2026-08-28; 39 files, +2,475/−405) closes Wave 2
with three GitHub-mirror fixes and a session-identity cluster at the process boundary. No
`BREAKING CHANGE:` footer and no `!` subject. The through-line: identifiers the platforms
mint (UUIDv7, semantic session ids) were rejected or dropped by readers built for UUIDv4
only — every resume looked foreign — and call sites that needed a repo root passed
`undefined` instead, so fail-open paths ran on every invocation.

This tag is also the first to carry the thirty-three commits of the 2026-08-22 → 2026-08-25
line (`141d418..0626387`: 20 `fix`, 11 `feat`, 2 `docs`; 297 files, +24,535/−1,318 — measured
2026-09-06 @ `befdda47`, `git log --format=%s v3.22.0..0626387` → 33 subjects). A `3.22.1` cut
dated 2026-08-25 had changelogged that line, but the release commit never left its working
copy — no push, no tag, no registry tarball — and the next day's work resumed from `0626387`
without it. Its entries are carried below under *From the 2026-08-22 → 2026-08-25 line*. One
fact sits under all of them: **a surface that said it was live, and a measurement that never
had a caller.** Cursor advertised as a first-class platform and loaded only `.cursor/rules` —
no slash commands, no skills, no hook bridge; `/session` was chat text. The same class shows up
in the rest of the line: 18 probes with zero mechanical callers, three vault writers that could
not report their own failure, gates that wrote and never read, and `detectPlatform()` walking
to `$HOME` until 63 of 84 telemetry records said `platform=pi`.

### Added

- **Eight core rules ship as sanitized copies, with a report-only leak scanner (#1098).**
  `rules/always-on/` holds sanitized copies of `ask-via-tool`, `build-value`,
  `loop-and-monitor` and five more; `scanVendoringLeaks()` now reaches stderr as one line
  per finding in both CLIs, folded into `validate-vendored-rules` with a rule-ID census.
- **Session Config fails loud on an unparsable line (#1097).** `collectUnparsableLines`
  gates warn/strict via one `htmlCommentSkipper` shared by extractor and classifier — a
  commented-out `enforcement: strict` had been reading as live config.
- **A read-only protection-audit CLI, with a runbook naming the order (#1079).** States
  the required sequence; never flips a setting itself.
- **The scope guard now emits `orchestrator.wave_dispatch.scope_checked` (#1092).** Closes
  the ledger half that never recorded a signal-free ALLOW.
- **`/journey-audit` — a user-facing product audit as a repeatable skill (#1161).** Skill,
  command and manifest template; skill/command counts measured at 48/28.
- **A Cursor `.mdc` parity guard that reads the directory, not a fixed list (#1093).**
  Catches bare `PID=`/`${VAR}` IDs a name-sampling guard had missed; `priority::` swept
  across all 5 files.
- **The old root now formally leaves before the new one claims it, wired at all four
  promotion sites (#1069).** `scripts/lib/session-transition.mjs` → `leaveSourceRoot()`:
  `deregisterSelf()` → lock-release (reusing `on-session-end`'s ownership rules) →
  `orchestrator.session.root_left` (carries `source_root_hash`/`source_root_basename`,
  never an absolute path — the payload also travels over the optional webhook). Review
  found the Phase-1.2.1 promotion site had been missing the call; it is now wired at
  session-start Phase 0.5, Phase 1.2.1, and both `parallel-aware-*` docs. A real
  `git worktree` two-root integration test goes phantom-peer red → green.

#### From the 2026-08-22 → 2026-08-25 line

- **Cursor is a native surface, not a rules overlay.** `.cursor/commands/*.md`
  (27 slash-command wrappers), `.cursor/skills/<name>/SKILL.md` (47 wrappers),
  `.cursor/hooks.json` → `scripts/lib/cursor-hook-bridge.mjs`. Same pattern as
  Pi: one maintained body in `commands/` and `skills/`, generated adapters,
  `validate-plugin` check (`check-cursor-adapter.mjs --check`). `cursor-install.mjs`
  now links commands and skills and writes `hooks.json` for other projects.
  Cursor has no Skill tool — commands Read `skills/<name>/SKILL.md`.
- **The coordinator can see its agents, and a foreign model can build (#1113, #1150).**
  Wave supervision plus `cursor-agent` CLI foreign dispatch (detached worktree,
  filesystem-measured result, mandatory Claude review, `NEVER_FOREIGN_ROLES`).
- **`/reconcile` ran for the first time in seven weeks** — 10 learnings became
  conditional rules (`feat(rules)`).
- **Express-path** gained a mechanical caller and a single documented reading.
- **18 measurement probes** now have a mechanical caller (`feat(probes)`);
  session-start no longer depends on prose to run them.
- **Vault writers report their own failure**; the vault mirror reports that it
  is healthy (`feat(vault)`, `feat(telemetry)`).
- **Ingest server** counts answers; the weekly digest has a caller.

### Fixed

- **Six onboarding contradictions + a `cursor-install` TARGET guard (#1078).** First
  production diff authored by a foreign model (composer-2.5) under mandatory Claude review.
- **Amber is now a documented normal state (#1062).** `docs/ci-setup.md` names it instead
  of leaving it an unexplained CI color.
- **A tracked directory no longer hides an untracked hook beside it (#1158).**
  `hooksPathIsTracked` now requires a GIT-tracked file directly under `core.hooksPath`.
- **`probe-stale` collapses to one vocabulary term (#1159).** A record older than 7 days
  is `probe-stale`, severity `warn`, never a live finding; the registry remap is gone.
- **The scope-cap saw operators, not shell keywords (#1145).** `splitSegments` now splits
  on keywords too, so `for t in a b c; do glab issue create; done` no longer slips a
  compound-statement head past the cap.
- **A missing `.claude/rules/` was misdiagnosed as a loop (#1132).** `err.code` now
  separates silent `ENOENT`/`ENOTDIR` from loud `EACCES`; the repeat was caller fan-out.
- **Two probes measured the wrong population (#1143/#1148).** One counted a whole file and
  cited a line that had already drifted 338 lines from the finding; both scanners read the
  filesystem instead of the git index, censoring 5 gitignored files as documentation.
- **Generated rules now self-verify offline (#1101).** Each carries a sha256 seal over its
  own Evidence block instead of a learning-key into a gitignored file; 23 rules sealed,
  fresh-clone warnings 23 → 0.
- **`reconcile.targets` had zero consumers (#1099).** Now wired through with a closed enum,
  visible rejection of unknowns, and per-target path confinement.
- **Orphaned wave-scope files survived re-materialization (#1103).** Reconciled by
  ownership proof now, never a blind directory wipe — two parallel sessions can share one
  wave-N directory (PSA-003).
- **Two rival vendoring paths wrote the same rule file (#1060).** `rules/` is now sole SSOT
  for PSA-001..007; `templates/_shared/rules/` is deleted.
- **`parseSessionId` accepted only UUIDv4; Codex mints v7 (#66/#1091).**
  `scripts/lib/session-id.mjs` now accepts RFC-9562 v1–8 (`UUID_RE`, additive `version`
  field — see `### Removed` below for the alias this superseded). Every SessionStart had
  minted a fresh v4; a
  resumed/compacted thread read its own lock as a foreign session. `hooks/on-stop.mjs` and
  `hooks/on-session-end.mjs` now apply the writer's acceptance rule, closing a lock-leak via
  non-UUID stdin ids.
- **phase-3-7a called `updateFrontmatterFieldsOnDisk(undefined, …)` (#65/#1036).**
  `skills/session-end/phase-3-7a-recommendations.md` requires a repo root; every `/close`
  took the fail-open path — 0 of 5 recommendation fields were ever written. The snippet now
  binds `repoRoot` via `git rev-parse --show-toplevel`; the catch logs the cause.
- **Codex MCP launch resolved to `/scripts/mcp-server.sh` from non-git cwd (#64).** Codex
  copies a marketplace plugin to
  `~/.codex/plugins/cache/<marketplace>/session-orchestrator/<version>/` and starts the MCP
  child with no plugin-root env var. `.mcp.json` and `scripts/lib/plugin-root.mjs` gained a
  plugin-cache scan tier (name-matched `package.json`, newest by mtime). Codex snapshots
  `.mcp.json` at install time — existing installs need a reinstall.
- **`scoring.mjs` read a flat `completion_rate` 0 of 281 records carry (#1071).** The value
  lives under `effectiveness`, so the high-completion bonus was unreachable.
  `resolveMemoryDir`/`buildLiveSignals` now take an explicit `repoRoot` instead of
  `process.cwd()`.
- **mission-status recovery aborted on the first non-canonical body line (#1104).**
  `scripts/lib/state-md/mission-status.mjs` now skips per line, reports skipped lines
  (`recoverFrontmatterMissionStatusDetailed`). `setMissionStatus` refuses ids outside the
  grammar with a stderr WARN from the on-disk wrapper.
- **`enterWorktree` failed with `already used by worktree` when the source branch was
  checked out (#1067).** `scripts/lib/autopilot/worktree-pipeline.mjs` now creates
  `so/<sessionId>` from that branch and returns `branch`/`promotedFrom`.
- **Phase 4a auto-promoted-worktree cleanup was dead under the #1069 process boundary.**
  Since the promoted worktree runs as a brand-new session with its own id,
  `detectAutoPromotedWorktree()`'s basename match against the CURRENT session id could
  never fire post-#1069. `enterWorktree()` now writes `.orchestrator/promoted-from.json`
  (source-root hash + basename, source session id, branch) at creation time;
  `detectAutoPromotedWorktree()` tries this marker FIRST (`source: 'marker'`), falling back
  to the legacy basename match (`source: 'basename'`) for pre-marker worktrees.
  `isWorktreeClean()` discounts exactly the marker's own untracked line so the marker never
  reads as dirty by itself. Re-promoting onto an existing `so/<sessionId>` branch now reuses
  it (`reusedBranch: true`) or reports a typed refusal, instead of failing outright. See
  ADR-0013.
- **`.mcp.json`'s bash bootstrap had silently drifted from `resolvePluginRoot()`'s tier
  order.** The shell mirror now matches the same five env tiers in the same order, tests
  `package.json`'s `name` field via `node -e JSON.parse(...)` instead of a substring `grep`
  (which had matched `session-orchestrator-fork`), and ties the newest-by-mtime cache scan
  to the same strict `-nt` comparison as the JS side. Two drift tests pin JS and shell
  staying in sync.
- **`ci-status-banner.mjs` escaped control bytes in the JSON preview but not in the
  parse-error message.** `JSON.stringify()` alone is not enough — V8 quotes the raw
  offending input inside `SyntaxError.message`, carrying control bytes straight to the
  terminal. `escapeControlBytes()` now covers both the preview AND `err.message`;
  `parseCliJson()` also gained a shape-gate against `null`/`[]`/a bare `"ok"` string, all of
  which parse as valid JSON but are not the object/array shape callers expect.
- **A refused mission-status write warned on only 1 of 5 call paths.**
  `setMissionStatusDetailed()` now reports the refusal reason (e.g. `id-grammar`) to every
  caller, and the on-disk wrapper emits the stderr WARN on all 5 paths instead of the one it
  happened to cover before.
- **`check-unwired-features` exempted itself from its own edge census via its allowlist
  string, hiding a real orphan behind it.** The `SELF_REL` self-reference correctly
  contributes zero edges by design, but the same code path was masking that
  `scripts/lib/webhook-url.mjs` has zero callers repo-wide — genuinely unreachable, not a
  false positive. Follow-up issue pending to wire it in or remove it.
- **Three load-sensitive tests hardened with margin instead of being disabled.**
  `tests/scripts/validate-plugin.test.mjs`'s child-process timeout moved from 30s to 120s (a
  killed child returns `status: null`, which had read as a different failure than a real
  one) plus a matching `hookTimeout` bump, and two further spawn-timeout assertions gained
  the same discriminating margin between contention and a genuine failure.

#### From the 2026-08-22 → 2026-08-25 line

- **Cursor hooks were a reference mapping that silent-no-op'd (#919).**
  `hooks/hooks-cursor.json` is the live handler map. The bridge projects Cursor
  stdin onto `tool_name` / `tool_input` and maps deny envelopes onto Cursor
  `{ permission }`. `afterFileEdit` remains post-hoc (cannot unwrite).
- **`detectPlatform()` walked to `$HOME` and found `~/.pi`** — 63 of 84
  telemetry records carried `platform=pi` (`fix(platform)`).
- **Identity compared the machine name, then the working copy, then DHCP**
  (#1072, #1085 follow-through). Budget and lock judge the session. One machine,
  one name.
- **A vault probe wrote into the operator's real vault**; the mirror now writes
  on the second run too (#1033).
- **Guards that saw the wrong thing or discarded the right one** (#1029, #1130,
  #1077, #1125, #979, and a second trio that measured nothing).
- **Reconcile: a No stays a No**; three paths that turned a rule into silent
  reject; gates that wrote and never read (EventDrop #484, #386, #385, #429);
  a type registry is enough (#1042, #1151).
- **STATE.md: a broken list item costs the item, never the document (#1111).**
- **Telemetry: noop-silent mirror, recorded aborts, budget reaper (#1151, #1127).**
  Consent question and flush no longer hang on prose.
- **Privacy: the operator's machine name leaves the examples (#1151 Review).**
- **Archive: a citation is not a declaration; foreign files are not mine
  (#1112, #1123).**
- **Scope manifest knows whom it belongs to, and the hook asks (#1123, #1082).**
- **Security copy claimed SAST/Semgrep levels that never existed**; the private
  npm scope no longer rides along with a copied baseline.
- **A dying suite at import is counted as what it is**; a blocking guard now
  says why.

### Changed

- **Host-registry census counts `semantic_session_id` (#1066).** `hooks/on-session-start.mjs`
  falls back to `session_id` only when it is itself semantic. Before, only UUIDs were
  projected — the n-increment discards them — so the registry contributed nothing to
  numbering.
- **Session end events carry `semantic_session_id`; backfill event + supersede (#1068).**
  `orchestrator.session.ended` / `.stopped` carry `semantic_session_id` (omitted when
  unknown). A new `orchestrator.session.backfill_completed` event records each backfill outcome. A
  completed record may now supersede an `abandoned` stub (append-only, `supersedes` marker).
  Readers preferring the newest record are a follow-up.
- **`.semgrep.yml` regains two taint-mode rules (#1129).** `json-parse-untrusted-input` and
  `prototype-pollution-object-assign` aimed at this repo's real trust boundary (hook stdin,
  child-process stdout); 27 rules total. `unsafe-llm-output-rendering` stays excluded with a
  measured reason (no DOM code).

### Removed

- **`UUID_V4_RE` removed** from `scripts/lib/session-id.mjs` — deprecated alias of
  `UUID_RE` with zero importers repo-wide (measured @ 7daa3d2; hits in
  `tests/telemetry/anon-id.test.mjs` and `server/ingest/validate.mjs` are unrelated local
  constants of the same name). Use `UUID_RE`, any RFC 9562 version 1–8.

### Notes

Two of this session's diffs were authored by foreign models under the #1150 adapter —
#1078 by composer-2.5, #1093 by grok-4.6 — each passing mandatory Claude review before
landing. First production use of the foreign-dispatch path.

npm 3.22.0 never contained the 2026-08-22 → 2026-08-25 line; 3.23.0 is the first registry
tarball that does (`npm view session-orchestrator versions` @ 2026-09-06: 3.22.0, 3.23.0,
3.24.0 — no 3.22.1). The 3.22.1 cut that first documented the line was a release commit that
was never pushed, which is why those entries surface here, one release late, re-measured at
`befdda47` rather than quoted.

## [3.22.0] - 2026-08-22

Twenty commits (12 `fix`, 5 `docs`, 2 `feat`, 1 `chore`; 150 files, +17,312/−2,575), no
`BREAKING CHANGE:` footer and no `!` subject. One strand runs through all of it:
**an instrument that reported confidently and measured the wrong quantity.** Not a
threshold set too tight, not a rule too strict — the wrong quantity, reported with the
same certainty as the right one. The host-resource warning fired on **99.0% of 1,477
measured session starts** and nobody could falsify it, because the two fields it was
computed from were never persisted. A hygiene probe announced "37 files (11 MB)" while
sizing the entire directory instead of the 37 — factor ~18 — and proposed deleting
version-controlled source files, because it never asked git. And the questions this tool
puts to its operator had never been measured at all: **21 of 72 passed** on first
measurement, and the single dominant cause was a 12-codepoint header limit that silently
truncated 30 of them.

### Added

- **The operator's questions are now measured before he reads them (#1107).** `scripts/auq-audit.mjs`
  scores every `AskUserQuestion` block and every prose fallback list against eight criteria
  (K1–K8) plus two hard hurdles, all thresholds sourced from one registry — no hard-coded
  number in the scorer. The census found **three populations, not one**: 40 Claude Code blocks,
  10 Codex prose lists, and 17 that every prior count had missed (9 `.cursor/rules/*.mdc` with
  10 choice blocks, plus 6 runtime questions inside `.mjs`). Baseline **21 of 72 (29%)**, after
  the pass **72 of 72**. The dominant cause was structural: `header` caps at 12 codepoints and
  the tool truncates silently — **26 of 42 headers exceeded it**, and one NFD-composed header
  measured 12 visible characters as 14, destroying the question. Population A — the operator's
  own path — ran 62% failing against 0% for the Codex and Cursor forms, which carry no header
  at all.
- **A guard on the guard (#1122 groundwork).** The `AskUserQuestion` PreToolUse hook denies only
  the two hard hurdles and reports the rest to stderr; `check-auq-clarity` is wired into
  `validate-plugin` and blocks on H1/H2 with a measured 0% false-positive rate. Its own wiring is
  proven by fake regression, not asserted.
- **`/eli5`** — say the last answer again in plain words, same facts, in the order the operator
  needs them.

### Fixed

- **The warning that fired on 99.0% of all session starts (#1089).** Measured over **1,477
  `orchestrator.session.started` events across 18 repos**: `ram_free_gb < 2` fired on 84.0%,
  `claude_processes >= 5` on 93.6%, any of the three on **99.0%** — against 4,884 stop events
  with zero OOM markers. Three independent measurement errors, no threshold among them.
  (a) `os.freemem()` on Darwin reports only `Pages free` — median **0.4 GB** on hosts with
  24–128 GB — so memory is now judged on `memory_pressure_pct_free` and a better signal
  *replaces* a worse one rather than suppressing it. (b) `concurrent-sessions-warn` is
  denominated in sessions and was compared against a process count; measured ratio **6.0:1**.
  Same threshold, right denominator: 93.6% → **4.2%**. (c) One noisy axis could cap a wave;
  now two independent soft signals must agree. The verdict had reached `sessions.jsonl` for
  **15 of 1,734 sessions (0.9%)**, which is why the false alarm survived four months while six
  repos independently wrote it into their learnings store — one at confidence **1.0**.
  Codified as `.claude/rules/host-resources.md` (HR-101..106).
- **A standing condition is not a second opinion (#1089 follow-up).** The first live run of the
  rebuilt verdict returned `warn | cap 2 | soft: ["cpu","zombies"]`. Zombie processes are idle
  by definition — they cannot cause the load they are paired with — and are always present
  (6, 13 and 9 in three readings minutes apart). They report; they never count toward the
  two-signal rule.
- **A probe that sized the wrong set and never asked git.** `checkStaleArtifacts` computed its
  byte figure over the whole of `.orchestrator/` instead of the aged subset it names —
  the 37 files weighed **0.68 MB against 11 MB reported**. Independently, it consulted git
  nowhere and therefore proposed pruning seven tracked, runtime-read source files, among them
  the policy file `pre-bash-templates-first.mjs` reads and the two steering documents
  session-start loads. The git exclusion is fail-**silent**, not fail-open: falling back to
  "nothing is tracked" *is* the defect it closes.
- **Only the raw session ID releases a lock (#1085).** One session carried three identities —
  STATE.md said `session-2`, the lock said `session-3`, issue-budget counted against a third
  UUID — and `findPeers` reported the session's own presence as a foreign peer. The semantic
  ID is now attribution metadata only; a release requires exact raw-ID equality, and a
  semantic hint resolves to a raw ID only against a verified local binding. The companion fix
  to issue-budget removed a data-loss path in which an identity-less invocation wrote its
  fresh state into the shared counter and erased a live session's parked overflow.
- **The npm receipt is the boundary — and a `throw` lay behind it (#1088).** `publish()` threw
  *after* a successful `npm publish`, during a 12-second registry-propagation poll: published,
  not tagged, not pushed, reported as "failed, retryable". Propagation now returns a typed
  outcome, each post-receipt step gets exactly one attempt, and everything else is collected as
  `post-publish-reconciliation`. Leak detection decides on an extracted path rather than a regex
  over `npm notice` prose (`contest` ≠ `tests`), and `ensureGithubRelease` became tri-state so
  an auth or network answer no longer reads as "no release exists".
- **`--silent` leaked into the children, and the gate discarded its own diagnosis (a4f93cf).**
- **The empty sidecar path skipped the collision gate with exit 0 (#1083).** A coordinator that
  wrote only the aggregate form of the two-shape scope declaration ran **six waves and ~27
  dispatches with zero `FILE-SCOPE` injection** — and the absence looked identical to a clean
  run. `scripts/materialize-wave-scope.mjs` is now the canonical writer of both shapes.
- **The GitLab target comes from the explicit path (#1065).** `glab repo view --output json`
  materialised a full project response to read one field; on 2026-08-17 that response carried a
  runner registration credential into a tool transcript. Project identity is now
  `(host, URL-encoded namespace/project)` with traversal and double-encoding rejection, and the
  same minimisation reached the vault-backfill path.
- **The restore froze after its first run (#state-md).** Also: a `gitleaks` allowlist that grew
  by one proper name per incident now matches on a word boundary instead.
- **Four count claims where the checker could only see two (#docs-parity).** `docs/components.md`
  claimed 46 skills and 26 commands against 47 and 27 on disk. The drift checker reported two —
  its patterns are `^##`-anchored — while a census of the same two surfaces in the same file
  found four; the two inside the Mermaid diagram were already self-contradictory before the fix.

### Notes

`.claude/rules/host-resources.md` is new and always-on. `.claude/rules/bash-harness-pitfalls.md`
gained no rule text this line, but two measurement traps were recorded against it in the
learnings store: `rg -rn` is not `grep -rn` (`-r` is `--replace` and silently substitutes every
match), and zsh does not word-split an unquoted variable, so a multi-path `git log -- $paths`
reports zero matches with exit 0 — both produce a plausible wrong number without erroring.

## [3.21.0] - 2026-08-19

Twenty-one commits (12 `fix`, 4 `feat`, 4 `docs`, 1 `chore`; 209 files, +28,906/−2,893), no
`BREAKING CHANGE:` footer and no `!` subject. Three strands — guard mechanics, the public
site, cross-session messaging — that turned out to be one strand: **a fact maintained in two
places, and a guard that is green without biting.** The sharpest instances are always the
quiet ones. **12 of 46 `SKILL.md`** and **14 of 16 `agents/*.md`** carried frontmatter that is
not YAML while every frontmatter checker in the tree had reported them clean for as long as
they existed. The v2 vault renderer's branch condition was **structurally false from its first
commit** and matched **0 of 253** session records. And the scope guard shipped, in the agent's
own channel, the command that disarms it.

### Added

- **Cross-session messaging is wired (#1049, #1050, #1047 — Epic #1048, waves 1–2 of 5).**
  New always-on rule `.claude/rules/cross-session-messaging.md` with CSM-001..005, and the PSA
  decision tree gained a peer-inform branch that hangs **below** the sibling check on the
  foreign-scope leg — so it can mask neither the sibling branch nor the PSA-002 pause. That is
  structural, not prose. The two-axis rationale is re-founded rather than restated: the moat
  was never "peer sessions cannot talk to each other" — two independent sessions in one working
  copy exchanged full round-trips on 2026-08-16 — it is that they share **one working copy**,
  and what crosses the channel is information, never isolation. `READ_ONLY_TOOLS` gained
  `SendMessage` + `ListAgents`, the mechanical precondition for upward agent escalation;
  `validateTierConsistency` filtered on `WRITE_TOOLS` and therefore named **no culprit at all**
  for an unknown tool.
- **The public site is rebuilt, and it now proves its own numbers (#1043–#1046).** The live
  page had served v3.19.0 against v3.20.0 in the repo for six days, all five legal paths were
  404, and the `Measured` block — the honesty argument of the whole page — carried five wrong
  numbers.
  - The load-bearing element is a band whose station height **is** the file count a wave may
    write: `--unit: 15px` = one file. Discovery is therefore not a box of height 0 but a stroke
    on the baseline — the moment the limit becomes visibly geometric rather than requested.
  - Mobile had **no** navigation at all (`display:none`, no hamburger, 20 viewports of scroll);
    6 links are now visible at 390px. Twelve sections in identical scoring and 27 equally loud
    cards became 10 sections with three loud blocks, cutting rendered height to **52.4%**
    (10,350 → 5,428px — the factor is the durable figure, the viewport count is not: 5.98
    viewports holds at 908px window height and becomes 6.03 at 900px). State no longer rides on
    red/green alone but on shape **and** glyph. Cold-load acceptance without `<script>`: 115
    content-bearing elements, 0 invisible, identical height — the page hangs on no condition.
  - **`vercel.json`** carries the deploy configuration **versioned** instead of clicked into a
    dashboard: `outputDirectory: "site"`, CSP/HSTS/`frame-ancestors` headers, www→apex redirect.
    The Vercel Git integration is connected, so a push to the GitHub mirror publishes the site.
    In `scripts/release.mjs` the checklist line `cd site && vercel --prod` is replaced by
    `verifyLiveSite()` with four **distinguished** outcomes (version mismatch / success /
    network error / surface moved), not collapsed onto a flat "not ok".
  - **`scripts/site-numbers.mjs`** — 13 metrics read from the repo and written into
    `data-metric` spans, ending the hand-maintained number block. Its first run against the new
    page caught three real errors, one of them the coordinator's (`skills: page says 47, repo
    says 46` — `skills/_shared/` has no `SKILL.md` and is not a skill) and one a contract defect
    that would have produced `vv3.20.0` at the next release. Wired into `--set-version`,
    deliberately **not** into CI: `sessions` and `learnings` grow every session, so a pipeline
    gate on them would be permanently red.
  - **`/guide`** — the four install paths, the first session with the honest number (7–10
    questions, 2–4 minutes, not the claimed "one question"), and a section on recognising that
    a session failed silently. The reason is a census, not a hunch: **all four issues opened by
    people outside this repo are install or environment failures, not one a feature request**
    (`gh issue list -R Kanevry/session-orchestrator --state all`; authors ≠ owner: #53, #54,
    #62, #63).
  - **`/impressum` and `/datenschutz`.** The privacy policy is not copied but written from the
    measured processing: 0 external requests, no analytics, self-hosted fonts (3 files,
    55,052 B), Vercel server logs only. The sibling site's Vercel-Analytics sections are absent
    on purpose — `grep` shows they are not active here. Sitemap 1 → 4 URLs.
- **Two catchers for the untracked-test-dependency class (#1081), because one was structurally
  blind.** A differential run compares test *status*, and a test that swallows a missing file
  and passes in **both** trees produces no difference at all. Statically:
  `scripts/lib/validate/check-untracked-test-deps.mjs`, rules R2 (a statically resolved repo
  root passed into an import closure that names an untracked path) and R4 (cwd-relative read),
  measured 2/2 recall at 0 false positives where the obvious variants sat at 98–99% FP.
  Structurally: `.husky/pre-push` materialises the sha being pushed via `git clone
  --no-hardlinks` (1.04 s; `git archive` is unusable — without `.git`, 21 tests fail with "not a
  git repository") and runs the gate **there**. It came within one function of being worthless:
  `findProjectRoot()` checks `CLAUDE_PROJECT_DIR` and five siblings **before** cwd, so the gate
  would have started in the tmp tree and read the working-tree files anyway.
- **A tracked census snapshot, `site/_census.json`, with ledger precedence** — per-metric opt-in
  for exactly `sessions`, `learnings`, `counted-sha`, never a blanket fallback, which would have
  made `collect()` blind to "wrong root". `optional: true` was considered and rejected by name:
  it would have let the two tiles fall silently back into the hand-maintained state while
  `--check` still reported the page as current — verbatim the silent-failure class the script
  was written against. A known metric id without a value is now a third contract violation
  (`unresolved`).
- **`tests/setup/scrub-git-env.mjs`, `scripts/lib/git-config-drift.mjs`, and
  `check-test-git-config-target.mjs`** — the mechanism against this session's own accident (see
  Notes). The setup file removes nine redirect-capable git variables once per worker; that is
  the root, because the two lines that caused the damage passed a **correct** cwd, and `GIT_DIR`
  beats cwd, `-C`, and even the positional of `git init <dir>`. The drift probe covers the
  surface `git status` does not show, and runs with a filtered environment — a set `GIT_DIR`
  would otherwise redirect the probe itself into a foreign repo and call this one clean. The
  lint rule is WARN-only out of measurement, not caution: its first formulation had 11 hits,
  **all** false positives (each a `git init <dir>` where the positional **is** the target); it
  ends at 150/150 with a target.
- **`scripts/lib/validate/check-skills.mjs` — a real parser over all 46 `SKILL.md`**,
  registered in `validate-plugin.mjs`. The sibling `check-agents.mjs` validated frontmatter
  with line-oriented regexes, which is precisely how the 12 broken blocks stayed invisible:
  an unquoted `description:` containing a `: ` (`"Iron Law: NO FIXES"`) is not YAML, but a
  regex looking for `^description:` sees nothing wrong, and Claude Code's own loader is
  lenient enough that the defect never surfaced at runtime. A `js-yaml` `CORE_SCHEMA` parse
  is now rule R8 and the five field rules hang off it — none of them can be evaluated on a
  block that does not parse. Deliberately **not** implemented: a block-scalar ban (see
  below), and length ceilings on `name`/`description`, because no spec vendored in this repo
  states one and an invented requirement is worse than none.
- **`scripts/lib/validate/frontmatter-block.mjs` — shared extraction, deliberately unshared
  rules.** The extractor stood verbatim in two checkers, so the next change to the block
  format would have landed in one copy and one gate would have started accepting what the
  other rejects — with no test able to see it, because each gate tested its own copy. The
  module owns the byte range and nothing else, and its header records why the three checkers
  must stay **contradictory**: `check-agents.mjs` bans `description: >` because the agent
  loader cannot read a folded scalar, while `check-skills.mjs` must tolerate it — for
  `SKILL.md` the folded form is the only shape that makes the `: ` collision structurally
  impossible, 35 of 46 files now use it, and porting the agent ban here would red 35 of 46
  and forbid the very fix that made the gate green. The divergence is the requirement, not
  drift.
- **`scripts/lib/mirror-issues-banner.mjs` — the GitHub-mirror blind spot**, wired into
  `skills/session-start/SKILL.md` Phase 4. VCS auto-detection picks exactly one platform, so
  in a GitLab-origin repo with a public GitHub mirror no code path ever read the mirror's
  issues: everything filed by an external reporter was structurally invisible to every
  session. The probe asks the other side, hard-pinned to `github` (auto-detecting here would
  reproduce the defect it exists to compensate for) and **self-disabling** — no `github`
  remote resolves to `undefined` → `null` → no spawn, no network call. No new Session Config
  key by design: the spec comes from `git remote`, and a key would be a second SSOT drifting
  against it. The return is three-valued, not two — `null` means *never asked* or *asked and
  clean*; a `degraded` field with a closed enum means *the query failed and the state is
  unknown*. Collapsing that third state into `null` is what makes a dead probe
  indistinguishable from a healthy repo, which is the next section's recurring theme.
- **`atomicWriteWithBackup()` in `scripts/lib/io.mjs`** — the tmp-write/rename/`.bak-<ISO>`
  primitive that `writeJsonAtomicSync` now delegates to, ending a second copy of the same
  sequence (#734).
- **`check-unwired-features` signal S3 `orphaned-prose-module`** — a config key is not the
  only thing prose can promise; a document can also assert that a module does a job nothing
  calls. S3 fires only where the claim is in the **passive voice with a bare filename and no
  exported symbol**: "…*are validated* against `foo.mjs`" asserts that something happens by
  itself, whereas "dispatch via `runWavePool()`" addresses a reader who will do it. The
  broader check — every export with no non-test importer — was measured and rejected: 1366
  exports, 779 unimported, 93.2% false positives naive and still 81.2% after four exclusion
  rules. A gate that prints 282 lines is switched off in week two, which is this file's own
  disease one level up. Measured 2026-08-14, the cascade narrowed 452 production modules to
  **2**; one of those is deleted below, so the live report now stands at 1. Read a near-empty
  report as designed, not broken — S3 is a relapse guard, not a cleanup tool.
- **[ADR-0012](docs/adr/0012-pseudonym-map-privacy.md)** — host-local pseudonym map for
  owner-leaky repo namespaces. An ADR rather than a code comment precisely because every
  element reads like removable defensiveness at its call site while being load-bearing for a
  property no test can observe directly: a green suite is fully compatible with the leak.

### Fixed

- **The documented Claude Code install path was guaranteed broken.** `claude plugin dir` is not
  a subcommand: `claude plugin dir session-orchestrator` answers `error: unknown command 'dir'`
  and exits 1 (measured on Claude Code 2.1.235; `claude plugin --help` lists 14 subcommands and
  `dir` is none of them). The `||` fallback therefore fired **every** time and pointed at
  `~/.claude/plugins/session-orchestrator`, a directory that does not exist — so `cd` failed,
  `&&` aborted, `npm install` never ran, and the hooks could not find `zx`. That is the cause
  behind external issues #62/#63, whose reporters described the symptom. Replaced by a
  layout-independent resolution through the plugin cache, extracted from the **shipped**
  `site/index.html`, entity-decoded and executed verbatim: exit 0, correct path. Second defect
  in `migration-v3.md` fixed alongside — a marketplace-installed plugin sits in a managed cache,
  not a git checkout, so `git pull` there does nothing; replaced by `/plugin update`. Five sites
  in five files; the two remaining matches for `claude plugin dir` are deliberate quotes on the
  site explaining the breakage.
- **The v2 vault renderer was dead at birth (#1074).** `total_agents` stands in
  `REQUIRED_FIELDS`, so the branch condition `total_agents === undefined` is structurally false
   — written 4h23m before the first record that could have matched it, and matching **0 of 253**
  records since. The obvious fix would have been **worse than the bug**: `agents` is polymorphic
   — measured over 599 wave objects, **210× a number and 14× an array** — so a naive `??` would
  have written `[object Object]` into the vault. A type-aware alias chain repairs 239 agent, 5
  files and 280 quality cells at changed-existing = 0. The fixtures are now harvested golden
  records; the old `makeV2Entry` invented a record no production writer can produce.
- **Four blind probes and one active fail-open (#1039).** `backlog-scan` folded "no remote" and
  "query failed" into a silent `null`, from which the mode selector read "contributes 0 delta" —
  a 40-issue backlog with critical labels looked like an empty one. `ci-status-banner` forbade in
  its own comment at ~line 490 the very form it still used. `named-vault-resolver` had one
  `source: 'fallback'` covering two causes (honestly relativised by the agent:
  `resolveNamedVault` has zero production callers, so the defect was real but not yet reachable).
  `scope-baseline` hardcoded `'origin/main...HEAD'` twice, and the damage sat elsewhere than in
  the name: `wave-loop.md:874` renders `skipped: 'unresolvable-ref'` **silently**, so the
  scope-drift tripwire was permanently inert in every non-origin repo and never said so.
  `vault-mirror/namespace` `deriveRepo()` degraded silently to `basename(cwd)` and wrote that as
  repo **identity** into the vault — which then happened for real this session. And
  `harness-audit` category 6 was an active fail-open: outside a git repository it awarded **2/2
  with "no github mirror remote configured — skipped"**; a non-measurable query now fails 0/2,
  full marks only for genuine absence. Census correction: **14** executable origin hardcodings,
  not the 4 the issue named.
- **The scope guard shipped the instructions for its own disarmament (#1057).** The
  stale-manifest branch ended on ``remove it with `rm -f <path>` and let the coordinator write a
  fresh manifest`` — and that string travels through `emitDeny()` in `permissionDecisionReason`,
  i.e. into the context of the very agent whose edit was just denied. Reproduced end to end from
  the coordinator: `rm -f` is **allowed** by `pre-bash-destructive-guard.mjs` (14 rules, only
  `rm-rf-destructive` bites); with the manifest removed, an in-project `Edit` is ALLOW **and** a
  `Write` to `/etc/` is ALLOW — not only gate 7 (allowedPaths) falls but gate 6 (containment),
  because under the exit-0 protocol the hook then emits no decision at all and exit 0 is not a
  veto. The guard was off for the rest of the session. Not theoretical: a **parallel** session in
  the same working copy received this suggestion for the **live** `wave-scope.json` of this one,
  and declined only on an indicator (mtime 7 minutes old), not on knowledge — at one hour of age
  the same indicator points the other way. The new text names both cases instead of guessing one
  and routes to `blocked` per PSA-001/003/007. `systemMessage` was examined and deliberately not
  used: `io.mjs:329` says the operator sees that field, `hook-development/SKILL.md:108` says it
  is "shown to Claude" — a session-wide guard disarmament does not belong on a channel whose
  reader the repo cannot name consistently.
- **The owner-leakage scanner was blind to `.html` (#1076) — and its first sighted run found a
  real defect.** The naive fix would have excluded **nothing**, because the sanctioned form
  carries no `www.` while the site uses `www.` throughout; the result would have been a
  permanently red gate including pre-commit. Then the eighth planted defect: seven forms had been
  driven against the new coverage and all seven bit, but all seven sat in the CP1 corridor, and
  CP1 is the **only** one of the eleven rules that runs through `canonicalizeLine()` — revived,
  in other words, for 1 of 11. The eighth is an entity-encoded dot in the **domain**, which a
  browser resolves and the scanner did not. CP2/CP3/CP7/CP8 now additionally check the canonical
  form, and the discriminator is measured rather than guessed: dot-anchored rules gain
  (canonicalisation can never fabricate a dot from a separator) while slash- and slug-anchored
  rules stay raw (canon folds `-` onto `/`, which flips CP4/CP10 false→true and shreds CP6).
  `isAllowlisted()` on the canonical form would have been **wrong** — canon breaks its own
  allowlist — so the design uses an occurrence counter instead: if the canonical form carries
  more domain tokens than the raw one, the surplus came from a decoding and can never be the
  sanctioned publication. Cost repo-wide: 0 new findings.
- **Admitting `.html` to `check-unicode-safety.mjs` immediately found a live legal defect.**
  `site/datenschutz/index.html:277` carried a `U+00AD` at end of line. A soft hyphen does **not**
  suppress the break — HTML collapses newline plus indentation into a space — so the page
  rendered "Auftragsverarbeitungs vereinbarung", visibly broken, on a legally required page.
  Deleting the `U+00AD` alone would not have fixed it; the space would have remained.
- **Six findings from the review panel against the site, the most important a false promise
  (#1043/#1044/#1046).** The privacy policy described a network request that does not exist — a
  reload of `leaderboard.json` including `credentials: 'omit'` — while a grep for
  `fetch(|XMLHttpRequest|sendBeacon|WebSocket|EventSource|import(|new Image` across all four
  pages returns 0 hits. The direction is harmless (over- rather than under-declared); the place
  is not, because §10 expressly invites the reader to open the network tab and check. It was the
  single statement on the page a reader could falsify in two minutes — on a page whose thesis is
  checkability. Struck without replacement. The other five: a `TODO` placeholder shipped live in
  public source (`index.html:665`), a `/guide` canonical that contradicted itself three ways,
  missing og/twitter tags on all three subpages (a shared guide link rendered as a bare URL in
  Slack, X and Discord — for a product distributed through shared links, the one classically
  SEO-adjacent item with real return), `offers` struck from the JSON-LD, and an unquantified
  absolute ("Every issue an outside user has ever opened" → "All four issues opened by people
  outside it") two paragraphs above where the number already stood correctly.
- **A character allowlist in the census generator, and a `ReferenceError` on the path that
  enforces it.** `rewrite()` checked `/[<>]/` against the **old** cell content, never the new
  value. Eleven of the thirteen metrics are digits or hex by construction, but `version` is
  whatever `package.json` says and `readPackageVersion` only checks "non-empty string" — a
  prepared version literal could have closed the `<span>` and opened a tag. The precondition is
  write access to `package.json`, which in this repo's trust model already means full access, so
  this is defence in depth rather than an open hole — but it is the one place where `script-src
  'unsafe-inline'` would stop being theoretical. A rejection now refuses the **whole** file, not
  just the value, and the allowlist hangs on the write rather than on HTML-span presence (before,
  `"rules": "../../etc/passwd"` landed unchecked in the shipped file). Building it caught a defect
  of its own: the error path called a `writeStderrLine` that never existed in that file — a
  `ReferenceError` exactly where the guard bites. Error paths have to be executed, not read.
- **Accessibility: one real AA violation, and controls at 1.71:1.** `/guide` had no live region —
  the copy button only changed its visible text, so a screen-reader user got **nothing** on
  success and nothing on the error path, where "Press Ctrl+C" is the only way forward (WCAG
  4.1.3). Seven copy buttons shared one name and were indistinguishable in a screen reader's
  element list, the one place they are read outside their context; names are now derived from the
  nearest label in **document order** plus the first non-comment line, measured 7 of 7 unique
  against 1 of 7 before. `button.copy` and `a.btn-2` sat at 1.71:1, under the 3:1 a control
  boundary needs (WCAG 1.4.11); raising `--line-2` globally would have been a design change, since
  the same token draws decorative hairlines where 3:1 is loud, so a separate `--line-ui` token
  carries the purpose: measured 3.40:1 / 3.57:1 / 3.57:1 on the rendered element. Two comments
  that claimed too much were corrected — the print comment said "only the two moving 1px lines are
  dropped" while the rule below hides five selectors including the whole six-link navigation.
- **Page and receipt came from two different measurements.** `site/index.html` said
  252/135/`6fa214d`, `site/_census.json` said 253/140/`6f6bf58` — both tracked, both publicly
  shipped, `--check` reporting 3 drifted. The earlier instance had the same shape and a
  coordinator cause: a `--write` step run against a file still inside a live agent's file scope —
  the PSA-002 scope collision this repo built `--assert-disjoint` against, one level above the
  wave mechanics. The new coupling test compares the two **committed** artefacts against each
  other rather than against the repo, so "both stale together" is green and only a divergence is
  red; it asserts on drift **or** stale, because `counted-sha` is `provenance: true` and a
  drift-only assertion would have been green against exactly the state it forbids.
- **The mirror push had a fourth state, and it was fail-open.** Today's own fix distinguished
  three (no remote / push ok / push failed). Outside a git repository, `git remote get-url github`
  fails with `fatal: not a git repository` — by exit code indistinguishable from "no such remote" —
  so the block reported `no 'github' remote configured — skipping (not an error)` on stdout and
  exited **0**: a broken environment reported as a healthy one, inside the fix that was closing
  fail-open. Found by a reviewer explicitly briefed to **refute** the measurements; not by the
  author, not by the test, not by the gate. Fixed with a `git rev-parse --git-dir` guard, all four
  states executed individually in throwaway repos, `bash -n` **and** `sh -n` green (the bash-3.2
  trap from this repo's own rule file).
- **CI had been red for six consecutive pipelines (#1081).**
  `tests/scripts/site-numbers.test.mjs` calls `collect(REPO_ROOT)` and requires `missing === []`;
  two of the 13 metrics read `.orchestrator/metrics/*.jsonl`, untracked per `.gitignore:40`.
  Locally 23/23, on CI `23 tests | 23 skipped`, because a throwing top-level `beforeAll` takes the
  whole file with it in Vitest — the blast radius was 23, not 9. Measured now: #7276, #7277,
  #7280, #7281, #7286 and #7288 red, green again from #7334 (`glab ci list --per-page 20`).
- **The new pre-push gate produced three defects of its own before it worked**, each measured
  rather than reasoned. (1) `git clone <path>` points the clone's origin at a **filesystem path**
  while CI's checkout points at the real remote URL, so any code that *parses* that URL behaves
  differently in the two trees — the opposite of the hook's purpose: `deriveRepo()` returned
  `unknown-repo` where working tree and CI both return `session-orchestrator`, and
  `namespace.test.mjs:199` asserts exactly against that. The real origin URL is now carried into
  the clone. (2) A repo-pointing git environment beats both `-C <path>` and cwd, so
  `git -C "$tree" checkout --detach` ran against the **original** repository. (3)
  `GIT_CONFIG_PARAMETERS` propagates into every child once any `-c` was passed anywhere: pushing
  with `git -c core.hooksPath=.husky push` carried that override into the materialised clone,
  where every throwaway repo the gate's own test suite creates then fired the repository's
  **real** pre-commit hooks — `test: fail, total: 0` three times in a row, green the moment the
  `-c` was dropped. Both scrub sites are now namespace sweeps with a justified keep-set instead
  of name lists, because the denylist has shipped with a hole **twice in a row**, each time at
  the sibling form of the name just closed (`GIT_CONFIG_COUNT` without `GIT_CONFIG_PARAMETERS`,
  then the exact mirror image). The source-derived census found **six of nine** unscrubbed
  platform variables where the panel had reported three of four — including `PLUGIN_ROOT` (rung
  **one** of `resolvePluginRoot`, above every `CLAUDE_`/`CODEX_`/`PI_` name the old list already
  unset) and the Cursor plugin root, which is not `CURSOR_PLUGIN_ROOT` but `CURSOR_RULES_DIR`;
  adding the guessed name would have read in review as a complete fix and closed nothing.
  `GIT_EDITOR` and `GIT_EXEC_PATH` are kept by measurement, not hygiene: Claude Code sets
  `GIT_EDITOR=true`, and a blind sweep drops git back to `vi` and hangs the worker.
- **Two silent instrument errors (#979, #1062).** `scanBacklog` read 50 of 89 open issues and
  reported the window nowhere: `limit 50 => total 50, critical 0, high 10, stale 0` against
  `limit 100 => total 89, critical 1, high 18, stale 5`. All three numbers wrong, and the mode
  selector at Phase 7.5 computed with them — `staleCount 0` meant not one stale issue was visible
  to it. The cause was the window alone, not the `priority::`/`priority:` split. Separately,
  `memory_cleanup_at` hung on a prose instruction that failed once: `stampMemoryCleanup()` had
  zero production callers, every reference was text asking the coordinator LLM to remember. On
  2026-08-14 a `/memory-cleanup` ran with documented yield, the step was skipped, all three
  session records of that day carried `memory_cleanup_at: null`, and the banner reported "last
  cleanup 29 days ago" against the 3 days of its own notes — with **no** mechanical signal
  anywhere: zero `orchestrator.memory.cleanup*` events in 22,887. `/memory-cleanup` now emits the
  event and `emit-session.mjs` derives the stamp from it.
- **The host registry lost living sessions (#1047).** `hooks/on-stop.mjs` deleted the registry
  entry and refreshed the lock heartbeat 12 lines further down — same `if` block, at every turn
  end — while `heartbeat()` had had zero production callers since its birth commit. The fix is
  atomic (deletion out **and** heartbeat wired), because either half alone would have been worse
  than the bug: entries would survive but never age, drop out of `detectPeers` after 15 minutes
  and be swept as "stale" after 60. Deregistration moved to `hooks/on-session-end.mjs`. The
  platform census in the comment was wrong at first and corrected by review — pi does deregister,
  Cursor never registers and so cannot leak; Codex is the only affected platform.
- **`gh repo view -R` broke the GitHub CI banner for every external user of this plugin
  (#1022).** `gh repo view` takes the repository as a **positional** argument and has no
  `-R`/`--repo` flag at all, so the host-pinning added in #872 made `gh` exit 1 with
  `unknown shorthand flag: 'R'` — an error `checkCiStatus`'s outer catch swallowed to
  `null`, leaving the session-start Phase 4 banner silently dead on every GitHub repo. Fixed
  positionally, and the swallow narrowed: a CLI that is *present* but fails now reports
  `degraded` instead of `null`. The asymmetry across the three neighbouring call sites is
  real and must not be unified — `glab repo view` **does** take `-R`, and `gh api`/`glab
  api` take neither `-R` nor a positional, only `--hostname`.
- **`hooks/on-stop.mjs` printed a 10-frame stack trace at every turn end when `node_modules`
  was absent** (interrupted install, EPERM sandbox, half-synced plugin cache): a static
  `import { $ } from 'zx'` fails at module-load time, with no hint that `npm install` is the
  fix. `zx` is now imported lazily and the failure degrades to one rate-limited stderr line
  per 6h window, mirroring the missing-`node` degradation in `hooks/run-node.sh`.
  (GH Kanevry/session-orchestrator#63.)
- **A board row inherited one legacy entry's terminal status to every same-named repo,
  permanently (#871).** `_active-sessions.md` rows were keyed by `path.basename(repoRoot)`,
  so two repos with the same directory name under different parents were one row — and both
  are enumerable, since the dispatcher walks to depth 2. Rows are now keyed by a path-derived
  hash, case-folded on APFS/NTFS so `…/Some-Repo` and `…/some-repo` do not split back into
  the duplicate rows #719 had already fixed at the name layer. The key length carries a named
  ceiling and a revisit trigger, not an intention to revisit.
- **Two vault write channels published unmasked secrets (#974/#1025)** —
  `scripts/lib/vault-status/narrative-mirror.mjs` and `scripts/lib/vault-mirror/process.mjs`,
  both writing tracked, pushed artefacts. The masker's needle set is a function of the
  caller's env, which is not a defect but does mean two runs over the same records mask
  differently: a consumer comparing a written artefact against a fresh candidate must treat
  an already-redacted span as a **wildcard** (`matchesModuloRedaction`), or a later
  partially-populated run rewrites the raw value it had already redacted. The tempting fix —
  persist the needle set — is rejected: it puts a plaintext secrets file on disk to defend
  against secrets on disk. Masking runs **after** `extractNarrative`, never before, because
  masking the raw string first lets `[REDACTED]` land inside the structure and silently
  delete a whole table from the mirrored file.
- **`agents/*.md` frontmatter is parsed, not pattern-matched.** `check-agents.mjs` gained the
  same `js-yaml` `CORE_SCHEMA` rule, reported alone with no fall-through to the field rules —
  a field rule evaluated on an unparseable block is a guess. Its existing block-scalar ban
  stays: that rule forbids one particular *valid* YAML form, which is a different question
  from whether the block is YAML at all.
- **Deleted `scripts/lib/mission-status-schema.mjs`** — 4 exports, zero production callers,
  while three prose locations promised its application. Found by S3 above, on the day S3 was
  written.

### Notes

- **3.18.0 is missing from npm.** The tag `v3.18.0`, the GitHub release and the CHANGELOG entry
  all exist; the registry has never seen the version — `npm view session-orchestrator versions`
  returns `…, "3.16.0", "3.17.0", "3.19.0", "3.20.0"`. That gap is the incident #978 names and
  the reason `scripts/release.mjs --publish` now creates the annotated tag only **after** a
  registry-verified publish. Nothing in this line republishes it; the version number stays
  skipped rather than reused.
- **This session damaged its own repository, and the first recovery missed half the damage.** A
  coordinator diagnostic command exported `GIT_DIR` and ran the suite. In the real `.git`: HEAD
  detached, three fixture commits, plus a foreign remote and the fixture identity in
  `.git/config` — which then authored two commits. The first recovery checked HEAD, the index and
  all 1614 files and **overlooked `.git/config` entirely**, because `git status` does not show it;
  an agent measuring something else found it two hours later. History rewriting failed on both
  remotes' branch protection (GitLab "not allowed to force push", GitHub GH006), so authorship
  was corrected via `.mailmap` — which incidentally refutes part of this session's own #1079
  finding: `enforce_admins: false` does **not** make the protection bypassable for force-push.
  One correction to the record: the claim "git runs every hook with `GIT_DIR` set", written in
  `334ea2d`, is **false** — measured on git 2.x / macOS, `pre-push`, `pre-commit`, `commit-msg`
  and `post-commit` all run with `GIT_DIR` unset. The scrub remains right as defence in depth;
  its stated reason was not.
- The sharpest instances of the frontmatter class are the ones where the check was green. A
  hand-rolled `getDescription` regex in
  `tests/lib/validate/skill-description-quality.test.mjs` terminated on the first folded line,
  measuring **97 characters where the real YAML value is 329** (`session-start`; `autopilot`
  107 vs 555, `bootstrap` 98 vs 341) — which made its own `>= 250` assertion **vacuous** for
  every skill already using the block-scalar form. It surfaced only because repairing the 12
  unparseable blocks moved files across the threshold. `yaml.load` is now the single reader.
- **Found and deliberately not fixed here**, because both sit outside this line's scope and
  both are the same shape as the bugs above: `agents/eval-judge.md` is **valid** YAML and
  loses **96%** of its description — 1180 raw characters, 51 visible to a parser — because
  ` #803` opens a YAML comment at column 51, so no parse rule can catch it. And
  `skills/discovery/probes-arch.md:39` calls
  `npx madge --circular --extensions ts,tsx,js,jsx src/` in an all-`.mjs` repo that has no
  `src/` directory: it processes zero files and reports success.
- **Open, deliberately.** `site/llms-full.txt` still carries its numbers by hand — the generator
  only touches HTML. They were corrected and cross-checked against the generator, but that is
  exactly the defect this line fixes. Revisit trigger: as soon as a second text file carries
  measured values. And the `scanBacklog` fix has no test of its own; it is covered only by the
  171 existing tests of the touched modules staying green, with the fake regression outstanding.
- One reported number did not survive review and is recorded here rather than quietly dropped:
  "274 elements checked" for the site's contrast sweep is **not reproducible** — two independent
  checkers measure 260 / 282 / 288 depending on an inclusion rule that was never shipped with the
  number. The **value** 4.81:1 (worst text contrast) is stable across all three counting rules;
  the count is not. A count without its rule is worthless — the same lesson #979 taught once
  already in this line.
- Gate at `8984224`, re-run for this entry rather than quoted: **587 test files, 14,244 passed
  / 0 failed / 11 skipped** (`npm test`, exit 0), typecheck **404 files OK** (exit 0),
  owner-leakage **1548 files / 0 findings**, unicode-safety 0 findings. The release commit also
  reports `validate-plugin` 172/0 and lint 0 at that sha; both are quoted from it, not
  re-measured here.

## [3.20.0] - 2026-08-13

Memory-pipeline line. The learning store had been accumulating for 233 sessions and delivering
to nobody: 100 learnings, and not one read path into a dispatched agent. Closing that gap
surfaced two defects underneath it — agent-authored text reaching every agent's project
instructions unfiltered, and a prune path that deleted learnings into neither the store nor
the archive. The headline is not a feature; it is that the review panel then found the same
class **inside the fix**: this line hardened one delivery channel and shipped a second,
unhardened one alongside it.

### Added

- **Learnings reach wave agents (#1014)** — a per-agent index in the dispatch prompt, selected
  from the agent's own declared file scope. Two-tier with **split** caps (scope-matched, then
  top-scored fill), because only 17 of 100 learnings carry `file_paths` and a single shared cap
  lets the global tier crowd out the per-agent guarantee that is the acceptance criterion.
  Rides the channel the coordinator already composes — `docs/instruction-delivery.md` §5
  measured a *separate* injection path at **+72.3%**; this one costs **+0.69% to +1.15%** of a
  178,096-byte baseline. Factor 92. `LEARNINGS_INDEX_MAX_CHARS = 2000` is derived (1.12% of the
  measured baseline, 0.92× the median rule file), not chosen. Delivery is observable rather than
  assumed: `orchestrator.learnings.index.injected` carries entry count, scope-matched split and
  byte size, so "did the injector run?" is a grep instead of an inference.
- **Semantic dedup + contradiction detection (#1016)** — IDF-Dice top-K candidate pool
  (K=8, FLOOR=0.085), per-seed, bounded, deliberately **non-transitive**: even at K=3 the
  similarity graph collapses into a 99-of-100 giant component, so a clustering pass returns
  "the corpus". `type` neither filters nor boosts — both strongest ground-truth links are
  cross-type, and a type gate drops connectivity 6/6 → 4/6 while still retaining 26% of pairs.
  A German stoplist is mandatory, not cosmetic: without it the top-scoring pair in the whole
  corpus is two records sharing function words. The judgment layer is fail-closed and atomic
  (one malformed decision voids the batch, never the valid subset), and enforces structurally
  that **rendering an AUQ from an unreadable judgment is itself a write** — the AUQ renderer
  sits in the same effect map as the archive writer, behind one gate.
- **`scripts/print-learnings-index.mjs`**, **`scripts/backfill-learnings-from-vault.mjs`**,
  **`scripts/lib/validate/check-learning-provenance.mjs`**, and a `--prune` mode on
  `sweep-expired-learnings.mjs`. Five new leaf modules under `scripts/lib/learnings/`
  (`affinity`, `select`, `candidates`, `judgment`, `kebab`).

### Fixed

- **Agent-authored text is neutralised at the render point (#1015)** — the reconcile renderer
  interpolated it verbatim into `.claude/rules/<slug>.md`, a file Claude Code then delivers to
  every agent in every session as a project instruction, with no revocation: the frontmatter
  gates are not a containment boundary for the body. Machine values now **reject** (a repaired
  `learning-key` breaks idempotency; a dropped `globs[]` element can leave `globs: []`, which
  the loader excludes everywhere — silently dead beats loudly rejected); prose is framed and
  capped without a meaning filter, because a blocklist is the guard that looks green and does
  not bite. Two premises in the issue text did not survive verification and are corrected in
  the issue: `description` was already defended against newlines, and the frontmatter parser is
  hand-rolled — so the newline is the *only* escape and a YAML-shaped sanitiser would have been
  both wrong and destructive (14 `description` lines in the corpus carry a second colon).
- **The learning store is durable again (#1017)** — `/evolve` pruned by rewriting the store with
  **no archive append**; 11 of 13 provenance pointers in generated rules resolved to nothing, in
  neither store nor archive nor any backup. Both callers now share one `archiveThenRewrite()`
  (KEEP-probe, archive **before** rewrite so a crash leaves the record in both places and never
  in neither, `.bak` snapshot), and `pruneLearnings()` archives any record the caller's next
  generation omits — reconciled by `id`, or by content fingerprint when a record carries no
  usable one. **11 of 11 lost records were recovered** from the vault mirror with per-field
  origin labelling; dangling pointers **11 → 0**.
- **Rule-block delivery boundaries were unrecoverable without any adversarial input** — rule
  `content` is byte-identical to disk *including* its YAML fence, so joining rules on `---`
  produced 56 separator-shaped lines for 18 rules. Replaced with per-rule fence tags carrying a
  content-derived token and the source path (information an agent previously could not get at
  all). An existing test had pinned the broken contract.
- **`learning_key` was derived five ways** on a *stored* contract, with the writer kebabbing the
  `type` half and all four readers not. One shared derivation now; proven byte-identical across
  107 records, all 13 stamped keys reproduced.
- **Two tracked `.mjs` files carried NUL bytes** and were therefore skipped **silently** by every
  grep-based audit — exit 1, no output, which reads exactly like a clean result.

### Notes

- The review panel (security, qa, architect, independently) returned **FIX_REQUIRED** with four
  HIGH blockers; all four were closed in one fix cycle, which found three further holes of the
  same class. Every new guard is proven by fake regression, not by a green test.
- `CANDIDATE_FLOOR` is deliberately **unchanged** despite its justification no longer
  reproducing: this line's own backfill moved every IDF weight, and the wave-1 ground-truth
  labels were never persisted. A constant whose evidence has evaporated is not re-guessed from
  the evaporated evidence (#1021).
- Follow-ups: #1018, #1019, #1020, #1021 — each with a named revisit trigger.

## [3.19.0] - 2026-08-04

Guard-hardening and release-mechanics line. Headline: the destructive-command guard closed
six measured wrapper bypasses and inverted a silent redirect-truncation allow into a deny;
the session lock gained an ownership proof; and the release process itself became one
dispatch — whose very first preflight run surfaced that v3.18.0 had been tagged but never
published to npm (the registry still served 3.17.0), the exact incident class #978 names.

### Added

- **`scripts/release.mjs` — Release als ein Dispatch (#978, local half)** — one surfaces
  table drives both the mechanical version rewrite (`--set-version`: 12 version literals
  across 10 files, Codex cachebuster rotation, package-lock sync) and the preflight
  (`--check`: surface parity with pattern-dead-is-a-failure semantics, CHANGELOG entry +
  folded-[Unreleased] gate, tag collision on local/origin/github, npm-registry collision,
  a `git grep` drift sweep over ALL tracked files, CI-green-on-HEAD via
  `ci-status-banner.mjs`, the seven-pattern leakage gate over `npm pack --dry-run`).
  `--publish` runs the token flow from `skills/npm-publish/SKILL.md` and creates the
  annotated git tag only AFTER a registry-verified publish — eliminating the
  "tagged but unpublished" state v3.18.0 fell into. Its first run surfaced two real gaps:
  v3.18.0 was tagged but never published to npm (the registry served 3.17.0), and the
  hidden `.codex-plugin` manifest was invisible to a plain ripgrep census — hidden
  directories need `git grep`. The CI half of #978 (a `$CI_COMMIT_TAG` release stage)
  stays open.
- **`skills/contract-version-bump/`** — reusable, distributable skill for version-bumping machine-readable contracts (JSON Schema, API specs, config schemas): classifies the change against the contract's OWN versioning rule (not generic semver instinct), finds every version literal and vendored copy across this repo and `cross-repos:`, checks whether every known consumer evaluates each new/changed schema keyword, applies the bump consistently, writes a Keep-a-Changelog entry, and reports downstream drift. Codifies three traps from a real case (GitLab issue #17, `aiat-enablement` repo, 2026-07-25). Ships with `/contract-version-bump`.
- **Wrapper unwrapping in the destructive-command guard (#982)** — table-driven
  `WRAPPER_UNWRAP` (`sudo`/`doas`/`env` flags/`nohup`/`timeout`/`nice`/`stdbuf`,
  `sudo -i/-s` → sh, `su` as interpreter) plus depth-capped `-c`/`env -S` payload
  recursion; `rm` parsers rewritten segment-based on the shared splitter. Six measured
  bypasses closed, zero regressions (differential-proved).
- **Blocked-commands policy floor (#972)** — new `scripts/lib/blocked-commands-policy.mjs`
  with floor ∪ overlay semantics: escalate-only, whole-rule-wins on block collisions,
  fail-to-floor on empty/malformed overlays. Replaces first-hit-wins.
- **Redirect target denylist (#983)** — new redirect token class (longest-match
  `&>>`/`&>`/`>|`/`>>`/`>`/`N>`-forms) and rule 14 `redirect-truncate-protected`: the
  premise was inverted — `&> CLAUDE.md` was a silent ALLOW-with-truncation before.
- **Session-lock ownership proof (#987)** — owner proof persisted at lock genesis
  (`.orchestrator/runtime/lock-owner-proof.json`); session-end releases only on
  self-rotation and refuses foreign same-day collisions — closes the #926 residual.
- **session-orchestrator.com redesign** — terminal hero, wave pipeline, leaderboard shell,
  measured-numbers section (reconcile-rule count corrected 11 → 13 by the close-review).

### Fixed

- **Guard bypass residuals (three follow-up rounds)** — the denylist read repo-relative
  while an attacker wrote absolute paths; the guard evaluated the working tree instead of
  the rules corpus; a backslash split what bash glues together; and the review panel found
  two further holes the repair session had itself opened. Each round closed with
  fake-regression proofs.
- **Session identity (#926, #914)** — an ending session no longer deletes a living foreign
  session lock, and the abandoned-session backfiller no longer deletes the alarm it was
  built to raise.
- **Instrument deduplication** — nine instances of one fact wearing two faces consolidated
  to single sources; the panel found three more in the repair itself.
- **Reconcile expiry tests** — two tests measured the floor constant instead of
  `ruleExpiryDays`; they now measure the contract.

### Security

- **Two high-severity transitive vulnerabilities** unreachable by `npm audit fix` closed
  via `package.json` overrides (same class, found twice — the second instance surfaced
  after the first override landed).

## [3.18.0] - 2026-07-31

Panel-follow-ups & consolidation release. Headline: the review panel's own findings from the
prior deep session, fixed and re-verified — three self-silencing vectors closed in the
bash-write-verify scope detector, a credential that could no longer reach argv, and the
instrument deduplication the panel asked for — alongside an instruction-corpus diet (always-on
directive budget 471→~440) and a TV-003 consolidation tranche (net test count down, catch-power
up via a mutation sweep). The panel found real bugs in this session's own coordinator-written
code again — the sixth consecutive confirmation — and W5 fixed all four.

### Added

- **Instruction-delivery measurement + baseline ablation axis (#936)** — `evals/instruction-ablation/run.mjs` gains a `--rules-source repo|baseline` switch (host-local precedence, no hardcoded path) so one command ablates the fleet-wide `projects-baseline` corpus; the decision rule is pre-registered in `docs/instruction-delivery.md §6`, fail-safe toward KEEP because a wrong cut regresses fourteen repos.
- **hooks-symmetry Check 6 (#942)** — `check-hooks-symmetry.mjs` now compares handler SETS per event, not just event keys, so a hook wired on only one platform is no longer structurally invisible; `post-bash-write-verify` ported to Pi, Codex documented as an exception.
- **Mutation-testing sweep (#910)** — 8 modules, 46 mutants, 70% kill-rate; eight need-gated tests close the highest-value survivors (boundary comparisons + wiring/fail-safe), report at `docs/mutation-testing/`.
- **CPU 5-minute load axis (#943)** — `resource-probe` emits `cpu_load_5m`/`cpu_load_5m_pct`; the wave resource gate judges CPU on `min(1m, 5m)` so it no longer halves a wave on the decaying 1-minute average of its own just-finished quality run.
- **Two glob-scoped reconcile rules** — `a-green-quality-gate-is-not-CI-evidence` and `a-file-wide-toContain-judges-one-block` (both conditional, zero always-on ceiling cost); the bash-3.2 `case`-in-`$()` gotcha folded into `bash-harness-pitfalls.md`.
- **CI advisory job `test-value-bans`** — non-blocking, runs the narrowed ban scanner; `.husky/pre-push` gate + schema-drift marker guard now have tests (#940).

### Fixed

- **#907 credential leak (CWE-214)** — remote-URL credentials are stripped at the single source (`resolveRawRemoteUrl`) before reaching any `-R`/`--repo`/`--hostname` argv position or verbose log. The W4 panel then found the first fix incomplete for a raw `@` inside the token (`user:gl@token@host`); the userinfo class now binds to the LAST `@` before the authority ends, closing the residual. A bare `git@host` SSH login is left untouched.
- **#938 bash-write-verify self-silencing (3 vectors)** — the control file left the hook's own ignore list; the gate is read from the recorded snapshot, not the live file; a re-baseline reports out-of-scope dirt via mtime. W4-panel follow-through: the `isMain` guard now realpath's both sides (a symlinked plugin install no longer silently disables the hook), and the mtime re-baseline uses `>=` with the scope file excluded (equal-mtime false-negative closed).
- **#914 fabricated completion timestamp** — the abandoned-session backfill no longer stamps the backfill-run wall-clock as `completed_at` (which produced ~64h of phantom runtime on real records); it uses the last events-attested timestamp with a `_completed_at_estimated` flag.
- **#939 orphan telemetry records** — the `subagents.jsonl` orphan-stop class (harness-side phantom SubagentStops) is documented on BOTH the lifetime (51%) and running-window (~93%) numbers, with a `start_record_found` write-time discriminator.
- **#911 check-test-value-bans over-reporting + ReDoS** — B1 narrowed from 352 findings to 9 (dynamic-derived counts only), the length regex hardened from >2min to 0ms on pathological input, and B4 extended with a scope-gated `systemMessage` key.
- **#886 eight instruction contradictions (C-1…C-8)** resolved against the code; the C-3 divergence (code-implementer test-writing ban vs the wave-executor's need-gated briefing) reconciled to the need-gated model on both surfaces.

### Changed

- **Instruction-corpus diet** — always-on directive budget 471→~440/480: `loop-and-monitor.md` pointer-ised into ADR-0010 (71→37 directives, body halved) with all eight delta-sync footers preserved (#885); `lsp.md`/`owner-persona.md`/`quality-gates-autofix.md` trimmed to pointers (#884).
- **TV-003 consolidation** — 51 prose-presence pin tests removed and one implementation-mirror deleted (#912-T1); `config.test.mjs` split into seven feature-domain files (151 tests preserved); tests:src 1.7135→1.69.
- **Chores** — test badge floor `12,000+`, `.vault.yaml` techStack `bash`→`nodejs`, CHANGELOG archive split into `docs/changelog/`.

## [3.17.0] - 2026-07-21

Telemetry & curation release. Headline: **opt-in anonymous usage telemetry** (#841) — a
consent-gated client with anonymous IDs, a local queue, and a record_kind-generic ingest
path, off by default — alongside the **vault-curation probe trio** (#831 B2/B4/B5), the
completed abandoned-session sweep, and a review-panel/gates hardening pass. The npm
distribution line is complete: this release ships the `npm-publish` token-runbook skill
distilled from the v3.16.0 first publish (#825) plus broadened registry metadata.

### Added

- **Opt-in anonymous usage telemetry (#841)** — `scripts/lib/telemetry/` (`anon-id`, `consent`, `queue`, `schema`, `sync`): consent-gated client with anonymous IDs and a local queue, record_kind-generic ingest, `flush()` with `ownerConfig` injection for hermetic tests. Off by default; PRD at `docs/prd/2026-07-20-anonymous-usage-telemetry.md`.
- **Vault-curation probes B2/B4/B5 (#831)** — `moc-staleness` (frontmatter-`updated:` MOC banner), `context-coverage` (registered projects missing `context.md`/`_passive.md`), `worktree-orphans` (candidates-only sweep — never deletes, PSA-003) — all opt-in via Session Config; abandoned-session sweep completed.
- **`skills/npm-publish/`** — token-based publish runbook (granular-token requirements, temp-userconfig flow, leakage gates, failure-mode table) distilled from the v3.16.0 first publish (#825).

### Fixed

- **Review-panel findings** — base-branch injection in diff-range resolution, fail-open quality gates, torn-write `jq` invocation, inert analyzer wiring.
- **vault-sync register/mode + abandoned-session signal integrity** — register/mode vocabulary alignment, enumerate scan depth, signal-integrity guards on the abandoned-session sweep.
- **Telemetry `flush()`** — accepts `ownerConfig` injection so hermetic tests never read the host `owner.yaml`.

### Changed

- **npm discoverability** — broadened `keywords` + explicit `bugs` field in `package.json`.

## [3.16.0] - 2026-07-19

Hardening release. Headline: the **bold-key parser fix goes class-wide** — a shared
`matchBlockHeader` helper closes the `- **key:**` blind spot across all 34 Session-Config
block parsers (#830) — plus the **vault-namespace de-collapse** (`VAULT_CLEAR_SLUGS`
carve-out) that ends the shared `redacted-repo/` bucket without weakening the public-mirror
leakage scanner, a **TTL staleness pass** for the vault live-status board (#829), and a
**provenance-honest `/bootstrap --refresh-lock`** that finally makes the freshness banner's
recommendation actionable. Everything is additive and backward-compatible.

### Added

- **`scripts/lib/config/block-header.mjs` (#830)** — zero-import `matchBlockHeader(line, key)` + `hasBlockHeader(content, key)`: bold-bullet-tolerant block-header detection (`key:`, `- key:`, `**key:**`, `- **key:**`), adopted at 30 literal parser sites, both dynamic-key sites (fixing their latent unescaped-key bug), and the dispatcher-autonomy-capture presence guard. Inline-comment headers still reject (documented gotcha preserved); drift-check raw-parity holds by construction. Guarded by a helper contract matrix, a 6-parser bold-adoption regression suite, and a drift-check parity test.
- **`/bootstrap --refresh-lock` (Kanevry#57)** — new `scripts/lib/bootstrap-lock-refresh.mjs`: upserts only `refreshed-at` + `refreshed-plugin-version` (newline-sanitized, atomic tmp+rename), every original provenance line byte-identical. `checkBootstrapLockFreshness` gains refresh-aware precedence tiers and a `details.reason` enum on every return path; session-start/discovery banners now name the RIGHT remediation per reason instead of the no-op `--retroactive` for valid locks.
- **`tests/lib/vault-status/hostpaths-guard.test.mjs` (#829)** — meta-test pinning the #783 hermetic-`hostPaths` hardening: any vault-status test calling `mirrorBoard`/`sweepBoard`/`mirrorNarrative` without explicit `hostPaths` fails the suite.

### Fixed

- **Vault-namespace de-collapse (Kanevry#59)** — `VAULT_CLEAR_SLUGS` carve-out in `check-owner-leakage.mjs`: the 5 already-public slugs regain distinct per-repo vault namespaces (provenance restored, no more mixed `redacted-repo/` bucket re-pollution), while `runScan`'s tracked-file CP6 scanner keeps blocking all 7 private slugs in the public mirror. A subset-invariant test guards against dead carve-out entries.
- **Vault live-status board staleness (#829)** — `mirrorBoard` re-derives PRESERVED in-progress rows against `DEFAULT_TTL_HOURS` (exact `>=` boundary pinned by tests, fail-open on unparseable heartbeats, idempotent): dead leases and historical fixture rows now self-heal to `force-closed` instead of rendering in-progress forever. `narrative-mirror` loose-matches candidate slugs against existing `01-projects/` folders (exactly-one-match guard) so `GotzendorferV2`-style names stop minting duplicate folders.

### Changed

- **`.claude/rules/loop-and-monitor.md`** — re-verified against Claude Code v2.1.215 (zero functional delta for the /loop family).

## [3.15.0] - 2026-07-19

Feature release. Headline: an honest, deterministic-first **session-process evaluation**
(`/eval`, standard `aiat-llm-eval/1.0`, Epic #803) lands, `owner.yaml` gains **per-context
baseline paths** (#819), a new **`bash-harness-pitfalls`** path-scoped rule (#818) codifies
four false-green shell failure classes, and the **distribution foundation** ships
(`session-orchestrator.com` landing page + Vercel, plus a fetch-verified channel-research +
submission kit). An opt-in out-of-scope shell-write guard and a parser/config hardening
bundle round it out. Everything is additive and backward-compatible.

### Added

- **Session-process evaluation — `/eval`, standard v1 (#803).** A deterministic-first scoring of the last completed orchestrator session against the pre-registered `rubric-v1` dimensions (`aiat-llm-eval/1.0`), with an optional advisory LLM judge (`eval.judge: off|haiku|sonnet`, uncalibrated) and a rebuildable HTML run-report under `.orchestrator/eval/reports/` (gitignored). Wired as session-end Phase 3.7d (advisory — never blocks close). Ships the `/eval` skill, the `eval-judge` agent, and the deterministic `session-eval` engine.
- **Opt-in out-of-scope shell-write guard (#684-adjacent).** A warn-only, non-blocking PostToolUse hook flags Bash writes that land outside a wave agent's declared file-scope, complementing the Edit/Write scope gate. Off by default.
- **Per-context baseline paths — `baselines:` array in `owner.yaml` (#819).** Resolve a different `baseline-path` per repo/context instead of a single host-wide default. Host-local, never committed; lenient consumers drop invalid entries at point-of-use.
- **`bash-harness-pitfalls` path-scoped rule (#818).** Four anonymized false-green failure classes — `grep -c || echo 0` double-print, stdout-capture pollution in value-returning bash functions, aggregate-verdict-from-file discipline, and `perl -pi` multi-line script surgery — codified as a review checklist.
- **Distribution foundation.** `session-orchestrator.com` landing page + Vercel deploy setup, plus a fetch-verified channel-research + submission kit (official-marketplace SHA-pin refresh, npm publish prep, per-harness install matrix).

### Fixed

- **Parser & config hardening bundle (#823, #828, #820, #822).** Bold-key parse fix in `parseSessionConfig` (the `- **key:**` discriminator; class follow-up filed as #830), a `promoteAndClear` drain guard closing an argument-typo silent-drain, `owner.yaml` optional-section tolerance + an `owner-config-banner` surface (#820), and an eval newest-wins record selector (#822).
- **`wave-scope-commit-guard` precedence + destructive-guard event emission.** The commit guard now reads `findScopeFile` precedence correctly and the destructive-guard emits its event on block.
- **Distribution review findings.** `files[]` npm-whitelist completeness (`.mcp.json` inclusion), canonical catalog descriptions, and submission-kit link targets corrected.

### Changed

- **CI resharded 4 → 2 shards (#702).** `min-tests 3000`, inner cap `600s`, outer `10m` — rebalanced so no shard crowds its inner timeout under the Hetzner autoscaler.
- **`js-yaml` moved from `devDependencies` to `dependencies`.** Runtime `owner.yaml`/config parsing needs it at install time, not only in dev.

### Testing

- **`state-md-lock` cross-process flake fixed (#813).** Test-local acquire timeout + speaking assertions.
- **Dialectic parser backfill + guard-event E2E chain (#816).** Dead `blockCommand` call removed.

### Documentation

- **`/loop` & monitor delta-syncs (v2.1.211 → v2.1.212).** ultracode-keyword human-origin scope, Routines per-account cap, and a zero-delta re-verify at v2.1.212.
- **Steering hook-count sync (#814)** — 15 matcher / 20 wired (21 on-disk) + the full phase chain.
- **Dead-PID stale-marking documented as an explicit no-go (#799).**
- **aiat-llm-eval PRD** authored, linked to Epic #803, and archived to the Meta-Vault on close.

## [3.14.0] - 2026-07-13

Hardening release. Headline: the STATE.md yaml-parser becomes a true parse/serialize
inverse and the frontmatter-safe write guard switches on (#747), wave dispatch gains a
pre-dispatch scope-union subset assertion (#796), and three sharp-edge fixes land —
peer-discovery self-exclusion (#798), a `paths:` rule-frontmatter alias (#795), and a
memory-proposals write guard with archive-before-clear (#797). `/loop` cadence guidance
is re-derived for the 1-hour prompt-cache TTL. Everything is additive and
backward-compatible.

### Added

- **Pre-dispatch scope-union subset assertion (#796).** `assertFileScopeSubset()` (pure, hook-safe, in `scope-gate.mjs`) verifies every wave agent's declared file-scope is a subset of the wave's scope-union before fan-out; `validate-wave-scope --assert-subset` exits 1 with the missing-list on violation (exit 2 on I/O errors). The wave-loop gains the pre-dispatch assertion plus a re-union rule for fix-pass/re-dispatch batches; the F1 incident is pinned as a fake-regression proof and the glob-vs-glob comparison limitation is documented + test-pinned. The Bash-heredoc bypass (sub-problem 2) is split out as follow-up #800.

### Fixed

- **STATE.md yaml-parser is now a parse/serialize inverse — and the frontmatter-safe write guard is active (#747).** `parseScalar` JSON-unescapes the double-quoted branch (try/catch fallback, never-throw contract kept) and `serializeScalar` force-quotes coercible strings (bare only when `parseScalar(s) === s`) — killing the silent type-flip class. With the asymmetry closed, `evaluateFrontmatterSafe` now runs inside `writeStateMd` (frontmatter-block-scoped, refuse+WARN, `opts.throwOnFrontmatterUnsafe` opt-in throw) — catching future serializer/parser drift at the write choke-point instead of the symptom layer. Round-trip convergence is pinned as a regression test; a gitleaks FP-allowlist entry covers the guard's option identifier (third precedent in the JS-member-access FP class).

- **findPeers self-exclusion for `source=discovered` peers (#798).** Both discovery surfaces now filter the caller's own registry entry (`s.sessionId !== mySessionId`, mirroring the detectPeers/on-session-start prior art) — a session no longer reports itself as a parallel peer. JSDoc pins the UUID-space contract for `mySessionId`; dead-PID stale-marking is deliberately excluded (Epic #583 D2-regression risk) and tracked as follow-up #799.

- **rule-loader accepts `paths:` as frontmatter alias for `globs:` (#795).** `parseGlobsFrontmatter` generalizes block/inline sequence parsing via `activeSeqKey`; `globs:` wins silently when both keys are present. The instruction-budget guard is transitively fixed (it delegates to `loadApplicableRules`); a budget-layer regression test pins that a `paths:`-scoped rule is excluded from the always-on directive count.

- **memory-proposals `writeApproved` arg-typo guard + archive-before-clear (#797).** `writeApproved` throws `TypeError` on unknown-key/non-array shapes (the `proposals:` vs `approved:` typo class previously wrote nothing, silently); `clearProposalsJsonl` archives pre-clear content to `.orchestrator/runtime/proposals-archive.jsonl` for best-effort recovery; session-end Phase 3.6.3 enforces the ordering invariant — write before clear, clear only when `written === approved.length`.

### Testing

- **stale-mr-sweep mixed-envelope hardening (#749).** The mixed `ok:true/false` JSON envelope of the stale-MR sweep is now pinned by tests across the CLI paths.

### Documentation

- **`/loop` cadence re-derived for the 1-hour prompt-cache TTL + v2.1.207 delta-sync (#764).** LM-003 cadence selection is now observation-rate-primary: on a Claude subscription the main conversation gets the 1-hour prompt-cache TTL automatically, so there is no cache cliff anywhere in the [60s, 3600s] wakeup range — the classic 300s trap is scoped to 5-minute-TTL configs (usage overage, API-key/Bedrock/Vertex/Foundry auth). v2.1.207 itself is zero functional delta for the `/loop` family; the full re-verify fixed three pre-existing doc gaps (Skill deny-rule in the dispatch gate, Workflows monorepo save-location nuance, usage-view version-gate precision). `.claude/loop.md` gains host-detection instead of a hardcoded flag plus the `ScheduleWakeup stop:true` note; monitor-patterns Pattern-4 rationale reworded.

## [3.13.0] - 2026-07-10

Fleet-patterns & PM-toolkit release. Headline: eleven fleet-validated orchestration
patterns from the July fleet-mining wave land as first-class mechanisms (contract-lock
serialization, issue premise verification, over-delivery sizing signal, broken-window
budget, path-cousin guard), the PM-skills adoption completes across `/grill`,
`/brainstorm`, `/plan`, and `/discovery` (Epic #750), and a host-local
confidential-names guard (CP11) keeps customer/repo names out of the public mirror —
enforced at the scanner's print choke-point, never committed. Everything is additive and
backward-compatible; new config keys are opt-in or fail-open.

### Added

- **Fleet patterns: contract-lock, premise-check, over-delivery signal, path-cousin guard (Epic #730).** Wave dispatch serializes on a contract-lock before fan-out (wave-executor + session-plan Step 6); session-start Phase 7.1 verifies each candidate issue's core state-claims against the code (SHIPPED / GAP / FALSE-PREMISE) before scope alignment; planned-vs-actual file counts persist as an `over_delivery_ratio` sizing signal; a pre-dispatch path-cousin guard plus a code-implementer grep-before-create backstop prevent near-duplicate file creation.
- **Broken-window budget (#730).** Session-end Phase 2.6 aggregates knowingly-broken shipments (stubs, overridden findings, unresolved MED/LOW) and files hard-due-date closure issues (`glab` native `--due-date`, `gh` body-line fallback, task-hash idempotent, fail-open). Override events at three existing decision points feed a new `effectiveness.override_ratio` metric. New opt-in `broken-window-budget` Session Config block (default off).
- **CP11 Confidential-Names-Guard (#728).** `check-owner-leakage.mjs` scans tracked files against a host-local, never-committed name list (`owner.yaml` `paths.confidential-names-file` / `SO_CONFIDENTIAL_NAMES_FILE`; loader: `scripts/lib/validate/confidential-names.mjs`). Redaction is enforced at the print choke-point via order-independent span-merge (`redactSpans`) — a confidential name never reaches the public CI log, regardless of which CP rule fired or how names overlap. Inert when unconfigured or when helper modules are absent (standalone single-file vendoring keeps working). Plus repo-audit Category-6 token-grep row + SEC-021 rule.
- **rules/ library activated (#743).** The 6 dead exemplar rules (backend, backend-data, frontend, swift, security-web, prompt-caching) moved verbatim from `.claude/rules/` into `rules/opt-in-stack/` + `rules/opt-in-domain/` with `[archetypes:]` tags and provenance headers — the opt-in buckets are no longer empty. See-Also refs and consumer citations repointed.
- **PM-cribs S4–S6 (Epic #750 complete).** `/grill` gains kill-assumption operationalization (Fails-if / Evidence-this-week / Kill-criterion / Cheapest-test), steelman-then-attack, and a new pre-mortem Tactic 6 with Tiger/Paper-Tiger/Elephant taxonomy plus a VUVF coverage sweep (#754); `/brainstorm` gains three-lens GENERATE + Mom-Test grounding (#755); `/plan` gains Opportunity Score, 2×2 effort/impact triage, and job-story PRD format incl. reviewer/template parity (#756).
- **Discovery probes: feature-request-cluster + ssot-code-diff.** Feature-request clustering with Opportunity-Solution-Tree framing and an explicit evidence-vs-judgment fork (#757–#759); a conservative 4-entry claim registry diffs doc count-claims against live code/FS values — docs are never canonical, code is (#730).
- **persona-panel: bundled 3-lens preset + grounding re-derive.** PM/Designer/Engineer preset ships in the catalog (#760); opt-in `--grounding` re-derive mode has personas independently re-derive supporting sources with an advisory grounding-diff in the consolidator — never gating the verdict (#730, #793).
- **`/spinout` skill (#730).** Guided 5-phase venture-spinout / sanitized-fork runbook: target + sphere AUQ, confidentiality HARD-GATE, copy + fresh-init, SNAPSHOT-FREEZE marker in the source repo, remotes + registration.
- **Handover-gate telemetry (#773).** New `orchestrator.handover.gated` event covering all four gate paths (incl. fail-open and continue-working); carryover counting re-anchored to the Phase-1.65 gate carry-list, fixing a blind spot; additive `open_questions_asked/answered/deferred` session metrics.
- **plans-archive custom phase (#786).** `archive-closed-plans` reuses the generalized `archive-closed-prds.mjs` CLI (`--prd-dir docs/plans`) to move closed features' plan artifacts to the operator vault — zero new production code, same fail-closed epic-state check.
- **Bootstrap hard-rules fast-tier scaffold (#730).** The canonical hard-rules table is wired into both fast-tier CLAUDE.md generation paths; What-Not-To-Retry entries now require a concrete file citation as evidence.
- **First `/reconcile` dogfood rules.** The learning→rule pipeline produced its first two auto-generated, operator-approved rules from 0.9-confidence learnings — glob-scoped and TTL-bounded, never always-on.
- **lock-reconcile DI seam (#748).** Session-end lock reconciliation extracted from `hooks/on-session-end.mjs` into `hooks/_lib/lock-reconcile.mjs` (mirrors `lock-bootstrap.mjs`), making the best-effort swallow contract and `reconcile_attempted` payload in-process testable.
- **/evolve over-delivery aggregation (#794.7).** The effective-sizing analyzer now specifies a median `over_delivery_ratio` aggregation (Discovery/Finalization excluded), activating session-plan Step 0.5's learnings-first read path.

### Fixed

- **enforce-scope allowlist-first for absolute out-of-repo grants (#792).** Gate 6 (project-root containment) structurally shadowed Gate 7 — an explicit absolute out-of-repo `allowedPaths` entry could never match. A new pre-gate honours only `path.isAbsolute` entries against the realpath-resolved candidate; relative entries can never escape the repo and traversal stays blocked.
- **auto-dream stale-index refuse-guard (#788).** `applyPendingDream` no longer clobbers interim MEMORY.md edits: `mtime(MEMORY.md) > sidecar generated_at` → refuse with `{applied:false, reason:'stale-index'}` (APFS sub-ms mtime resolution handled via `Math.floor`).
- **vault containment guard (#793).** `archiveFileToVault` refuses any target outside the vault root — throws before write and before the dry-run report; v2 session renderer carryover fallback aligned to `n/a`; persona `version` documented and converged as string.
- **broken-window due-days MAX-guard (#794).** `_parseBrokenWindow` rejects values > 3650 with a WARN instead of letting `computeDueDate` overflow into a RangeError; W4 review-leftover test gaps closed (enforce-scope corrupt-JSON fail-closed, persona TypeError branch, grounding-diff backslash, ssot-code-diff EISDIR write-failure).
- **loop-and-monitor LM-003 fact-fix.** On Bedrock/Vertex/Foundry a truly bare `/loop` (no prompt) prints the usage message and does not run at all — only prompt-only `/loop <prompt>` falls back to the fixed 10-minute schedule. Delta-sync footer v2.1.205→v2.1.206 (zero functional delta); monitor-patterns gains Pattern 6 (WebSocket source incl. silent >1 MiB termination warning).
- **vault-mirror #740 (documented wontfix).** Learning coverage after a store recovery is restored by the next mirror run; the narrative-chars gate is sessions-only and never filters learnings — now documented and pinned by a regression test.

### Documentation

- **Setup docs + CONTRIBUTING realigned (#789–#791).** USER-GUIDE, codex-setup, and cursor-setup gain the missing `npm install` first-run step and aligned Session Config examples; CONTRIBUTING reframed multi-harness with live component-count links; a 24-test drift guard runs every fenced config example through the real parser plus in-process schema validation (the CLI exit code is not a schema gate under `enforcement:warn`).
- **gitlab-ops hardening notes (#746, #763).** 403 `relates_to` fallback for non-Premium GitLab instances, close-keyword negation footgun, and the shared-file single-writer defer rule in state-ownership.
- **loop-and-monitor delta-sync v2.1.197 → v2.1.206 (#764).** ScheduleWakeup `stop:true` + 20-min fallback, Workflows OTel attributes + Large-workflow advisory warning, workflow-size setting and `--effort ultracode` on-ramps, Channels org-gate, `/background` loop-carryover.
- **pm-skills marketplace companion (#761).** Install-alongside guidance for the external PM-skills marketplace instead of bulk-vendoring 68 skills into the roster.
- **Fleet rule kleinteile (#730).** Corpus-freeze marker convention, guard/threshold category-separation principle, Unfaithful-Double test anti-pattern #8, file-disjoint `isolation:none` invariant; `.git-blame-ignore-revs` seeded with the two alias-rollout sweeps.

### Testing

- +40 targeted tests across 23 files: stale-mr-sweep `main()` CLI paths (#749), reap/reconcile event payloads (#748), `section()` throw-on-miss (#737), CP11 E2E through the real pre-commit hook (incl. inert-degrade), choke-point/prefix-overlap redaction regression pins.
- +58 W4 fleet-pattern guards: broken-window `parseSessionConfig` integration, premise-check/contract-lock/path-cousin prose-wiring, GitHub dedup-hit branches, over-delivery cross-file prose contract; bundled persona presets now schema-validated; discovery scope-enum test derives from the SKILL.md SSOT marker (#762).

## [3.12.0] - 2026-07-09

Curation & handover release. Headline: the session handover becomes an **explicit, gated
loop** instead of an implicit convention — open questions survive the session boundary as a
first-class STATE.md channel, carryover filing runs through an operator-triaged alignment
gate, and wave dispatch turns fail-loud. The public `docs/` tree was curated down to
user-facing content (68 process records moved to the private Meta-Vault behind a green
sensitivity gate) with three permanent guards against re-bloat. Everything is additive and
backward-compatible; new config keys are opt-in or fail-open.

### Added

- **Handover-Alignment-Gate at session close (#769, #770).** Session-end no longer files carryover issues scattered across four phases: Phases 1.2–1.6 now *collect* candidates, a new Phase 1.65 routes them through a pure classifier (`autoCarry` = critical/high priority, spiral-failed, or no-origin-issue; the middle band goes to an operator triage AUQ), and Phase 5.3 files gate-authoritatively. Answered-question marking is atomic with filing — closing a silent-forget-on-abort race. Fail-open when disabled, headless, or under autopilot. New `handover-gate` Session Config key (`enabled`, `max-open-questions`).

- **`## Open Questions` — a cross-session STATE.md channel (#771, #772).** Wave agents can raise an optional `OPEN-QUESTIONS:` report line; the coordinator collects entries lock-guarded at inter-wave checkpoints (`readOpenQuestions`/`appendOpenQuestion`/`markOpenQuestionAnswered`, cloning the What-Not-To-Retry trio). Idle Reset preserves the section across sessions; session-start Phase 6.5.2 renders unanswered entries as a HISTORICAL-guarded forced-read and Phase 8 folds them into the alignment AUQ as explicit decision candidates. The gitlab-ops carryover template gains an `### Open Questions` section.

- **`docs/` public-split with three permanent guards (Epic #774).** 68 process records (retros, audits, session transcripts) moved from the public tree to the private Meta-Vault — gated by a sensitivity scan (0 CRITICAL/HIGH across all 69 content files; checker exit 0 over 1351 tracked files) and a scoped `vault-sync --mode hard` (0 errors). Living docs repaired, references converted, a `docs/README.md` router published. The guards: drift-check **Check 10 docs-parity** (components.md count-claims vs on-disk, template↔reference config-key parity, metrics-path liveness — #780), a **docs-staleness probe** for `/discovery` (config-gated mtime probe over living root docs with 1×/2×/3× severity escalation — #781), and an **epic-close archive routine** (`archive-closed-prds.mjs`, fail-closed dry-run-default CLI on the reusable `vault-archive.mjs` helper, wired as a repo-declared custom-phase — #782; dogfood: it archived its own epic's PRD at close).

- **/plan Phase 5.5 PRD-commit HARD-gate (#784).** A generated PRD must be committed to HEAD before any issue may reference it — an issue pointing at an uncommitted file is an unverifiable claim (the pattern had occurred three times). Verification via `git status --porcelain` + `git ls-files`; the epic backlink lands as a separate non-amend follow-up commit.

- **Fail-loud wave dispatch (#724).** The wave-executor's single-message fan-out mandate is replaced by a small-batch default (3–4 `Agent()` calls per message — fleet evidence showed large fan-outs drop calls silently), plus a Dispatch Verification step (planned-vs-started count, re-dispatch of missing agents) and an Edit-Persistence Verify step (git-diff evidence gates agent `STATUS: done`). `shouldSkipIncremental()` gains a `waveRole` param: the Quality wave hard-skips the incremental cache, making the full-gate invariant mechanical.

- **Session-end mechanical tail skip-plan + abort-gate diet (#724).** `planTailPhases()` computes a mechanical run/skip plan for the 3.6.3–3.6.8 tail from existing lib fast-paths (config-gate first, side-effect-free, fail-open); 333 lines of inline prose moved to progressive disclosure (SKILL.md 1149→858 lines). Abort-attractor gates convert to warn+carryover+continue so a close can no longer strand mid-phase on a soft failure (one gate — 2.5 custom-phases `mode: hard` — deliberately stays a block per operator contract).

- **PSA-007 — subagent git-write ban (#724).** Dispatched subagents must never run git write operations (`add`/`commit`/`stash`/`mv`/`rm`/`push`/`reset`) — the index and stash are shared session resources; fleet evidence recorded `index.lock` collisions and stash-lost sibling work. Ban line wired verbatim into all five repo-write agent definitions; the PSA decision tree gains a sibling-vs-external disambiguation branch; VBC gains a subagent-edit-persistence rule.

- **sessions.jsonl staleness banner (#724).** Session-start now compares the last ledger record against the newest pre-session `events.jsonl` activity (self-exclusion via the current lock's `started_at`) and warns >8h / alerts >24h — the close-through gap is visible at the start of every session instead of accumulating silently.

- **Loop-Engineering delta-sync (#764–#768).** `loop-and-monitor.md` re-synced against upstream (Routines research-preview rewrite with teach-it-don't-run-it posture, `/schedule` gating, `/goal` introspection + clear-aliases + trust-gate, Workflows `args`/usage-view/model-routing/resume); monitor-patterns updated to ten kill-switches; the loop-readiness banner now also detects `CLAUDE_CODE_DISABLE_CRON` and the 25,000-byte loop.md truncation limit (#767) — a present-but-disabled-or-truncated loop.md no longer reads as healthy.

- **/discovery `feature` scope (#750 S1–S3).** New scope token with a standalone-only Feature-Scope router (explicit-only activation, never bare `all`), an intent-drift probe (doc-line anchoring) + stubbed-dead-feature probe, and a wiring-guard test pinning all tracked surfaces.

- **VCS-hygiene slice (Epic F, #727).** repo-audit gains a github-mirror-sync check (warns on local commits missing from the mirror); new `stale-mr-sweep` CLI (open MRs/PRs >14d, pure core + injectable exec); gitlab-ops SKILL hardening (label PUT-replaces-set semantics, project-ID re-resolve, canonical pagination/dedupe enumeration pattern); bootstrap gains a best-effort GitHub-mirror remote step.

- **Rule-scoping & instruction-layer sanitation (Epic A, #722).** New `validate-vendored-rules` vendoring validator + rules-sync gate (catches `paths:`/`globs:` defects, dangling cites, foreign globs before they land in consumer repos), drift-check Check 9 rule-scoping, and a `claude-md-budget-lint` raw-file property lint wired into bootstrap.

- **Reconcile pipeline dogfood hardening (#741).** Rule `expires-at` is floored at `now + min-rule-days` (default 7 — no more born-dead rules excluded by the loader at approval time); a placeholder-insight gate rejects stub learnings before rule conversion (`min-insight-chars`, default 24); `memory-propose --dry-run` validates without writing (new `dry-run-ok` status).

- **`LEARNING_TYPE_REGISTRY` as single type-SSOT (#733, #723).** A frozen capability registry (16 types × `{ttlDays, agentProposable, ruleConvertible}`) from which `PROPOSAL_TYPES`/`CONVERT_TYPES` are derived — agents can now propose `convention`/`architecture-pattern`/`design-pattern` learnings, and formerly TTL-less convert types carry explicit TTLs.

### Fixed

- **Session-lock heartbeat-first liveness + lock reaper + STATE.md size guard (#744, #739, #745).** A dead ephemeral PID no longer vetoes a fresh heartbeat (`isLockLive` is the sole active gate) — ending the live-session-hijack incident class. A lock reaper sweeps orphaned registry claims (heartbeat-age, archive-move, TOCTOU re-read, fresh-preservation); `release()` gains an ownership-scoped post-delete verify, eliminating a race where a retry could delete a sibling session's re-acquired lock. STATE.md writes are size-guarded (256 KB + >5× ratio, refuse+WARN) against the balloon-incident class.

- **Hook commands routed through a node-resolver shim (GH-53).** All command-shaped hook configs invoked a bare `node`, which fails on every tool call when the harness's hook-exec PATH cannot resolve it (Homebrew on Apple Silicon, nvm/volta/asdf — hook shells never source `~/.zshrc`). `hooks/run-node.sh` resolves node via `SO_NODE_BIN` > PATH > well-known install dirs > nvm, and degrades to exit-0 with one rate-limited warning instead of per-tool-call spam.

- **Historical abandoned-session migration self-block (#731).** The migration CLI compared every historical candidate against the *current* live lock — always live during a CLI run (dry-run evidence: 137 total / 0 backfillable). An additive dead-by-age relaxation bypasses the guard only for candidates whose own last event is older than the lock TTL; post-fix: 103 backfillable. The SessionEnd hook never opts in.

- **vault-mirror session-note repo attribution leak (#732).** The raw `deriveRepo()` value no longer reaches the vault: `processSession()` threads the leak-guarded, pseudonym-mapped namespace into both the write path and the rendered frontmatter; all three generators emit `source-repo:` (learning-note convention), and legacy `repo:` notes self-heal on regeneration.

- **Hermetic host-config test isolation (#783).** `mirrorBoard`/`sweepBoard`/`mirrorNarrative` (and the config layer) forward an optional `hostPaths` DI seam, so tests inject a hermetic `owner.yaml` instead of reading the host's real one — closing an 18-test local-red/CI-green divergence.

- **Living-doc repairs + reference convention (#775, #778).** Five stale root docs + examples repaired; ~55 dead-path references across scripts/hooks/tests/skills/agents/rules/commands/ADRs converted to the citation convention; the `expandTilde` adopter bug in `common.mjs` fixed along the way (its committed default previously wrote a literal `./~` directory on hosts without an `owner.yaml`).

- **Reconcile default-loader normalization + vault reader-first close fixes.** The reconcile engine's default learnings loader now routes through dialect normalization; reader-first session fixes for the vault close path.

## [3.11.0] - 2026-07-03

Reliability release. Headline: the orchestrator now **closes its own loops mechanically**
instead of relying on prose discipline — a crashed session leaves no orphaned lock and no
missing ledger entry, the learning store backs itself up before every rewrite and sweeps
its own expired tail, and the CI mirror can no longer report a silent green. Everything is
additive and backward-compatible; autonomous behaviours stay opt-in and off by default.

### Added

- **Self-healing session ledger — SessionEnd abandoned-backfill + deterministic lock release (#724).** Close-through was 27% fleet-wide (this repo: 137 sessions started vs 36 recorded) because `sessions.jsonl` was only written by `/close` prose and `release()` was called by no hook. The `on-session-end` hook now (a) backfills a validated `status: abandoned` stub from `events.jsonl` via the UUID↔semantic lock bridge (dedupe-first ordering, wx-marker TOCTOU guard, synthetic sha256-derived ids) and (b) releases the session lock deterministically when ownership matches (UUID or semantic). Additive schema fields (`status`, `_backfill_source`, `_backfill_incomplete_fields`, `_session_type_inferred`, `_synthetic_session_id`); a one-time historical-migration CLI ships dry-run-default.

- **Orphaned `session.lock` reaper + persistence-health audit check (#724).** Locks from crashed sessions stayed forever (10 fleet orphans; one blocked this repo's own session-start). New `reapStaleLocks` (host-wide CLI, dry-run default) + `reapRepoLock` (own-repo, best-effort SessionStart splice) with hard invariants: never live, never cross-host, never own-host-pid-alive, archive-move before unlink, TOCTOU re-read before the destructive step. harness-audit category 4 gains an orphaned-session-lock check; a TTL-parity test guards the SSOT mirror against drift.

- **Learning-store backup-on-rewrite + `dryRun` seam (#721).** `rewriteLearnings` now snapshots `.bak-<ISO>` (keep 3) before every atomic rewrite and validates-without-writing via `{ dryRun: true }` — the safe probe path whose absence destroyed 107 entries in a live incident on 2026-07-02 (incident + contract documented in `learning-patterns.md`).

- **Learnings schema-dialect SSOT normalization + backfill CLI (#723).** `normalizeDialects` (read+write funnel) unifies the fleet dialects — `files`→`file_paths`, `session_id` dedupe, `last_seen`→`updated_at`, timestamp re-serialization, `schema_version` stamping. New `backfill-learnings` CLI (dry-run default, self-contained atomic write, byte-identical pass-through for parse errors). Live run: 74/108 entries normalized — the reconcile pipeline can finally read the corpus.

- **Mechanical learning expiry/archive sweep (#723).** Expired learnings stayed resident forever (fleet-wide; one vault corpus was 70% expired). `sweepExpiredLearnings` partitions on `expires_at` + 14-day grace, moves the tail append-only into `learnings-archive.jsonl`, and rewrites the store through the #721 backup path; a dry-run validation probe aborts with zero disk writes on any invalid KEEP record. CLI dry-run default; advisory session-end wiring.

- **Reconcile auto-nudge banner at session-start (#723).** The learning→rule pipeline existed but was never executed fleet-wide (`reconcile.enabled` defaults false = silent no-op; repos with 100+ learnings had 0 runs). Session-start now renders an advisory banner when reconcile would pay off: ≥20 active learnings with no run on record, delta >15 since the last run, or ≥3 rule-eligible learnings. Config-key-free, warn-only, fail-silent.

- **Host-wide vault-board staleness sweep at session-start (#716).** Every busy candidate repo's lease is re-derived at session-start, so a crashed session in *any* repo renders force-closed on the shared vault board from any other repo's session-start. Free repos are excluded (no board noise); prior rows are preserved via the idempotent merge; enumerate failure degrades to the pre-#716 single-repo write with a stderr WARN. Live-proven: 5 dead foreign leases rendered force-closed.

- **vault-mirror: readable slugs, source-repo attribution, host-local pseudonym mapping (#725).** v1 learning slugs pre-map whitespace to hyphens (previously ~32% of vault slugs collapsed into unreadable character runs); learning notes carry an optional source-repo frontmatter field (attribution without layout change); owner-leaky repo namespaces resolve through a host-local pseudonym map (`owner.yaml` `paths.namespace-map-path` / `SO_NAMESPACE_MAP`, never committed) instead of collapsing into one `redacted-repo/` dir — restoring per-repo write isolation for private repos while unmapped leaky segments still redact as before.

- **Autonomy-verdict learning analyzer for `/evolve` (#683).** A pure, no-throw, no-I/O analyzer combines autopilot-effectiveness and advisory skill-judge signals into one `autonomy-verdict` learning (TTL 90d, scope private) — Epic #673's final PRD item (FA-3). `ready` structurally requires >0 autopilot sessions and *correlated* per-record judge evidence (two correctness defects found in independent review and fixed before landing). Deliberately excluded from agent-writable memory-proposal types.

- **Package-manager recurrence-guard (#715).** New stdlib-only `scripts/check-package-manager.mjs`: working-tree drift checks (pnpm store in `node_modules`, symlinked vitest, stray root `pnpm-lock.yaml`) plus committed-tree invariants via `git ls-files`; warns locally, fail-closed in CI. Wired via explicit chaining in the `test:*` scripts, husky pre-commit, CI `before_script`, and a standalone validate job (this repo's `.npmrc` `ignore-scripts=true` makes npm lifecycle hooks dead, so wiring must be explicit). Root-cause hardening in the same line: gitignore-aware package-manager detection unified across all three detectors, and npm command defaults restored (#715 remains open for the node_modules-contamination root cause, which a code-level fix cannot prevent).

- **Tier-aware rule-loader + instruction-surface trims + discovery-validator default-on (#692, #688, #690).** `rule-loader.mjs` parses optional `tier:` frontmatter (`always` | `coordinator-only` | `wave-only`); wave agents now exclude the 4 coordinator-only rules (verified 11→7 always-on in waves). Five rules trimmed to pointers and the SEC-020 command-injection trust model folded into `security.md`; always-on directive budget 460→425. `discovery-validator.enabled` default flipped to `true` (log+warn-only, exit-0-always).

- **Vault namespacing coverage-lift — `repo:` backfill (#700, phase-2 follow-up).** The flat-corpus relocation engine (`scripts/relocate-vault-corpus.mjs`) gains an opt-in `--with-backfill` mode that infers the owning repo for the ~466 session notes that lack a `repo:` frontmatter field, lifting their (and their transitive learnings') namespace-derivability. The signal is **authoritative, not heuristic**: a session note's `id:` is joined against each sibling repo's own `.orchestrator/metrics/sessions.jsonl` `session_id` — an exact single-repo match is `HIGH` confidence; a unique `branch+date` match is `MEDIUM`; anything ambiguous (an id present in >1 repo, or a colliding branch+date) stays `_unsorted` (`SKIP`). A new pure module `scripts/lib/vault-repo-backfill.mjs` (`inferRepoForSession` / `buildBackfillIndex` / `isBackfillDerivable`) does the inference; every inferred slug still routes through the `resolveRepoNamespace()` CP1/CP6/CP10 leak-guard (private slugs → `redacted-repo` → never a confident move). `namespaceForSession(frontmatter, opts?)` gains an additive optional 2nd arg (backward-compatible — all existing 1-arg call-sites unaffected). New `--repos-root <dir>` flag (default: parent of `--vault-dir`) bounds the sibling-repo scan (`Archiv`/dot-dirs excluded; malformed jsonl tolerated). A pre-flight **intra-batch dest-uniqueness detector** surfaces same-basename collisions in dry-run that the runtime `existsSync` guard would otherwise only catch at apply-time. Measured on the live vault: confident moves rise **307 → 897** (backfill 232 + transitively-lifted learnings 531 + existing signals). Without `--with-backfill` the output is byte-identical to before. 38 new tests; full gate green.

- **Cross-repo learnings harvest into `.claude/rules` — #672.** Promoted ~41 stranded cross-repo DOMAIN learnings from the shared vault into path-scoped rule files (budget-neutral — always-on instruction count 425→427/480, headroom 53): `backend.md` (+3: MCP-tool SEC-009 mirror, webhook-recreate-rotates-signing-secret, SEC-009 raw-content→`details` field), `backend-data.md` (+14: PostgREST schema-cache SIGUSR1 reload / `PGRST204`, RLS-dead-code-under-`service_role` + verify-under-`authenticated`, `SECURITY DEFINER` `current_user` paradox, node-`pg` numeric→`z.coerce`, backdated-migration ordering, `migration repair` history-drift, non-recursive migration glob, `config.toml` seed paths, singleton-table PK pattern, mutating-dataset keyset, BAO immutable-retention, PostgREST 1000-row silent cap), `security-web.md` (+1: localhost single-process in-memory rate-limit carve-out), `development.md` (+2: whitespace-env `.trim()` truthy trap, dead-env / three-wirings audit), `frontend.md` (+5: `redirects()` identity-loop, `router.refresh` action-replay bounce, `router.push`-in-`startTransition` hang, shared-component `data-testid` forwarding, react-compiler `eslint-disable` block), `testing.md` (+18: Vitest mocking gotchas — `clearAllMocks` / `vi.hoisted` / ESM-`spyOn` / `new`-class mocks / centralized-env `resetModules`, Playwright `isVisible`-ignores-timeout / comma text-selector / `testIgnore` cascade / `.first()` hydration discriminator, `--project` & `bail:N` CI traps, `Promise.race` `timer.unref`, busy-wait forks-pool orphan, fake-regression-for-negative-tests, security-test-must-not-encode-vuln). BE-012 family + RLS confirmed **already-covered**; ~10 ops/runbook/repo-specific learnings **archived** with rationale. Baseline-sync issue filed for the cross-repo subset. `validate-plugin` 122/0; full See-Also integrity (0 dangling); markdown fences balanced.

- **Forward-looking planning note: token-frugal MCP-only driver path — #708.** Documents in `skills/mcp-builder/SKILL.md` (Phase 1, after the Tool-Hosting decision tree) the canonical token-sparing path for a *future* MCP-only capability: `mcporter generate-cli <server> --bundle` mints a schema-baked standalone CLI dispatched via Bash and parsed from disk (same pattern as `playwright-driver`/`peekaboo-driver`), with `mcporter call <server>.<tool>` as the one-shot skill-per-MCP alternative (a concrete impl of the projects-baseline **MCP-002** discipline, framed as a cross-repo reference — not a local mandate). Makes the MCPJungle-vs-mcporter tension explicit — baseline MCP *aggregation* is MCPJungle (machine-level gateway, not wired here), so mcporter is an *alternative* recipe for MCP-only drivers, not a drop-in, and stays optional (SEC-020; `skills/repo-audit/SKILL.md` Cat 9 already uses it with graceful-degrade). Doc-only (+9 lines, no code/dependency/new-skill); the `mcporter` contract (`generate-cli --bundle`, `call <server>.<tool>`) was verified live against 0.7.3 in Discovery (`--help` + `flags.js`), not assumed from the ticket.

### Changed

- **sessions.jsonl `schema_version` bumped to 2 (#372).** Writers stamp v2 automatically via the shared constant; the validator has accepted `[0..3]` since #576, so the bump is drift-free across the fleet. Gate evidence: 135/135 production entries validate through the full read path.

- **Coordinator-critical skills inherit the session model.** `session-plan`, `wave-executor`, `plan`, and `architecture` pinned `model: opus` in their frontmatter — a quality *floor* when opus was the top tier that inverted into a *ceiling* once the operator's session model can sit above it (a top-tier session silently dropped to opus for planning and all wave execution). `model: inherit` lets the session-model choice win; per-subagent pins (cost tiering) are intentionally unchanged, and a routing test ratchets against fixed pins creeping back in.

- **session-lock + hardening module split (barrel-preserving).** `session-lock.mjs` (1225 LOC) and `hardening.mjs` (691 LOC) split into focused modules (`scripts/lib/locks/{state-md-lock,staging-fence-lock,lock-body}.mjs`; env-check / scope-gate / command-blocker); the originals remain re-export barrels so all 46 importers resolve unchanged (0 churn; command-blocker tokenizer moved byte-for-byte, security-reviewer-verified no blocked-command bypass). `io.mjs` gains shared `readJsonlLines`/`readJsonlFile` helpers.

- **README positioning + onboarding polish (#705, #706).** Install paths reframed by mechanism (plugin marketplace vs git clone — explicitly *not* a tier split), per-platform hook-enforcement caveats at both claim sites, one vendor-neutral non-affiliation disclaimer instead of three, and the 10,000+ test badge grounded in a reproducible in-repo methodology section (`docs/telemetry/telemetry-claims.md`).

- **`/loop` rule upstream re-verify (2026-07-02).** Monitor WebSocket source (v2.1.195+) and non-Anthropic provider fallback (fixed 10-min schedule, no `loop.md` read) documented; all LM version gates re-verified against live upstream docs, ADR-0010 verdicts unchanged.

- **repo-audit Category 9 (MCP Configuration) — file-existence → server health-probe — #707.** The audit's MCP check is upgraded from "`.mcp.json` exists" to "each declared server is actually healthy". A new per-server health-probe row (additive — the existing existence + server-match rows are kept) verifies reachability/auth state against repo-local `.mcp.json` servers only (explicitly **not** the user-machine MCPJungle gateway or globally-imported servers). `mcporter` is offered as **one optional** implementation path using its real contract (`mcporter list --json` → per-server `status` ∈ {ok, auth, offline, error}; the issue's `--status --exit-code --quiet` flags do not exist and were not used), **never a hard dependency** (SEC-020): when `mcporter` is absent the probe gracefully degrades to `skipped` with an install hint. Status mapping wired into the Phase 4 report + JSON sidecar (`ok`→pass, `auth`→warn + `mcporter auth <server>` remediation, `offline`/`error`→fail). SKILL.md-only change (repo-audit has no backing code; the unrelated `harness-audit/categories/category9.mjs` was untouched); 3 new tests. Full gate green.

- **`RUBRIC_GLASS_V2` env-var → schema-validated profile flag `rubric_features: ['glass-v2']` — #403.** The peekaboo-driver's glass-modifiers conformance-artifact emit was gated by the globally-scoped `RUBRIC_GLASS_V2` env var, which bypassed the profile schema validator and could not express per-profile behaviour. The gate is relocated to a per-profile `rubric_features` flag in `.orchestrator/policy/test-profiles.json` (added to `mac-gate` + `mac-target-onboarding`), validated as an optional `string[]` by `scripts/lib/profiles/schema.mjs` (mirrors `checks`/`tags`). The driver now reads the flag for the active `PROFILE` via `jq`, preserving default-off behaviour (PROFILE unset / profiles file missing / `jq` absent → no emit) and the conditional-emit safety invariant (gate precedes emit). Pure seam relocation — **no v2 rubric authored** (`rubric-v2.md` does not exist); the canary test was rewritten to guard the profile-flag gate. Full gate green.

- **Generalized dead-bridge / dormant-seam validator — #671** (consolidates the 3 point-guards). A new `scripts/lib/validate/check-dead-bridge.mjs` (with pure `dead-bridge-detectors.mjs` + `dead-bridge-corpus.mjs`) subsumes the three standalone dead-bridge guards — `check-subagent-types.mjs` (#614), `check-rules-references.mjs` (#445), `check-baseline-fetch-bridge.mjs` (#618) — behind one rule engine, and **retires** all three. Class-(a) **dangling-reference** detection reproduces every old guard's behaviour verbatim (subsumption proven by a corpus equivalence test: each old guard's positive/negative case is an anchor); a NEW class-(b) **bridge-balance** detector flags set-but-never-read / read-but-never-set against a *declared* producer/consumer registry (registry-bounded → zero false positives, so the new gate keeps CI green). Wired into `validate-plugin.mjs`; the sunset-walker boilerplate-site exemption was repointed from the retired guard to the new validator files. 37 new tests; full gate green.

### Fixed

- **auto-dream sidecar fail-close hardening (#717, #720).** `applyPendingDream` now refuses unsupported sidecar shapes — git-style diffs (`---`/`+++`/`@@` signatures), multi-fence bodies, and foreign-tagged single fences (e.g. a lone ` ```js ` fence) — returning `{ applied: false, reason: 'unsupported-format' }` without touching `MEMORY.md` or unlinking the sidecar, instead of writing literal fence markers or malformed diffs into memory. The generator contract is rewritten to a single complete-body markdown fence, and the steering-doc claim is aligned to the #614 nudge-only reality. Fake-regression-verified guards included.

- **memory-proposals atomic clear + summary-sidecar reset (#723).** `clearProposalsJsonl` truncated the JSONL but never reset the per-wave summary sidecars, so `collectProposals` re-summed stale `queued>0` counters against a 0-byte file forever (reproduced fleet-wide in 3 repos). Clear is now tmp+rename atomic and removes all summary sidecars (best-effort, never throws).

- **vault-mirror per-record mapper-crash resilience + `isValidSlug` type guard (#718).** `RegExp.test` coerced `undefined` to the string `'undefined'`, bypassing the uuid-fallback and crashing on `.length`; mapper crashes now emit `skipped-invalid` with `reason: 'mapper-crash'` and the run continues (Node system errors still exit 2 unchanged). A previously unmirrorable archive file now processes 67/67 lines.

- **vault-status case-insensitive board merge key (#719).** All four merge/compare sites fold the repo-name key to lowercase (display casing preserved); duplicate board pairs — same repo, two casings — collapse to one row on the next sweep, fresh rows winning collisions.

- **CI: nested vault-sync install reverted to `npm install` — its lockfile is gitignored,** so the prior `npm ci` change EUSAGE-failed every fresh CI clone at the shared node-setup step (root `npm ci` unaffected; the root lockfile *is* committed).

- **vault-mirror dangling `[[wiki-link]]` emission — #704.** Learning-note mirroring wrapped every `source_session` in an Obsidian `[[wikilink]]` unconditionally — emitting `[[unknown]]` (310 occ / 155 files) when the source was unresolvable, plus malformed/branch-style slugs (`[[develop-…]]`, `[[feat/…]]`, `[[sessionmain-…]]`, ~454 occ) that never matched a real `50-sessions/<project>/<slug>` note. Together ~83% of all dangling links in the vault. New `resolveSourceSessionLink(source_session, opts)` (`scripts/lib/vault-mirror/utils.mjs`) decides link-vs-plain-text by **existence** — a target is linked only when its basename is present in the vault's `50-sessions/` index (built once per run in `process.mjs`, read-only, `dryRun`-safe); `unknown` / empty / provenance-tag (`@`) values render as plain text, never a dangling link. Without a vault index (unit / vault-less runs) it falls back to strict `parseSessionId` format validity. Existence-based resolution restores legacy `main-<date>-<HHmm>` links whose notes genuinely exist (a format-only heuristic wrongly dropped 13/97) and suppresses not-yet-mirrored ids that would dangle. `source_session` is now a canonical content-diff field (`learningContentMatches`), so a **normal re-mirror self-heals** the ~912 historical dangling notes — no `--force` required. PSA-006-verified: in-memory render of all 97 live learnings → 162 links, **0 `[[unknown]]`, 0 dangling**; `narrative-mirror` confirmed not a contributor (no emitter change outside `vault-mirror/`). 45+ new/updated tests incl. a hermetic corpus 0-dangling sweep, a bare-basename lockdown guard, the 3 prior bug-encoding tests inverted to the plain-text contract, and a fake-regression-verified `regression #704` guard; full gate green (10481 passed / 0 failed).

### Security

- **CI gate + supply-chain hardening.** The GitHub-mirror workflow now runs the fail-closed `scripts/ci/assert-vitest-green.mjs` verifier on the Vitest and Coverage steps, replacing a grep PASS/FAIL heuristic that could report a silent green (the #1-documented failure class; GitLab CI already had it). Lint + typecheck jobs added to the canonical GitLab pipeline (previously GitHub-mirror-only); `gitleaks` v8.30.1 / Node 24.18.0 / `glab` v1.105.0 pinned with sha256 checksum verification. Dependency bumps: `brace-expansion` 5.0.6 (ReDoS, GHSA-jxxr-4gwj-5jf2), `js-yaml` 4.2.0 (quadratic-merge DoS fix + loader depth guards).

- **gitignore hardening for host-identifying artifacts.** Reaped-lock archives (embed `os.hostname()` + session metadata), backfill idempotency markers, `docs/_private/` fleet-analysis reports (may reference private repo names; sanitized versions go to `docs/research/`), and transient Codex/Cursor platform artifacts are all excluded from the public repo.

## [3.10.0] - 2026-06-23

Additive, backward-compatible release. Headline: a **cross-repo dispatcher** that picks
the next-best repo to work on, a **learning → rule reconciliation engine** that turns
session signals into reviewable `.claude/rules/` proposals, and the foundation +
activation of **skill self-evolution**. Every new behaviour is opt-in and off by default;
existing sessions are unaffected. CI green across the full suite on every commit.

### Added

- **Cross-repo Dispatcher — Epic #673** (closes #674, #675, #676, #677, #678, #679, #680, #681, #682). A new `/dispatcher` command (+ `dispatcher` skill) that enumerates the repos you are *not* currently working on, resolves free/busy from each repo's `session.lock` lease, ranks the free ones by **backlog priority × staleness × readiness**, recommends the single best one via `AskUserQuestion`, atomically claims its lease so two sessions can't collide, and routes you to the chosen entry command.
  - **#674/#675 — cross-repo session board + durable narrative mirror:** a `_active-sessions.md` vault board plus durable session-narrative mirroring so the dispatcher and the operator share one view of what's running where.
  - **#679/#680 — suitability verdict engine:** a new `dispatcher-autonomy` config block and a fail-closed suitability check that must pass before any autonomous launch.
  - **#681/#682 — autonomy wired end-to-end:** one-time capture of the committed autonomy posture + verdict-gated launch. Autonomous launch is **opt-in and off by default** (`dispatcher-autonomy.autonomy: off`); the effective dial resolves host-locally (`SO_DISPATCHER_AUTONOMY` env > `owner.yaml` > committed > `off`).
- **Learning → Rule Reconciliation Engine — Epic #693** (closes #694, #695). A new `/reconcile` command (+ `reconcile` skill) that converts confidence-scored session learnings into **conditional `.claude/rules/` proposals** — every write is operator-AUQ-gated and rules are never auto-applied.
  - **#694 — rule-loader activation:** wired the previously-dormant per-wave rule-loader and conditional rule frontmatter (glob / mode / host-class / expiry gating), so `.claude/rules/*.md` can load conditionally instead of always-on.
  - **#695 — reconciliation engine (FA2):** the learning → conditional-rule proposal pipeline.
  - **FA3/FA4 — advisory delivery + guardrails:** session-end advisory rule-proposal delivery (gated on a new `reconcile:` config block, default off), a `check-rules.mjs` CI guard (never-always-on firewall for generated rules), and a per-type rule-expiry TTL (`rule-expiry-days: null` → per-type default).
- **Skill Self-Evolution — Epic #643** (closes #643, #645, #647, #651). Foundation + activation of the self-evolution surface: token rollup, a `skill-evolution` config block, and L1/L2 telemetry; a **C2 tiered auto-repair engine** that gates auto-apply per artifact type; the C2 real apply-path plus an opt-in L3 session-end LLM judge (advisory only); and a closeout pass adding per-skill health, an MCP fix, and security hardening. Autonomous-apply is armed only for the safest drift shape (root-instruction command-count, filesystem-fact-sourced) behind a quadruple gate; plugin/local/remote targets are always MR-only. New `skill-applied-judge` agent.
- **Per-project Vault namespacing + relocation — #660, #700.** Per-project vault namespacing with a commit-isolation guard (#660), plus a phase-2 relocation engine and **walk-up named-vault resolution** (#700): vault-mirror can now route to the vault whose `match.org-prefix` matches the current repo's remote slug, configured via an optional `vaults:` list in `owner.yaml`. Absent config degrades byte-identically to single-vault behaviour.
- **Instruction-budget guard — #687.** An always-on session-start directive-budget banner (warn-only, growth-ratchet) that fires when the always-on structural-directive count exceeds a ceiling — a guard against silent instruction-file bloat. Paired with a PSA operator-session re-scope (#689) clarifying the operator-session vs in-run axes.
- **Frontend-slop detector — #684.** An impeccable-inspired deterministic frontend-slop detector and an opt-in `PostToolUse` hook (default off, warn-only, profile-gated) that flags templated-default UI tells after UI-file edits, plus the `frontend.md` design-ban rule markers it enforces.
- **Optional User-Story intent layer in `/plan` — Epic #654.** An opt-in user-story intent layer for the `/plan` flow.
- **Host-local, privacy-clean path resolution — #653.** `vault-dir` and `baseline-path` now resolve host-locally (env > `owner.yaml` `paths:` > committed default), so a machine can point at its own paths without committing personal locations to the repo.
- **gitlab-ops MR/PR evidence block — #669** and **SubagentStop feed-back-and-continue — #666** (additional context fed back to the coordinator on subagent stop).
- **6-surface drift detection — #663.** `claude-md-drift-check` extended to detect count drift across six surfaces.
- **LSP-posture rule** — new `.claude/rules/lsp.md` documenting the deliberate no-LSP-MCP navigation posture (ripgrep + steering map), so the absence reads as intentional.

### Changed

- **Config-cycle break + memory time-decay — #664, #670.** Refactored to break a config-module dependency cycle and wired memory time-decay into recall.
- **Doc-sync + loop-engineering positioning.** Corrected verified surface counts that had drifted across README and the session-injected steering docs, and reframed positioning around the "loop engineering" discipline. `.orchestrator/steering/{structure,product}.md` refreshed (skills/commands/agents/hooks inventory + mission re-centered on the durable moat: mechanical guards, telemetry, skill self-evolution, public-repo-safe shared memory, parallel-operator-session safety, multi-harness portability).
- **Native-overlap ADR refresh + prompt-caching rule demoted** to path-scoped (the orchestrator itself uses no LLM SDK).
- **README slimmed** to a leaner public landing page with private-tracker references removed.

### Fixed

- **Security — owner-leakage scanner canonicalize-before-match (#661)** and a scanner-safe Google-API-key test fixture (secret-scanning #5).
- **Metrics — pre-write JSONL round-trip self-validation + repair (#662)**, so a malformed metrics line is caught and repaired before it lands.
- **resource-probe — judge macOS RAM-criticality on *available*, not *free* (#667)**, fixing spurious resource-overload trips on healthy machines.
- **CI green-baseline restoration (#685, #686).** Sharded the vitest suite for the root Hetzner autoscaler; guarded chmod-as-root tests; replaced a root-only `/proc` `mkdirSync` that hung a shard with a uid-uniform unwritable-path helper; closed a CI false-green blind-spot in the fail-closed verifier; widened per-shard timeouts with headroom; hardened a multi-session-registry concurrency test and excluded the coverage lane double-count.
- **Reliability sweep** — CI-flake poll, vault-mirror drift correction, memory-cleanup stamp, and metrics hygiene.
- **deps** — `vite` overridden to `^8.0.16` (resolves a Dependabot HIGH advisory); `vitest` bumped to `^4.1.0` in the `claude-md-drift-check` skill.

## [3.9.0] - 2026-06-13

### Added

- **Pi harness adapter — session-orchestrator runs under `pi` (earendil-works/pi) like Codex/Cursor** — closes #639 (MR !23, merged from `codex/pi-adapter-639`). New `pi/` prompt-wrapper surface (20 generated prompts via `scripts/generate-pi-prompts.mjs --check`), `scripts/lib/pi-hook-bridge.mjs` (Pi native-hook manifest bridge, 328 LOC), `scripts/pi-install.mjs` installer, `hooks/hooks-pi.json` manifest, and `docs/pi-setup.md`. Platform/config/state/scope resolution now recognise the `.pi` marker dir + `PI_*` env vars (`PI_PLUGIN_ROOT` / `PI_PROJECT_DIR`) across `platform.mjs`, `config.mjs`, `common.mjs`, `plugin-root.mjs`, and `state-md-peer-guard.mjs` — **additive and backward-compatible** (claude/codex/cursor behaviour unchanged). New `check-pi-package.mjs` + `check-pi-prompts.mjs` validate-plugin checks (138 checks total, 0 failures) plus extended hook-symmetry coverage. CI pipeline #5577 full 7-job suite green (test 257s, coverage 439s).
- **Autonomy-Commands ADR + /loop-Anchoring + Owner-Leakage-Hardening** — closes #631, #633, #634; #632 repo-local part (deep session main-2026-06-10-deep-1, 5 waves / 16 agents + coordinator fold-ins).
  - **#633 Hebel 1 (ADR-0010)** — new `docs/adr/0010-native-autonomy-commands.md`: per-primitive verdicts for the native autonomy family verified against live docs on v2.1.170 (**/loop = Adopt** — `.claude/loop.md` IS the native customization seam; **/goal = Adapter** — adopt native per-turn continuation, keep judgment deterministic: the Haiku evaluator reads the transcript only and runs no tools, so it never replaces exit-code quality gates; **/batch = Stay** — GitHub-PR-shaped, no kill-switch/telemetry parity with `autopilot-multi`; **/background = Adapter** — gated on a detachment empirical test instead of pre-building autopilot Phase C-5). Closes the keystone gap next to ADR-0002/0003/0004.
  - **#633 Hebel 2 (LM rules)** — `.claude/rules/loop-and-monitor.md`: `/goal` completion-condition axis as the new first LM-001 branch (with an explicit "NOT: until CI goes green → Monitor" counter-example), new **LM-008 "Use /goal When …"** section, 2 new `/goal` anti-patterns (quality-gate misuse, unbounded goal), LM-005 "never reimplement /goal" bullet.
  - **#633 Hebel 3 (loop.md baseline)** — new vendorable `templates/_shared/loop.md` (repo-agnostic: VCS auto-detect glab/gh without `-R` hardcodes, Session-Config-gated vault check, CLI-error-tolerant) wired into all 4 bootstrap templates; host-wide `~/.claude/loop.md` user-baseline written (strictly read-only, account-neutral, privacy-clean). Project `.claude/loop.md` stale vault path fixed to the Session-Config SSOT.
  - **#633 Hebel 6 (readiness banner)** — new `scripts/lib/loop-readiness-banner.mjs` (`checkLoopReadiness`, sync, fail-silent, null when repo OR user loop.md exists) + 13 tests + session-start Phase-4 wiring: warns when bare `/loop` would fall back to the generic Anthropic maintenance prompt.
  - **Doc-drift sweep** — kill-switch count corrected to the runtime SSOT (`kill-switches.mjs:18-32` = **10**) in `skills/autopilot/SKILL.md` (table + pseudocode + frontmatter), `commands/autopilot.md`, and LM-005; `.orchestrator/steering/structure.md` inventory rows added. Dogfooding: the session itself ran a 30-min `/loop` maintenance cron on off-minutes (`7,37 * * * *`) per LM-003 hygiene.
- **Agent-status side-channel helper + ecc test-depth** — closes #565, #628 (deep "Value-Drain" session).
  - **#565 (agent-status)** — new `scripts/lib/agent-status.mjs`: a lean, no-throw, best-effort per-agent status push helper (`setStatus` / `setProgress` / `readCurrentStatus`) writing an append-only `.orchestrator/runtime/agent-status.jsonl` (`O_APPEND`-atomic) + a lock-serialised last-write-wins `agent-status-current.json` map. Self-contained `linkSync` create-or-fail mutex with PID-liveness stale-detection (reuses `isPidAliveOnHost`), PSA-003-compliant release (owner pid/host match), cross-host locks never overridden. Wave-executor integration documented at 3 anchors (dispatch / agent-end / wave-end rollup, gated `persistence: true`, fire-and-forget); opt-in `tmux-layout --with-status-pane` 5th pane. Tests include a **true cross-process race test** (8 spawned `node` processes — PoC-verified falsifiable: 4/8 keys lost without the lock) plus stale-lock recovery.
  - **#628 (ecc test-depth)** — new `tests/lib/config/{loop-guard,config-protection}.test.mjs` (parser clamping, mode validation, `allow-config-weakening` plain-vs-dead-bold form), corrupt-ring recovery + `session_id` path-traversal containment pins for the loop-guard hook, a `threshold > window` self-heal clamp in `scripts/lib/config/loop-guard.mjs`, a 50 MB `statSync` transcript size-bound in `hooks/subagent-telemetry.mjs`, and legacy `.eslintrc*` detection pins for the config-protection guard.
- **Sunset-review surface tooling + rule-reference hygiene** — closes #444, #445, #617; closes #446 (moot). Reduces backlog/review noise and gives the plugin a read-only way to find its own dead skills/agents/commands.
  - **#444 (sunset-review)** — new `scripts/lib/sunset/walker.mjs`: a READ-ONLY surface walker that enumerates the skill/agent/command surface, combines agent-dispatch telemetry (`subagents.jsonl`, `event==="start"` only) with static reference scanning, and classifies each item into a 4-tier verdict (Active / Investigate / Demote / Retire). Low-confidence guardrail downgrades every Retire to Investigate when telemetry coverage < window. JSON-first CLI (`--json`/`--window-days`/`--kind`, exit `0/1/2` per cli-design.md). **W5 hardening:** a command whose `skills/<name>/SKILL.md` linkage points at a skill no longer present on disk now classifies **Investigate** (reason "command invokes a skill not present on disk"), not Active — the exact staleness a sunset tool must catch (R1 architect + R3 qa convergent MED). +6 test-depth gaps (command read-error graceful degradation, `--kind` filter + hand-computed summary tally, skill-kind README boilerplate exclusion, scanned-dir `node_modules` skip).
  - **#445 (rule merge)** — merged `.claude/rules/test-quality.md` into `.claude/rules/testing.md` § "Test Quality — False-Positive Prevention"; redirected the 3 orphaned intra-repo links (`skills/wave-executor/SKILL.md` ×1, `skills/write-executable-plan/SKILL.md` ×2) to the new section, plus a `check-rules-references` guard against future dangling rule links.
  - **#617 (review-noise)** — stop auto-filing MED/LOW review findings as backlog issues (the backlog-noise root cause; #614 sibling).
  - **#446 (moot)** — closed; folded into the vault decisions log.

### Fixed

- **Owner-leakage scanner: trailing-slash blindspot (P1) + dash-encoded form (P9) + dotfile-allowlist reachability** — closes #631 (priority:high), #634 (priority:high, W4 panel finding).
  - **#631** — P1 regex `/\/Users\/bernhard[a-z.]*\//` required a slash AFTER the username, so bare `/Users/<owner>.` strings (end-of-line, before `&&`, before newline) passed undetected. Replaced with Candidate F `/\/Users\/bernhardg[a-z.]*(\/|\b)/` (tightest false-positive profile: rejects `bernhardo`/`bernhardt`/`bernhardg9`/`bernhardg_x` lookalikes — all pinned by tests); neutralized the 2 genuinely-leaking scanned lines (`memory-paths.mjs` docstring, `memory-paths.test.mjs` literal → dynamic-construct); 5 issue-form regression tests + 7 W3 edge-case tests + word-boundary intent pins. Fleet re-run with the fixed scanner across 43 repos posted to #632 (19 repos with P1 hits / ~233 occurrences quantified).
  - **#634** — W4 security-review found a committed dash-encoded home path (`-Users-<owner>--…`, the Claude Code projects-dir encoding) in `docs/migrations/2026-05-18-461-persona-reviewers-rename.md` shipping to the public mirror, structurally invisible to P1–P8. Scrubbed the line, added **P9** `/-Users-bernhardg[a-z.]*-/`, added the encoding-contract fixture file to SELF_EXCLUSIONS, + positive/negative regression tests. Residual: git history retains the old line (same accepted-residual class as #632 point 2).
  - **Dotfile-allowlist reachability (W3-P3 finding, folded in-session)** — `isTextFile()` checked `extname()` first, but `extname('.env.example')` is `'.example'` (truthy), making the `DOTFILE_ALLOWLIST` entry unreachable — `.env.example` was silently never scanned. Now basename-first + 2 reachability regression tests.
- **Dead baseline-fetch rule-bridge in bootstrap** — closes #618 (priority:high). Bootstrap Step S99/D99 gated the on-demand baseline fetch on `scripts/lib/fetch-baseline.sh`, a file the plugin never ships (only the Node port `fetch-baseline.mjs` exists since #218) — so the guard was always false and the step silently never fired in real installs. Flipped all bootstrap guards/refs (`skills/bootstrap/{_shared-template,SKILL,standard-template}.md`) to drive the shipped `fetch-baseline.mjs` single-file CLI in a per-manifest-rule `while`-loop (404→skip→continue) followed by an inline `node -e` `.baseline-fetch.lock` write, and plumbed the GitLab host from the `gitlab-host` Session Config key / `GITLAB_HOST` env (the `.mjs` keeps the host **mandatory with no private-host default** — privacy-correct for the public mirror; the issue's suggestion to reintroduce the legacy `.sh`'s hardcoded private-host default was a leak trap and was avoided). Mechanically pinned by a new `scripts/lib/validate/check-baseline-fetch-bridge.mjs` validate-plugin check (asserts the guarded file exists + zero dead `.sh`/shell-function refs) and a 19-test regression guard, so the dead-bridge class cannot silently recur.
- **Undocumented `--no-verify` in vault-mirror auto-commit** — closes #603. The vault-mirror auto-commit's `git commit --no-verify` (`scripts/lib/vault-mirror/auto-commit.mjs`) is now documented inline + in `skills/vault-mirror/SKILL.md` (rationale: only generator-stamped mirror artifacts ever reach the commit — already validated — and the unattended session-end close must not block on interactive/slow vault-side hooks) and pinned by a regression test asserting the flag on the real commit args.
- **Housekeeping: cut dead session-end auto-consolidation dispatch + add a dispatch-target guard** — closes #614, #606; closes #489 (moot). Express-path coordinator-direct session.
  - **#614 (dead automation)** — session-end Phase 3.6.5 (Auto-Dream) and Phase 3.6.7 (Auto-Dialectic) dispatched `subagent_type`s (`memory-cleanup`, `evolve`) that were never built in `agents/`, so the dispatch never fired — the symptom was a recurring "auto-dream/dialectic SKIPPED (agent-types unavailable)" line in 7+ session memories. Replaced both dead `Agent({…})` blocks with a lean manual-cadence nudge (run `/memory-cleanup --dry-run` / `/evolve --dialectic --dry-run` manually next session); the `shouldDispatchAutoDream` / `shouldDispatchAutoDialectic` decision helpers + `auto-dream.mjs` / `auto-dialectic.mjs` libs are preserved (they compute the signal that drives the nudge). Auto-dialectic advances `.orchestrator/dialectic-last-run` on nudge-emit so the reminder surfaces once per cadence window rather than every session. Also corrected the documented `writeDialecticLastRun(repoRoot, …)` positional call to the real `{ repoRoot, isoTimestamp }` object signature.
  - **New guard** — `scripts/lib/validate/check-subagent-types.mjs` asserts every `subagent_type: "session-orchestrator:<X>"` reference under `skills/**` resolves to an existing `agents/<X>.md`; wired into `scripts/validate-plugin.mjs` (exits non-zero on any unresolved reference). Supports an inline `check-subagent-types:ignore` marker for documenting historical/example dead references. +9 unit/integration tests.
  - **#606 (docs)** — documented the `VAULT_MIRROR_CANONICAL_SUFFIX` env-var (host-qualified canonical-vault guard override; environment-only, intentionally not a Session Config key) in `docs/session-config-reference.md`.
  - **#489 (moot)** — closed: the Windows-only test failures it tracked can no longer occur after `windows-latest` was dropped from the test matrix (v3.8.0 / `990be3a`).

## [3.8.0] - 2026-05-29

### Added

- **Housekeeping: GitHub public-safety + green CI** — restored a clean public mirror (github.com/Kanevry/session-orchestrator) with a green CI badge.
  - **CI green:** dropped the never-green `windows-latest` leg from the GitHub Actions test matrix. The orchestrator is POSIX-first (paths, `/tmp`, symlinks, shell built-ins) and Windows was never a required check nor a supported target; ~49 suites failed on Windows. CI now runs ubuntu + macOS + security (macOS is the priority platform). Also made the quality-gate test stand-ins cross-platform (`node -e` replacing POSIX `true`/`false`/`test -f`) as a correctness improvement.
  - **Privacy hardening:** scrubbed the literal internal subnet from the SSRF example in `security.md`; extended `check-owner-leakage.mjs` with P8 — full RFC1918 dotted-quad detection (placeholder `.x`/CIDR forms and TEST-NET exempt; the IP-redaction test allowlisted) + 5 regression tests; genericized SSH-string test fixtures to TEST-NET (192.0.2.x). Leak-scanner GREEN (1073 files, 0 leaks).
  - **Repo hygiene:** removed internal session-exhaust artifacts that had been committed to the public repo (`.orchestrator/{scratch,session-artifacts,consumed}`) and added gitignore rules for these + `.orchestrator/STATE.md` (was untracked-but-unignored). Project-private feature names (Clank, vault, projects-baseline) are intentional and retained per maintainer decision.

- **Vault + Observability Follow-up Drain (session-1, resumed)** — closes #602, #604, #607, #611, #612, #613. Resumed a crashed deep session at the W2→W3 boundary: verified the crashed W2 work (vault track + #612, 420 tests green / no regression), then completed W3–W5. 5 waves / 7 subagents / 8232 tests passing (+6 net from the W5 fold-in), 0 fail. 0 BLOCKER across 4 W4 reviewers (security PROCEED clean). Follow-ups #615 (#607 items 4/5/7 vault-path housekeeping), #616 (W4 test-depth + emitEvent `{filePath}` contract).
  - **#611 (Track A observability)** — additive `emitEvent(type, payload, { filePath })` path override (default unchanged; 9 existing 2-arg callers grep-verified unbroken); new `scripts/emit-event.mjs` CLI (`--type/--payload/--file/--json/--help`, exit `0/1/2` per cli-design.md); `compute-grounding-injection.sh` routed through the CLI (jq `--arg`/`--argjson` escaping preserved); `grounding_injected` → `orchestrator.grounding.injected` rename with both consumers (`metrics-collection.md` jq filter + `wave-loop.md`) updated in lockstep.
  - **#612 (Track B observability)** — mechanical `orchestrator.wave.{started,completed}` lifecycle in `post-tool-batch-wave-signal.mjs` (diff `.claude/wave-scope.json` `.wave` vs persisted `last_wave`); verified live in-session (dogfooded). W4 panel (3 reviewers converged) surfaced a `wave.started` double-emit; **root cause + fix:** `on-session-start.mjs` full-overwrote `current-session.json`, dropping `last_wave` on every SessionStart — now preserves `last_wave`/`last_batch` when `semantic_session_id` is unchanged (stable across clear/compact/resume; the UUID `session_id` is not). +5 idempotency/coverage tests.
  - **#602 (vault-mirror frontmatter)** — slugify-at-the-generator: `buildTag`/`slugifyIdSafe` sanitize non-kebab interpolated segments + uppercase-`T` ISO ids across all session/learning generators (root cause: re-reading the raw entry after the caller sanitized only the filename).
  - **#607 (vault testability/hardening — items 1-3 + 6)** — `_normalizeRemote` exported for unit tests; `rewriteMissingSegment.originalContent` made required + `isOwnedByUsernamePath` extracted; `scripts/lib/vault-consolidate-fs.mjs` extracted (deep module; #514 symlink guards preserved) + `import.meta.url` entry-guard; `VAULT_MIRROR_CANONICAL_SUFFIX=""` boundary pinned. Items 4/5/7 → #615.
  - **#604 / #613 (coverage)** — `vault-migration-rules.mjs` suite (41 tests; never-throws envelope contract pinned); 4 observability test refinements (quality_gate `incremental` variant, on-stop populated commit path, `wave_number:0` boundary, `isIso8601` Date.parse guard).

- **Epic #583 Hardening Close-out (deep-6)** — closes #590 (HIGH), #591, #592. 5 waves / 18 subagents / +38 net tests (7826 → 7864 passing). 0 BLOCKER / 0 HIGH across 5 W4 reviewers. Follow-ups #594 (findPeers caller-migration), #595 (v1→v2 lock-schema sunset, target 2026-08-25), #596 (residual MED+LOW bundle).
  - **#590-2 (HIGH) — `acquire()` TOCTOU fix:** fresh-acquire write migrated from tmp+`renameSync` (last-writer-wins) to a new `createSessionLockExclusive()` helper using `fs.linkSync` (POSIX-atomic create-or-fail), mirroring the existing `createStateLockExclusive`/`createStagingFenceLockExclusive` idiom in the same file. Two concurrent `SessionStart` hooks can no longer both win the lock. EEXIST-loser path re-reads + re-classifies (active/stale-*) with an ENOENT-vanish defensive fallback to `active`. `forceAcquire()` + `updateHeartbeat()` intentionally KEEP rename-based `writeLockAtomic` (they must overwrite). Proven by NEW `tests/integration/session-lock-cross-process.test.mjs` (5 sibling processes → exactly-one-winner; falsifiable — fails against the old tmp+rename impl).
  - **#590-1 — bootstrapLock conflict signal:** when `acquire()` returns `active` for a FOREIGN session, `hooks/_lib/lock-bootstrap.mjs` now records `conflict_with_session_id` + `conflict_detected_at` into `.orchestrator/current-session.json` via atomic read-modify-write (preserving concurrently-written fields) before its (unchanged) `null` bail. Best-effort — FS errors never block the hook.
  - **#590-3 — `updateHeartbeat()` cadence extension:** wave-executor inter-wave checkpoints (`wave-loop.md` §3a) + session-end entry (before Phase 3.8 lock-release) now refresh the session.lock heartbeat, covering long-idle deep sessions with no PostToolBatch activity for >4h.
  - **#592 MED-1 — `findPeers(repoRoot)` 3-surface union unifier:** NEW `scripts/lib/peer-discovery.mjs` unions lock+registry (via `discoverActiveSessions`) with STATE.md (via `checkPeerStateMd`) into one provenance-tagged flat list, fail-open per surface (never throws). Library-only; Phase 0.5 + Phase 1.2.1 caller-migration tracked in #594.
  - **#592 MED-2 — `mapToKnownMode` encapsulation:** deleted the shadow mode-enum from `lock-bootstrap.mjs`; `acquire()` gained an additive `{ quiet }` param that suppresses the unknown-mode stderr-warn. lock-bootstrap now passes the raw mode + `quiet: true`. Genuine coupling reduction (the matrix vocabulary lives in one place).
  - **#592 MED-3 / LOW-1 / LOW-3:** sessionId-equality invariant precondition comment in `state-md-peer-guard.mjs`; `isPidAlive` → `isPidAliveOnHost` rename + `@forensic` JSDoc (clarifies it is NOT the discovery-path liveness check — that is heartbeat-age); local `writeJsonAtomic` in `lock-bootstrap.mjs` consolidated to the canonical `writeJsonAtomicSync` from `io.mjs`.
  - **#591 — 6 boundary/race test gaps + 2 anti-patterns:** H1 exact-TTL strict-`<` boundary, H2 `updateHeartbeat` empty-repo, H3 `registryReaderFn` throw fail-open (SUT re-attributed to `session-discovery.mjs`), H4 real cross-process race, H5 STATE.md no-`session:`-field, H6 3+ peer E2E; AP1 `setTimeout`→`vi.useFakeTimers()` (2 sites), AP2 `toBeTruthy()`→dual-format-regex (NOT strict-UUID — semantic-OR-uuid id-space, RCR-006 pushback on issue text).

- **Value+Maintainability Sweep (deep-4)** — single coherent bundle of 1 medium feature + 5 refactors + 1 continuation + 1 test-quality + 1 epic close. Closes #566, #510, #511, #512, #513, #542, #581, #515, #378. Net +73 tests vs deep-3 (7658 → 7731 passing).
  - **#566** `auto-dream.min-confidence` config knob (numeric `[0.0, 1.0]`, default `0.5`) — second confidence gate above `memory.proposals.confidence-floor`, applied at session-end Phase 3.6.3 `collectProposals()` collect-emit time (NOT at `auto-dream.mjs` proposal-emit time as the original AC literally said — `scripts/lib/auto-dream.mjs` has no per-item proposal stream; grep-verified 0 confidence references). User-AUQ-approved adaptation. New `scripts/lib/config/auto-dream.mjs` parser (mirrors `cold-start.mjs` structure + `vault-mirror-quality.mjs` float-range validation); `minConfidence` parameter on `collectProposals()` with back-compat `null` default; 1-line docstring pointer in `auto-dream.mjs` for AC-traceability; SSOT parity across CLAUDE.md + `docs/session-config-template.md` + `docs/session-config-reference.md` (drift-check Check 6 GREEN).
  - **#510** `scripts/lib/cli-flags.mjs` (NEW) — shared `parseColumnFlags({knownBool, knownString, defaults, onUnknown})` helper around `node:util parseArgs` strict mode + typed `CliFlagError`. Migrated 4 scripts (`vault-consolidate.mjs`, `vault-mirror.mjs`, `migrate-cold-start-seed.mjs`, `migrate-vault-paths.mjs`) to the SSOT. **Intentional behavior change**: `vault-mirror.mjs` unknown-flag policy changed from silent-ignore → exit-1 (grep-verified no caller in `skills/` passes unknowns; tightening, not loosening); wet-run-default preserved on `vault-mirror.mjs`.
  - **#512** `scripts/lib/memory-paths.mjs` (NEW) — `resolveMemoryDir` extracted from `auto-dream.mjs` (no re-export — auto-dream did not use it internally); single production consumer (`memory-banner.mjs`) re-pointed.
  - **#511** `emitAction` in `scripts/lib/vault-mirror/process.mjs` — 6 positional params → options-object `{action, path, kind, id, vaultDir, meta}` (param renames `filePath→path`, `fileKind→kind`); 15 call sites + 2 test sites updated. `scripts/vault-backfill.mjs` separate `emitAction` untouched.
  - **#542** memory-banner.mjs test-only exports renamed with `_`-prefix convention (`_truncateLine`, `_formatLearningLine`, `_formatStatsLine`, `_extractCardExcerpt`, `_formatBanner`) matching `scripts/lib/session-id.mjs:218` precedent; public API (`renderMemoryBanner`, `readBannerInputs`) unchanged.
  - **#581** 4-item residual bundle (Items 1-3 = tests, Items 4-5 = code/doc):
    - Item 1+2: NEW `tests/lib/config/discovery-validator.test.mjs` (17 parser unit tests + 2 `parseSessionConfig` integration assertions, mirrors `tests/unit/slopcheck.test.mjs` style).
    - Item 3: 2 new edges in `tests/hooks/post-subagent-discovery-validator.test.mjs` (multi-violation count → 2 events; `TAIL_RECORDS=8` window boundary with 10-record fixture).
    - Item 4: `scripts/lib/autopilot/loop.mjs:312` hardcoded `.claude/STATE.md` → `${SO_STATE_DIR}/STATE.md` (platform-resolved via `scripts/lib/platform.mjs`, INERT pre-cloud-flip).
    - Item 5: `docs/adr/0009-worktree-path-layouts.md` 3 line-range citations → symbol/section anchors matching ADR-0008 style.
  - **#515** 3 test-quality assertions tightened: cold-start-detector banner-lines (exact-string `toContain` instead of `.length > 0`); learning-memory-modernization integration (`readFileSync` + `toContain(insight)` + length floor); migrate-vault-paths classification (`JSON.parse` + `toHaveLength(1)` + `toMatchObject` + symmetric no-leak guard, replacing `.find()` + `toBeTruthy`).
  - **#378** epic `/test [scope]` umbrella verified + closed — all 9 sub-issues (#379-#387 including WEB-PROOF and MAC-PROOF) confirmed closed; scaffolding shipped at `commands/test.md` + 4 skills (`test-runner`, `playwright-driver`, `peekaboo-driver`) + `agents/ux-evaluator.md` + `scripts/lib/test-runner/issue-reconcile.mjs`.

- **Epic #583 — Parallel-Session Detection Wiring Hardening** (closes #584 #585 #586 #587 #588): Five compounding wiring defects in Epic #568's parallel-aware detection were identified via live-verification (session ran parallel-aware-preamble against a real active peer and detection did not fire):
  - **#584 — Mechanical lock trigger (D1):** `hooks/_lib/lock-bootstrap.mjs` (`bootstrapLock()`) is now invoked from `hooks/on-session-start.mjs` on every `SessionStart`. Closes the Disziplin-statt-Mechanik gap: the `session.lock` was previously only written when the coordinator-LLM executed Phase 1.2 prose — a silent-skip risk on every session that skipped that prose step.
  - **#585 — Heartbeat-based liveness (D2/D3):** Lock schema v2 adds `last_heartbeat` (ISO-8601, updated by `SessionStart` + `PostToolBatch`/`Stop` hooks). Heartbeat-based liveness `(now - last_heartbeat) < ttl_hours * 3600 * 1000` replaces PID-liveness (`isPidAlive(pid)`). The recorded `pid` in v1 locks was the hook subprocess PID (~500ms lifetime), making every lock appear stale immediately after hook exit. This is the PostgreSQL pattern — use an application-level heartbeat, not process-table interrogation.
  - **#586 — `resolveSemanticSessionId` history-aware (D3):** Now consults `sessions.jsonl` + sibling worktree STATE.md files before computing `n`. Previously returned duplicate `deep-1` when `discoverActiveSessions()` returned an empty array (which it always did — D2 meant all locks looked pid-stale).
  - **#587 — `semantic_session_id` always surfaced (D4):** Lock schema v2 adds `semantic_session_id` field alongside `session_id`. On Claude Code, stdin always provides a UUID-v4 → the semantic-id code path in `on-session-start.mjs:236-313` was unreachable. Now `bootstrapLock()` derives and records the semantic form independently so peer-detection can use the human-readable id.
  - **#588 — Host-registry `mode` field (D5):** `session-registry.mjs:registerSelf()` entry schema-v2 adds a `mode` field. Without it, every cross-repo registry entry contributed `mode=undefined`, which `classifyMode(undefined)` bucketed into `parallel-ok` — bypassing the exclusivity-matrix for all cross-repo peers.
  - New module `scripts/lib/state-md-peer-guard.mjs` — `checkPeerStateMd()`. Defense-in-depth Phase 1b guard: reads STATE.md frontmatter and detects when a *different* active session currently owns it, even when the session.lock has been swept. Fires the Worktree-Promotion AUQ before STATE.md is overwritten.
  - **W5-F1c — `on-stop` heartbeat-refresh, NOT release (post-W3-P3 correction):** Initial W3-P3 wiring called `release({sessionId, repoRoot})` from `hooks/on-stop.mjs`. Stop fires per-turn-end (NOT per-session-end), so release-on-Stop would delete the lock after the first assistant turn → session goes blind. W5-F1c swaps `release` for `updateHeartbeat({sessionId, repoRoot})`: the lock stays live, heartbeat is refreshed every turn, TTL handles eventual cleanup. Closes W4-Q3 H2 cadence finding.
  - **W5-F1c — Q5 H1: `semantic_session_id` propagation:** Previously, `semantic_session_id` lived only on the `session.lock`. Issue #587 AC said both `current-session.json` and the host-registry entry must also carry it. Fixed: `hooks/on-session-start.mjs` payload + `scripts/lib/session-registry.mjs:registerSelf()` entry both record the field. Live-verified: `current-session.json` after hook fire shows `{session_id: "<UUID>", semantic_session_id: "<branch>-<date>-deep-N", ...}`.

### Fixed (pre-existing, surfaced by Epic #583 review)

- **Pre-existing latent bug uncovered by W2-I1 PSA-006 grep + fixed in-scope:** `hooks/on-session-start.mjs:278` destructured `entryPath` from `session-registry.mjs` but `entryPath` was never exported (only `function entryPath(...)`). The `TypeError: entryPath is not a function` was silently swallowed by the surrounding try/catch and the semantic-id generation degraded to UUID-fallback — defeating Issue #573 (P2.2) entirely on Claude Code. Fixed by inlining `path.join(activeDir(), \`${sessionId}.json\`)`. Surfaces in the CHANGELOG separately from Epic #583's intended scope because it predates this Epic but landed in the same commit (W2-I1 grep verified non-exported via `grep -n "^export function entryPath\\|^function entryPath" scripts/lib/session-registry.mjs`).
- **eqeqeq lint violation in `scripts/lib/state-md-peer-guard.mjs:135`:** W2-I4 wrote `parsed.frontmatter == null`. Tightened to `=== null` per ESLint `eqeqeq` rule.

- **#593 vault-integration parser block-form collision** — `_parseVaultIntegration(kv)` read `enabled` from a shared KV map collapsed across all Session Config blocks (the same `enabled:` key is used by `docs-orchestrator`, `vault-staleness`, `slopcheck`, `templates-first`, `verification-auto-fix`, `discovery-validator`, `state-md-lock`, `cold-start`, `memory.banner`, `memory.proposals`, `events-rotation`, `test`, `gitlab-portfolio`, `drift-check`, `vault-sync`, `wave-reviewers` — 16 collision-eligible blocks). Whichever block declared `enabled:` LAST in `CLAUDE.md` (currently `discovery-validator: enabled: false`) silently overwrote `vault-integration.enabled: true` — disabling vault-sync + vault-mirror at session-end. Converted to `_parseVaultIntegration(content)` mirroring the `cold-start.mjs` content-based block parser pattern (every other peer parser was already content-based; this was the lone kv-based outlier). Inline-form path from #497 preserved with precedence over block form. Regression tests in `tests/lib/config/vault-integration.test.mjs` (`issue #593 regression` describe) + integration test in `tests/lib/config.test.mjs` (realistic peer-block layout asserts vault-integration.enabled:true wins over peer enabled:false). The pre-#593 side-effect that quoted-boolean `'false'` on nested `memory.proposals.enabled:` exited non-zero is now gone — `_parseMemory` strips quotes and treats `'false'` as `false` (tolerant parsing — see `tests/scripts/parse-config.memory-proposals.test.mjs`).

- **Epic #568 Phase 3 — Worktree-Auto-Promotion + Hybrid Cleanup** (closes #574, #575):
  - `enterWorktree({basePath, sessionId, branch, repoRoot})` named export in `scripts/lib/autopilot/worktree-pipeline.mjs` — creates sibling worktree at `<basePath>/<repo-name>-<sessionId>/`, idempotency check, security boundary via `realpathSync` + `validateWorkspacePath`.
  - `skills/_shared/parallel-aware-auq.md` + `parallel-aware-preamble.md` — PROMOTION_OFFER outcome wired to `enterWorktree()` (no more "P3.1 #574 stub").
  - `skills/session-end/SKILL.md § Phase 4a` — Auto-Promoted Worktree Cleanup. Clean → auto-remove + WARN. Dirty → 3-option AUQ (Behalten/Löschen/Manuell). PSA-003 compliance. Runs AFTER Phase 4 commit+push (respects #490 durableCommit ordering).
  - `skills/memory-cleanup/SKILL.md § Phase 4.5` — Worktree-Stale-Sweep. Stale auto-promoted worktrees (age > `stale-branch-days`, default 7d) offered for batch-removal in housekeeping prune flow.

### Fixed

- **#576** `validateSession` accepts `schema_version ∈ {0, 1, 2, 3}` (additive contract). Previous validator rejected ADR-364 schema_version=3 entries written 2026-05-24; read-path tolerance only — `CURRENT_SESSION_SCHEMA_VERSION` unchanged at 1.

### Tests

- New `tests/integration/worktree-auto-promotion.test.mjs` (Gherkin rows 1-2 coverage)
- New `tests/skills/session-end-cleanup.test.mjs` (Gherkin rows 2-3 + PSA-003)
- New `tests/skills/housekeeping-stale-sweep.test.mjs` (Gherkin row 4)
- Extended `tests/lib/session-schema/validator.test.mjs` with additive contract assertions (schema_version 0/1/2/3 accept, -1/4/99/"1"/1.5 reject)

- **Epic #568 Parallel-Aware Sessions Phase 1+2 (#570 #571 #572 #573)** — extends Phase 1.1 foundational helpers (#569, shipped in d012db9) with:
  - **P1.2 (#570)** `acquire()` exclusivity-matrix integration in `scripts/lib/session-lock.mjs`. New return reasons (`active-incompatible-exclusive`, `active-compatible-parallel`, `active-readonly-bypass`) + `exclusivityClass` field on all returns. Backward-compatible: 4 existing reasons unchanged when `activeSessions` arg omitted. Sync function (caller pre-computes `discoverActiveSessions(repoRoot)`).
  - **P1.3 (#571)** new `skills/_shared/parallel-aware-preamble.md` (mirrors `bootstrap-gate.md` 7-section pattern; `HARD-GATE` for exclusive, `SOFT-GATE` for parallel-ok, pass-through for always-ok) + new `skills/_shared/parallel-aware-auq.md` (3 AUQ variants per PRD §3 P1 with Codex CLI/Cursor numbered-Markdown fallback per AUQ-004). Adopted at Phase 0.5 in all 5 orchestrator SKILL.md (autopilot, session-start, session-plan, wave-executor, session-end); session-start Phase 1.2 stale-lock AUQ delegates to the preamble (not replaced — local-lock semantics preserved).
  - **P2.1 (#572)** new `scripts/lib/session-id.mjs` exporting `resolveSemanticSessionId({branch, mode, activeSessions, repoRoot})` → `<branch>-<YYYY-MM-DD>-<mode>-<n>` (n = max+1 monotonic, serialized via `withStateMdLock`), `parseSessionId(id)` dual-format reader (semantic OR UUID-v4 per PRD §3 P2 row 3 backward-compat), `SEMANTIC_ID_RE` + `UUID_V4_RE` regexes.
  - **P2.2 (#573)** `hooks/on-session-start.mjs:resolveSessionId` switched from `randomUUID()` to semantic-first via `resolveSemanticSessionId`. Consults host-wide registry for cross-repo n-uniqueness (not just worktree-local `discoverActiveSessions`). Atomic O_EXCL slot claim with silent UUID-v4 fallback on collision (parallel hooks). `source` label now distinguishes `generated-semantic` / `generated-uuid-fallback` / `generated-uuid-fallback-collision` / `stdin`. `skills/_shared/state-ownership.md` schema-doc updated to match P2 format with `since #573` anchor.
  - +28 net tests: 8 acquire matrix scenarios + 20 session-id (parseSessionId dual-format × 8, resolveSemanticSessionId happy/backward-compat × 8, regex × 4). All existing tests preserved; 2 hook contract tests updated for new source labels.
  - Closes P3 dependencies on Phase 2 — only #574 P3.1 (Worktree-Auto-Promotion AUQ) + #575 P3.2 (Hybrid Cleanup) remain in Epic #568, both externally blocked by #448 (autopilot-multi terminal-reason race) + #490 (durableCommit for 3-File-Commit).
- **`/tmux-layout` skill (opt-in)** — operator-side tmux visualization for session side-channels (STATE.md tail, vcs-aware CI-watch, events.jsonl tail). Renders a 4-pane default layout via a printable one-liner the operator pastes into a second terminal. Pane 1 is a scratch shell (AUQ-001 compliant — coordinator chat stays in original terminal). PSA-003-compliant session-collision policy (`--force` required to replace). Per **ADR-0007** (`docs/adr/0007-tmux-visualization-substrate.md`). Closes #561, #562 (debug variant), #563 (telemetry + promotion gate).
- **#557 staging-fence test-depth bundle** — added 33 unit tests for `hooks/pre-bash-staging-fence.mjs` (G1-G6 gate ladder + 14 GIT_ADD_REGEX variants), `withStagingFenceLock` (timeout/stale-PID/holder-mismatch/fn-throws/TypeError paths — fs-error path flagged as production-code testability seam follow-up), and `wave-scope-commit-guard` lock-failure branch. Closes #557.
- **tmux-layout best-practice hardening** — coordinator E2E verification (14 practical CLI tests) found and fixed `jq --line-buffered` (not supported in jq 1.7+) → `--unbuffered`. Added 9 gap-fill tests (Pane 4 jq filter content + Pane 2 STATE.md path resolution + shellQuote injection safety + vcs-detector contract per platform). WebSearch research confirmed alignment with claude-squad/workmux/Anthropic Agent Teams patterns (different use-case: side-channel observability vs. multi-agent visualization).

### Security / Hardening

- **#577** `execSync` → `execFileSync` arg-array conversion in worktree-cleanup + worktree-sweep — shell-injection impossible when branch/path args are passed as array elements (never interpolated into a shell string). PSA-003-aligned.
- **#567** New opt-in `hooks/post-subagent-discovery-validator.mjs` SubagentStop hook — mechanical PSA-006 grep-transcript enforcement; flags discovery output that asserts distributional claims without a quoted `grep`/`rg` transcript (logs a `discovery_validator_violation` event + stderr WARN; non-blocking in v1 — exits 0 always, never rejects the subagent). Default-off (`discovery-validator.enabled: false`); enable per-session or globally in Session Config.

### Refactor

- **#578** `SKILL.md` inline JS stubs replaced with authoritative-impl reference pointers — eliminates SSOT drift between `SKILL.md` documentation and `.mjs` implementation files. No behaviour change.
- **#580-DI-001** Documented divergent sync/async DI seams (`execFileFn` vs `zx-$`) across worktree-cleanup and worktree-sweep — cross-references added in both SKILL.md and relevant `.mjs` files so future contributors understand the seam boundary intentionally.

### Fixed

- **#490** `durableCommit` now commits `autopilot.jsonl` + `sessions.jsonl` + `STATE.md` as a 3-file atomic commit before setting `autopilot.enabled: true` — HARD GATE closes the window where a crash between `enabled:true` write and commit could leave sessions without telemetry entries.
- **#580-HARDEN-002** Phase 4.5 stale-sweep AUQ now warns when live peer sessions are detected (`discoverActiveSessions` count > 0) before offering batch worktree removal — prevents accidentally removing a worktree that another active session is using.

### Tests

- **#579** Closed 3 MED coverage gaps: multi-match worktree disambiguation, branch-flag command-capture in `execFileSync` arg-array path, exclusive-vs-parallel EARS ordering contract.

### Docs

- **ADR-0008** (`docs/adr/0008-worktree-cleanup-ordering.md`) — records the decision that Phase 4a cleanup runs AFTER Phase 4 commit+push (not before), rationale: preserves `#490` durableCommit ordering so `sessions.jsonl` + `STATE.md` are persisted to origin before worktree removal.
- **ADR-0009** (`docs/adr/0009-worktree-path-layouts.md`) — records the two distinct worktree path layouts (`enterWorktree` sibling-flat `<basePath>/<repo-name>-<sessionId>/` vs `setupWorktree` 2-level `<base>/<basename>/<issueIid>/`), when to call which, and the deliberate sync/async DI-seam divergence (`execFileFn` vs zx `$`) — kept divergent, not unified.
- **#580-AUQ-001** Phase 4a dirty-worktree AUQ option order is intentionally `[Behalten (Recommended) / Löschen / Manuell]`, not the PRD §3 P3 Row 3 literal `[Löschen / Behalten / Manuell ich mach's selbst]`. The inversion is PSA-003-aligned: placing the non-destructive "Behalten/Keep" option first + recommended means an accidental Enter keypress never destroys a worktree. Treat the implementation as the authoritative spec; the PRD row was updated retroactively.

## [3.7.0] - 2026-05-23

Eighteen sessions spanning **9 days** (2026-05-15 → 2026-05-23) since v3.6.0. The headline is the **F2 Memory & Personas cluster** — agent-writable `memory.propose`, the session-start memory banner, USER.md + AGENT.md peer cards, and the dialectic-deriver — alongside the **gsd Pattern Adoption Epic #517** (4 mechanical hardening patterns) and a **Persona-Panel Foundation** (`/persona-panel` skill + 4 templates). Tests grew from **5001 → 7360** (+2359, ~47%), validate-plugin **43 → 94/94**, typecheck **67 → 230 files**, zero breaking changes, zero CI regressions, zero open issues/MRs on either GitLab or GitHub at the cut point.

1. **2026-05-15 deep-1 — Wire + Visibility cluster (#386 #449–#456)** — wave-executor visibility wiring (#449 #450), language-mapper `ExportAllDeclaration` fix (#454), GitLab CI `NODE_VERSION` bump 22→24 (#456 follow-on), CI plugin-schema-validate fix (#84455b ajv-cli local fetch).
2. **2026-05-15 deep-2 + deep-3 — BP-2026 ADR cluster (#437–#447, #480–#487)** — strategic ADRs, EARS personas, persona-gate hook tests, polish triplet, H3/Routines scaffolds, deep-3 implementation pass (#487 #482 #483 #484 #485). Architecture vocabulary firmed up per LANGUAGE.md.
3. **2026-05-16 deep-1 — Persona-Panel Foundation (#457–#480)** — NEW `skills/persona-panel/` + `commands/persona-panel.md` + `templates/personas/` (4 personas: buyer, expert, compliance, custom). Three modes: voting-quorum, hard-gate-threshold, coordinator-summary. Writes timestamped sidecars to `.orchestrator/persona-panel/`. Polish triplet (#474 #479 #480).
4. **2026-05-16 deep-3 — harness-audit Cat-8 flagship + correctness/debt cluster (#472 #473 #475 #476 #477 #478)** — `skills/harness-audit/` 8-category rubric scoring, `commands/harness-audit.md`, baseline drift detection. Correctness/debt sub-cluster surfaces architectural smells.
5. **2026-05-16 → 17 — Marketplace + SEC + Anthropic + Superpowers ecosystem cluster (GH #34 #35–#40 #43 #44 #45 + GL #213 #409–#414)** — five-pattern Anthropic adoption (`hooks/operator-steer.mjs` mid-wave guidance via STEER.md, `mcp-builder` in-process MCP docs, OTel `gen_ai.*` aliases on subagents.jsonl, security-reviewer Hard Exclusions reducing FPs ~35%→15%, knowledge-work-plugins submission prep), five-pattern superpowers adoption (`/brainstorm` Socratic design dialogue, `/debug` 4-phase root-cause workflow with Iron Law, `write-executable-plan` skill with placeholder linter, `verification-before-completion` always-on rule, `receiving-review` always-on rule), Marketplace + SEC (vault-dir CWE-22 guard, Codex composer icon, awesome-codex-plugins + composio + knowledge-work-plugins submission docs).
6. **2026-05-17 → 18 — Clawpatch + CC 2.1.x + prompt-caching cluster (#415–#436, #421, #426)** — six architectural patterns borrowed from Clawpatch (#415–#420), Claude Code 2.1.x adoption matrix (#427–#436 + #426 monitor when=on-skill-invoke), NEW `.claude/rules/prompt-caching.md` path-scoped rule with PoC outcomes (#421).
7. **2026-05-18 deep-1 — Phase 1 Learning & Memory Modernization (#499 #500 #502 #504 #507)** — auto-dream automation foundation (#499), cold-start nudges (#500 — `cold-start.enabled` / `nudge-after-hours` / `silence-after-sessions`), memory-cleanup soft-limit (#502 — `memory-cleanup-soft-limit`), vault-mirror quality gates (#504 — `vault-mirror.quality.min-narrative-chars` / `min-confidence`), session-start cold-start banner (#507).
8. **2026-05-19 — Privacy/Public-Mirror Epic (#462 #461 #468 #469 #470 #471)** — generic vault-migration scripts via per-user config, deep-1 operator-narrative docs moved to vault, CHANGELOG slug-leak fix (#5d1f241), private-slug template rename to neutral identifier (#ae6d5e9).
9. **2026-05-22 — gsd Pattern Adoption Epic #517 (#518 #519 #520 #521)** — four mechanical hardening patterns: STATE.md write-lock via `withStateMdLock` + real cross-process mutex (tmp+linkSync) (#518), `pre-bash-templates-first` PreToolUse hook + `/templates-ack` bypass + transcript-history helper (#519), Slopcheck (LLM-hallucinated package detection) via `scripts/lib/slopcheck.mjs` + `classifyPackages` + supply-chain-slopcheck probe + plan Phase 3.5 Package Legitimacy Audit (#520), bounded auto-fix loop via `runQualityGateWithRetry` + RCE-equiv docs + drift banner + diagnostics redaction (#521). +122 unit tests across all 4 patterns.
10. **2026-05-23 — F2 Memory & Personas cluster (#501 #503 #505 #506 + Pattern-Quality Follow-Up #522–#528 + Memory-Proposals Hardening #540–#549)** — **F2.1** `memory.propose` agent-writable CLI + session-end Phase 3.6.3 AUQ collector + `memory-proposal-collector` agent (#501), **F2.3** session-start memory banner via `renderMemoryBanner` (#505), **F2.4** Peer Cards Foundation (USER.md + AGENT.md per-repo behavioural identity) (#503), **F2.5** Dialectic-Deriver orchestrator pattern with cadence helper + session-end Phase 3.6.7 auto-trigger (#506), Pattern Quality Follow-Up (#522–#528) shipping in-session cleanups, Memory-Proposals Hardening Sweep (#540–#545) + Tail-End Sweep (#495 #509 #546–#549).

No breaking changes. The new commands (`/brainstorm`, `/debug`, `/persona-panel`, `/memory-cleanup`, `/harness-audit`, `/repo-audit`, `/portfolio`, `/templates-ack`, `/autopilot-multi`) are opt-in additions; the F2 Memory cluster fields in Session Config (`memory.proposals.enabled`, `memory.banner.enabled`, `cold-start.enabled`, `state-md-lock.enabled`, `slopcheck.enabled`, `templates-first.enabled`, `verification-auto-fix.enabled`) all default to safe values (most opt-in, banner opt-out). Existing `/session`, `/go`, `/close`, `/evolve`, `/plan`, `/discovery`, `/autopilot`, `/test`, `/bootstrap` flows are byte-equivalent for repos that haven't enabled the new opt-ins.

### Added

- **F2 Memory & Personas cluster — F2.1 Agent-writable Memory-Tool (#501)** — `scripts/memory-propose.mjs` CLI (env-gated by `SO_WAVE_AGENT=1`, exits with status dict: `queued=0`, `quota-exceeded=1`, `rejected-low-confidence=2`, `rejected-wrong-context=3`, `error=4`), 4-module store at `scripts/lib/memory-proposals/` (schema/store/collector/sink), atomic linkSync quota lock, session-end Phase 3.6.3 AUQ multi-select with 4-per-batch FIFO pagination, confidence floor 0.5, privacy-redacted `hooks/pre-bash-memory-propose-audit.mjs`. NEW `agents/memory-proposal-collector.md` (reference documentation, not dispatchable). Session Config: `memory.proposals.enabled` (default `true`), `memory.proposals.quota-per-wave` (5), `memory.proposals.confidence-floor` (0.5). +193 tests.
- **F2 Memory & Personas cluster — F2.3 Session-Start Memory Banner (#505)** — `scripts/lib/memory-banner.mjs` `renderMemoryBanner()` reads top-5 surfaced learnings + memory-stats line + USER.md/AGENT.md excerpt; renders at session-start Phase 6.7. Operator-confidence anchor borrowed from doobidoo/mcp-memory-service. Session Config: `memory.banner.enabled` (default `true`). Silent no-op when disabled or persistence is off.
- **F2 Memory & Personas cluster — F2.4 Peer Cards Foundation (#503)** — `.orchestrator/peers/USER.md` + `.orchestrator/peers/AGENT.md` per-repo behavioural identity files, `scripts/lib/peer-cards/staleness-banner.mjs` (`checkPeerCardsStaleness`) surfaces banner at session-start Phase 4 when card age >30d (suggests `/evolve --dialectic`). Cross-reference: `.claude/rules/owner-persona.md` (host-wide `owner.yaml`) and vault `type: peer-card`.
- **F2 Memory & Personas cluster — F2.5 Dialectic-Deriver (#506)** — NEW `agents/dialectic-deriver.md` (read-only, haiku, bounded budget) reasons over top-50 learnings + last-10 sessions + existing peer cards to derive USER.md/AGENT.md updates. **Pattern B (orchestrator-pattern)** — does NOT use Anthropic SDK directly (per `.claude/rules/prompt-caching.md:3` scope rule). Session-end Phase 3.6.7 auto-trigger via cadence helper mirroring auto-dream API.
- **gsd Pattern Adoption Epic #517 — STATE.md Write-Lock (#518)** — `scripts/lib/state-md-lock.mjs` `withStateMdLock(repoRoot, fn)` + real cross-process mutex via tmp-file + `linkSync` atomic acquire (TOCTOU-safe), PID-liveness stale-detection with atomic override on dead PID + WARN on stderr, 10s default timeout. PSA-005 mechanical complement to PSA-003/PSA-004. Session Config: `state-md-lock.enabled` (default `true`), `state-md-lock.timeout-ms` (10000).
- **gsd Pattern Adoption Epic #517 — Templates-First Hook (#519)** — NEW `hooks/pre-bash-templates-first.mjs` PreToolUse hook + transcript-history helper. Detects `gh`/`glab` issue/MR/release create without prior template read, prompts operator to read template first. Per-session bypass via `/templates-ack` command. Session Config: `templates-first.enabled` (default `true`), `templates-first.hosts` (default `[github, gitlab]`). Symlink rejection via `lstatSync` + symmetric bypass-prefix strip.
- **gsd Pattern Adoption Epic #517 — Slopcheck (#520)** — NEW `scripts/lib/slopcheck.mjs` `classifyPackages()` defends against LLM-hallucinated package names ("Slopsquatting"). Classifications: `LEGITIMATE` (download_count > threshold), `ASSUMED` (very new / low downloads — warning), `SUS` (audit warning — confirmation required), `SLOP` (not in registry — hard block in plan-flow). NEW `skills/discovery/probes/supply-chain-slopcheck.mjs`. NEW plan Phase 3.5 Package Legitimacy Audit. Session Config: `slopcheck.enabled` (default `false`), `slopcheck.sources` (`[plan, discovery]`). Complementary to SEC-020 baseline (`ignore-scripts=true`, `block-exotic-subdeps=true`, `minimum-release-age=1440`).
- **gsd Pattern Adoption Epic #517 — Bounded Auto-Fix Loop (#521)** — `runQualityGateWithRetry()` from `scripts/lib/quality-gate.mjs` dispatches up to N code-implementer fixer-agent retries on inter-wave Quality-Gate failure before hard abort. Fixer prompt MUST include `.claude/rules/test-quality.md` "test-the-mock" anti-pattern reminder. Diagnostics-bundle written to `.orchestrator/metrics/verification-failures/<ISO>.json` on retry exhaustion. NEW `.claude/rules/quality-gates-autofix.md`. NEW `scripts/lib/qg-command-drift-banner.mjs` (RCE-equiv trust-anchor banner). Session Config: `verification-auto-fix.enabled` (default `false`), `verification-auto-fix.max-retries` (2).
- **Persona-Panel Foundation (#457–#480)** — NEW `skills/persona-panel/` skill + `commands/persona-panel.md` + `templates/personas/` (4 personas: buyer, expert, compliance, custom). Three reconciliation modes: voting-quorum (majority approves), hard-gate-threshold (any veto blocks), coordinator-summary (advisory only). Dispatches N persona agents in parallel via `Agent` tool, writes timestamped sidecars to `.orchestrator/persona-panel/<ISO>/`. Polish triplet (#474 #479 #480) hardens reconciliation contract.
- **harness-audit Cat-8 flagship (#472–#478)** — NEW `skills/harness-audit/` 8-category rubric scoring (Anthropic large-codebase best-practice rubric), `commands/harness-audit.md`. Surfaces architectural smells via correctness/debt sub-cluster.
- **repo-audit baseline compliance skill** — NEW `skills/repo-audit/` 9-category checklist (Configuration, Code Quality, Git Hygiene, CI/CD, Testing, Security, Documentation, Clank Integration optional, MCP Configuration). Markdown report + JSON sidecar at `.orchestrator/metrics/repo-audit-<timestamp>.json`. Companion `commands/repo-audit.md`.
- **gitlab-portfolio skill (GL #41 #42)** — NEW `skills/gitlab-portfolio/` + `commands/portfolio.md`. Cross-repo health dashboard discovered from vault `01-projects/*/_overview.md` frontmatter. Aggregates open issues, MRs, critical labels, stale signals via parallel `glab`/`gh` calls; writes idempotent `_PORTFOLIO.md`. Session-start Phase 2.7 dry-run banner when `gitlab-portfolio.enabled: true`.
- **Auto-skill-dispatch meta-skill** — NEW `skills/using-orchestrator/` phrase-match meta-skill. Silent no-op when `auto-skill-dispatch: false` (default).
- **Architecture skill** — NEW `skills/architecture/` (LANGUAGE.md vocabulary — Module / Interface / Implementation / Depth / Seam / Adapter / Leverage / Locality). Surfaces shallow modules and hypothetical seams.
- **convergence-monitoring skill** — NEW `skills/convergence-monitoring/`. Three signals (shrinking diff, pass-rate plateau, velocity) drive Stop/Continue/Investigate at inter-wave checkpoints. Primary consumer: `/autoresearch` loops + wave-executor inter-wave checkpoints.
- **Frontmatter-guard skill** — NEW `skills/frontmatter-guard/`. Injects canonical vault frontmatter schema snippet into agent prompts before vault-write tasks. Wired into wave-executor pre-dispatch hook.
- **Mode-selector skill — Phase B-2 wired** — `selectMode(signals)` pure function, first wired invocation at session-start Phase 7.5 (pre-Phase-8). High-confidence output pre-selects AUQ option. Writes `mode-selector-accuracy` learning to `learnings.jsonl` (Phase B-4).
- **prompt-caching always-on rule (#421)** — NEW `.claude/rules/prompt-caching.md` path-scoped rule (PC-001..PC-007). Documents Anthropic prompt-caching placement discipline (last shared block, never on per-request placeholder), 5-min vs 1h TTL trade-off, `max_tokens: 0` pre-warm pattern + hard rejections, Vercel AI SDK adapter shape, breakpoint budget + order (4 max, tools→system→messages, 20-block lookback), PC-007 smoke test for cache-hit verification.
- **verification-before-completion always-on rule (#38)** — NEW `.claude/rules/verification-before-completion.md`. Iron Law: *NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE*. 5-step Gate Function (IDENTIFY → RUN → READ → VERIFY → STATE), Common Failures table, 9 banned phrases.
- **receiving-review always-on rule (#40)** — NEW `.claude/rules/receiving-review.md`. 6-step pattern (READ → UNDERSTAND → VERIFY → EVALUATE → RESPOND → IMPLEMENT). 6 forbidden phrases (no performative agreement). Source-specific posture table. YAGNI check before "implement properly" suggestions.
- **loop-and-monitor always-on rule** — NEW `.claude/rules/loop-and-monitor.md`. Decision tree for `/loop` vs `Monitor` vs Routines/Desktop scheduled tasks. Coverage rule: Monitor filter must match every terminal state (silence is not success).
- **Marketplace + SEC cluster (GH #44 + #43 + #34 + GL #213)** — four-issue cluster shipped via 2026-05-16 deep-4. User-facing additions: SEC path-traversal guard on `--vault-dir`/`vault-integration.vault-dir` (CWE-22), Codex composer plugin icon, awesome-codex-plugins icon-enhancement submission docs, ComposioHQ/awesome-claude-plugins submission refresh (v3.2→v3.6 + new "Session & Workflow Orchestration" category proposal). Tests **5285 → 5303** (+18: 5 Q1 path-traversal + 12 Q2 R6 composerIcon + 1 foreign-session port-fix). validate-plugin **43 → 46** (+3 R6 codex `composerIcon` field-presence + file-exists + valid-XML/SVG-root checks).
  - **`scripts/lib/gitlab-portfolio/cli.mjs` path-traversal guard (#44, SEC)** — applies `validatePathInsideProject` (two-phase: lexical + symlink) to resolved `--vault-dir` against `os.homedir()`. Rejects `/tmp/../etc`, symlink escapes, and out-of-home absolutes with `exit 2`. Mirrors `scripts/lib/playwright-driver/runner.mjs:130-142` pattern (set in #402, deep-5). New `### Security` subsection in `skills/gitlab-portfolio/SKILL.md` documents the contract.
  - **Codex plugin icon (#43, NEW)** — `assets/icon.svg` (512×512, currentColor, five-wave glyph representing the 5-wave orchestration model). `.codex-plugin/plugin.json` `interface.composerIcon` field references it. Also bumped stale `.codex-plugin/plugin.json` version 3.5.0 → 3.6.0 to match `package.json`.
  - **Marketplace submission prep (#34 + #213)** — `docs/marketplace/awesome-codex-plugins-submission.md` (NEW) + `docs/submissions/awesome-codex-plugins-pr-body.md` (NEW) for the hashgraph-online/awesome-codex-plugins icon-enhancement PR (existing listing). `docs/marketplace/composio-submission.md` refreshed v3.2.0→v3.6.0 (feature summary + comparison table updated). `docs/submissions/composio-awesome-claude-plugins-pr-body.md` (NEW) for ComposioHQ/awesome-claude-plugins. Both upstream PRs deferred to operator action.

- **Anthropic-adoption cluster (GL #409 #410 #411 #412 #414 + GH #45)** — six patterns adopted from Anthropic-published reference repos. User-facing additions: new operator-steering hook (mid-wave guidance via `STEER.md` handshake), `mcp-builder` SKILL.md docs for `@tool` decorator + in-process MCP, security-reviewer false-positive exclusions reducing noise ~35%→15%. Components: 37 skills, 16 commands, **11 hook handlers** (+1: `hooks/operator-steer.mjs`), **11 hook matchers** (+1: PostToolBatch entry for operator-steer). Tests **5255 → 5285** (+30). validate-plugin 43/43.
  - **`hooks/operator-steer.mjs` (#409, NEW)** — PostToolBatch hook reading `.orchestrator/STEER.md`, emitting contents as `{ systemMessage }` JSON to stdout, then truncating. Lets the operator inject guidance mid-wave without aborting. Errors swallowed (always exit 0). Adopted from `anthropics/cwc-long-running-agents` claude-code-config/.claude/hooks/steer.sh. Registered in both `hooks.json` (Claude Code) and `hooks-codex.json` (Codex CLI). Cursor support deferred (different event model).
  - **`skills/mcp-builder/SKILL.md` updated (#410)** — new "Tool-Hosting Pattern wählen — In-Process vs Stdio MCP" section after Phase 1 (Research & Plan). Decision tree (≤5 tools / latency-critical / no external auth → in-process; else stdio), `@tool` decorator Python + TS `registerTool` snippets, `readOnlyHint`/`destructiveHint` annotation docs, trade-off table. Adopted from `anthropics/claude-agent-sdk-python` (examples/mcp_calculator.py). File grew 164→250 lines.
  - **OTel `gen_ai.*` aliases on subagents.jsonl (#411, additive)** — `gen_ai.usage.input_tokens` + `gen_ai.usage.output_tokens` + `gen_ai.system: "anthropic"` alongside existing `token_input`/`token_output`. Schema-version stays at 1 (additive optional fields, backwards-compat). Source: `anthropics/claude-code-monitoring-guide` OTel Semantic Conventions for GenAI. **Scope-reduced** during Discovery: events.jsonl Stop hook does not carry token/model fields; `gen_ai.request.model` and `gen_ai.response.finish_reasons` require upstream harness changes and are deferred.
  - **`agents/security-reviewer.md` Hard Exclusions section (#412)** — 5 new FP-pattern sub-classes (Open Redirect without CWE-601 surface, Memory-Safety in GC'd languages, Regex catastrophic backtracking without trigger, SSRF in HTML-only routes, Memory Leak without reproducer) plus cross-references to existing Exclusions and Confidence Calibration sections. Empirical FP-reduction ~35%→15% per upstream `anthropics/claude-code-security-review` (claudecode/findings_filter.py:L20-100). File grew 183→212 lines.
  - **`docs/marketplace/knowledge-work-plugins-submission.md` + `docs/submissions/knowledge-work-plugins-pr-body.md` (#414, NEW)** — submission prep for `anthropics/knowledge-work-plugins` Cowork-Marketplace. Manifest already compliant with Anthropic's canonical schema (`name`, `description`, `author.name`, `author.email`); npm-style additions (`version`, `homepage`, `repository`, `license`, `keywords`) are tolerated extensions. Trimmed README plan + PR body drafted. External branch/PR step intentionally deferred (manual).
  - **`scripts/lib/gitlab-portfolio/aggregator.mjs` execWithTimeout refactor (GH #45)** — `promisify(execFile)` does NOT honor AbortSignal; refactored to raw `spawn()` + `controller.signal` per `scripts/lib/playwright-driver/runner.mjs:232-245` pattern (set in #399). Opts-override key renamed `execFile`→`spawn`. EventEmitter-style child mock helper added to `tests/lib/gitlab-portfolio/aggregator.test.mjs`. Signature `execWithTimeout(cmd, args, opts) → { stdout, stderr }` preserved.

- **Superpowers-adoption cluster (GH #35 umbrella + #36 #37 #38 #39 #40)** — five high-leverage patterns adopted from [obra/superpowers](https://github.com/obra/superpowers). User-facing additions: 2 new slash commands (`/brainstorm`, `/debug`) + 2 always-on rules. Components: **37 skills** (+3: `brainstorm`, `debug`, `write-executable-plan`), **16 commands** (+2: `/brainstorm`, `/debug`), **17 rules** (+2: `verification-before-completion`, `receiving-review`). 11 NEW files + 9 cross-ref edits across `wave-executor`, `code-implementer`, `session-reviewer`, `plan`, `session-plan`, and 3 existing rule files. Tests **5129 → 5256** (+127). validate-plugin 43/43.
  - **`/brainstorm` skill + command (#36)** — lightweight Socratic design dialogue (3–5 AUQ rounds) before any implementation work. HARD-GATE prevents Edit/Write tool use until the user approves the design. Writes spec to `docs/specs/YYYY-MM-DD-<slug>-design.md`. Hand-off to `/plan feature` (primary) or `/write-executable-plan` (alternative). Use BEFORE `/plan feature` when scope/UX is still ambiguous.
  - **`/debug` skill + command (#37)** — 4-phase systematic debugging with Iron Law: *NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST*. Phases: Root Cause → Pattern → Impact → Solution. Writes a Phase-1 artifact at `.orchestrator/debug/<session>-<n>.md` that fix-implementers must reference. wave-executor Error Recovery now routes bugfix-classified tasks through this skill.
  - **`.claude/rules/verification-before-completion.md` (#38, always-on)** — Iron Law: *NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE*. 5-step Gate Function (IDENTIFY → RUN → READ → VERIFY → STATE), Common Failures table, 9 banned phrases ("should pass", "looks correct", "Great!", "Done!", etc.). Bidirectional See-Also with `development.md`, `testing.md`, `cli-design.md`. Same enforcement weight as `ask-via-tool.md` (AUQ-001) and `parallel-sessions.md` (PSA-001).
  - **`.claude/rules/receiving-review.md` (#40, always-on)** — 6-step pattern for handling code-review feedback: READ → UNDERSTAND → VERIFY → EVALUATE → RESPOND → IMPLEMENT. 6 forbidden phrases (no performative agreement: "You're absolutely right!", "Great point!", "Excellent feedback!", "Let me implement that now"). Source-specific posture table (human=trust-after-understanding, plugin reviewers=skeptical, security=take-seriously). YAGNI check before "implement properly" suggestions.
  - **`skills/write-executable-plan/` skill (#39)** — bite-sized executable-plan format consumed by other skills. Mandatory 5-step structure per Task: write-failing-test → run-to-confirm-failure → implement → verify-pass → commit. Phase 4 placeholder linter rejects `TBD` / `TODO` / `FIXME` / `XXX` / "add appropriate error handling" / "similar to Task N" / `<placeholder>`. Reference dogfood plan at `docs/plans/2026-05-16-superpowers-cluster.md` documents this very session in the format. wave-executor, code-implementer, and session-plan updated to prefer bite-sized plans when present.
- **`/memory-cleanup` command + skill** — Manual memory consolidation (Dream-equivalent). Ports the previously personal `~/.claude/skills/memory-cleanup/` into the plugin so all users get it. Implements the 4-phase Dream process (Orient → Gather Signal → Consolidate → Prune & Index) against `~/.claude/projects/*/memory/`.

- **Clawpatch Borrow cluster (#415–#420)** — six architectural patterns borrowed from the upstream Clawpatch repo. Patterns adopted at hook, skill, agent, and rule levels.

- **Claude Code 2.1.x adoption matrix (#427–#436, #426)** — harness-alignment for Claude Code 2.1.x cohort. Monitor cadence `when=on-skill-invoke` (#427 follow-up), echo-stub detector for skill-output validation, deeper hook signal propagation. Documentation in `docs/research/2026-05-17-clawpatch-cluster.md` + companion notes for prompt-cache pre-warming (#421 follow-on).

- **Pre-commit owner-leakage hook (#494)** — `.husky/pre-commit` now invokes `scripts/lib/validate/check-owner-leakage.mjs` against the staged tree, blocking commits that leak private slugs, home paths, or non-public hosts. Closes the `git add <leak> && git commit` window that CI couldn't catch. `--no-verify` remains available but logs a warning per `.claude/rules/development.md` Git Safety Protocol. Regression test in `tests/husky/pre-commit-owner-leakage.test.mjs`.

- **Privacy/Public-Mirror Epic (#462 #461 #468 #469 #470 #471)** — generic vault-migration scripts via per-user config, deep-1 operator-narrative docs moved to vault, CHANGELOG slug-leak fix (#5d1f241), private-slug template rename to neutral identifier (#ae6d5e9). `scripts/migrate-vault-paths.mjs` + `scripts/run-migrate-v2-cross-repo.mjs` + `scripts/sync-vault-schema.mjs` are now config-driven.

- **GitHub social-preview tooling (`scripts/upload-social-preview.mjs`)** — Playwright-based GitHub social-preview (OG card) upload. NEW `assets/og-card.svg` + `assets/og-card.png` (1280×640) exported source. Companion `chore(launch-readiness): README rewrite + GitHub settings + steering drift + 3 mermaids + OG card` (#701edde).

### Changed

- **`scripts/parse-config.mjs` — preserve `~/`-paths + inline YAML object literals (#497)** — earlier `parse-config.mjs` collapsed `~/path/foo` → empty string and dropped inline `{ … }` YAML object literals (e.g. `memory:\n  banner: { enabled: true }`). Both now round-trip correctly. Fix shipped via `fix/parse-config-tilde-paths-497` MR merged 2026-05-22.
- **Coordinator model — sonnet → opus on Claude Code path** — `skills/wave-executor/SKILL.md` + `commands/go.md` now default to `opus` for the coordinator role on Claude Code (parity with Codex default). Brings the orchestrator within the Anthropic best-practice cohort for orchestration agents. Override remains available via `agent-mapping.coordinator-model`.
- **`/session` slash-command model-invocable (#cfb4f3a)** — `commands/session.md` no longer carries `disable-model-invocation: true`. Slash-command-via-model lookup now succeeds. Companion lint canary in `validate-plugin.mjs` enforces the naming convention.
- **CLAUDE.md lean-root refactor** — top-level CLAUDE.md trimmed; deep narrative moved to `.orchestrator/steering/` + Meta-Vault `01-projects/session-orchestrator/decisions.md`. New `agents/AGENTS.md` (path-scoped) carries sub-agent authoring spec.
- **`.orchestrator/runtime/` gitignored (#20aa844)** — fixes W5 2026-05-22 deviation drift artifact noise.

### Fixed

- **`/evolve` null-subject dedupe collapse (#284, consumer repo)** — `consolidateDuplicates` in Phase 3.5 step 7 and Phase 4.4 step 4 previously keyed on `${type}::${subject}`, which coerced `null` to the string `"null"` and collapsed all null-subject entries of the same type to a single survivor. Fix: entries with null/empty/missing `subject` are now keyed by their always-unique `id`, so each is preserved individually. Named-subject entries continue to dedupe as before (highest confidence wins). Companion regression test: `tests/skills/evolve/dedupe-null-subject.test.mjs` (10 assertions: 7 positive contract + 3 negative proof of old breakage).
- **`scripts/vault-consolidate.mjs` isUtf8() binary detection (#508)** — `vault-consolidate` previously crashed on binary files in vault paths (PNG/PDF). Now uses `Buffer.isUtf8()` to skip non-text files cleanly. Closes #508.
- **Memory-Proposals W2 + W4 regression cluster (#543 #544 #545 #540 #541)** — hardening sweep on F2.1: W2-I1 PID-liveness in `scripts/lib/memory-proposals/store.mjs` + path-utils `canonicalizeRoot` (mirrors `session-lock.mjs`); W2-I2 `SO_WAVE_AGENT=1` env-var guard via prompt-injection (full status dict: queued=0, quota-exceeded=1, rejected-low-confidence=2, rejected-wrong-context=3, error=4); W2-I3 `_parseMemoryProposals` locality move -121 LOC; W2-I4 3-layer ISO-8601 defense (placeholder→programmatic, validator regex, regression test). +44 tests.
- **Memory-Proposals Tail-End Sweep (#546 #547 #548 #549 #495 #509)** — surfaced real audit-hook regex leak (#546) + latent W? waveId contract violation (#547) during W4 review. Both fixed in-session.
- **CI green restoration follow-ons** — owner-leakage CI guard `SELF_EXCLUSIONS` for persona content-lint test (#68e6195), plugin-schema-validate ajv-cli local fetch (#84455b), windows-runner cross-platform `process.execPath` + `path.delimiter` + CRLF normalization (#6d41c71), gitleaks image entrypoint disable (#a8a64a9), test timeout 30s + apt-get guard (#479181c).
- **`scripts/lib/language-mappers/typescript.mjs` ExportAllDeclaration handling (#454)** — TypeScript mapper now correctly parses `export * from 'x'` statements (`ExportAllDeclaration` AST node type). Previously silently dropped re-exports.
- **W5 security-reviewer MEDs (#aebc1df)** — npm argv injection guard + bypass-prefix boundary fix shipped via security-reviewer follow-on.

### Quality verification

- 7360 vitest passing, 12 skipped, 0 failed (was 5001 at v3.6.0 cut, **+2359 tests, ~47% growth**)
- typecheck 230 files OK (was 67 at v3.6.0 cut)
- lint 0 errors
- validate-plugin **94/94** (was 43 at v3.6.0 cut)
- doc-consistency 0 findings
- check-owner-leakage **0 leaks across 995 scanned files**
- gitleaks 0 leaks
- npm audit 0 high-severity vulnerabilities
- GitLab CI pipeline #4713 on `00684df` — all 6 stages green (gitleaks-scan, npm-audit, owner-leakage, test, schema-drift-check, coverage)

### Components (current)

- **37 skills** (+3 since v3.6.0: `persona-panel`, `harness-audit`, `repo-audit`; net unchanged from [Unreleased] mid-point as `gitlab-portfolio`, `convergence-monitoring`, `architecture`, `using-orchestrator` were all added during v3.6.0→v3.7.0 cycle)
- **18 commands** (+6 since v3.6.0: `/brainstorm`, `/debug`, `/memory-cleanup`, `/persona-panel`, `/harness-audit`, `/repo-audit`, `/portfolio`, `/templates-ack`, `/autopilot-multi` — net +6 visible)
- **13 agents** (+2 since v3.6.0: `dialectic-deriver`, `memory-proposal-collector`; `analyst` was added earlier in [Unreleased])
- **14 hook handlers** (+3 since v3.6.0: `pre-bash-memory-propose-audit`, `pre-bash-templates-first`, `cwd-change-restore`, `enforce-commands`, `operator-steer`)
- **20 always-on + path-scoped rules** (+5 since v3.6.0: `verification-before-completion`, `receiving-review`, `loop-and-monitor`, `prompt-caching`, `quality-gates-autofix`)

## [3.6.0] - 2026-05-14

Five deep sessions plus three intermediate fix-clusters since v3.5.0. The headline feature is the agentic `/test` command — a web/macOS end-to-end test orchestrator with a 4-check UX rubric, two test drivers (Playwright + Peekaboo), a `ux-evaluator` reviewer agent, and issue-tracker reconciliation. Tests grew from 4430 to **5001** (+571), validate-plugin 27 → **43** (+16 R5 grep-canaries), zero breaking changes, zero CI regressions.

1. **2026-05-10 deep — ADR-364 thin-slice (#364–#374)** — autopilot `--multi-story` v1 scaffold (`scripts/autopilot-multi.mjs`), `gc-stale-worktrees` defence (#374 realpathSync symlink-escape fix), `validateWorkspacePath`, lazy `zx` import to dodge a CI fork-pool regression (#341).
2. **2026-05-10 deep-2 — CI restoration (#367 #368 #369)** — 8-pipeline silent regression fixed (root cause: `skills/vault-sync/pnpm-lock.yaml` tracked despite gitignore conflict with `engine-strict=true`).
3. **2026-05-14 deep-1 (Track A) — `/test` command Track A (#379 #380 #382)** — `agents/ux-evaluator.md` (4-check rubric: onboarding step-count ≤7, axe critical/serious, console-errors, Apple-Liquid-Glass `.glassEffect()` conformance on SwiftUI 26+), `skills/playwright-driver/SKILL.md` (MCP-wired driver with screenshots / traces / `axe-*.json` / `console.ndjson` artifact layout), `skills/test-runner/` skeleton (phase model: Setup → Drive → Evaluate → Reconcile → Report), 4 NEW helper modules in `scripts/lib/test-runner/`. validate-plugin 28 → 31.
4. **2026-05-14 deep-1 — `/test` command + CI restore + reconcile glab (#383 #384 #388 #389)** — `commands/test.md` + Session Config `test:` block + profile registry schema (web-gate + mac-gate seeds at `.orchestrator/policy/test-profiles.json`), extended `issue-reconcile.mjs` with `listExistingFindings/createFinding/updateFinding/triageDecision` + sentinel-injection hardening + maxBuffer/body-length cap parity with `mr-draft.mjs`. validate-plugin 31/31.
5. **2026-05-14 deep-2 — `/test` Track B + mechanism-proof (#381 #385)** — `skills/peekaboo-driver/SKILL.md` (macOS native UI driver, 3-phase platform + permissions + remediation gate, AX-snapshot pattern), `scripts/lib/playwright-driver/runner.mjs` (260 LOC `spawn` + `AbortSignal` + profile registry integration + axe-core soft-skip + exit-code mapping 0/1/2), mechanism-proof via `--dry-run` against an EspoCRM web target. validate-plugin 31 → 34.
6. **2026-05-14 deep-3 — `/test` live-run + cluster (#385 #390 #391 #393 #394 #387 #392)** — first end-to-end live execution against a containerized EspoCRM web target, runner reporter syntax fix (`html,json` canonical, not `html:<path>` Jest-style), `isPathInside` traversal validation in `config/test.mjs` and `profiles/schema.mjs`, peekaboo SKILL.md polish, `.claude/rules/testing.md` § "Shared-Hardware Runner Contention (Mac shell executors)" subsection added. validate-plugin 34 → 36.
7. **2026-05-14 deep-4 — `/test` pipeline housekeeping (#395 #396 #397 #398 #399 #400 #401)** — Division-of-Responsibility doc-sync (driver writes scaffold + HTML/JSON/traces; test fixture writes `ax-snapshots/` + `axe-*.json` + `console.ndjson`), `shared/profiles` → `profiles/` rename, runDir path-traversal hard-error guard, AbortController test gap closed, two-phase realpath guard in 3 callsites, `@lib/*` vitest alias scaffolded, `RUBRIC_GLASS_V2=1` env-gate for glass-modifiers stub. validate-plugin 36 → 39.
8. **2026-05-14 deep-5 — `validatePathInsideProject` helper + @lib alias rollout + boundary tests (#402 #404 #405 #406; #407 filed)** — NEW `scripts/lib/path-utils.mjs` `validatePathInsideProject(p, root)` (tagged-union `{ ok, realPath, lexicalPath, reason }`, adopted at all 3 callsites with 3-line adapters preserving each callsite's original semantics), 33 test files migrated to `@lib` vitest alias, NEW `tests/lib/playwright-driver/runner-boundary.test.mjs` (3 falsification tests: PATH_MAX-via-traversal, null-byte env-strip, double-abort idempotency), TOCTOU fix in `tcProfilesPath` storage (symmetric in `schema.mjs` rubric), `mkdtempSync` worktree-freshness flake eliminated. validate-plugin 39 → **43**. Tests 4982 → **5001p/0f/12s**.

No breaking changes. The `/test` command and `commands/autopilot.md --multi-story` are opt-in additions; existing `/session`, `/go`, `/close` flows are byte-equivalent.

### Added (Unreleased) — /test command (#378–#407)

- **`/test` command** (commands/test.md, 86 LOC) — agentic end-to-end test orchestrator. Drives web flows (Playwright) and macOS native UI (Peekaboo); evaluates against `skills/test-runner/rubric-v1.md`; reconciles findings with the open issue tracker via `scripts/lib/test-runner/issue-reconcile.mjs`. Wraps upstream tools (no forks). Hard-gates Playwright MCP for browser drive (4× token cost vs CLI per Microsoft's own benchmark).
- **`skills/test-runner/`** — phase model (Setup → Drive → Evaluate → Reconcile → Report), profile registry with web-gate + mac-gate seeds, `rubric-v1.md` (4 checks).
- **`skills/playwright-driver/`** — thin driver wrapper around `playwright@1.60.0`. Captures token-frugal AX-tree snapshots + screenshots + console.ndjson under `.orchestrator/metrics/test-runs/<run-id>/`. `runner.mjs` (260 LOC) with `spawn` + `AbortSignal` + axe-core soft-skip + DI seams.
- **`skills/peekaboo-driver/`** — thin driver wrapper around `@steipete/peekaboo@3.1.2`. Captures native macOS UI snapshots. 3-phase guard (platform + permissions + remediation).
- **`agents/ux-evaluator.md`** — read-only opus agent that applies the 4-check rubric (onboarding step-count ≤7, axe critical/serious, console-errors visible to user, Apple-Liquid-Glass `.glassEffect()` conformance on SwiftUI 26+) against driver-captured artifacts and emits stable-fingerprint findings JSON.
- **`scripts/lib/path-utils.mjs`** — `validatePathInsideProject(p, root)` two-phase lexical+realpath guard with tagged-union return. Adopted at 3 callsites with 3-line adapters preserving silent-skip vs throw vs hard-error semantics.
- **`scripts/lib/test-runner/`** — `fingerprint.mjs`, `artifact-paths.mjs`, `issue-reconcile.mjs`, profile registry+schema. Pure helper modules, DI-friendly.
- **R5 grep-canary validators** — `check-playwright-mcp-canary.mjs`, `check-peekaboo-driver-canary.mjs`, `check-path-utils-canary.mjs` extend `validate-plugin.mjs` from 28 to 43 PASS checks.

### Added (Unreleased) — autopilot multi-story (#364–#374, #341)

- **`scripts/autopilot-multi.mjs`** — `--multi-story` orchestration mode running N parallel issue pipelines in isolated git worktrees with per-loop kill-switches. Built on the ADR-364 substrate (sessions.jsonl optional fields, autopilot.jsonl extensions, `STALL_TIMEOUT` kill-switch, `gc-stale-worktrees`, `validateWorkspacePath`).
- **`gc-stale-worktrees`** — realpathSync symlink-escape defence (#374) prevents removal of worktrees that resolve outside the configured root.

### Changed (Unreleased)

- **`shared/profiles` → `profiles/` rename** (#400) — 6 mechanical refs + 2 `git mv` + 3 new R5 anchor canaries (ARCH-PD-MED-3 closure).
- **`@lib/*` vitest alias** — 33 test files now use `import … from '@lib/…'` instead of `'../../../../scripts/lib/…'` (reduces relocation churn). Remainder (~25 files + 2 dynamic-import files) deferred to #407.
- **CLAUDE.md trim** — 46.3k → 8.9k chars. 5 deep-session narratives moved to vault `decisions.md` as canonical long-form; CLAUDE.md keeps one-line summaries + commit index.
- **README test-count badge** — 4944 → 5001.
- **`.gitignore`** — `.orchestrator/scratch/` added; `.orchestrator/metrics/test-runs/` covered.

### Fixed (Unreleased)

- **CI red on `cb3e942` (#367–#369)** — 8-pipeline silent regression. Root cause: `skills/vault-sync/pnpm-lock.yaml` tracked despite `.gitignore:63` forbidding it — lockfile conflict + `engine-strict=true` → silent `npm install` exit-1. Also: `.github/workflows/test.yml` `fetch-depth: 1 → 0` (gitleaks revision-range was breaking).
- **runner.mjs reporter syntax bug (deep-3)** — Playwright rejected `--reporter html:<path>,json:<path>` (Jest/Vitest-style) with "Cannot find module 'html:<path>'". Fixed inline via canonical `--reporter html,json` + env vars `PLAYWRIGHT_HTML_OUTPUT_DIR` / `PLAYWRIGHT_JSON_OUTPUT_FILE` / `PLAYWRIGHT_HTML_OPEN=never`. 8 regression canaries with `/html:/` + `/json:/` Jest-style substring-rejection lockdown added by Q1 test-writer in deep-3.
- **runDir path-traversal MED (#398)** — `scripts/lib/playwright-driver/runner.mjs:110` lacked traversal check on `path.join(runsRoot, runId)`. Hardened with `isPathInside` guard + Phase 2 realpath upgrade in deep-4 Q-polish.
- **TOCTOU in `tcProfilesPath` storage (#405)** — raw user-supplied path stored before realpath resolution. Now stores `result.realPath || result.lexicalPath`. Symmetric pattern applied to `profiles/schema.mjs` rubric storage as Q2-LOW-2.
- **worktree-freshness flake (#406)** — `mkdtempSync` per-run unique suffix replaces `randomUUID()` suffix (atomicity + cleanup parity with existing `tmpdir()` pattern). 3/3 clean post-fix.
- **AbortController test gap (#399)** — 5 falsification-passing tests cover timer-firing on timeout, AbortSignal propagation, clearTimeout on normal exit, exit-code 2 mapping on SIGTERM, custom `timeout_ms` from profile-registry override.
- **`glabPath` arbitrary-binary injection (Q2 HIGH deep-1)** — removed `glabPath` parameter from `issue-reconcile.mjs`; replaced with `opts.execFile` DI seam mirroring `mr-draft.mjs`. `checkBodyLength` added (Q2 MED-1). Sanitizer regex flag `→ gi` (Q2 MED-2). +11 regression tests.

### Quality verification

- 5001 vitest passing, 12 skipped, 0 failed (was 4430 at v3.5.0 cut, **+571 tests**)
- typecheck 67/67
- lint 0 errors
- validate-plugin 43/43 (was 27/27 at v3.5.0 cut, **+16 R5 grep-canary checks**)
- doc-consistency 0 findings
- gitleaks 0 leaks
- npm audit 0 high-severity vulnerabilities

### Components (current)

- 32 skills (added: `test-runner`, `playwright-driver`, `peekaboo-driver`, `frontmatter-guard` from v3.4.0 onward)
- 12 commands (added: `/test`)
- 11 agents (added: `ux-evaluator`)
- 10 hook event matchers / 10 handlers

### Carryover to v3.7.0 cycle

- **#386** — mac-gate end2end (bootstrap-cost gated)
- **#403** — `RUBRIC_GLASS_V2` profile-config flag (v2 rubric gated)
- **#407** — @lib alias rollout remainder (~25 test files + 2 dynamic-import files)
- **#41** — `feat(gitlab-portfolio)`: cross-repo issue dashboard skill
- **#42** — `bug session-end quality-gate must execute test-command in execution env`
- **#35–#40** — `superpowers-adoption` umbrella + 6 sub-issues (read-only, brainstorm, verification-before-completion, bite-sized plans, receiving-code-review, systematic-debugging)
- **#297 / #298** — data-gated on autopilot RUN-Volumen (need ≥10 runs)

## [3.5.0] - 2026-05-09

Four deep sessions on top of v3.4.0 plus a non-tracked architectural refactor. Twenty issues closed (#344 to #363), tests grew from 3138 to **4430** (+1292), zero CI regressions, zero breaking changes. All sessions on `main`, isolation:none, enforcement:warn, cap=6.

1. **2026-05-08 deep-2** — 6 discovery-derived issues (#344 #345 #346 #347 #348 #349): refactor + test-coverage + validator + doc-drift. Tests 3138 → 3591 (+453). validate-plugin 22 → 27.
2. **2026-05-09 deep-1** — 5 repo-audit cluster (#350 #351 #352 #353 #354): DX + security tooling. CI security gates (gitleaks + npm-audit), git-hooks (Husky + commitlint + lint-staged), Prettier ignore-rules expansion, CLAUDE.md trim + vault long-form archival. Tests stable at 3591 (config-only changes).
3. **2026-05-09 deep-2** — 4-issue parallel-subagent cluster (#355 #356 #357 #358): CI critical fix + 16-module test backfill + 4 complexity-hotspot splits + 9th autopilot kill-switch. **5 waves × 6 parallel subagents** (first non-coord-direct deep session in 14+ session streak — explicit user override). Tests 3591 → 3888 (+297). 5 NEW production submodules + 1 NEW schema-leaf (Q3 follow-up).
4. **2026-05-09 deep-3** — 5-issue Anthropic-canonical agent-authoring alignment cluster (#359 #360 #361 #362 #363): primary-source-grounded refactor of all 10 agent files + validator alignment with [code.claude.com/sub-agents](https://code.claude.com/docs/en/sub-agents). Tests 3888 → 3896 (+8 net; existing color-pin tests rewritten, new validator-form tests added). 4 NEW frontmatter-validator behaviors (tools-array form, full model IDs, expanded color palette, error-message expansion). All 10 agents now Anthropic-reference-compliant for body length (500-3000w) + Output Format + Edge Cases sections.
5. **2026-05-09 deep-4** — 6-hotspot-split cluster (deep-4): 6 file-disjoint hotspots ≥400 LOC split into submodules <300 LOC each. Public APIs preserved via barrel re-exports — zero behavior change. Tests 3896 → 4430 (+534). 26 NEW `*.test.mjs` files. validate-plugin 27/27.

No breaking changes.

### Changed (Unreleased) — 2026-05-09 deep-3 (#359–#363)

- **#359 — `check-agents.mjs` + `agent-frontmatter.mjs` aligned with Anthropic canonical spec.** Three validator behaviors corrected against [code.claude.com/sub-agents](https://code.claude.com/docs/en/sub-agents):
  - **`tools` accepts both forms.** Comma-separated string (`Read, Edit, Write`) AND JSON array (`["Read", "Edit", "Write"]`). Anthropic's own reference agents (`anthropics/claude-code/plugins/plugin-dev/agents/agent-creator.md`, `plugin-validator.md`, `skill-reviewer.md`) all use array form — our previous validator would have rejected them. Malformed arrays (non-string elements, parse failures) still rejected with new rule codes `array-strings-only` and `malformed-array`.
  - **`color` palette expanded to 9.** Canonical Anthropic 8-color palette (`red, blue, green, yellow, purple, orange, pink, cyan`) plus `magenta` from plugin-dev SKILL.md for backward-compat. Was 6 (`blue, cyan, green, yellow, magenta, red`).
  - **`model` accepts full model IDs.** Pattern `^claude-(opus|sonnet|haiku)-\d+-\d+(-\d{8})?$` matches `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001` alongside the existing alias set (`inherit, sonnet, opus, haiku`). The canonical doc explicitly accepts both forms.
  - 9 NEW unit tests in `tests/scripts/validate/check-agents.test.mjs` + 5 NEW in `tests/lib/agent-frontmatter.test.mjs`. 1 existing test rewritten (`expect(color).toBe('blue')` → `'cyan'` for docs-writer post-color-fix).
- **#360 — 4 implementer agents normalized to Anthropic-reference body length.** `code-implementer` (187w → 815w), `db-specialist` (226w → 803w), `ui-developer` (236w → 845w), `test-writer` (294w → 996w). Each gained `## Output Format` (concrete report template) and `## Edge Cases` (4-7 bullets of project-relevant unusual scenarios) per the structural pattern from `anthropics/claude-code/plugins/plugin-dev/agents/agent-creator.md`. Existing strengths preserved: code-implementer's "DO NOT" list, db-specialist's data-loss safeguards, ui-developer's WCAG AA standards, test-writer's Falsification-Rule + computed-value prohibition. test-writer also gained an inline worked example for the Falsification-Rule (#363 partial).
- **#361 — `docs-writer` + `qa-strategist` Anthropic-section compliance.** docs-writer Output-Format upgraded from short status-line to full report template; gained `## Edge Cases` section (7 bullets covering missing-git-diff, audience-ambiguity, forbidden-path requests, conflicting sources, etc.). qa-strategist gained `## Edge Cases` section (8 bullets covering coverage-tool-missing, weak-assertion-quality, mock-without-assertion, property-test-opportunity, in-wave test-quality regression).
- **#362 — Color-collision fix across 4 agents.** Three same-color groups resolved (`yellow` ×2, `blue` ×3, `cyan` ×2 → 9 distinct co-dispatchable colors). Changes: `db-specialist` blue→purple, `docs-writer` blue→cyan, `session-reviewer` cyan→pink, `test-writer` yellow→orange. `qa-strategist` (cyan) + `docs-writer` (cyan) is the only remaining shared color but the two never co-dispatch (qa-strategist is opt-in `persona-reviewers`, docs-writer is Impl-Polish sub-slot). Mapping rationale: orange/pink/purple were validated unblocked by #359.
- **#363 — Worked examples for novel rules.** `test-writer` Falsification-Check inline example (VALID `add(2,3)→5` vs WORTHLESS `typeof === 'function'` vs WORTHLESS tautological-computation, ~12 lines). `session-reviewer` Silent-failure differentiation block (6 contrast cases distinguishing *graceful* fallback from *bug*: optional cache lookup vs auth error swallow, optional feature flag vs config load swallow, UI render fallback vs data-pipeline-drops-records). `security-reviewer` fully-filled HIGH SQL-injection finding with all required fields populated (file:line, category, confidence 0.95, exploit payload, impact, remediation citing the project's existing parameterized helper).
- **`CLAUDE.md` "Agent Authoring Rules"** rewritten with primary-source citations and accurate field descriptions. Removed the misleading "MUST be a comma-separated string, NOT a JSON array" claim (was stricter than Anthropic itself). Added body-convention guidance (Anthropic reference structure + 500-3000w range) and read-only-vs-implementer tool-set patterns.

### Fixed (Unreleased) — 2026-05-09 deep-2 (#355–#358)

- **#356 — harness-audit JSON truncation at byte 8188 (10+ failed CI runs since 2026-05-01)** — `scripts/harness-audit.mjs:256` was async-writing the final JSON to stdout, then immediately calling `appendAuditRecord()` + `writeSummary()` + `process.exit(0)`. On CI runners (both GitLab and GitHub Actions, both macOS and Windows), the process exited before the stdout pipe drained, truncating output at the OS pipe-buffer boundary (8 KiB on many Linux kernels = 8188 = 8192 − 4). Local runs were green because pipes drain instantly on dev hardware. **Fix:** Pattern A drain-aware `process.stdout.write(payload, callback)` — `appendAuditRecord` + `writeSummary` + `process.exit(0)` now run inside the drain callback. Q1 follow-up: added explicit `return;` after the error-path `process.exit(2)` to prevent fall-through to success-path side effects on stdout-write failure. NEW regression test in `tests/integration/harness-audit.integration.test.mjs` asserts `stdout.length > 8500` (floor above CI failure boundary, per test-quality.md floor/ceiling rule) AND `JSON.parse(stdout)` does not throw AND `stdout.trim().endsWith('}')`. Local verification: 12,519 bytes ending with `}\n`. Issue closed.

### Added (Unreleased) — 2026-05-09 deep-2 (#355–#358)

- **#355 — autopilot cumulative token-budget kill-switch (cross-repo from baseline#252)** — 9th kill-switch (`TOKEN_BUDGET_EXCEEDED: 'token-budget-exceeded'`). Implemented per the 9-step plan in the issue body: `FLAG_BOUNDS.maxTokens = { min: 0, max: 10_000_000, default: 500_000 }`; new entry in `KILL_SWITCHES`; check in `preIterationKillSwitch` after MAX_HOURS, before RESOURCE_OVERLOAD (cheap check first); accumulator in `runLoop` after `state.iterations_completed += 1` reading `sessionResult.usage.output_tokens` (or `total_tokens` as fallback); `total_tokens_used` field on `AutopilotState`; additive update to `skills/wave-executor/SKILL.md` Return Shape Contract documenting the `usage?` field. **Forward-compat:** when sessionRunner doesn't emit `usage`, `cumulativeTokens` stays 0 and the kill-switch never fires. Q4 follow-up: defaulted `opts.maxTokens` resolution to `0` (off, opt-in) instead of `FLAG_BOUNDS.maxTokens.default` to prevent a silent 500k cap on every existing caller once wave-executor starts populating `usage`. 3 NEW tests in `tests/lib/autopilot.test.mjs` (fires correctly, forward-compat preserved, accumulator monotonic). Issue closed.
- **#357 — backfill 16 untested modules in `scripts/lib/`** — 16 NEW `*.test.mjs` files, ~3,000 LOC of previously per-file-untested production code now covered. Distributed across 5 W2/W3 batches (file-disjoint, parallel-safe per W1 D5 conflict matrix). +297 net tests (3591 → 3888). Coverage:
  - `tests/lib/harness-audit/categories/category{1..7}.test.mjs` (7 NEW, 58 tests) — happy-path + failure + edge per category
  - `tests/lib/resource-probe/{probe-platform,evaluate,parsers}.test.mjs` (3 NEW, 99 tests) — pure functions hardcode-asserted, mocked spawn for branch coverage
  - `tests/lib/{owner-interview,workspace}.test.mjs` (2 NEW, 34 tests) — mocked owner-yaml + zx git
  - `tests/lib/ecosystem-wizard/{config-writer,wizard-prompt}.test.mjs` (2 NEW, 42 tests) — file-I/O + readline answer-injection
  - `tests/lib/vault-backfill/{glab,template}.test.mjs` (2 NEW, 32 tests) — mocked spawnSync + template cache
  - All files follow `.claude/rules/test-quality.md`: hardcoded expected values, floor/ceiling for dynamic counts, no Assert-Nothing / Test-the-Mock / Tautological Computation. Issue closed.
- **#358 — 4 complexity hotspots split into submodules** — 5 NEW production submodules:
  - `scripts/lib/autopilot/kill-switches.mjs` (148 LOC) — `KILL_SWITCHES` enum (now 9 entries) + `preIterationKillSwitch` + `postSessionKillSwitch`. `autopilot.mjs` 535 → 412 LOC.
  - `scripts/lib/state-md/recommendations.mjs` (57 LOC) — `parseRecommendations`. `state-md.mjs` 610 → 563 LOC.
  - `scripts/lib/mode-selector/context-pressure.mjs` (157 LOC) — `computeContextPressure` + 3 inlined helpers. `mode-selector.mjs` 572 → 480 LOC. Per W1 D3 risk warning, full per-signal scorer split (carryover/confidence/bootstrap/learnings) deferred — only the cleanly-separable `computeContextPressure` extracted this session.
  - `scripts/lib/learnings/io.mjs` (120 LOC) + `scripts/lib/learnings/filters.mjs` (43 LOC). `learnings.mjs` 529 → 39 LOC barrel after Q3 follow-up.
  - **Q3 follow-up:** NEW `scripts/lib/learnings/schema.mjs` leaf (~330 LOC) — extracts the schema/validator layer to break the circular-import topology that the original split produced (`learnings.mjs` re-exporting children that imported back from parent). Both children now import from sibling `./schema.mjs`; `learnings.mjs` is a thin barrel. Unidirectional dependency graph: `schema → io → filters → barrel` (no cycle).
  - All 4 hotspots now < 500 LOC. All public APIs preserved via barrel re-exports — verified by NEW `tests/lib/refactor-stability.test.mjs` (24 adapter tests asserting symbol presence + 1 smoke call per re-export). Issue closed.

### Quality verification

- 184 test files / **3888 pass** / 11 skip / 0 fail (baseline 3591 → +297). typecheck 66/66. lint 0 errors. validate-plugin 27/27. check-doc-consistency 0 findings. harness-audit emits 12,519 bytes ending with `}\n` locally — root cause of the 10+ run CI streak fixed.

### Carry-forward (next session)

- W4 Q2 MEDIUM: `tests/lib/resource-probe/probe-platform.test.mjs` overly-generous predicate for `processCounts` null-output branch; `tests/lib/workspace.test.mjs` runtime branching in test body. Both work correctly today; tighten in next session.
- W4 Q3 MEDIUM: `state-md/recommendations.mjs` and `learnings/filters.mjs` are shallow per LANGUAGE.md. Justification today is the LOC budget; future split should target contract-boundary seams instead.
- W4 Q3 MEDIUM: `clamp` / `round2` / `safeArray` duplicated between `mode-selector.mjs` and `mode-selector/context-pressure.mjs` (4 lines each). Extract to a private `mode-selector/_helpers.mjs` when the deferred scorer split lands.
- W4 Q5 MEDIUM: pre-existing silent-drop of `malformed` in `scripts/export-hw-learnings.mjs:343` — the `readLearnings` extraction surfaced the contract; surface it in stderr in a follow-up.
- W4 Q4: plumb `--max-tokens` through `parseFlags()` + `commands/autopilot.md` so the CLI surface matches the runtime opt.

### Added (Unreleased) — 2026-05-09 deep-1 (#350–#354)

- **#350 gitleaks pre-commit + CI step** — `.gitleaks.toml` (already tracked from prior baseline-vendor work) extended with plugin-specific allowlist patterns (`.test.mjs` / `.spec.mjs` / `tests/` / skill+rule+command+agent Markdown / `AGENTS.md` / `README.md` / `CHANGELOG.md`). NEW `gitleaks-scan` job in `.gitlab-ci.yml` (new `security` stage, runs first, fails on findings). NEW `security` job in `.github/workflows/test.yml` (gates `test` job via `needs:`, uses `gitleaks/gitleaks-action@v2.3.7` pinned by SHA). NEW `.husky/pre-commit` invocation `gitleaks protect --staged --redact --no-banner` with graceful skip when binary absent. Local dry-run: 0 leaks across 258 commits. Issue closed.
- **#351 npm audit step in CI pipelines** — NEW `npm-audit` job in `.gitlab-ci.yml` (`security` stage, runs `npm audit --audit-level=high --omit=dev`, configurable via `AUDIT_LEVEL` CI variable for false-positive triage). Mirrored as a step in the GitHub `security` job. Local dry-run: 0 vulnerabilities. Bypass mechanism documented in yaml comment. Issue closed.
- **#352 Husky + commitlint + lint-staged** — NEW `.husky/pre-commit` (gitleaks + lint-staged) + `.husky/commit-msg` (commitlint). NEW `commitlint.config.mjs` (extends `@commitlint/config-conventional`, type-enum matches `.claude/rules/development.md`, `header-max-length: 120`). NEW `.lintstagedrc.mjs` (`*.mjs → eslint --fix`). `package.json` adds 4 devDeps (husky 9.1.7, @commitlint/cli + config-conventional 19.6.0, lint-staged 15.2.10) + `prepare: husky` + `format` / `format:check` scripts. README `## Development` section gains "Pre-commit hooks" subsection documenting the `npx husky` post-install requirement (because `.npmrc` ships `ignore-scripts=true` for SEC-020 supply-chain defence). Issue closed.

### Changed (Unreleased) — 2026-05-09 deep-1 (#350–#354)

- **#353 Prettier configs** — `.prettierrc` already existed (committed 2026-04-19); `.prettierignore` extended from 5 to 24 lines (added `coverage/`, `dist/`, `build/`, lock-file siblings, `.husky/_/`, backup patterns; preserved existing `*.md` + `docs/` + `.orchestrator/` ignores). `package.json` adds `format` / `format:check` scripts (prettier already devDep). Repo-audit's `quality.prettier-config: fail` was a glob false-negative — file is committed and content is canonical. Issue closed.
- **#354 CLAUDE.md trim** — Working-tree compaction completed: HEAD CLAUDE.md (110L) → working tree (88L), within ≤100L lean target. Two verbose multi-paragraph session bullets (2026-05-08 deep-2 + 2026-05-08 PM v3.4.0) extracted from HEAD and prepended to `~/Projects/vault/01-projects/session-orchestrator/decisions.md` (191L → 260L, +69L). Migration disclaimer in vault note updated with `**Update 2026-05-09:**` annotation. Information-loss check: every removed bullet now in vault. Issue closed.

### Added (Unreleased) — 2026-05-08 deep-2 (#344, #347)

- **#344 crypto-digest-utils.mjs** — DRY 6 sha256-hash sites. NEW `scripts/lib/crypto-digest-utils.mjs` (~90L, 4 named exports: `digestSha256Short` (default 8-char hex prefix), `digestSha256` (full digest), `digestSha256WithSalt` (salt+\\x00+value pattern with required salt + TypeError validation), `digestMultiBufferSha256` (sequential multi-buffer update, array-required)). Migrated 6 callers byte-equivalent: `spiral-carryover.mjs` (computeTaskHash), `host-identity.mjs` (hashHostname — preserves salt+NUL+hostname order invariant), `quality-gates-cache.mjs` (computeDependencyHash multi-buffer), `frontmatter-guard.mjs` (computeSchemaHash), `session-registry.mjs` (repoPathHash — NOTE: keeps `crypto` import for `randomBytes`), `vault-sync-baseline.mjs` (computeSchemaHash). 29 vitest cases with hardcoded hex literals (no tautological computation). Issue closed.
- **#347 check-hooks-symmetry validator** — NEW `scripts/lib/validate/check-hooks-symmetry.mjs` (127L after simplifier) with 4 sequential checks: (1) hooks.json↔hooks-codex.json event-key parity (must match exactly); (2) hooks-cursor.json documented-asymmetry policy via `DOCUMENTED_ASYMMETRIES` constant (9 cursor-missing-from-main + 2 cursor-only); (3) handler files exist on disk for all referenced .mjs (skips `_lib/*` library modules); (4) orphan-detection (informational PASS for unreferenced hooks/*.mjs). Wired into `scripts/validate-plugin.mjs` after L123 (2 lines added). Simplifier extracted `loadJson(filePath, required)` helper (-9L). 30 vitest cases via synthetic temp-dir fixtures + 3 real-plugin happy-path cases. Issue closed.

### Tests (Unreleased)

- **#345 scripts/lib/config/ unit-test cluster** — 8 NEW test files in `tests/lib/config/`, **221 cases** (target ~112, +97%). Modules: coercers (77 cases — every throw exercised, override-syntax `"6 (deep: 18, fast: 4)"` parser covered, all 11 exported coercers covered), drift-check (21), section-extractor (18), vault-sync (19), docs-orchestrator (19), vault-staleness (17), config/events-rotation (22 — for the CONFIG parser), vault-integration (28). Test-quality compliant: hardcoded expected values, no tautological computation, error-paths covered. Issue closed.
- **#346 scripts/lib/vault-mirror/ unit-test cluster** — 5 NEW test files in `tests/lib/vault-mirror/`, **148 cases** (target ~78, +89%). Modules: utils (48 — all 7 utility functions), render-learnings (32 — schema detection + 8 v1/6 v2 throw paths + confidence boundary 0.8 + title truncation), render-sessions (36 — schema detection + 9 v1 + 7 v2 throw paths + completion-rate rounding + platform-skipped regression #343), auto-commit (11 — all 8 action paths: no-mirror-dirs, not-a-git-repo, git-add-failed, git-diff-failed, no-staged-changes, non-mirror-staged-changes, git-commit-failed, auto-commit-created), process (21 — deriveRepo regex parsing + caching, processLearning state machine, processSession sanitisation). Issue closed.
- **#349 events-rotation.mjs unit tests** — NEW `tests/lib/events-rotation.test.mjs`, **25 cases** (target 16-18, +56%). Coverage: 9 input-validation throws (logPath/maxSizeMb/maxBackups including range boundaries), 6 early-return reasons, 4 happy-path rotations including exact-1-MiB threshold, 3 ring-buffer shifts with maxBackups=3, 3 error-handling tests via real read-only-dir injection (vi.spyOn blocked on ESM named exports — used `chmodSync(dir, 0o555)` instead). Issue closed.

### Changed (Unreleased)

- **#348 doc-drift sweep** — README test-count line refreshed (`2623+ tests` → `3138+ tests` in W2; bumped to `3591+ tests` in W5 to reflect post-session count) + NEW `### Cursor IDE Support` section after Platform Support documenting `hooks/hooks-cursor.json`'s 2 events and Cursor IDE limitations (no SessionStart equivalent + post-hoc afterFileEdit). PRD `2026-04-21-vault-docs-orchestration.md` Status header updated `Draft` → `Complete (2026-05-01, Epic #229 closed)` (line 360 already confirmed completion). CLAUDE.md `2026-04-30 main-2026-04-30-1635` narrative bullet stale doc-drift parenthetical replaced with `[RESOLVED 2026-05-08 in v3.4.0: ...]` historical marker. Issue closed.

### Refactor (Unreleased) — 2026-05-09 deep-4

- **Hotspot-splits cluster (deep-4, 2026-05-09)**: 6 file-disjoint hotspots ≥400 LOC split into submodules <300 LOC each. Public APIs preserved via barrel re-exports — zero behavior change.
  - `scripts/lib/state-md.mjs` (563→31 LOC barrel) — split into `state-md/{yaml-parser, frontmatter-mutators, body-sections, mission-status, recommendations}.mjs`
  - `scripts/lib/mode-selector.mjs` (480→149 LOC orchestrator) — split into `mode-selector/{constants, scoring, alternatives, rationale, context-pressure}.mjs`
  - `scripts/lib/session-schema.mjs` (462→70 LOC barrel) — split into `session-schema/{constants, validator, normalizer, timestamps, aliases}.mjs`
  - `scripts/lib/owner-config.mjs` (459→28 LOC wrapper) — split into `owner-config/{constants, error, defaults, coerce, validate, merge, index}.mjs`
  - `scripts/lib/worktree.mjs` (418→15 LOC barrel) — split into `worktree/{constants, meta, listing, lifecycle, index}.mjs`
  - `scripts/lib/autopilot.mjs` (418→39 LOC barrel) — split into `autopilot/{kill-switches, flags, telemetry, loop}.mjs`. The legacy `scripts/lib/autopilot-telemetry.mjs` is retained as a one-line backward-compat re-export.
- **Test coverage**: +534 net tests (3896→4430), 26 new `*.test.mjs` files under `tests/lib/{state-md,mode-selector,session-schema,owner-config,worktree,autopilot}/**`. The `tests/lib/refactor-stability.test.mjs` adapter extended 24→46 tests pinning all 6 hotspot public APIs.
- **Quality gates**: typecheck 66/66 OK · lint 0 errors · validate-plugin 27/27 · doc-consistency 0 findings · test-quality audit 0 anti-pattern violations.
- **Pattern**: 5W×6A coordinator-direct parallel dispatch with file-disjoint allowedPaths per agent. Continues the deep-2 (2026-05-09) hotspot-split pattern.

### Quality (Unreleased)

- typecheck: 66/66 OK (was 65 at v3.4.0; +1 NEW module crypto-digest-utils.mjs in deep-2)
- lint: 0 errors
- tests: **4430 passed / 12 skipped / 0 failed** (was 3138 at v3.4.0, **+453 in deep-2**, stable in deep-1, **+297 in deep-2** [#355–#358], **+8 in deep-3** [#359–#363], **+534 in deep-4**)
- validate-plugin: **27 passed, 0 failed** (was 22 at v3.4.0; +5 from check-hooks-symmetry validator in deep-2)
- CI security gates pre-validated locally: gitleaks 0 leaks (258 commits), npm audit 0 vulnerabilities (production deps, audit-level=high)

## [3.4.0] - 2026-05-08

Deep session shipping 7 issues (#327 #328 #330 #342 #332 #325 #326) on top of the prior consolidated `[Unreleased]` work since v3.3.0 (Benchmark P1-P5, convergence-monitoring, baseline propagation, doc-drift sweep, #321/#323/#343/#329/#322/#324, #320 Express Path persistence). 5W×6A coordinator-direct on `main`, isolation:none enforcement:warn cap=6. Tests grew 2942 → 3138 (+196). No breaking changes.

### Added (v3.4.0)

- **#327 vault-sync baseline-diff reporting** — `scripts/lib/vault-sync-baseline.mjs` (NEW, 152L) exports `computeSchemaHash` (8-char SHA-256 prefix), `writeBaseline` (atomic with header), `readBaseline` (null on missing/invalid, no-throw), `diffBaseline` (set-difference by `(file, path, message)` triple). `skills/vault-sync/validator.mjs` extended `--mode` to accept `baseline|diff|full` (legacy `hard|warn|off` unchanged). Default mode `hard` preserved (zero behavior change for existing callers). Schema-hash mismatch falls back to full enforcement with stderr WARN. 25 vitest cases (16 unit + 9 integration). Issue closed.
- **#328 Frontmatter-Guard skill** — NEW `skills/frontmatter-guard/SKILL.md` (126L) + `scripts/lib/frontmatter-guard.mjs` (240L). Reads canonical zod schema from `~/Projects/projects-baseline/packages/zod-schemas/src/vault-frontmatter.ts` (8 type values, 7 status values, 4 required fields), generates deterministic Markdown snippet with per-type YAML examples. `wave-executor/wave-loop.md` adds Pre-Dispatch injection step gated on `detectVaultTaskScope()` (vault path or write-intent in task description). 31 vitest cases. Issue closed.
- **#330 distributed session-lock with TTL** — NEW `scripts/lib/session-lock.mjs` (215L) exports `acquire`/`forceAcquire`/`release`/`checkStale`/`readLock`. JSON payload `{session_id, started_at, mode, pid, host, ttl_hours}` at `.orchestrator/session.lock` (added to `.gitignore`). `DEFAULT_TTL_HOURS=4` (derived from sessions.jsonl p95=2.62h). `session-start` Phase 1.2 wires acquire+AUQ recovery (active/stale-pid-dead/stale-pid-alive branches; cross-host PID check returns `pidAlive: null` and recommends Abort). `session-end` Phase 3.8 wires release. `commands/close.md` notes the release. 16 vitest cases. Issue closed.
- **#342 4 hook events adoption** — 4 NEW handlers in `hooks/`: `post-tool-failure-corrective-context.mjs` (PostToolUseFailure, appends to `.orchestrator/current-session.json` corrective_context array, capped 20), `post-tool-batch-wave-signal.mjs` (PostToolBatch, deterministic batch-resolved signal), `subagent-telemetry.mjs` (SubagentStart+SubagentStop, writes to NEW `.orchestrator/metrics/subagents.jsonl`), `cwd-change-restore.mjs` (CwdChanged, records cwd_changes capped 20). NEW `scripts/lib/subagents-schema.mjs` (Zod-free schema mirroring `learnings.mjs` style; 8 exports incl. ValidationError, validateSubagent, normalizeSubagent, migrateLegacySubagent, appendSubagent, readSubagents). NEW `scripts/migrate-subagents-jsonl.mjs` (v3 migrate-CLI clause). `hooks/hooks.json` + `hooks/hooks-codex.json` register all 4 new event keys (SubagentStop extended additively, on-stop.mjs preserved). CLAUDE.md `## Structure` updated 6→10 hook event matchers / 6→10 handlers. 41 vitest cases (29 schema + 12 hook). Issue closed.
- **#332 mode-selector context-pressure signal** — `scripts/lib/mode-selector.mjs` exports new `computeContextPressure(signals)` returning `{score, components: {scope, keywords, carryover}, level: 'low'|'medium'|'high'}`. Components: scope=`min(0.5, max(0, (priorityCount-3)/10))`, keywords=`+0.25` if cross-cutting regex matches `taskDescriptionText`, carryover=`min(0.25, max(0, ratio-0.3))`. Integrated into `computeDelta()` as additive penalty: high level → feature −0.15 / housekeeping −0.10; low → feature +0.05. `selectMode()` output now includes `context_pressure: {score, level}`. `skills/mode-selector/SKILL.md` documents the heuristic; `skills/session-start/phase-7-5-mode-selector.md` Step 4.5 wires AUQ annotation when level is medium/high. 13 vitest cases. Issue closed.

### Changed (v3.4.0)

- **#325 ecosystem-wizard 5-way modularization** — `scripts/lib/ecosystem-wizard.mjs` (was 636L) split into `scripts/lib/ecosystem-wizard/{ci-detector,package-manager-detector,config-parser,config-writer,wizard-prompt}.mjs` (5 sub-modules); root file becomes a re-export barrel preserving all 12 public symbols. Behaviour-preserving: 56 existing ecosystem-wizard tests still pass byte-equivalent. Zero cyclic imports. 68 new vitest cases for sub-module isolation + barrel re-export count (floor/ceiling assertion ≥10, ≤50). Issue closed.
- **#326 autopilot.mjs telemetry split** — NEW `scripts/lib/autopilot-telemetry.mjs` extracts `writeAutopilotJsonl`, `defaultRunId`, `readHostClass`, `finalizeState` from `scripts/lib/autopilot.mjs` (was 575L → 507L, ~12% reduction). Root file re-exports for backward compat; existing 73 autopilot tests pass unchanged. 27 new vitest cases for telemetry module. Issue closed.

### Quality (v3.4.0)

- typecheck: 65/65 OK (was 56 before W2; +9 NEW modules: vault-sync-baseline, frontmatter-guard, session-lock, subagents-schema, autopilot-telemetry, 4× ecosystem-wizard sub-modules)
- lint: 0 errors, 0 warnings (4 inline coordinator fixes during W4: 2 eqeqeq in post-tool-failure hook, 1 unused destructured var in frontmatter-guard, 1 unused-import in autopilot-telemetry test)
- tests: **3138 passed / 12 skipped / 0 failed** (was 2942 entering W4; **+196 tests** across W2/W3/W4)
- validate-plugin: 22 passed / 0 failed
- Simplifier pass (W4-D5): 4 production files simplified — hoisted dynamic import in post-tool-failure hook, replaced `pad2` with `String.padStart` in autopilot-telemetry, extracted `safeBootstrapLock` helper + collapsed nested ternary to if/else chain in mode-selector, replaced 3 nested ternary `firstNonEmptyString` blocks in subagent-telemetry hook
- 2 production bugs caught by W4 Full Gate D6 + fixed coord-direct in `frontmatter-guard.mjs`: regex stem boundary issue (`generat` → `generat[a-z]*` for `WRITE_INTENT_RE`; same fix applied to write/creat/emit/mirror/updat/add/insert), trailing-newline cosmetic patch on snippet generator

### Added

- **Benchmark P1-P5 adoption epic (#336 #337 #338 #339 #340)** — five area-orthogonal features adopted from the 2026-05-07 benchmark synthesis (Cursor / obra-superpowers / Pimzino / BMAD / ab-method). All opt-in, default-off, zero-behavior-change for existing call sites. **#336 (P1, Cursor)** — glob-scoped rules: `scripts/lib/rule-loader.mjs` exports `loadApplicableRules({rulesDir, scopePaths})` filtering `.claude/rules/*.md` by optional `globs:` frontmatter intersected with the wave's `allowedPaths`; rules without `globs:` stay always-on (backward compat); parse errors fall back to always-on (never silently dropped). 6 rules renamed `paths:` → `globs:` (`backend.md`, `backend-data.md`, `cli-design.md`, `security-web.md`, `testing.md`, `test-quality.md`); 7 cross-cutting rules remain always-on. **#337 (P2, obra/superpowers)** — `skills/using-orchestrator/SKILL.md` (140L meta-skill) with 23-entry de+en phrase map (covers /plan {new,feature,retro}, /session {housekeeping,feature,deep}, /discovery, /evolve, /close, /bootstrap), 4-tier confidence scoring (0.95 exact-slash / 0.90 exact-natural / 0.60 partial / 0.40 semantic), 0.85 dispatch threshold, AUQ-disambiguation when delta < 0.15. Gated on `auto-skill-dispatch: true` (default false = silent no-op). **#338 (P3, Pimzino)** — `.orchestrator/steering/{product,tech,structure}.md` (3 stable repo-fact docs, ~50L each) loaded by new session-start Phase 2.6 (silent no-op when dir absent); `/plan new` Step 7 scaffolds the docs from planning answers; CLAUDE.md narrative kept as historical record. **#339 (P4, BMAD/superpowers)** — `agents/{architect-reviewer,qa-strategist,analyst}.md` (3 read-only persona-reviewer agents); new wave-loop.md step 5a dispatches enabled reviewers in parallel between Quality-Lite and session-reviewer. Gated on `persona-reviewers.enabled: true` + non-empty `reviewers:` array (default empty = no-op). Reviewer findings written to `.orchestrator/audits/wave-reviewer-<wave>-<reviewer>.md` (already gitignored). **#340 (P5, ab-method partial)** — `scripts/lib/mission-status-schema.mjs` exports the 5-value enum `brainstormed | validated | in-dev | testing | completed` + transition validator (forward + idempotent same-state + rollback-to-brainstormed allowed); `state-md.mjs` adds `parseMissionStatus`/`writeMissionStatus`; session-plan emits `### Wave-Plan Mission Status (machine-readable)` block (parallel to docs-tasks pattern); session-end Phase 1.9 classifies items by enum (completed→Done, testing/in-dev→Carryover, brainstormed/validated→Not Started). Backward-compat: absent `mission-status:` field → behave exactly as pre-#340. New Session Config fields: `auto-skill-dispatch: false`, `persona-reviewers: { enabled, reviewers, mode }`. CLAUDE.md `## Structure` line updated (27 skills, 10 agents, +steering/ pointer). 149 new vitest cases across 8 test files; total **2875 passed / 11 skipped / 0 failed** (was 2727, +148). Typecheck 56/56, lint 0, validate-plugin 22/22 (10 agents).
- **convergence-monitoring skill (#223)** — Iterative-loop convergence detector. Three signals (shrinking diff, pass-rate plateau, velocity) drive a Stop/Continue/Investigate decision at each inter-wave checkpoint. Distinct from /evolve (retrospective) and session-reviewer (correctness review): convergence-monitoring answers "are we making progress?". Primary consumer: /autoresearch loops and wave-executor inter-wave checkpoints.
- **Baseline propagation MR (#314 #315 #318 #240)** — Cross-repo MR to `projects-baseline` vendoring `.claude/rules/architecture.md` (5 editorial patches), `.claude/rules/owner-persona.md` (new file + setup-project.sh wiring + CLAUDE.md.template bullet), `docs/adr/000-template.md` (3-criteria gate `### What qualifies` + `### What does not qualify` sub-sections), and `tests/setup-project.bats` (3 vault-provisioning test cases for #240).
- **Plugin doc-drift sweep** — README.md skills count 25→26 (added convergence-monitoring), hook handler count 7→6 (corrected), test count 2160+→2623+ (refreshed). CLAUDE.md `## Structure` line synced.

### Fixed

- **#321** sessions.jsonl writer hardening — recurring timestamp-inversion bug fixed at the writer boundary. New exports `clampTimestampsMonotonic(entry)` + `aliasLegacyEndedAt(entry)` in `scripts/lib/session-schema.mjs` (pure, never throw, never mutate input). `scripts/emit-session.mjs` calls both before `validateSession`: monotonicity violations are clamped (`completed_at = started_at`) with `_clamped: true` + `_original_completed_at: <iso>` forensic markers and a STDERR `WARN session_id=...; clamped` line; STDOUT remains pure JSON. Legacy `ended_at` records get aliased to canonical `completed_at` and `duration_ms` is dropped once both timestamps are present. 4 invalid rows in `.orchestrator/metrics/sessions.jsonl` (lines 69, 71, 75, 76 — including the 2026-05-07 deep session that re-triggered the bug) patched in-place; dry-run unmappable: 4 → 0. No rogue writers detected — every JSONL write site already funnels through `emit-session.mjs`. 22 new vitest cases for clamp + alias unit behaviour, 6 integration cases for emit-session.
- **#323** learnings.jsonl `expires_at` TTL pruning gap — `scripts/lib/learnings.mjs` exports `LEARNING_TTL_DAYS` (frozen 9-entry per-type policy: 30d for `mode-selector-accuracy`, 60d for `hardware-pattern` / unknown types, 45d for `fragile-file` / `effective-sizing` / `recurring-issue`, 90d for `workflow-pattern` / `proven-pattern` / `anti-pattern` / `autopilot-effectiveness`) and `deriveExpiresAt(createdAt, type)`. `appendLearning()` auto-stamps `expires_at` (and `created_at` if absent) when caller omits it; respects caller-supplied values for idempotency. New `scripts/backfill-learnings-expires.mjs` CLI (dry-run safe by default; `--apply` patches missing `expires_at` + writes `<file>.bak.<isoDate>` backup; `_backfilled_expires_at: true` forensic tag; idempotent on re-run). 1/129 records in live `learnings.jsonl` backfilled. 11 new unit tests (deriveExpiresAt + appendLearning auto-stamp) + 7 backfill integration tests.
- **#343** vault-mirror frontmatter quality drift — V1 `generateSessionNote` no longer emits literal `"undefined"` from template-literal coercion; new `fmLine(key, value)` skip-emit helper handles null/undefined/empty. Both V1 and V2 generators now emit a `repo:` frontmatter field for cross-repo aggregation; new `deriveRepo()` in `scripts/lib/vault-mirror/process.mjs` parses `git config --get remote.origin.url` (ssh + https forms) with `path.basename(cwd)` fallback, cached across calls. JSDoc note codifies canonical filename pattern (`<session_id>.md`). 11 V1/V2 frontmatter tests + 4 deriveRepo tests.
- **#329** vault-sync exclude-list parser ignored on bare invocation — `skills/vault-sync/validator.mjs` now reads `vault-sync.exclude:` from `<VAULT_DIR>/CLAUDE.md` (or `AGENTS.md` via `resolveInstructionFile()`) unconditionally on every invocation, BEFORE argv parsing. CLI `--exclude` flags remain additive. try/catch silent fallback on missing/unparseable config. Dynamic-imports `_parseVaultSync` from `scripts/lib/config/vault-sync.mjs` so the existing parser stays the SSOT. SKILL.md docs the new behaviour. 7 new integration tests (bare-invocation, additive CLI, missing/unparseable fallback, AGENTS.md alias, VAULT_DIR env precedence, non-array safety).
- **#322** circular import between `scripts/lib/workspace.mjs` ↔ `worktree.mjs` removed — re-export shim at `worktree.mjs:421` deleted; the single test caller (`tests/lib/worktree.test.mjs:423`) split into two cache-busted dynamic imports (`workspace.mjs?hardening` for `resolveWorkspaceRoot`/`restoreCoordinatorCwd`/`validatePathInWorkspace`; `worktree.mjs?hardening` for `createWorktree`/`removeWorktree`). Cycle now uni-directional (workspace.mjs → worktree.mjs only). All 36 `worktree.test.mjs` cases still pass.

### Changed

- **#324** decompose 3 long validator functions (behaviour-preserving refactor, no public API change). `scripts/lib/wave-resource-gate.mjs::evaluateWaveResourceGate` (109L) → thin orchestrator + `extractMeasurements` + `applyDecisionRules` private helpers. `scripts/lib/session-schema.mjs::validateSession` (181L) → 8 module-private section validators (`_validateSchemaVersion` / `_validateRequiredFields` / `_validateSessionId` / `_validateSessionType` / `_validateTimestamps` / `_validateWaves` / `_validateAgentSummary` / `_validateOptionalFields`); the W2 #321 additions (`clampTimestampsMonotonic` / `aliasLegacyEndedAt`) preserved. `scripts/lib/owner-config.mjs::validate` (212L) → 7 per-section private validators (schema-version / owner / tone / efficiency / hardware-sharing / defaults / metadata). All error messages, throw semantics, and aggregate ordering byte-equivalent. 154 existing tests (107 + 47 consumers) still pass without modification.
- `.gitignore` extended for `.orchestrator/metrics/*.jsonl.bak.*` (dot-separated date suffix from new backfill script) and `.orchestrator/metrics/*.jsonl.archive-*`.

### Fixed (continued)

- **`evaluate.mjs` macOS pressure-first verdict** (commit `afcdf12`, branch `fix/macos-ram-pressure-aware-probe`) — `os.freemem()` on macOS reports only `vm_statistics.free_count` (Pages free), excluding the `inactive` pool that the OS reclaims on demand. Real-world Mac sessions with 11+ GB inactive routinely showed <1 GB "free", triggering `critical` verdict + `cap=0` (coordinator-direct) even when `memory_pressure` reported 60-80% free. Same pattern for swap: macOS accumulates swap over multi-day sessions even after pressure normalises. Fix: when `memory_pressure_pct_free >= 30%` (Activity Monitor's green/yellow boundary, `MACOS_HEALTHY_PRESSURE_PCT`), suppress free-RAM and swap signals; CPU + claude-process-count + zombie signals continue to fire. Backwards-compatible (Linux/Windows untouched). Verified live: `5219 MB swap + 0.5 GB ram_free + 81% pressure` previously → `critical / cap=0`, now → `warn / cap=null`. Aligned with new `AAG-006 Resource-Aware Throttling` rule. 72 unit tests (was 70); full suite 2629 pass. Sources: Apple "Viewing Virtual Memory Usage", OSXDaily 2026-04, psutil/psutil#1277, Cordum 2026 circuit-breaker DEGRADED-state.
- **#320** Express Path persistence — `commands/go.md` now detects the Express Path activation banner and (a) appends the express-path deviation to STATE.md `## Deviations` via `appendDeviation()`, (b) auto-invokes `session-orchestrator:session-end` skill, (c) verifies `status: completed` after close. Adds 2 new exports in `scripts/lib/state-md.mjs` (`appendDeviation`, `markExpressPathComplete`) + 17 vitest cases. Closes the audit-trail gap where every Express Path housekeeping run silently dropped its sessions.jsonl record.

## [3.3.0] - 2026-04-30

Iterative release covering the work since v3.2.0 (2026-04-27): Owner Persona Layer (D-axis complete), bash-free milestone, AGENTS.md alias parity, vault-staleness banner, autopilot-effectiveness skeleton, two epic closures (#309 Architecture-DDD-Trio, #271 v3.2 Autopilot), 50+ closed sub-issues, and a major refactor pass (vault-mirror / config / categories / worktree / resource-probe splits). Tests grew 1871 → 2623 (+752). No breaking changes.

### Added

#### Owner Persona Layer (#161 epic — D-axis complete)
- `scripts/lib/owner-yaml.mjs` (184L, 5 exports) — schema, validator, loader, writer, defaults; plain-JS validation (no Zod dep). 36 tests.
- `scripts/lib/owner-interview.mjs` (130L) — 5-question interview (language, tone, output level, preamble, hardware-sharing consent). C4 hardware-sharing consent (#173) merged into question 5; generates 64-char hex `hash-salt` via `crypto.randomBytes`. Idempotent (`force: true` archives to `owner.yaml.bak-<timestamp>`).
- `scripts/lib/soul-resolve.mjs` (98L) — mustache-style `{{slot}}` resolver. Pure `resolveSoul()` + `loadAndResolveSoul()`. Falls back to defaults silently for missing slots. 10 tests.
- `scripts/lib/owner-config{,-loader}.mjs` (313L + 113L) — earlier config foundation (#174) + 41 tests + `docs/owner-config-schema.md`.
- `skills/session-start/soul.md` + `skills/plan/soul.md` — both contain `{{owner.language}}`, `{{tone.style}}`, `{{efficiency.output-level}}`, `{{efficiency.preamble}}` slots.
- `skills/bootstrap/SKILL.md` — Phase 3.5 owner-interview integration; Phase 3.6 = (former 3.5) Rules-Fetch Bridge.
- `.claude/rules/owner-persona.md` (79L, 7 sections) — owner.yaml location, slot system, `--owner-reset` re-trigger, privacy guarantee (path-only, never content).
- `tests/integration/owner-persona-flow.test.mjs` (15 tests) — interview → write → load → soul resolve.

#### AGENTS.md alias parity (#33 + #30)
- `skills/_shared/instruction-file-resolution.md` — alias-rule SSOT (CLAUDE.md → AGENTS.md → null).
- `scripts/lib/common.mjs` adds `resolveInstructionFile()` helper.
- 30 sites updated: 16 skills, 11 scripts, 1 command, 1 example.yaml — transparent alias everywhere.
- `skills/claude-md-drift-check/checker.mjs` alias-resolves at runtime; JSON now emits `resolved_path` / `resolved_kind`.
- `tests/skills/instruction-file-alias-coverage.test.mjs` — sweep test prevents regression.
- `docs/session-config-template.md` — ~70-field baseline template for adopters.
- `scripts/check-doc-consistency.sh` — POSIX H2-parity, count-parity, alias-phrasing CI gate (exit 0/1/2).
- `skills/claude-md-drift-check` gains session-config-parity check (template-vs-local key diff, 2 new CLI flags) and a fifth command-count probe (#269).

#### Architecture-DDD-Trio (#309 epic, closed)
- 3 skills + `skills/discovery/probes-arch.md` (235L, 4 probes incl. architectural-friction) + 20 tests adopted from `mattpocock/skills@90ea8ee` (MIT). Plugin-scope items complete; cross-repo work (#314, #315) deferred to projects-baseline MR.

#### Vault & Discovery Infrastructure
- `scripts/vault-integration-watcher.mjs` (260L, #306) — vault staleness watcher + 12 tests + GitLab Scheduled Pipeline.
- `scripts/lib/vault-staleness-banner.mjs` (#319) — 2-tier severity banner (`warn`/`alert`) wired into `session-start` Phase 4. Reads last line of `.orchestrator/metrics/vault-staleness.jsonl`. Silent no-op on absent/malformed file or `stale_count === 0`.
- `docs/vault-docs-architecture.md` (#237, 297L, 31 source citations, 9 sections).

#### Autopilot Foundation (#271 epic, closed)
- `scripts/lib/evolve/autopilot-effectiveness.mjs` (#298) — `/evolve` learning type 8 skeleton; data-gated on ≥20 paired manual+autopilot runs per mode (returns `[]` until threshold).
- All v3.2 Autopilot phases (A/B/C-1/C-1.b/C-1.c/C-2/C-5) confirmed shipped at v3.2.0; epic closed in this cycle. Sub-issues `#297` (cap calibration) and `#298` (effectiveness data) remain open, blocked on real RUNS.

#### Repository tooling
- `skills/repo-audit/SKILL.md` (#215, 258L) + `commands/repo-audit.md` + 32 tests; Clank section opt-in; config-driven.
- `skills/_shared/instruction-file-resolution.md` baseline-fetch in `skills/bootstrap/standard-template.md` S99 manifest.

#### Express Path (#214)
- `session-start` Phase 8.5 + `session-plan` Express Path Short-Circuit + docs section. Codifies the 13× consecutive coord-direct pattern observed in 2026-04 deep sessions. Activates for housekeeping ≤3 sequential issues. +20 tests.

#### Marketplace prep (#213)
- `docs/marketplace/composio-submission.md` (114L) — submission draft for ComposioHQ/awesome-claude-plugins (entry text, 9-row comparison vs maestro-orchestrate, PR mechanics, risk/fallback path).

#### Cross-repo baseline propagation prep
- `docs/baseline-diffs/` — three MR-ready preview documents for #318 (owner-persona), #314 (architecture rule), #315 (ADR template gate). Plugin-side only; baseline MR is a separate session.

### Changed

#### Refactor pass (lower complexity, preserved public APIs)
- `vault-mirror.mjs` (#283) — 679L → 152L CLI orchestrator + 6 modules under `scripts/lib/vault-mirror/`. CLI flags byte-identical; 51/51 existing tests unchanged.
- `config.mjs` (#284) — 1075L → 294L orchestrator + 8 per-section parsers under `scripts/lib/config/`. Public API frozen; 152/152 tests pass.
- `categories.mjs` (#285) — 956L → 17L re-export barrel + 7 per-category files; `RUBRIC_VERSION='2026-05'` unchanged; audit JSON output identical.
- `worktree.mjs` (#287) — 589L → 420L; new `workspace.mjs` 198L re-export shim.
- `resource-probe.mjs` (#287) — 564L → 89L + new `resource-probe/{parsers,probe-platform,evaluate}.mjs`.
- `validate-plugin.sh` (#122) — 364L → 79L orchestrator + 5 helpers under `scripts/lib/validate/`.
- `run-quality-gate.sh` (#121) — 412L → 132L dispatcher + 4 gate handlers + helpers under `scripts/lib/gates/`.
- `bootstrap` templates (#288 batch 1+2) — `_shared-template.md` partial extraction; `session-start` 862→486L (4 phase siblings); `session-end` 636→446L (2 phase siblings).
- `harness-audit` pass()/fail() (#227) — options-object signature with backward-compat positional shim (warn-once + forward); +19 tests.
- `ecosystem-wizard` + `worktree-freshness` (#208) — 9 helpers extracted; all functions ≤57 lines; #289 idempotent merge fully preserved.

#### Bash-free milestone (#218 / #317)
- 16 .sh scripts ported to .mjs (10 nested under `scripts/lib/gates/` + `scripts/lib/validate/`, 6 top-level); `find scripts/lib -name '*.sh'` returns zero.
- New pure-ESM modules: `scripts/lib/gates/gate-{baseline,incremental,full,per-file,helpers}.mjs` (475L), `scripts/lib/validate/check-{plugin-json,component-paths,json-files,agents,commands}.mjs` (447L). Exact JSON-on-stdout contracts and exit codes preserved.
- `scripts/run-quality-gate.mjs` and `scripts/validate-plugin.mjs` switched from `bash` → `node` spawn. Public CLI/env API unchanged.
- `.claude/rules/cli-design.md` — "Shared Shell Library" section rewritten as "Shared Module Library (common.mjs)" reflecting bash-free state.
- `CONTRIBUTING.md` updated: `platform.sh` → `scripts/lib/platform.mjs (detectPlatform)`, `common.sh` → `common.mjs`.
- 12 doc/skill/rule files updated to reference .mjs paths instead of .sh.

#### Hooks profile gate (#211)
- `hooks/_lib/profile-gate.mjs` (`shouldRunHook`) imported by all 6 handlers. `SO_HOOK_PROFILE` (full/minimal/off) + `SO_DISABLED_HOOKS` per-name override. Backward compatible (unset env = full). +10 tests.

#### Webhook URL centralization (#228)
- `scripts/lib/webhook-url.mjs` (`resolveWebhookUrl` + `WebhookConfigError`) — env > Session Config > error precedence. Personal-domain default removed from `scripts/lib/events.mjs` + `hooks/on-stop.mjs`. +22 tests.

#### Plugin root resolution (#212)
- `scripts/lib/plugin-root.mjs` — robust 4-level fallback (env CLAUDE_PLUGIN_ROOT > CODEX_PLUGIN_ROOT > walk-from-import-meta > walk-from-cwd > `PluginRootResolutionError`). `platform.mjs` delegates. Backward compat preserved. +10 tests.

#### Schema enforcement
- `learnings.jsonl` writer (#303) — Zod-equivalent validation; `evolve` SKILL.md Step 3.5 writer prompt now mandates `schema_version:1` + UUID `id` + `insight` (not `description`/`recommendation`). `scripts/migrate-learnings-jsonl.mjs` (--dry-run/--apply, idempotent). +14 tests.
- `sessions.jsonl` writer (#304) — canonical schema header doc; `scripts/migrate-sessions-jsonl.mjs` maps OLD scalar shape → NEW `agent_summary`/`waves[]`/`total_agents`/`total_files_changed`. +19 tests.
- `bootstrap.lock plugin_version` (#290 + #203) — `readPluginVersionFromPackageJson` + `classifyVersionMismatch` (major=alert, minor/patch=info, legacy=soft). `MS_PER_DAY` constant. Live lock backfilled with `plugin-version`.

#### Discovery & ecosystem
- `discovery-on-close` session-type-aware default (#264) — `housekeeping=false`, `feature/deep=true`; user override always wins.
- `ecosystem-wizard` idempotent re-runs (#289) — diff-aware merge (JSON.stringify equality gate), `overwrite` param on `writeSessionConfigBlock`. +11 tests.
- `zombie-threshold-min` end-to-end wiring (#178) — config schema (default 30 min), `parseEtimeToMinutes` + `countZombieProcesses`. Verdict escalates when `zombie>=1 AND claude_processes_count>0`. +21 tests.
- `close` skill auto-strip `status:*` labels (#308) — `scripts/lib/issue-close-strip-labels.mjs` (glab + gh paths, idempotent, fail-open). +10 tests.
- `skills/evolve/SKILL.md` — Type 8 entry added; counter 6→8 sync (also picked up pre-existing missing `hardware-pattern` #7 in Step 3.5 enum).
- PSA-001 vs PSA-002 refinement (#156) — decision-tree, scope-overlap examples, separate behavior blocks; PSA-003/004 untouched. +8 tests.

#### Tooling upgrades
- ESLint 9 → 10 (#286) + `@eslint/js` 10 + `jiti` 2.6.1; 4 breaking-change rule fixes.
- `js-yaml` devDep added — fixes pre-existing `architecture-ddd-trio` test (20 tests unblocked).

#### Documentation
- `README.md` expanded as single source (live counts: 25 skills / 10 commands / 7 agents / 6 hooks).
- `CLAUDE.md` stripped 127 → 81 lines (pointer + runtime-critical: Session Config block byte-preserved, Destructive-Command Guard, Agent Authoring Rules, Current State).

### Fixed

- **#382** ecosystem-health body-status precedence — drop `curl -f` so 4xx/5xx still deliver body; add `-w 'HTTP_STATUS:%{http_code}'`; body-first JSON parse: `{status: degraded}` → DEGRADED on any 200-599. Report Format updated to list DEGRADED + DOWN as flag-worthy.
- **#400** `sessions.jsonl` writer alias — `scripts/lib/session-schema.mjs` adds `waves_completed → total_waves` to `SESSION_KEY_ALIASES` so legacy coord-direct entries normalize cleanly. `skills/session-end/session-metrics-write.md` gains MANDATORY WRITE PATH callout forbidding hand-composed JSONL writes; `emit-session.mjs` documented as the only sanctioned writer.
- **#32** events.jsonl `agent:"unknown"` 100%-of-the-time bug — `hooks/on-stop.mjs` `agent_name` (invented) → `agent_type` (Claude Code contract). 11 fixture rewrites; 3 new tests including a contract-pin guarding against `agent_name` reintroduction.
- **#222** harness-audit integration JSON truncation — root cause: `spawnSync` default `maxBuffer` <12KB on some CI; fixture pollution from prior dev runs. Fix: explicit `maxBuffer=16MB`, `unlinkSync` guard in `copyFixtureToTmpdir`. 10/10 pass.
- **#279** schema-drift CI 403 — `.gitlab-ci.yml` uses `SCHEMA_DRIFT_TOKEN` (deploy token / PAT) instead of `CI_JOB_TOKEN`; missing-token fallback skips gracefully. `docs/ci-setup.md` documents creation steps.

### Removed

- `scripts/lib/common.sh` — superseded by `common.mjs` (`die`, `warn`, `requireJq`, `findProjectRoot`, `resolvePluginRoot`).
- `scripts/lib/platform.sh` — superseded by `platform.mjs`.
- `scripts/lib/gates/gate-{baseline,incremental,full,per-file,helpers}.sh` (5 files, 365L).
- `scripts/lib/validate/check-{plugin-json,component-paths,json-files,agents,commands}.sh` (5 files, 458L).
- 5 top-level .sh scripts: `codex-install.sh`, `cursor-install.sh`, `run-quality-gate.sh`, `validate-plugin.sh`, `lib/fetch-baseline.sh`.

### Security

- **#247** vault-backfill YAML injection (CWE-1336) — `yamlScalar(JSON.stringify)` helper applied to user-supplied `owner` and `gitlabPath`. Newline-injection regression test proves no extra YAML keys emitted. +14 tests.
- **#108** bootstrap security — atomic lock-write, claude-init guard, cp-rP symlink fix. +7 regression tests.

### Closed Issues

- **Epics:** #309 (Architecture-DDD-Trio adoption), #271 (v3.2 Autopilot), #181 (harness-retro), #265 (META-AUDIT triage), #161 (Owner Persona D-axis).
- **Owner Persona D-axis:** #173 (consent merged), #175, #176, #177 (D2/D3/D4).
- **Refactor splits:** #283, #284, #285, #287, #288, #122, #121, #208.
- **Bash-free milestone:** #218, #317, #124 (Windows native superseded by v3.2.0).
- **Bug fixes / hardening:** #303, #304, #290, #203, #289, #178, #211, #227, #228, #308, #269, #212, #214, #247, #156, #266, #264, #279, #113, #112, #222, #382, #400.
- **AGENTS.md alias parity:** #33, #32, #30.
- **Vault & docs:** #232, #230, #237, #223, #144, #319.
- **Tracking & verification:** #143, #152, #153, #154, #119, #86, #174, #215, #286, #306, #108.
- **Spawned during this cycle:** #317 (closed same cycle), #318 (G-axis tracker, kept open).

### Quality

- Tests: 1871 (post-v3.2.0) → **2623** (+752), 12 skipped.
- Typecheck: 54 files OK.
- Lint: 0 errors / 0 warnings.
- Coverage above thresholds 70 / 65 / 70 / 60.
- Banner-version-sync regression test (`tests/hooks/banner-version-sync.test.mjs`) keeps `hooks/{hooks,hooks-codex}.json` echo banners aligned with `package.json`.

### Migration

- **No breaking changes.** All public APIs (config keys, CLI flags, JSON outputs) preserved.
- The `learnings.jsonl` and `sessions.jsonl` schema evolutions ship with idempotent migrate-* CLIs (`scripts/migrate-learnings-jsonl.mjs`, `scripts/migrate-sessions-jsonl.mjs`); existing legacy entries are normalized on next read via `SESSION_KEY_ALIASES` (read-time backwards compat).
- `bootstrap.lock` v1 entries gain a new optional `plugin-version` field; absence triggers a `soft` info banner only.
- The bash-free refactor is internal — `node_modules`-managed `node` is the only runtime; no `bash` is invoked from any plugin script after upgrade.


## [3.2.0] - 2026-04-27

Consolidated stable release covering the v3.0.0 (Windows native), v3.1.0 (environment-aware sessions), and v3.2.0 (Mode-Selector + Autopilot) work since v2.0.0. Supersedes the `v3.0.0-rc.1` pre-release.

### ⚠ BREAKING CHANGES (carried from v3.0.0)

- **Node.js 20+ is required.** The plugin runs as ES modules (`.mjs`) and uses native `fs.promises`, `fetch`, and `AbortSignal.timeout`. Node 18 and earlier are unsupported.
- **`npm install` is required once in the plugin directory** before hooks fire. `zx` is a runtime dependency; without it, hooks fail at load time.
- **Hooks are now `.mjs` files instead of `.sh`.** `hooks/hooks.json` points to the Node runtime. Custom consumer configs that referenced `.sh` hook paths must be updated.
- **`jq` and Bash are no longer hard dependencies for hooks.** Scope/command enforcement and session state reads use native Node JSON parsing. `jq` remains a soft recommendation for policy-editing workflows.
- **`bats` test suite retired.** Development and CI use [vitest](https://vitest.dev/) exclusively.

See [`docs/migration-v3.md`](docs/migration-v3.md) for the upgrade path from v2.x.

### Added — Windows native + Node.js migration (v3.0.0 surface)

- **Native Windows support** — no WSL or Git-Bash required. All file paths use `path.join`, tmp paths use `os.tmpdir()`, filesystem walks terminate at drive roots, glob matching normalizes backslashes. CRLF-tolerant config parsing and `.gitattributes` EOL rules prevent autocrlf breakage.
- **GitHub Actions CI matrix** across `ubuntu-latest`, `macos-latest`, and `windows-latest` with `fail-fast: false`, concurrency grouping, and per-OS `jq` install steps.
- **Vitest test framework** (`npm test`) replacing the `bats` shell harness. 1871 passing tests with byte-exact parity checks against the retired Bash implementations.
- **`package.json` at plugin root** with `type: "module"`, `engines.node >= 20`, `zx ^8.1.0` runtime dep, ESLint v9 + Prettier v3 + Vitest dev deps. `npm ci` bootstraps a reproducible tree.
- **Pre-bash destructive-command guard** — `hooks/pre-bash-destructive-guard.mjs` blocks `git reset --hard`, `rm -rf`, `git push --force`, and related destructive operations in the main session, with a 13-rule policy at `.orchestrator/policy/blocked-commands.json`. Opt-out via `allow-destructive-ops: true` in Session Config.
- **Canonical `parallel-sessions.md` rule** vendored via bootstrap. Documents PSA-001 through PSA-004 (detect before acting, ask before assuming, never destroy what you didn't create, isolate your changes).
- **ESLint v9 flat config + Prettier** with Node 20 globals, `_`-prefix allowlist for unused vars, markdown excluded.

### Added — Environment-aware sessions (v3.1.0 surface)

- **Resource-gate dispatch** — `scripts/lib/wave-resource-gate.mjs` reads live RAM/CPU/concurrent-session metrics before dispatching each wave. Eight-rule decision chain returns `proceed`, `reduce` (halve agent count), or `coordinator-direct` (0 agents). Configurable via `resource-thresholds` in Session Config; failures degrade to `proceed` so the gate never blocks.
- **`worktree-exclude` Session Config field** — string array of top-level directories skipped when creating agent worktrees. Default 10-pattern list (`node_modules`, `dist`, `build`, `.next`, `.nuxt`, `coverage`, `.cache`, `.turbo`, `.vercel`, `out`). Empty array disables. Cuts worktree clone size dramatically on large repos.
- **Multi-session registry** — `scripts/lib/session-registry.mjs` tracks active sessions across the host with heartbeat + sweep semantics. `on-session-start` hook registers the session and detects peers; `on-stop` deregisters cleanly. Enables resource-gate to factor in concurrent Claude/Codex sessions.
- **Anonymized hardware learnings** — `scripts/export-hw-learnings.mjs` exports anonymized hardware-pattern learnings for sharing. Promotion pipeline + anonymization regex tuning (no machine names, no usernames, no absolute paths).
- **CWD-drift guard** — `restoreCoordinatorCwd` runs after every worktree-isolated `Agent` dispatch so subsequent coordinator Edit/Write/Bash calls cannot silently route to a worktree branch.
- **Coordinator snapshots** — pre-dispatch `git stash create` refs under `refs/so-snapshots/` provide crash recovery for unfinished sessions; surfaced to the user via `AskUserQuestion` on session-start when the prior session was `active` or `paused`.

### Added — Mode-Selector + Autopilot (v3.2.0 surface)

- **Mode-Selector** — pure-function recommendation engine that suggests the next session mode (`housekeeping` | `feature` | `deep`) based on live signals: prior STATE.md recommendations, recent `sessions.jsonl` trend, surfaced learnings, bootstrap tier, and live VCS backlog scan. Renders a `📊` banner at session-start when confidence ≥ 0.5; modifies `AskUserQuestion` option ordering to put the recommendation first.
- **STATE.md Recommendations Contract** — five forward-looking frontmatter fields (`recommended-mode`, `top-priorities`, `carryover-ratio`, `completion-rate`, `rationale`) written by `session-end` and read by the next session-start as a `📋` banner. Backwards-compatible: pre-v1.1 STATE.md files are silently no-op.
- **Autopilot loop** — `/autopilot` runs `session-start → session-plan → wave-executor → session-end` chained for N iterations with kill-switches: SPIRAL detection, FAILED-wave gate, carryover > 50%, max-hours, sub-threshold confidence, etc. Eight kill-switches total.
- **Autopilot headless CLI** — `scripts/autopilot.mjs` is a walk-away driver. `--headless` required flag, `--verbose` pipes child stdio, `--dry-run` round-trips without spawning. Spawns `claude -p '/session <mode>'` per iteration and propagates `AUTOPILOT_RUN_ID` for retro joinability across `autopilot.jsonl` ↔ `sessions.jsonl`.
- **Vault-mirror auto-commit** — opt-in via `--session-id <id>`. After mirroring artifacts, stages `40-learnings/` + `50-sessions/`, validates `_generator: session-orchestrator-vault-mirror@1` frontmatter on every staged file, commits as `chore(vault): mirror <id> — N learnings + M sessions` when staged set is all-mirror, or unstages + warns on mismatch.
- **Docs-orchestrator** — opt-in audience-aware doc generation (User / Dev / Vault). Three hook points: session-start Phase 2.5 (audience detection + AskUserQuestion), session-plan Step 1.5/1.8 (Docs role classification + docs-writer auto-match), session-end Phase 3.2 (per-task ok/partial/gap verification).
- **Harness-audit scorecard** — deterministic 7-category rubric (`RUBRIC_VERSION` pinned), JSON to stdout + JSONL trend in `.orchestrator/metrics/audit.jsonl`. Available via `/discovery audit` probe and standalone `/harness-audit` command.
- **Plan modes** — `/plan new` (project kickoff with repo scaffolding), `/plan feature` (compact feature PRD), `/plan retro` (data-driven retrospective with vault-backfill sub-mode). All modes share a researched Q&A engine that dispatches parallel Explore agents before each question wave.
- **Discovery probes** — modular probes adapted to the project's tech stack, including `vault-staleness`, `vault-narrative-staleness`, `state-md-staleness`, and `bootstrap-lock-freshness`.
- **Adaptive wave sizing** — complexity scoring (files × directories × issues) maps to agent counts per role. Cross-session learnings can override the formula based on historical data.
- **Intelligent agent dispatch** — project agents > plugin agents > general-purpose. Optional `agent-mapping` Session Config for explicit role-to-agent binding. Model selection matrix (haiku / sonnet / opus per task type).
- **`isolation: 'none'` default for new-directory waves** — Pre-Dispatch New-Directory Detection forces `isolation: 'none'` when any agent's target parent directory doesn't exist and `configIsolation: 'auto'`. Avoids the Claude Code merge-back regression where new-dir writes silently fail to sync back from worktrees. Explicit `isolation: 'worktree'` overrides are honored with a warning.

### Changed

- **All hooks and `scripts/lib/` helpers migrated from Bash to Node.js.** Security-critical hooks (`enforce-scope.mjs`, `enforce-commands.mjs`) include symlink-escape protection, shell-operator + quote-boundary parsing, and Windows backslash normalization.
- **Cross-platform path handling** — `os.tmpdir()` replaces `${TMPDIR:-/tmp}`, `path.join`/`path.sep` throughout, `path.parse(dir).root` for filesystem-walk termination.
- **Native JSON parsing** replaces all `jq` shell-outs inside hooks.
- **Vitest 4.1.5** — upgraded from vitest 2.1.9. Includes the GitHub Actions CI tinypool timeout wrapper for Windows.

### Removed

- `bats` test suite — retired in favor of vitest.
- Hard runtime dependency on `jq` and `bash` for hooks.

### Security

- Pre-bash destructive-command guard active alongside subagent waves.
- Symlink-escape protection in scope-enforcement (`fs.realpath` + ancestor-walk fallback for non-existent targets).
- Shell-operator + quote-boundary parsing in command-enforcement (catches `ls;rm -rf /`, `psql -c "DROP TABLE …"`, and similar bypass patterns).
- `CLAUDE_PROJECT_DIR` validated against the platform's state directory before being trusted by enforcement hooks.
- Coordinator-snapshot refs (`refs/so-snapshots/`) garbage-collected at session-end for completed sessions.

### Quality

- 1871 tests passing / 10 skipped across vitest suites.
- Coverage thresholds: 70 / 65 / 70 / 60 (lines / functions / statements / branches).
- ESLint v9 flat config, Prettier v3 — `lint:fix` idempotent on the full tree.
- TypeScript discipline via `node scripts/typecheck.mjs` (43 file(s) OK).

### Migration

Short version:

```bash
cd /path/to/session-orchestrator
git pull
npm install           # installs zx + vitest + ESLint + Prettier
# Restart Claude Code / Codex / Cursor so hooks.json is re-read.
```

For details, see [`docs/migration-v3.md`](docs/migration-v3.md). Rollback: `git checkout v2.0.0 && rm -rf node_modules` and restart the editor.

---

## Internal Development Trail (pre-v3.2.0)

Detailed per-session entries captured during the v3.0.0 / v3.1.0 / v3.2.0 development cycles. Retained for traceability; content is consolidated in the [3.2.0] release block above.

### Added — harness-retro Wave 1 (2026-04-19, Epic #181)

Promotes validated patterns from advanced consumer repos into bootstrap defaults.

- `scripts/lib/config-schema.mjs` + `scripts/validate-config.mjs` (#182): plain-JS Session Config validator (no zod dep). Enforces 7 mandatory fields (`test-command`, `typecheck-command`, `lint-command`, `agents-per-wave`, `waves`, `persistence`, `enforcement`). Wired into `scripts/parse-config.sh` with enforcement-aware behavior (off|warn|strict). Bypass via `SO_SKIP_CONFIG_VALIDATION=1`.
- Bootstrap canonical config block: `_minimal/CLAUDE.md.tmpl` + `fast-template.md` + `public-fallback.md` now emit all 7 mandatory fields on every tier.
- Bootstrap `--retroactive` config-field patcher (#182): fills missing mandatory fields with package-manager-aware defaults during retroactive adoption.
- `.orchestrator/policy/quality-gates.schema.json` + `quality-gates.example.json` + `scripts/lib/quality-gates-policy.mjs` (#183): JSON-Schema policy for canonical test/typecheck/lint commands. Readable from Node (`loadQualityGatesPolicy`, `resolveCommand`) and Bash (`scripts/run-quality-gate.sh` policy-first `extract_command`).
- `scripts/lib/package-manager.mjs` (#183): lockfile-based detection (`pnpm-lock.yaml` / `yarn.lock` / `bun.lockb` / `package-lock.json`) + per-PM default command triads. Null falls back to npm (most portable).
- `skills/bootstrap/standard-template.md` Step 6.5 (#183): bootstrap writes `.orchestrator/policy/quality-gates.json` with package-manager-aware defaults. Idempotent — never overwrites user edits.
- `skills/bootstrap/standard-template.md` Step 6.6 + `skills/bootstrap/STATE.md.template` (#184): bootstrap scaffolds an idle STATE.md placeholder.
- `scripts/lib/state-md.mjs` (#184): hand-rolled YAML-subset frontmatter helpers (`parseStateMd`, `serializeStateMd`, `touchUpdatedField`, `readCurrentTask`). Never throws.
- STATE.md schema v1 extended with 3 optional fields: `updated`, `session`, `session-start-ref`. Backward-compat for files that omit them.
- `skills/session-start/SKILL.md` Phase 1.5: current-task banner from STATE.md. Phase 4: command-availability check before quality baseline.
- `skills/session-end/SKILL.md` Phase 3.4: touches `updated: <ISO>` on session close.
- `skills/discovery/probes-session.md`: new `state-md-staleness` probe (warn >7d, info 2-7d) reading optional `updated` frontmatter with file-mtime fallback.
- vitest coverage: +68 tests across `tests/lib/{config-schema,quality-gates-policy,package-manager,state-md}.test.mjs` and `tests/integration/parse-config-validator.test.mjs`. Total suite: 546 pass, 10 skipped.

### Added — v3.1.0 sub-epic B resource-gate session (2026-04-19)
- `scripts/lib/wave-resource-gate.mjs` (#193): pre-dispatch gate consumed by wave-executor. Reads `resource-thresholds` from Session Config and live RAM/CPU/concurrent-session metrics from `resource-probe.mjs`. 8-rule decision chain returns `proceed`, `reduce` (halve agent count, min 1), or `coordinator-direct` (0 agents). Probe failures and missing-thresholds configs degrade to `proceed` — gate never blocks the dispatch loop. Exported `formatGateReport(result)` helper for coordinator progress updates.
- `skills/wave-executor/wave-loop.md` § 0.5 (#193): pre-dispatch resource-gate playbook + STATE.md deviation contract — `reduce` and `coordinator-direct` decisions append a single timestamped line to `## Deviations` with measurements, so future sessions and `/evolve` can mine for hardware-pattern learnings.
- `worktree-exclude` Session Config field (#192): string array of top-level directories to skip when creating agent worktrees. Default 10-pattern list (`node_modules`, `dist`, `build`, `.next`, `.nuxt`, `coverage`, `.cache`, `.turbo`, `.vercel`, `out`). Empty array disables the feature. Mirrored across `scripts/lib/config.mjs`, `scripts/parse-config.sh`, and the validator.
- `scripts/lib/worktree.mjs` `applyWorktreeExcludes(wtPath, patterns)` (#192): pure-fs helper extracted from `createWorktree` for unit-testability (dodges a vitest worker-thread + zx AsyncLocalStorage interaction). Best-effort `fs.rm` per pattern, stderr-logs each removal. `createWorktree` now reads the configured exclude list (or accepts `options.excludePatterns`) and applies the helper after `git worktree add` succeeds. Bash parity in `scripts/lib/worktree.sh`.
- vitest coverage: 12 cases for `wave-resource-gate` (each decision branch + `plannedAgents=0/1` edges + probe-failure mock + missing-thresholds defensive path), 5 cases for `applyWorktreeExcludes` (default list, custom override, empty disables, non-existent pattern, top-level-only nesting), 2 cases for `worktree-exclude` config parsing. Suite total: 533/543 pass, 10 pre-existing skipped, 0 failed.

### Context — sub-epic B
Issues #192 and #193 were filed in response to an OOM incident in a consumer repo (2026-04-19) where parallel sessions saturated host RAM. The exclude list keeps worktree clones lean; the resource-gate halves or escalates dispatches when free RAM drops below configured thresholds. Both ship on `feat/v3.1-resource-gate-192-193 → feat/env-aware-v3.1` (Sub-Epic A+B will land via MR !9 once env-aware reaches main).

### Added — libs + hooks session (2026-04-19)
- `scripts/lib/io.mjs` (#131): hook stdin/stdout helpers. `readStdin()` with 5 s AbortController timeout + 1 MB byte guard, `emitAllow`/`emitDeny`/`emitWarn`/`emitSystemMessage` matching the Claude Code hook I/O contract (exit 2 for deny, 0 for allow, single-line JSON on stdout). Pure Node stdlib, no external deps.
- `scripts/lib/events.mjs` (#133): JSONL append to `.orchestrator/metrics/events.jsonl` via `fs.promises.appendFile` + optional fire-and-forget webhook POST via native `fetch` with `AbortSignal.timeout(3000)` when `CLANK_EVENT_SECRET` is set. Network errors swallowed; graceful skip when env var unset.
- `scripts/lib/worktree.mjs` (#134): zx-based cross-platform git worktree helpers. `os.tmpdir()` replaces `${TMPDIR:-/tmp}`, `path.join` throughout for Windows separator safety, retry-once pattern in `createWorktree`, best-effort `removeWorktree` (always resolves, warns on uncommitted changes), `listWorktrees`, `cleanupAllWorktrees`.
- `scripts/lib/hardening.mjs` (#135): env/runtime checks (`assertNodeVersion`, `assertDepInstalled`, `checkEnvironment`) plus scope/pattern primitives used by the Wave 3 hooks (`findScopeFile`, `getEnforcementLevel`, `gateEnabled`, `pathMatchesPattern`, `commandMatchesBlocked`, `suggestForScopeViolation`, `suggestForCommandBlock`). Scope expanded beyond the original issue to absorb hook-primitive helpers — documented in the commit trailer.
- `scripts/lib/common.mjs` (#136): shared utilities (`makeTmpPath`, `utcTimestamp`, `epochMs`, `readJson`, `writeJson`, `appendJsonl`). Async `fs.promises`, auto-creates parent directories via recursive `mkdir`.
- `hooks/enforce-scope.mjs` (#137): PreToolUse hook blocking Edit/Write outside `wave-scope.json` `allowedPaths`. Node port of `hooks/enforce-scope.sh` with SECURITY-REQ-01..08 from security pre-review addressed: top-level try/catch emits `emitDeny` on any unhandled error (never exit 1), `fs.realpath` on file + ancestor-walk fallback for non-existent targets prevents symlink-escape, Windows backslash normalization before glob matching, relative `file_path` resolved against project root (not CWD), scope file read once per invocation.
- `hooks/enforce-commands.mjs` (#138): PreToolUse hook blocking dangerous Bash commands. Shell-operator-aware word boundary (catches `ls;rm -rf /`, `ls&&rm -rf /`, `(rm -rf /)`, `` `rm -rf /` ``, `$(rm -rf /)`) plus quote-boundary (catches `psql -c "DROP TABLE …"`). Fallback blocklist expanded: adds `git push -f` short form and `drop table` lowercase variant that the Bash predecessor missed.
- vitest coverage for Wave 2–3 artifacts: 179 tests across `tests/lib/{io,events,worktree,hardening,common}.test.mjs` and `tests/hooks/{enforce-scope,enforce-commands}.test.mjs`. Includes F-01 shell-operator-bypass regression block, F-02 symlink-escape regression (skipIf win32), 10-row `pathMatchesPattern` parity table, and 8-row `commandMatchesBlocked` parity table from the migration baseline spec. Total suite: 343 tests pass, 10 pre-existing skipped, 0 failed.

### Fixed — libs + hooks session
- `scripts/lib/worktree.mjs`: replaced 5 `$.nothrow($\`…\`)` call sites with the `nothrow` named export — in zx v8 `$.nothrow` is a boolean property, not callable; the original code threw `TypeError` on every cleanup path.
- `scripts/lib/hardening.mjs:commandMatchesBlocked`: extended boundary class from `\s` to `[\s;|&(){}`'"]`. Previously `ls;rm -rf /` bypassed the blocklist because the semicolon wasn't a boundary char; `psql -c "DROP TABLE …"` bypassed because the quote wasn't either. Both are now caught (7 regression tests).

### Also on this branch (parallel non-session commits)
Three vault-sync commits (`a76e180`, `e3c8e47`, `82be589`) landed alongside the v3 libs/hooks session. Scope: managed-mirror Zod schema sync with a drift gate, BEGIN/END sentinels on vendored schema, GitLab CI pipeline (`test` + `schema-drift-check` stages), and learning-provenance decoupling from session-file lifecycle. Not part of the `[131,133,134,135,136,137,138]` session plan — documented here for traceability. Requires a one-time `projects-baseline` CI/CD Token Access allowlist for `infrastructure/session-orchestrator` before the first pipeline run.

### Added — foundation wave (2026-04-18)
- `.gitattributes` (#125): cross-platform EOL rules. LF for `.sh`, `.md`, `.json`, `.yaml`, `.mjs`; CRLF for `.ps1`; `* text=auto` fallback. Prevents autocrlf breakage on Windows checkouts.
- `package.json` + `package-lock.json` (#126): plugin-root Node 20+ manifest with `type: "module"`, zx ^8.1.0 dep, ESLint v9, Prettier v3, and vitest ^2 devDeps. `npm ci`-installable. Version bumped to `3.0.0-dev`.
- ESLint v9 flat config + Prettier (#127): `eslint.config.js` with @eslint/js recommended, Node 20 globals, project rules (`no-unused-vars` with `_`-prefix allowlist, `prefer-const`, `no-var`, `eqeqeq`). `.prettierrc` uses single quotes, 100 columns, LF. `.prettierignore` excludes `*.md` because skill files have intentional formatting. Baseline green, `lint:fix` idempotent.
- CI matrix for ubuntu, macos, and windows-latest (#128): `.github/workflows/test.yml` extends to a 3-OS matrix with `fail-fast: false`. Preserves v2.0 hardenings (least-privilege `permissions`, `timeout-minutes`, SHA-pinned actions). Adds concurrency group, conditional typecheck (gated on `.mjs` existence), jq install per-OS, vitest placeholder for Wave 4.
- `scripts/lib/platform.mjs` (#129): Node port of `platform.sh` with Windows-safe filesystem walk (`path.parse(dir).root` replaces the Bash `/`-terminator that breaks on `C:\`). New exports: `SO_OS`, `SO_IS_WINDOWS`, `SO_IS_WSL`, `SO_PATH_SEP` alongside the six existing IDE/project constants. Five named helper functions for callers that need to re-detect.
- `scripts/lib/path-utils.mjs` (#130): CWE-23-safe pure path helpers backing the forthcoming `enforce-scope.mjs`. Rejects null bytes, empty strings, UNC paths (Windows), prefix-match confusion, cross-drive escapes. Locale-stable case normalization via `toLocaleLowerCase('en-US')` to avoid Turkish-I-style regressions. Exports a documented `CWE_23_ATTACK_PATTERNS` taxonomy for test self-check.
- `scripts/lib/config.mjs` (#132): Node port of `parse-config.sh` + `config-yaml-parser.sh` + `config-json-coercion.sh` combined into one module with private coercion helpers. CRLF-tolerant input, native JSON (no jq shellout). Byte-exact parity against the `.sh` version on the project's own `CLAUDE.md`.
- vitest coverage for foundation libs: 142 tests across `tests/lib/{platform,path-utils,config}.test.mjs` plus 5 fixtures under `tests/fixtures/`. `path-utils` tests cover every documented CWE-23 vector and are falsification-verified. `config` tests include a subprocess-bash parity diff gated on non-Windows.

### Fixed — foundation wave
- session-start: reset STATE.md to idle when previous session completed. Clears `current-wave`, sets `status: idle`, demotes `## Wave History` into `## Previous Session`, and empties `## Deviations`. Only triggers on the `completed` branch; `active` and `paused` paths remain user-interactive via AskUserQuestion. Prevents a fresh session from appearing "already completed". (closes infrastructure/projects-baseline#159)
- Pre-v3 `.mjs` lint baseline: removed an unused `fileURLToPath` import, replaced `== null` with explicit `=== null || === undefined`, prefixed intentionally-unused destructures and params with `_`, cleaned a `no-useless-escape`. `npm run lint` is now idempotent on the full tree.

### Migration — still pending
- Hook wiring (`hooks.json` → `.mjs`) is still on the bash files. `enforce-scope.mjs` + `enforce-commands.mjs` are implemented and tested but not yet activated — that lands with #142 in a later session.
- 3 lower-priority hook migrations remain (`post-edit-validate`, `on-session-start`, `on-stop`) — issues #139–#141.

### Migration
- Developer prerequisite: Node 20+ and `npm ci` after clone. Existing bash test suite (`scripts/test/run-all.sh`) continues to work on Unix while the foundation stabilizes; Windows users run `npm test` only.
- No user-visible breakage yet. Hook migrations in later waves will require `npm install` in the plugin directory before hooks fire.

---

## Older versions

To keep this file focused on the current **3.x** major line, changelog entries for the
earlier release lines have been archived:

- [`docs/changelog/v2.md`](docs/changelog/v2.md) — the **2.x** line (2.0.0 and its alpha/beta pre-releases, 2026-04-02 -> 2026-04-17)
- [`docs/changelog/v1.md`](docs/changelog/v1.md) — the **1.x** line (1.0.0, 2026-04-02)
