session-orchestrator 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +70 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +57 -4
- package/docs/components.md +12 -12
- package/docs/events-schema.md +11 -8
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +55 -6
- package/docs/session-config-template.md +11 -2
- package/hooks/_lib/hook-import-set.json +27 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +53 -0
- package/hooks/on-stop.mjs +3 -1
- package/hooks/post-tool-batch-wave-signal.mjs +3 -1
- package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +559 -72
- package/hooks/subagent-telemetry.mjs +96 -19
- package/package.json +3 -3
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +49 -53
- package/scripts/backfill-abandoned-sessions.mjs +1 -1
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/emit-session.mjs +296 -2
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +10 -6
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +71 -10
- package/scripts/lib/events.mjs +21 -2
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/instruction-budget-guard.mjs +69 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +43 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +62 -11
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +10 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +323 -302
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +72 -46
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/evolve/SKILL.md +2 -2
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +22 -60
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.4.0] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **One lock for every in-process `learnings.jsonl` read-modify-write** (`scripts/lib/learnings/io.mjs` `withLearningsLock()`, #1447 item 8). `appendLearning`, `rewriteLearnings`, the expiry sweep and prune, `apply-session-learnings --apply`, `export-hw-learnings --promote` and `run-migrate-v2-cross-repo --apply` (a locked repo is reported and left unwritten, exit 2) run their whole read-to-write inside `<store>.lock` (over `withFileLock`, reentrant per async call chain only). Before, an append landing between a rewriter's read and its rename was lost; on a 10 s lock timeout nothing is written now and the CLIs exit non-zero naming the lock file. The lock and rewrite temp files are gitignored in this repo. Not covered: the `/evolve --prune --entries` sidecar is built by the agent before the prune runs, so a record appended in between is still archived as `pruned` (#1486).
|
|
15
|
+
- **Per-model token breakdown for mixed-model subagents** (`hooks/subagent-telemetry.mjs`, `scripts/lib/subagents-schema.mjs`, #1470). A subagent record whose billable turns span two or more models carries `models_usage` (the four buckets per model); `rollupSessionTokens()` prices it part by part instead of leaving it unpriced.
|
|
16
|
+
- **`cost_records_priced` / `cost_records_total` reach `sessions.jsonl`** (#1475 variant B) as optional, validated fields, so the share of a session the cost covers is visible in the ledger.
|
|
17
|
+
- **`describeOriginForLog()`** (`scripts/lib/named-vault-resolver.mjs`, #1479 item 4): one formatter the three vault writers use for the origin in their refusal line — a local origin prints as `file://<local path>`, a remote one with credentials redacted.
|
|
18
|
+
- **Fleet protocol v1 — files instead of messages** (#1462, Epic #1461, PRD !53 S1). New reference `skills/_shared/fleet-protocol.md` (no rule, nothing under `.claude/rules/`) and the schema library `scripts/lib/fleet-protocol.mjs` — the only place that knows the `~/.config/navigator/` layout (override `NAVIGATOR_CONFIG_DIR`, the navigator-CLI's own variable), the file modes 0600/0700, the check-in fields, `CHECKIN_FALLBACK_MIN = 10` and lease validity: `readNavigatorLease()` returns exactly `active` \| `none` (missing or expired) \| `unreadable` (broken JSON, missing fields) and is fail-closed — an unreadable or expired lease is never "aktiv". `isSafeSessionId()` guards every path built from a session id (no `/`, `..`, NUL). Review hardening before merge: a lease whose `session_id`/`adresse` carries control bytes, whose `plattform` is unknown or whose timestamps lack the `Z` is `unreadable` (a `Z`-less value would be read in local time and could extend the lease west of UTC); `repo` must be a name, `kandidaten` issue numbers only — both travel in the event, which may leave the host over the optional webhook.
|
|
19
|
+
- **Fleet check-in: session-start Phase 7.6, operations-contract peer preflight, CLI and event** (#1462). `scripts/fleet-checkin.mjs` reads the check-in JSON from stdin, stamps `zeit` (`date -u +%FT%TZ` form), validates, writes `checkin/<session_id>.json` atomically with mode 0600 (directories 0700) and emits `orchestrator.fleet.checkin` (payload `session`, `repo`, `modus`, `kandidaten`, `navigator_state`; catalogued in `docs/events-schema.md`); exit 0 written, 2 invalid input (error list on stderr, no file), 1 write error. The new Phase 7.6 in `skills/session-start/SKILL.md` (+6 lines / +1 038 B against the +12 / +1 200 B ceiling) and one sentence in `references/operations-contract.md` (+4 lines / +318 B against +4 / +400 B) place the check-in before the first writing action; a hint to the navigator is optional and never awaited (CSM-004); after 10 min without `auflagen/<session_id>.json` the reference's Standard-Auflagen apply. Test isolation: `tests/setup/navigator-dir-guard.mjs` points `NAVIGATOR_CONFIG_DIR` at a tmp dir per vitest worker, so no test reads or writes the operator's real directory.
|
|
20
|
+
- **Registry field `role`** (#1462). `heartbeat(sessionId, { role })` in `scripts/lib/session-registry.mjs` accepts `'navigator'` or `null`, throws `TypeError` on anything else before touching the file, and the patch-less call `heartbeat(id)` (`hooks/on-stop.mjs`) preserves the field; `_validEntry` drops an entry whose `role` is neither `null` nor a string. Display only, never ownership — the navigator lease stays authoritative (`skills/_shared/state-ownership.md`). `registerSelf()` does not set it and `classifyMode` is untouched; this change ships no production writer (the `/navigator` skill is a later cut).
|
|
21
|
+
- **Navigator banner line** (#1462). `hooks/on-session-start.mjs` appends exactly one line after the peer lines, on every harness that runs the hook: `🧭 Navigator aktiv: <adresse ?? session_id>, Check-in nach ~/.config/navigator/checkin/`, `🧭 Navigator: keiner nachweisbar (keine gültige Lease)` or `🧭 Navigator: Lease unlesbar, Status nicht nachweisbar` — never more than 120 bytes UTF-8 (a long label is cut, measured with a 128-character `adresse`; a 200-character id is refused by the lease reader and renders the "unlesbar" line), never blocking. 🧭 joins the curated emoji set in `scripts/lib/validate/check-unicode-safety.mjs`.
|
|
22
|
+
- **Skill `navigator` — the fleet-coordinator persona as a user-invocable skill** (#1462 B5, Epic #1461). `skills/navigator/SKILL.md` (`/session-orchestrator:navigator ticker|status|start|fenster|handover`, `disable-model-invocation: true` because a coordinator role runs only on operator command; description 344 bytes against the ≤ 400 B ceiling of PRD 4.5) describes what the navigator measures, writes and refrains from: it grants no approvals („Freigaben erteile ich keine, maßgeblich ist dein Owner-Auftrag"), writes `auflagen/<session_id>.json` only through `auflagenPath()` after `isSafeSessionId()`, treats a merge "Zeichen" as its own measurement (MR opened + mergeable, head = pipeline = gate SHA, job list, main free, file overlap), starts headless runs only behind a slot gate and a busy gate, carries the verbatim foreground wait pattern, the offload path rule (worktree path, alias `m5-remote` only), the Codex rules, the account watch and the AI-budget rule, and sets `role: 'navigator'` via `heartbeat()` per ticker round — the first production writer of that registry field. Lease acquisition, renewal and handover stay S4 (agents/navigator#36): the skill writes no lease, so the session-start banner keeps reporting "keiner nachweisbar" until S4 lands. Schemas are not repeated; `skills/_shared/fleet-protocol.md` stays the single reference (its obsolete `path-check: planned` marker for the PRD is removed and it now links the skill). Generated mirrors under `.agents/`, `.codex-plugin/`, `.cursor/` and `pi/`; README and `docs/components.md` counts move to 52 skills / 28 slash commands / 26 user-invocable skills.
|
|
23
|
+
- **`skills/navigator/references/` — pure decision functions plus measurement-only shell wrappers** (#1462 B5). `fleet-decisions.mjs` exports `slotDecision` (free-memory bands 4 / 3 / 0 runs at ≥ 50 % / ≥ 35 % / below, load1 > 40 closes everything, an unreadable value yields cap 0), `busyDecision` (a Codex rollout for this checkout younger than 30 min or a `session.lock` younger than 6 h means busy; missing counts mean busy) and `chooseAccount` (usable = 5h < 97 % and 7d < 97 %, earliest 7-day reset within 3 days first, far accounts by consumption, 1 800 s tie tolerance, a row without data is never usable), all fail-closed, none reading the environment, with a stdin-JSON CLI (`slot|busy|account`, exit 0 allowed / 1 refused / 2 bad input). `slot-check.sh` and `account-check.sh` only measure (`memory_pressure`, `vm.loadavg`, `pgrep` anchored on the `timeout -k 60` wrapper because one Codex run spawns three matching processes, `find -mmin`, the operator's `NAVIGATOR_ACCOUNT_CMD`) and hand the numbers to the module; `account-check.sh` prints the decision only, never the account rows (SEC-008), and `--apply` merely shows the switch command because a switch affects every session on the host. Tests: `tests/skills/navigator/fleet-decisions.test.mjs` (24 cases, each naming its bug, every rule probed red before its fix, including that no account alias ever reaches the wrapper's output).
|
|
24
|
+
- **Session Config `offload-first` — placement before pressure** (#1465, navigator decision E4). New boolean key, default `false`; `false` or absent changes nothing. With `true`, `applyOffloadDecision()` in `scripts/lib/wave-resource-gate.mjs` fires on a `proceed` verdict too, not only on `reduce` / `coordinator-direct`: an offloadable role goes to the first declared `remote-hosts` entry whose `roles-allowed` carries it and that is witnessed ready, still under the HR-004 heavy-repo cap. The offloadable table (`OFFLOADABLE_WAVE_ROLES`: `quality`/`test` → `test`, `ui`, `perf`) is not extended — build, lint and audit ride on the Quality wave's `test` role. The gate still never probes the network, so the witness (`remoteReady` or `probeFn`) is mandatory for the switch to act. A wave that stays local names why: `offload-first: no ready host for '<role>' — staying local`, or `offload-first: true but no remote-hosts declared — staying local`; `validateSessionConfig()` adds the warn-level cross-field finding `offload-first-cross-field` for the second case and never flips `ok`. Named limits: `resource-awareness: false` stays a full opt-out (no probe, no cap, no placement); the `probe failed (ignored)` path does place, since placement needs no measurement; the key is committed-only — no env or `owner.yaml` precedence, because `resolveDispatcherAutonomy` is enum-specific and `owner.yaml` freezes its sections, so reuse would need a schema change.
|
|
25
|
+
|
|
26
|
+
- **`hooks/pre-edit-sessions-ledger-guard.mjs` — the sessions ledger is guarded on the Edit/Write lane too** (#1443 item 4). A PreToolUse hook on `Edit|Write|MultiEdit` (Claude Code; mirrored on Cursor and Pi, Codex recorded as a documented asymmetry in `check-hooks-symmetry.mjs`) denies a write whose `file_path` ends in `.orchestrator/metrics/sessions.jsonl` (or a rotated `sessions.jsonl.<N>`) and points at `scripts/emit-session.mjs --entry` / `scripts/check-sessions-integrity.mjs`. Every other input allows — malformed stdin, missing fields, unknown tool name, an internal error, a payload over 1 MB — so a broken hook can never block foreign edits; only the ledger path is fail-closed. Named limits in the header: the tracked fixture `tests/fixtures/harness-audit/clean-repo/.orchestrator/metrics/sessions.jsonl` is denied too (override `SO_DISABLED_HOOKS=pre-edit-sessions-ledger-guard`), the match is case-insensitive. Allow-path latency, median of 20 runs: 31.9 ms direct, 36.3 ms through `run-node.sh` (Node startup is 25.5 ms of it). **Live sessions pick the hook up only after the live checkout pulls this change.**
|
|
27
|
+
- **Skill `test-audit` — gate, audit and prune tests under written evidence** (`skills/test-audit/`). `/test-audit [gate|audit|campaign] [scope] [--goal "<goal>"]` applies one value bar at three depths: a four-question write gate before any new test is written, a focused audit of one scope, and a lane-based campaign (phases P1–P9 in `references/campaign.md`). Audit and campaign open with a stated goal — default: remove at least 20 % of the least useful tests measured in test lines, total coverage within 2 percentage points of M0, no production file below its own M0 coverage — that never licenses an unproven deletion: every test declaration in scope gets one ledger mark (R/F/C/D/N/B/O), and every consolidate, delete or repair needs eight written evidence fields plus a targeted production mutation the keeper catches, restored byte for byte. Test lines count only for files the runner actually includes. `references/mutate.sh` runs a mutation manifest after a green control run, counts a mutation as caught only when the run fails AND prints a red summary, aborts on a restore mismatch (exit 3) and logs `EXTERN:<lane>` rows as cross-lane references instead of running them. Coverage thresholds, runner config, CI, snapshot updates, new dependencies and product-bug fixes stay owner decisions. Inspired by openclaw/openclaw `.agents/skills/test-audit` (MIT), rewritten for this plugin.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **The orphan reaper is six modules behind a stable entry** (`scripts/lib/orphan-reaper/`, #1437). `scripts/lib/orphan-reaper.mjs` (1588 → 316 lines) re-exports the same 21 names and keeps the CLI; the hooks import only `scan-throttle.mjs`. The audit is read through a bounded, growing tail window and pruned past 1 MiB to its newest 512 KiB; `DEFAULT_KILL_GRACE_MS` now lives in `process-group.mjs` (still re-exported from `dispatch-common.mjs`).
|
|
32
|
+
- **tests:src ratio counts TypeScript and co-located tests** (`scripts/lib/tests-src-ratio.mjs`, #1451, envelope `tests-src-ratio/2`). `.ts/.tsx/.mts/.cts/.jsx` count, declaration files do not; the test side is `tests/` plus `*.test.*`/`*.spec.*` anywhere plus `__tests__/`. A TypeScript repo now gets a real number (navigator: 1.2847) instead of "not measurable". This repo moves 1.4006 → 1.4007 at the same tree.
|
|
33
|
+
- **B5 proves a clock seam only from a named clock key** (`scripts/lib/validate/check-test-value-bans.mjs`, #1478): `SEAM_CLOCK_ARG` for pass 1, the wide `CONTROL_CLOCK_ARG` for pass 2; seamed pairs 192 → 172, B5 findings unchanged at 0.
|
|
34
|
+
- **The offload threshold has one source** (`skills/_shared/fleet-protocol.md`, `skills/navigator/SKILL.md`, #1482): `navigator m5-ersatz --quiet` and its exit contract replace the prose numbers (< 12, `last_max`).
|
|
35
|
+
- **Dependency overrides:** `brace-expansion` ^5.0.12 (two high advisories in 5.0.9, #1481) and `fast-uri` ^3.1.8 (moderate GHSA-hrr3-gc8f-f4qj); `npm audit` reports 0 vulnerabilities.
|
|
36
|
+
- **`vault-mirror`'s refusal path has a subprocess test for its ledger record** (#1450 follow-up 4). `tests/unit/vault-mirror.test.mjs` spawns the CLI against a vault without a git origin and pins exit 2, the stderr refusal, exactly one `orchestrator.vault.mirror_run_completed` record with `aborted: 'vault-not-canonical'` and `total: 0`, and an untouched vault directory. The exit/stderr half was already covered by `tests/scripts/vault-mirror-entry-point.test.mjs`; the ledger record and the no-write assertion were not.
|
|
37
|
+
- **`board-writer.mjs` names the `vault-dirs:` cwd-match tier in its `hostPaths` JSDoc** (#1450 follow-up 2). The last precedence twin without the tier now matches `narrative-mirror.mjs`. Comment text only.
|
|
38
|
+
- **Vault-dir precedence comments name the `vault-dirs:` cwd-match tier** (#1450 item 2, remaining sites). `scripts/lib/vault-status/narrative-mirror.mjs` and `tests/unit/vault-mirror.test.mjs` no longer describe the chain as `SO_VAULT_DIR > owner.yaml paths.vault-dir > committed`; the three files the issue named already carried the tier. Comment text only.
|
|
39
|
+
- **The Codex entrypoints close-evidence handover is linked** (#1439 item 3). `docs/handover/2026-09-19-codex-session-entrypoints.md` now points at `2026-09-19-codex-session-entrypoints-close.md`, which no tracked file referenced before.
|
|
40
|
+
- **`OPTIONAL_FIELDS` declares the four session-record fields that were written but never listed** (`scripts/lib/session-schema/constants.mjs`, #1457 item 7). `total_tokens` and `matched_records` (the #1436 rollup, shape-checked by the validator), `raw_session_id` (`scripts/emit-session.mjs`, `scripts/lib/session-close-backfill.mjs`) and `_repair_source` (`scripts/lib/session-record-repair.mjs`) join the list; no runtime consumer reads it, so nothing changes in validation.
|
|
41
|
+
- **Test consolidation** (#1457 item 5, #1459 item 8). `tests/lib/autopilot/flags.test.mjs` drops the `--max-tokens` case that no mutation of `flags.mjs` could turn red (non-numeric input: default = min = 0) and the absent-flag case the empty-argv `toEqual` already pins, and asserts the bounds as literals so the behavioural cases catch a wrong constant on their own instead of only through the `FLAG_BOUNDS` pin; `tests/_helpers/markdown-fences.mjs` replaces the two private fenced-block parsers in the evolve-recipe and sweep-expired-learnings tests with one line-based, indentation-tolerant helper.
|
|
42
|
+
- **`wave-executor-state-init.md` names the third reader of `session-start-ref`** (#1457 item 11): `scripts/lib/scope-baseline.mjs` diffs `<ref>..HEAD` in the coordinator's checkout, so the worktree-base edge does not apply there and the init-time edge only under-counts.
|
|
43
|
+
- **`shellQuote` lives in one module** (`scripts/lib/sh-quote.mjs`, #1438). Five former copies — `scripts/cursor-install.mjs`, `scripts/lib/tmux-layout/layouts.mjs`, `scripts/lib/tmux-layout/tmux-shell.mjs`, `scripts/run-quality-gate.mjs` and an inline copy in `scripts/vault-integration-watcher.mjs` — now import it. New round-trip test `tests/lib/sh-quote.test.mjs` runs each quoted value through `sh -c 'printf %s …'` and expects it back byte-identical.
|
|
44
|
+
- **Validator messages name `claude-opus-5-5` as the full-model-ID example** (#1459 item 4). `scripts/lib/agent-frontmatter.mjs`, `scripts/lib/config/persona-gate-wave.mjs`, `scripts/lib/persona-panel/catalog-loader.mjs`, `scripts/lib/validate/check-agents.mjs` and `docs/session-config-reference.md` replace the `claude-opus-4-7` example; `claude-opus-4-7` stays a valid input.
|
|
45
|
+
- **`prompt-caching` is no longer vendored into `cli-tool` repos** (#1449). `rules/_index.md` drops the `cli-tool` archetype from `opt-in-domain/prompt-caching.md`: CLI repos without an Anthropic client received a 15k-character rule they can never apply; repos of the `nextjs-saas`, `express-service`, `docker-service` or `monorepo-oss` archetypes still get it; a `cli-tool` repo that does use the SDK no longer does, because a repo carries exactly one archetype (`bootstrap.lock`).
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Each subagent counts once in the session token and cost totals** (`scripts/lib/session-token-rollup.mjs`). The telemetry hook records the RUNNING total of an agent's whole transcript at every stop, so a resumed agent (e.g. continued via SendMessage) was summed once per stop. The rollup now keeps one record per `agent_id`: the last stop that found its transcript decides pricing, the last token-bearing one the token sums (they differ only for a transcript over the hook's 50 MiB read limit, which stays an unpriced candidate). **Series break:** measured over the fleet's local ledgers, 209 of 1,533 sessions change, output tokens fall to a median 0.88× (min 0.27×), costs to a median 0.83×; sessions without a resumed agent are unchanged. Historical `sessions.jsonl` values are not rewritten.
|
|
50
|
+
- **A partial session cost is no longer persisted as if complete** (`scripts/lib/session-token-rollup.mjs`, #1475 variant A). `total_cost_usd` is omitted whenever `cost_records_priced < cost_records_total` — an unknown or missing model, a found transcript without tokens, or a started agent whose transcript was never found (unless the same agent was priced from another stop). Phantom stops without a start record still never count.
|
|
51
|
+
- **A killed quality-gate run is recorded as `failed` in every variant** (`scripts/run-quality-gate.mjs`, #1459). The reporting variants exit 0 by design, so a run whose command the per-command timeout killed used to emit `orchestrator.quality_gate.passed`.
|
|
52
|
+
- **The autopilot stops on a backfill-stub iteration instead of counting it as 0 tokens** (`scripts/autopilot.mjs`, `scripts/lib/sessions-canonical.mjs` `isBackfillStub`, #1457 F2), and `emit-session` no longer takes a compact/resume re-emit as the session start ref (#1457 item 3).
|
|
53
|
+
- **Ledger prunes rewrite atomically and refuse symlinks** (`pruneReaperAudit`, `pruneGateProcessLedger`, `touchScanMarker`): a tmp file renamed over the target behind an `lstat` regular-file check; a symlinked audit can no longer truncate the file it points to.
|
|
54
|
+
- **The release regeneration moves `site/sitemap.xml` `lastmod`** (`scripts/site-numbers.mjs`, #1484): `lastmod := max(lastmod, page counted-at)`, never backwards; `--check` reports a stale `lastmod`.
|
|
55
|
+
- **The vault writers' refusal lines print no local paths** (#1479 items 4–6): origin via `describeOriginForLog()`, no `vaultDir`/source path in the `vault-mirror` refusal and not-found lines (the per-note SKIP lines in `scripts/lib/vault-mirror/process.mjs` still do, #1487), one `owner.yaml` load order, `SO_CONFIG_HOME` pinned in the entry-point test.
|
|
56
|
+
- **Learnings schema** (#1447 items 2, 10b, 1, 3a): strict validation type-checks `file_paths` with one shared predicate (`isRepoRelativePathEntry`) that `memory-propose --file-paths` and `validateProposalRecord` now use too, so an approved proposal can no longer fail at write time; migration strips `./` and moves array-typed path scopes into `file_paths` (scope words such as `project`/`repo` stay scopes); a missing baseline template or rule source is reported as `invalid-contract` / `invalid-rule-projection` instead of `unsafe-source`.
|
|
57
|
+
- **The narrative mirror refuses a non-canonical vault** (#1450 follow-up 1). `scripts/lib/vault-status/narrative-mirror.mjs` now runs the shared `checkCanonicalVault()` after the vault dir is resolved and before any slug lookup or write — the same guard the board writer and `vault-mirror` apply. A vault whose git origin is missing or does not end in a canonical suffix yields the new action `skipped-vault-not-canonical` (catalogued in `docs/events-schema.md`), nothing is written and one stderr line names the expected suffixes; `VAULT_MIRROR_SKIP_CANONICAL_CHECK=1` still bypasses it. The host context is loaded once and shared between the Session Config parse and the guard.
|
|
58
|
+
- **The board writer loads `owner.yaml` once per write** (#1450 follow-up 3). `mirrorBoardInner` resolved the host context twice — once inside `parseSessionConfig`, once for the canonical guard. `loadOwnerConfig()` has no cache and its section-drop warnings are not rate-limited, so a broken `owner.yaml` section printed every warning twice per board write. One `hostCtx` now feeds both; a regression test counts the warning lines.
|
|
59
|
+
- **`VAULT_MIRROR_SKIP_CANONICAL_CHECK=1` no longer prints `vaults:` warnings** (#1450 follow-up 5). `checkCanonicalVault()` evaluates the skip switch before resolving canonical suffixes, so a skipped check neither parses `owner.yaml` `vaults:` nor emits its WARN lines; the skip result carries `expected: []`, which no caller reads on the `ok` path.
|
|
60
|
+
- **`emitEvent` never writes relative to the working directory** (#1468). `scripts/lib/events.mjs` refuses an `opts.repoRoot` that is not an absolute path (empty string included) with one stderr WARN and no write, no directory, no webhook — previously `path.join` plus `mkdir -p` created `undefined/repo/.orchestrator/metrics/events.jsonl` under the cwd when an unset variable was concatenated into the root. An explicit `opts.filePath` is untouched, and `eventsFilePath()` keeps its return shape for readers. The four CLI entry points that forwarded an unresolved `--repo-root` (`release-session-lock.mjs`, `backfill-abandoned-sessions.mjs`, `wave-scope-binding.mjs`, `sweep-expired-rules.mjs`) now `path.resolve()` it at the parse site. `scripts/sweep-expired-learnings.mjs --repo-root` and an explicit root passed to `resolveRepoRoot()` in `scripts/lib/quality-gate.mjs` are resolved the same way, so a relative value there is no longer a silent telemetry drop.
|
|
61
|
+
- **The vault-status board writer refuses a non-canonical vault** (`scripts/lib/vault-status/board-writer.mjs`, #1450 item 1). `mirrorBoard()`/`sweepBoard()` now run the same canonical-vault guard `scripts/vault-mirror.mjs` has had since #600: when the resolved `vault-dir`'s git origin does not end in a canonical Meta-Vault suffix (or has no origin at all), the call returns `skipped-vault-not-canonical`, writes nothing, takes no lock — dry-run included — and prints one stderr line. The check itself is now ONE shared helper, `checkCanonicalVault()` + `normalizeRemote()` in `scripts/lib/named-vault-resolver.mjs`, and `vault-mirror.mjs` calls that helper instead of its private copy, so the two writers cannot drift apart again. Tests inject the origin reader through the existing `hostPaths` seam (`readOriginUrl`); the existing `VAULT_MIRROR_SKIP_CANONICAL_CHECK=1` switch is honoured from the injected env. The third writer, `narrative-mirror.mjs`, is closed by follow-up 1 below.
|
|
62
|
+
- **vault-mirror counts a collapsed duplicate session line instead of skipping it silently** (`scripts/vault-mirror.mjs`, #1291). The `--kind session` dedup pass gives a losing same-`session_id` line its own action, `skipped-duplicate-session`, through the ordinary per-entry path (stdout line, `orchestrator.vault.mirror_completed` record with `record_id` and `line`, no `path` — no target file exists for it), so `created + updated + skipped + failed === total` holds for session runs too. Both schema rows in `docs/events-schema.md` describe the new action; the #1186c ceiling test pins the invariant for both kinds.
|
|
63
|
+
- **Only a harness-written carrier record can finish a running agent in the liveness index** (`hooks/pre-task-scope-disjoint.mjs`, #1459 item 2 rest, security LOW, pre-existing). `buildTranscriptIndex()` matched `task-notification` on the raw transcript line, so a complete forged block placed first in a `tool_result` (a `Read` of a fixture), in assistant text or in a typed prompt marked a still-running agent's tool-use id as finished and turned a scope-collision DENY into an ALLOW. `notificationCarrierText()` now accepts exactly the three record forms the harness writes (`user` with `origin.kind: 'task-notification'`, optionally behind the system-notification preamble; `queue-operation` enqueue/remove; `queued_command` with `commandMode: 'task-notification'`), and `finishedNotificationIds()` requires the text to begin with the opener and the head to follow the harness tag order. Measured over 11,005 local transcript files: 0 genuine carrier ids stop being finished; the 20 lines the old code additionally accepted were all quotes. A remaining false-DENY for multi-notification preamble carriers is #1467.
|
|
64
|
+
- **Guards that were silently wrong** (#1455, #1451 fail-closed part, #1458 a+b, #1453, #1456, #1457 items 1–2). `hooks/pre-task-scope-disjoint.mjs`: `buildTranscriptIndex()` now treats every TERMINAL task-notification status (`completed|failed|killed|stopped`, census over local transcripts 2026-09-25) as finishing its tool-use-id, not only `completed` — a 429'd agent that notified `failed` and was resumed via `SendMessage` completed under the SendMessage's NEW id, so its dispatch never finished and the next wave was denied permanently (#1455; the resumed run itself is still unindexed — a false-ALLOW window, follow-up). `scripts/lib/tests-src-ratio.mjs`: 0 counted test files next to tracked test-like files (`*.test.ts`, `__tests__/`, anything under `tests/`) now yields `ratio: null`, `withinCorridor: null`, `reason: 'no-tests-found'`, `--check` exits 3, and the session-start probe WARNs "nicht messbar" instead of reading a TypeScript repo as green; `CODE_EXTENSIONS` is unchanged — whether TS tests COUNT is an open owner decision (#1451). `scripts/check-package-manager.mjs`: new advisory `checkHuskyHooksPath()` warns once, with `npx husky`, when `.husky/` exists but `core.hooksPath` is unset — `.npmrc` `ignore-scripts=true` skips the `prepare` script in every fresh clone, so commitlint, the owner-leakage scan and the pre-push gate were silently inactive there; never mutates, never changes the exit code, silent under CI; README names the step (#1458; the CI commitlint job is parked). `skills/evolve/references/evolve-analyze-mode.md`: the `/evolve analyze` sidecar moved from `.orchestrator/metrics/.learnings-next.jsonl` to `.orchestrator/tmp/learnings-next.jsonl`, outside the `ledger-delete-protected` denylist; a new test feeds the recipe with `$NEXT` spelled out through the real destructive guard (the literal `"$NEXT"` form was never blocked — the guard does not expand variables — so the collision only bit when an agent wrote the path out) (#1453). `persona-panel` presets, `templates/personas/*`, the `persona-gate-wave.dispatch-model` default and their docs use the alias `opus` instead of the stale pin `claude-opus-4-7`; two ID-shape examples in error messages are kept (#1456). `gate-full` publishes each command's inner kill ladder and `run-quality-gate.mjs` OR-joins it with the outer one into `orchestrator.quality_gate.*` via `joinGateKillFields()` — when the inner side was not measured and the outer did not fire the three keys are now OMITTED rather than written as `false/0/[]` (baseline/incremental/per-file publish it since the entry below, #1459 item 3); `emit-session.mjs` WARNs about a missing `session_start_ref` also without an own UUID and names what STATE.md contributed (#1457 items 1–2). Every behaviour change carries a test that is red on `0efb3e97`.
|
|
65
|
+
|
|
66
|
+
- **The sessions ledger is honest again: reader, writer, repair and token rollup** (#1443, #1442, #1436, #1439 items 1–2, #1440 C1/C2/C4). `canonicalizeSessions` no longer lets a later backfill stub (0 waves, `_backfill_source`) hide a real record with the same `session_id`, merges a stub and a real record that share a `raw_session_id` onto the real one, and treats repair provenance (`repair-invalid-sessions/*`) as real (fleet check 30 ledgers / 3135 records: 7 winner changes, all stub→real). `orchestrator.session.started` carries `head_sha`; the STATE.md template gains `session-start-ref`; `emit-session.mjs` falls back to the own event's `head_sha`, drops a short ref with a WARN (53 fleet records carry 7/8-char refs, so the validator does NOT enforce the format), stamps `raw_session_id` and runs `rollupSessionTokens` mechanically (new `total_tokens` = input + output) for the own UUID only — the prose step in `session-metrics-write.md` is gone, `readTailSession()` returns `usage`, so the autopilot `token-budget-exceeded` kill-switch fires again (wiring test through the real path). `validateSession` type-checks `total_tokens` and `matched_records`. `repair-invalid-sessions`: new class `timestamp_precision` truncates sub-millisecond timestamps (deliberately NOT listed in `_backfill_incomplete_fields`, which would discard the value downstream); post-verification now means "no worsening and every repaired line valid" — an unrepairable remainder is kept byte-identical and reported, exit 4 distinguishes it from a rollback (exit 3); the Bash ledger guard's deny text for repair variants names the one allowed command and says an in-command `SO_DISABLED_HOOKS=` prefix has no effect. `orchestrator.memory.propose_invoked` writes `cwd` project-relative or as `sha256:<12 hex>` (also when the project dir is `/`, home or an ancestor of home — pseudonymisation, not anonymisation), never an absolute path. `orchestrator.quality_gate.{passed,failed}` carry `timed_out`, `survivors`, `kill_signals` on both emitters (`failed_files` documented). `run-quality-gate.mjs` sits behind an `isMainModule` guard. The `over_delivery_ratio` / `planned_files_count` prose chain (0 of 173 waves ever carried it, no code wrote or read it) is removed instead of implemented; the two tests pinning it are deleted. `release.mjs` polls the registry 12 × 15 s instead of 5 × 3 s. Review fixes before merge: the token enrichment validates the enriched record first and, on a validation error, WARNs and appends the record WITHOUT the token fields (enrichment is never the reason the ledger gains no line); `repair-invalid-sessions` keeps an existing `_backfill_source` origin and records the repair in the additive `_repair_source` field, so a repaired zero-work stub stays a stub; a missing `session_start_ref` is WARNed; the `session.started` fallback takes the FIRST own event (its `head_sha` or nothing). **Autopilot `--max-tokens` now defaults to `0` (off):** with `readTailSession()` reporting `usage`, the dormant 500 000 default would have stopped autopilot after about one deep session (measured 450 499 subagent output tokens for one session, HR-101); the unit is cumulative subagent OUTPUT tokens, coordinator tokens are not counted. The reaper PRD now says an over-budget hook latency warns, it does not skip the scan.
|
|
67
|
+
|
|
68
|
+
- **`vault-dir` gains a per-directory tier, and a host can switch `vault-integration` off locally** (`scripts/lib/config/host-paths.mjs`, `scripts/lib/config.mjs`, `scripts/lib/owner-yaml.mjs`, `scripts/lib/named-baseline-resolver.mjs`, agents/vault#319, #1448). A single host-wide `owner.yaml` `paths.vault-dir` silently overrode every repo on the host, so a repo whose own vault lives elsewhere wrote its session closes into the wrong vault. `vault-dir` now resolves `SO_VAULT_DIR` > `owner.yaml` `vault-dirs:` cwd path-prefix match > `paths.vault-dir` > committed, through one helper (`resolveVaultDir`) for both the `vault-integration` and the `vault-sync` block, and the winning tier is exposed as `vault-integration.vault-dir-source` (`env|match|owner|committed`). The match tier reuses the #819 resolver, now parameterised by `section`/`envKey` (`name` stays required for `baselines:` only). The host switch `resolveVaultIntegrationHost` reads env `SO_VAULT_INTEGRATION=off|warn|strict` > `owner.yaml` `vault-integration: { enabled: false | mode: … }` > committed and may only LOWER the committed level, never enable or raise it; `vault-integration.host-override` names the tier that lowered it (`env:SO_VAULT_INTEGRATION` / `owner.yaml`, else `null`). An invalid `owner.yaml` `vault-integration:` section is dropped on its own via `droppedSections`; `vault-dirs:` is a lenient list section like `baselines:`. Session-end step 5 and the `/evolve` mirror step print `vault-mirror: bewusst aus auf diesem Host (<tier>)` instead of hitting `vault-mirror.mjs`'s exit 2 on a host without a vault, and log `vault-dir=<path> (Quelle: <source>)` when they do mirror. `claude-md-drift-check` still reads the raw committed values. A leading `~` in the two owner.yaml tiers (`vault-dirs[].path`, `paths.vault-dir`) is now expanded, and `scripts/vault-mirror.mjs` expands its `--vault-dir` at entry: before, a `~/…` value reached `existsSync(resolve(…))` as `<cwd>/~/…` and the mirror aborted with exit 2 `missing-vault-dir`. The #1244 whole-file discard (an invalid REQUIRED owner.yaml section) now keeps `vault-dirs:` raw alongside `paths:`, so an unrelated `tone:` typo no longer sends a repo under a declared prefix back into the host-wide vault. The test setup clears `SO_VAULT_INTEGRATION` (`tests/setup/scrub-session-env.mjs`), so an operator's exported switch cannot flip fixtures that assert the committed mode.
|
|
69
|
+
- **A configured baseline without its exporter is reported as `producer-missing`, not `unsafe-source`** (`scripts/lib/baseline-archetypes.mjs`, #1445, GH#73). `baselineSourcePath()` maps a missing `scripts/archetype-manifest.mjs` to `producer-missing` only when `lstat` on that path itself throws `ENOENT`; symlinks (dangling or not), symlinked parent directories, paths escaping the baseline, wrong file types and every other error stay `unsafe-source`. Contract-error results gain an additive, path-free `message` field; for `producer-missing` it names the missing exporter and points to `docs/baseline.md` § How bootstrap finds it. Exit code stays 2, and `scripts/baseline-archetypes.mjs --help` names the reason. The bootstrap docs (`skills/bootstrap/public-fallback.md`, `SKILL.md`, `private-contract.md`, `references/bootstrap-retroactive-flow.md`, `docs/baseline.md`) describe the two ways forward — migrate the baseline or unset the configured path — as the user's choice, never a silent downgrade to public.
|
|
70
|
+
- **Session-end learnings writes go through a validating CLI, and the migration repairs broken consumer records** (#1446, GH#69). `migrateLegacyLearning()` (`scripts/lib/learnings/schema.mjs`) coerces a path-like `scope` into `file_paths` plus `scope: private`, `scope: project|repo` to `private`, and `schema_version: "1"` / `2` to `1` — on the migration path only; the read funnel stays pass-through. `scripts/run-migrate-v2-cross-repo.mjs` reports invalid-record counts per validation error class before and after migration (`errorClassesPre` / `errorClassesPost`). The new `scripts/apply-session-learnings.mjs` (dry-run by default, `--apply` to write; strict `validateLearning()`, one invalid record writes nothing; passive decay from `learning-decay-rate`; emits `orchestrator.learnings.session_write_applied` only when an `--apply` run actually wrote the store — never on a dry run, never when nothing was written) replaces the shell `>` rewrite in `skills/session-end/learning-patterns.md` and `skills/plan/mode-retro.md`. `.cursor/rules/060-evolve.mdc` is not routed to that CLI: it now writes through the `/evolve` prune helper `scripts/sweep-expired-learnings.mjs --prune`, passing `--repo-root "$(pwd)"` like the canonical `skills/evolve/references/evolve-analyze-mode.md` so the helper records `orchestrator.evolve.completed`. All three learning templates now carry `scope` and `file_paths`. The sweep CLI's `--prune --entries` path strictly validates every sidecar record whose `id` is not yet in the store and exits 1 without writing when one is invalid. New blocking validator `scripts/lib/validate/check-learnings-shell-writes.mjs` rejects instruction prose that writes `learnings.jsonl` with `>`, `>>` or `tee`; a backticked operator counts when the store name sits within ±3 lines, a prohibition exempts a line only when its keyword precedes the operator, and Markdown blockquotes are ignored. The dead reference `GitLab #386` in the schema module now reads `EventDrop #386`; fixture `tests/fixtures/learnings-invalid-golden.jsonl` added.
|
|
71
|
+
- **The `baseline`, `incremental` and `per-file` gate variants publish the per-command kill ladder** (`scripts/lib/gates/gate-{baseline,incremental,per-file}.mjs`, #1459 item 3, #1457 item 12). `typecheck` and `test` in their JSON envelope are now objects `{ status, timed_out, kill_signals, survivors }` like `gate-full`'s; a skipped command carries `{ status: 'skip' }` only. `joinGateKillFields()` can therefore join the inner ladder for these variants too, so their `orchestrator.quality_gate.*` events carry `timed_out`/`kill_signals`/`survivors` instead of omitting them. Envelope change: these keys were bare status strings before. `skills/quality-gates/SKILL.md` already documented the object form for baseline and per-file; the incremental schema is updated.
|
|
72
|
+
- **A quoted `<tool-use-id>` no longer finishes a running agent** (`hooks/pre-task-scope-disjoint.mjs`, #1459 item 2, security LOW). `buildTranscriptIndex()` read every id on a notification line, so an id merely QUOTED inside another notification's `<result>`/`<summary>` counted as finished. Now only the line's FIRST `<task-notification>` block is read, and only its head before `<summary>`/`<result>`: the first `<tool-use-id>` counts, only when the first `<status>` is terminal and `</task-notification>` follows (fail-closed: the deny stays). Named ceiling: several DIFFERENT real notifications in one line report only the first — measured 0 times in this repo's transcripts.
|
|
73
|
+
- **Three doc corrections** (#1451, #1457). `skills/session-start/references/phase-4-ssot-environment-check.md` describes the `checkTestsSrcRatio` "nicht messbar" warn (`reason: 'no-tests-found'`, `ratio: null`) and the `reason?` result field; the `skills/autopilot/SKILL.md` pseudocode checks `max-tokens > 0` before comparing, matching `kill-switches.mjs` (default `0` = off); `hooks/pre-bash-memory-propose-audit.mjs` and `docs/events-schema.md` no longer call `events.jsonl` tracked — it is gitignored here (`.gitignore:57`), consumer repos may track it.
|
|
74
|
+
- **Vendored rules load path-scoped again: frontmatter on line 1, `paths:` beside `globs:`** (#1449). Claude Code reads a rule's `paths:` only when the frontmatter opens on line 1. The 17 library rules under `rules/` began with the provenance comment before `---`, and the 7 path-scoped ones carried only `globs:`, so every consumer loaded them always-on on every turn. The 7 rules with frontmatter (`backend`, `backend-data`, `frontend`, `swift`, `security-web`, `prompt-caching`, `bash-harness-pitfalls`) now open with `---`, carry a `paths:` list identical to `globs:` and the provenance comment directly after the closing `---`; bodies are byte-identical, the 10 rules without frontmatter are unchanged. The tooling now measures what Claude Code loads: `scripts/lib/rule-loader.mjs` reads a block only from line 1 and reports a displaced block as `defect: 'frontmatter-not-at-top'` (export `FRONTMATTER_NOT_AT_TOP`) with `globs: null`; `scripts/lib/instruction-budget-guard.mjs` counts such rules always-on and returns `defects[]` plus a banner line naming the files; `claude-md-drift-check` Check 9 drops its own header tolerance and reports the loader signal as an error. `scripts/lib/validate-vendored-rules.mjs` replaces the `paths-frontmatter` probe with `frontmatter-not-at-top` and `globs-paths-mismatch` (both errors; list comparison order-insensitive) and accepts the provenance header on line 1 (rule without frontmatter) or on the first line after the closing `---`; `scripts/lib/rules-sync.mjs` detects plugin ownership via the new `isPluginOwnedContent()` at both positions, so old-format copies stay plugin-owned and are lifted to the new format on the next sync instead of being frozen as local overrides. `docs/rule-authoring.md` documents the contract.
|
|
75
|
+
- **`ci-audit-job` reports an `include:`-supplied audit as unverifiable, not missing** (`scripts/lib/project-hygiene.mjs`, #1460). When `.gitlab-ci.yml` has no local audit step but declares `include:` (component/local/project/remote/template), `checkCiConfig()` cannot see the included jobs; the finding is now `fixable: false` with "CI dependency-audit step could not be verified: N include(s) not evaluated" instead of a `fixable: true` missing-audit finding. Without `include:` nothing changes, and a local audit job still wins.
|
|
76
|
+
- **Autopilot loads Session Config from a plugin path with spaces or non-ASCII characters** (`scripts/autopilot.mjs`, #1440). `loadConfig()` built the `parse-config.mjs` path from `new URL(...).pathname`, which keeps percent-encoding, so under such a path the config load failed and autopilot ran on defaults; it now uses `fileURLToPath()`. The Bash ledger guard's deny text additionally names the read-only check `scripts/check-sessions-integrity.mjs --session-id` to run after the writer succeeds.
|
|
77
|
+
- **The `semgrep` CI job gets an empty entrypoint and a source directory** (`.gitlab-ci.yml`). The job now sets `image.entrypoint: [""]` and `SEMGREP_SRC_DIRECTORY: $CI_PROJECT_DIR`, as the `gitleaks` job already did for its entrypoint (infrastructure/projects-baseline#550).
|
|
78
|
+
- **This repo's four project hooks read their input from stdin** (`.claude/settings.json`, contributor-facing). The destructive-command guard, the formatter, the `.env.example` reminder and the co-located-test hint read `$CLAUDE_TOOL_INPUT`, a variable Claude Code never sets, so none of them ever fired; they now parse the hook JSON from stdin (`.tool_input.command` / `.tool_input.file_path`) and answer with `hookSpecificOutput.permissionDecision`. The destructive-command guard is fail-closed: without `jq` on the PATH it denies every Bash call with an install hint instead of validating nothing (infrastructure/projects-baseline#501).
|
|
79
|
+
|
|
10
80
|
## [5.3.0] - 2026-09-22
|
|
11
81
|
|
|
12
82
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Session Orchestrator
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](CHANGELOG.md)
|
|
5
5
|
[](https://www.npmjs.com/package/session-orchestrator)
|
|
6
6
|
|
|
7
7
|
**Give your agents a working rhythm.**
|
|
@@ -39,6 +39,8 @@ You need **Node.js 24 or later**, a git repository, and one of the four agents b
|
|
|
39
39
|
| **Cursor IDE** | `git clone` the repo, `npm install`, then `node scripts/cursor-install.mjs /path/to/your/project` ([guide](docs/cursor-setup.md)). |
|
|
40
40
|
| **Pi** | `pi install npm:session-orchestrator` ([guide](docs/pi-setup.md)). |
|
|
41
41
|
|
|
42
|
+
After a clone-based install (Codex CLI, Cursor IDE, or contributing), run `npx husky` once: `.npmrc` sets `ignore-scripts=true` (SEC-020), which skips the `prepare` script that would otherwise activate the git hooks.
|
|
43
|
+
|
|
42
44
|
## Quick Start
|
|
43
45
|
|
|
44
46
|
**1. Run `/bootstrap` once in your project.** It creates the minimum structure and writes `.orchestrator/bootstrap.lock`. `/session` refuses to start until that file exists.
|
|
@@ -93,7 +95,7 @@ When you type `/session feature`:
|
|
|
93
95
|
.claude/STATE.md # wave progress and deviations (harness-specific directory)
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
The plugin is **
|
|
98
|
+
The plugin is **52 skills, 28 slash commands, 14 typed subagents and 29 hook files across 10 event types**. A slash command is a skill whose frontmatter says `user-invocable: true` (26 of them) or one of the two remaining `commands/*.md` files (`/session`, `/templates-ack`) — one definition per name, so nothing is listed twice in the `/` picker. Skills, commands and agents are Markdown with YAML frontmatter; the code that dispatches, validates and records runs in `scripts/lib/*.mjs` and `hooks/*.mjs`. There is no build step and no compiled artifact — when a session does something you did not expect, you can open the file that decided it. Full inventory: [`docs/components.md`](docs/components.md).
|
|
97
99
|
|
|
98
100
|
## Why it is built this way
|
|
99
101
|
|
|
@@ -101,7 +103,7 @@ The plugin is **50 skills, 26 slash commands, 14 typed subagents and 27 hook fil
|
|
|
101
103
|
- **Checks run between waves, not only at the end.** A mistake caught after wave 2 costs one wave. The same mistake found at `/close` has already been copied into every wave after it. Findings below the configured confidence threshold are not shown to you.
|
|
102
104
|
- **A crash does not lose the session.** `STATE.md` records which wave finished and what deviated from the plan. The next `/session` offers to continue from the last completed wave.
|
|
103
105
|
- **Two sessions in one working copy is treated as a real risk.** Two people, or two of your own sessions, in the same checkout share one git index, one filesystem and one `STATE.md`, and neither can see the other's uncommitted work. A heartbeat session lock, per-agent file-scope manifests, and the PSA rules in [`.claude/rules/parallel-sessions.md`](https://github.com/Kanevry/session-orchestrator/blob/main/.claude/rules/parallel-sessions.md) exist for exactly that case.
|
|
104
|
-
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy —
|
|
106
|
+
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy — 12 rules block, 4 warn — and file-scope enforcement run as real hooks on Claude Code, as bridges on Cursor and Pi, and as instructions only on Codex. Details: [`docs/components.md`](docs/components.md#other-surfaces).
|
|
105
107
|
- **What it learns is opt-in and readable.** Every session appends a record. After 5 or more sessions, `/evolve analyze` proposes patterns with a confidence score; you read them and delete the ones you disagree with. Nothing is applied without you.
|
|
106
108
|
- **GitLab and GitHub, both fully.** It detects which one your remote points at and drives issues and merge/pull requests for either.
|
|
107
109
|
|
|
@@ -111,7 +113,7 @@ How this compares to other orchestrators, with measured results kept separate fr
|
|
|
111
113
|
|
|
112
114
|
| Feature | Claude Code | Codex CLI | Cursor IDE | Pi |
|
|
113
115
|
|---|---|---|---|---|
|
|
114
|
-
| All
|
|
116
|
+
| All 28 commands | Native slash commands | Generated skills (`$session-orchestrator:<name>`) | Native `.cursor/commands` slash commands | Prompt templates |
|
|
115
117
|
| Parallel agents | Agent tool | Multi-agent roles | Sequential only | Sequential (parallel planned) |
|
|
116
118
|
| Session persistence | `.claude/STATE.md` | `.codex/STATE.md` | `.cursor/STATE.md` | `.pi/STATE.md` |
|
|
117
119
|
| Scope enforcement | Active PreToolUse hook; blocking in `strict`, reporting in `warn` | Instructions only; no compatible `apply_patch` handler | `preToolUse` + `beforeShellExecution` bridge; scope blocking requires `strict`; `afterFileEdit` is post-hoc | `tool_call` bridge; scope blocking requires `strict` |
|
|
@@ -121,14 +123,16 @@ How this compares to other orchestrators, with measured results kept separate fr
|
|
|
121
123
|
|
|
122
124
|
All four platforms share the same skills, commands and scripts; only the hooks differ, because each harness fires different events. Codex leaves its `PreToolUse` handlers empty because these guards do not yet match its tool names and edit payloads ([why](docs/codex-setup.md#why-our-pretooluse-guards-stay-unwired--the-reason-corrected)). Cursor and Pi have known event-coverage limits — see [`docs/cursor-setup.md`](docs/cursor-setup.md) and [`docs/pi-setup.md`](docs/pi-setup.md).
|
|
123
125
|
|
|
124
|
-
## Recent highlights (v5.
|
|
126
|
+
## Recent highlights (v5.4.0)
|
|
125
127
|
|
|
126
|
-
Highlights of the v5.
|
|
128
|
+
Highlights of the v5.4.0 line:
|
|
127
129
|
|
|
128
|
-
- **
|
|
129
|
-
-
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
130
|
+
- **Fleet protocol v1 and the `navigator` skill.** Parallel sessions now check in through files instead of chat messages: `scripts/lib/fleet-protocol.mjs` owns the layout under `~/.config/navigator/`, session-start writes a check-in (Phase 7.6), and the SessionStart banner shows in one line whether a navigator lease is active, absent or unreadable. `/navigator` runs the coordinator role on operator command only and grants no approvals (#1462).
|
|
131
|
+
- **`offload-first` places work before pressure forces it.** With the new Session Config key set to `true`, offloadable wave roles go to a ready declared remote host on a `proceed` verdict too, not only when resources are short. Default `false` changes nothing (#1465).
|
|
132
|
+
- **A `test-audit` skill for shrinking a suite without losing proof.** A four-question gate before a new test, a focused audit, and a lane-based campaign; every delete, merge or repair needs written evidence and a production mutation the remaining test catches.
|
|
133
|
+
- **The sessions ledger and the vault writers are honest again.** Backfill stubs no longer hide real records, token totals are rolled up mechanically so the autopilot token-budget kill-switch fires again once `--max-tokens` is set (its default is now `0`, off), and the board writer and narrative mirror refuse a vault that is not the canonical one. `vault-dir` gains a per-directory tier in `owner.yaml` (#1443, #1436, #1450, #1448).
|
|
134
|
+
- **Vendored rules load path-scoped again.** Library rules open their frontmatter on line 1 and carry `paths:` beside `globs:`, so consumers no longer load path-scoped rules on every turn; the loader, budget guard, drift check and validator now flag a displaced block (#1449).
|
|
135
|
+
- **Session costs never read as complete when they are not.** Each subagent now counts once in the token and cost totals (a resumed agent used to be summed per stop), a session with any unpriced agent stores no dollar figure but its coverage counters, and mixed-model agents are priced per model. Every `learnings.jsonl` read-modify-write runs under one store lock, so a concurrent append is no longer lost.
|
|
132
136
|
|
|
133
137
|
Full changes and verification: [CHANGELOG.md](CHANGELOG.md).
|
|
134
138
|
|
package/docs/agent-authoring.md
CHANGED
|
@@ -49,7 +49,7 @@ Agent files live in `agents/` as Markdown with YAML frontmatter. Required fields
|
|
|
49
49
|
---
|
|
50
50
|
name: kebab-case-name # 3-50 chars, lowercase + hyphens only
|
|
51
51
|
description: Use this agent when [conditions]. <example>Context: ... user: "..." assistant: "..." <commentary>Why this agent is appropriate</commentary></example>
|
|
52
|
-
model: inherit # inherit | sonnet | opus | haiku | fable — OR full ID like claude-opus-
|
|
52
|
+
model: inherit # inherit | sonnet | opus | haiku | fable — OR full ID like claude-opus-5-5, claude-sonnet-4-6, claude-haiku-4-5-20251001, claude-sonnet-5, claude-fable-5
|
|
53
53
|
color: blue # blue | cyan | green | yellow | purple | orange | pink | red | magenta
|
|
54
54
|
tools: Read, Grep, Glob, Bash # comma-separated string OR JSON array (both accepted; we prefer comma-string for consistency)
|
|
55
55
|
---
|
|
@@ -76,7 +76,7 @@ tools: Read, Grep, Glob, Bash # comma-separated string OR JSON array (bo
|
|
|
76
76
|
- **`sonnet`** — an explicit cost cap, NOT a default. Pin it only when an agent must stay on the workhorse tier even in an Opus session, and state the reason inline. A bare `sonnet` on a working agent silently overrides the operator's session model — the failure mode that moved the implementation and review catalog to `inherit`.
|
|
77
77
|
- **`inherit`** — **the default for the working catalog.** Lets the coordinator/session's active model tier flow through, so the agent runs at whatever tier the operator picked (an Opus session gives its implementers AND its reviewers Opus-quality judgment). Use it for implementation, test-writing, and review agents alike — `code-implementer`, `test-writer`, `security-reviewer`, `session-reviewer`, `db-specialist`, `ui-developer`, and the read-only analysis agents. Diverge only for a named cost/quality reason.
|
|
78
78
|
- **`opus`** — reserve for agents whose task genuinely needs the strongest available reasoning (e.g. `ux-evaluator`) and where the cost is justified because the agent runs rarely (dispatched solo, not fanned out across a wave).
|
|
79
|
-
- **Full model-ID pinning** (`claude-opus-
|
|
79
|
+
- **Full model-ID pinning** (`claude-opus-5-5`, `claude-sonnet-5`, `claude-fable-5-20260101`, …) — only when the agent has a demonstrated dependency on a SPECIFIC model version, e.g. a reproducibility requirement that needs a dated snapshot, or a known behavioral regression on newer models for this agent's exact prompt. Pinning trades away automatic model-family upgrades — document the reason inline as a frontmatter comment when you do this.
|
|
80
80
|
|
|
81
81
|
**Resolution order** (highest precedence first): `CLAUDE_CODE_SUBAGENT_MODEL` environment variable → the invocation-time model parameter (if the dispatcher passes one explicitly) → the agent's own frontmatter `model:` value. An agent's `model:` is therefore a default, not a guarantee — a session or environment override can supersede it.
|
|
82
82
|
|
package/docs/baseline.md
CHANGED
|
@@ -39,7 +39,16 @@ resolvers. Their diagnostics are contained in a bounded local process so paths
|
|
|
39
39
|
and private match names cannot leak through the bootstrap CLI. An absent or
|
|
40
40
|
missing directory keeps the public fallback. An existing directory with a
|
|
41
41
|
missing producer, unsupported schema, invalid metadata or unsafe source is an
|
|
42
|
-
explicit error; bootstrap does not silently switch to a public default.
|
|
42
|
+
explicit error; bootstrap does not silently switch to a public default. A
|
|
43
|
+
directory without the `scripts/archetype-manifest.mjs` exporter is reported as <!-- path-check: example -->
|
|
44
|
+
`producer-missing`, with a path-free `message` naming that file. A source the
|
|
45
|
+
exporter declares but the baseline does not contain is an incomplete baseline, not
|
|
46
|
+
a refusal: a missing archetype template directory (`templatePath`) is reported as
|
|
47
|
+
`invalid-contract`, a missing rule file the `rules` projection lists as
|
|
48
|
+
`invalid-rule-projection`. `unsafe-source` stays reserved for symlinks (dangling or
|
|
49
|
+
not), path escapes, wrong file types and any other lookup failure. The two ways
|
|
50
|
+
forward are the user's choice: migrate the baseline to the exporter contract
|
|
51
|
+
below, or unset the configured path so the public fallback applies.
|
|
43
52
|
|
|
44
53
|
The local producer is the baseline's `archetype-manifest.mjs export` CLI. Its reduced v1
|
|
45
54
|
JSON owns IDs, ordering, declarative detection signals, runtimes, package
|
package/docs/ci-setup.md
CHANGED
|
@@ -419,6 +419,46 @@ The trade-off that buys is registry availability — a registry outage fails thi
|
|
|
419
419
|
job, and therefore the pipeline, without any code being wrong. Retry the job;
|
|
420
420
|
do not make it `allow_failure`, which would restore the silent hole.
|
|
421
421
|
|
|
422
|
+
## `commitlint` job (#1458)
|
|
423
|
+
|
|
424
|
+
Runs `commitlint` (rule set: `commitlint.config.mjs`, Conventional Commits with
|
|
425
|
+
`header-max-length 120`) over the commits a pipeline introduces. CI is needed in
|
|
426
|
+
addition to the husky `commit-msg` hook because the hook is inactive in a fresh
|
|
427
|
+
clone until `npx husky` has run, absent for Codex / web edits, and skipped by
|
|
428
|
+
`--no-verify`.
|
|
429
|
+
|
|
430
|
+
- **Range** `<from>..$CI_COMMIT_SHA`, first match wins: `CI_MERGE_REQUEST_DIFF_BASE_SHA`
|
|
431
|
+
(MR pipelines), else `CI_COMMIT_BEFORE_SHA` (branch pipelines), else the first
|
|
432
|
+
parent of the pipeline commit (a new branch has an all-zero before-sha). The
|
|
433
|
+
job logs the range and the commits it lints.
|
|
434
|
+
- **Fail-closed.** `commitlint --from X --to Y` exits 0 over an *empty* range
|
|
435
|
+
(measured 2026-09-29, `@commitlint/cli` 19.8.1), so the script itself fails
|
|
436
|
+
when the range is empty or its base is missing from the clone. "Nothing was
|
|
437
|
+
checked" is never a pass.
|
|
438
|
+
- **`GIT_DEPTH: "0"`.** The project default clone depth is 20, so the range base
|
|
439
|
+
can be absent from a shallow clone. `semgrep` uses the same setting for its
|
|
440
|
+
baseline commit.
|
|
441
|
+
- **Skipped headers.** Merge commits, `Revert "..."`, `fixup!`/`squash!` and
|
|
442
|
+
version-only headers are skipped by commitlint's built-in default ignores
|
|
443
|
+
(`@commitlint/is-ignored`, not `config-conventional`; measured 2026-09-29), so
|
|
444
|
+
GitLab's own merge commits pass. The husky hook applies the same ignores.
|
|
445
|
+
- **Coverage gap (known, measured 2026-09-29).** An MR pipeline re-lints the
|
|
446
|
+
whole MR range on every push. A direct push is linted per push only
|
|
447
|
+
(`before-sha..sha`), and every job here is `interruptible`, so a push pipeline
|
|
448
|
+
auto-cancelled by the next push can leave its commits unlinted when that push
|
|
449
|
+
lands before the validate stage finishes. Of the last 100 `main` pipelines, 11
|
|
450
|
+
were cancelled (7 of them with direct-push commits), and in all 11 every
|
|
451
|
+
security and validate job had already finished — 0 were exposed. A squash
|
|
452
|
+
merge's commit (the MR title) is never linted by the MR pipeline, only by the
|
|
453
|
+
`main` pipeline after the merge, where a red job can no longer block it.
|
|
454
|
+
Two non-conforming commits reached `main` after `commitlint.config.mjs` was
|
|
455
|
+
added and before this job (`c99f57d9`, a 126-character header; `c5252e68`, a
|
|
456
|
+
`merge:` type). Revisit when a non-conforming commit lands on `main`
|
|
457
|
+
after this job: `npx --no -- commitlint --from <sha that added this job> --to origin/main`.
|
|
458
|
+
- Hard-`needs`-ed by `pipeline-gate` and on the shared gate rules, so it runs on
|
|
459
|
+
every non-scheduled pipeline. Behavioural tests: `tests/ci/commitlint.test.mjs`
|
|
460
|
+
(they execute the committed script block against temp git repos).
|
|
461
|
+
|
|
422
462
|
## `pipeline-gate` — the fan-in job
|
|
423
463
|
|
|
424
464
|
The last stage holds one job that depends on every blocking gate. It exists
|
|
@@ -443,10 +483,23 @@ nothing" are different failures:
|
|
|
443
483
|
`allow_failure` job never blocks a dependent job, so `needs:` alone is blind
|
|
444
484
|
to it.
|
|
445
485
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
486
|
+
`coverage` runs only on the default branch (`main`). It is the most expensive
|
|
487
|
+
job and re-runs a suite `test` has already run: across 14 MR pipelines
|
|
488
|
+
(2026-09-27..29) it took 5.0 to 12.9 min, median 6.6 min, 29% of the MR job
|
|
489
|
+
minutes (measured 2026-09-29 13:24 CEST). `pipeline-gate` requires
|
|
490
|
+
`.ci-markers/coverage.ok` when `CI_COMMIT_BRANCH` is the default branch (push,
|
|
491
|
+
web and api pipelines alike); on every MR and branch pipeline it prints
|
|
492
|
+
`coverage: NOT MEASURED here` — absent, but never silently absent.
|
|
493
|
+
|
|
494
|
+
Consequence: an MR no longer gets the thresholds (70/70/70/60, set in the
|
|
495
|
+
vitest configuration) nor a second, instrumented full-suite run; both fire
|
|
496
|
+
after the merge, so a coverage regression turns `main` red, not the MR. The
|
|
497
|
+
MR page also loses its coverage percentage. The second run has caught things
|
|
498
|
+
before: in the failed-job history (measured 2026-09-29, oldest 2026-05-11) 3 of
|
|
499
|
+
14 failed `coverage` jobs were in MR pipelines, each with every `test` shard
|
|
500
|
+
green. Revisit trigger: a `main` coverage failure whose MR pipeline was green,
|
|
501
|
+
or a switch to measuring coverage inside the three `test` shards with
|
|
502
|
+
`vitest --merge-reports`.
|
|
450
503
|
|
|
451
504
|
## Local pre-push gate
|
|
452
505
|
|
package/docs/components.md
CHANGED
|
@@ -7,8 +7,8 @@ Detailed component inventory and architecture reference for Session Orchestrator
|
|
|
7
7
|
```mermaid
|
|
8
8
|
flowchart LR
|
|
9
9
|
USER([Operator]) -->|invokes /session| COORD[Coordinator]
|
|
10
|
-
COORD -->|reads| SK[Skills<br/>
|
|
11
|
-
COORD -->|invokes| CMD[Commands<br/>
|
|
10
|
+
COORD -->|reads| SK[Skills<br/>52 user-facing]
|
|
11
|
+
COORD -->|invokes| CMD[Commands<br/>28 slash-cmds]
|
|
12
12
|
COORD -->|dispatches| AG[Agents<br/>14 typed sub-agents]
|
|
13
13
|
AG -.->|parallel waves| W1[code-implementer]
|
|
14
14
|
AG -.-> W2[test-writer]
|
|
@@ -18,7 +18,7 @@ flowchart LR
|
|
|
18
18
|
COORD -->|writes| METRIC[.orchestrator/metrics/<br/>sessions · learnings · events]
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
## Skills (
|
|
21
|
+
## Skills (52 user-facing)
|
|
22
22
|
|
|
23
23
|
- **Lifecycle:** `session-start`, `session-plan`, `wave-executor`, `session-end`, `quality-gates`, `using-orchestrator`
|
|
24
24
|
- **Authoring:** `mcp-builder`, `hook-development`, `frontmatter-guard`
|
|
@@ -26,17 +26,17 @@ flowchart LR
|
|
|
26
26
|
- **Architecture:** `architecture` (carries the former `domain-model` grilling flow plus the `CONTEXT.md`/ADR formats under `skills/architecture/references/`)
|
|
27
27
|
- **Cross-session:** `evolve`, `convergence-monitoring`, `memory-cleanup`, `reconcile`, `sunset-review`, `eval`
|
|
28
28
|
- **Vault & docs:** `vault-sync`, `vault-mirror`, `docs-orchestrator`
|
|
29
|
-
- **Ecosystem:** `bootstrap`, `gitlab-ops`, `gitlab-portfolio`, `ecosystem-health`, `mode-selector`, `autopilot`, `dispatcher`, `remote-offload`, `spinout`, `npm-publish`
|
|
30
|
-
- **Testing:** `test-runner`, `playwright-driver`, `peekaboo-driver`, `ux-grill`
|
|
29
|
+
- **Ecosystem:** `bootstrap`, `gitlab-ops`, `gitlab-portfolio`, `ecosystem-health`, `mode-selector`, `autopilot`, `dispatcher`, `remote-offload`, `navigator`, `spinout`, `npm-publish`
|
|
30
|
+
- **Testing:** `test-runner`, `playwright-driver`, `peekaboo-driver`, `ux-grill`, `test-audit`
|
|
31
31
|
- **Content review:** `persona-panel`
|
|
32
32
|
- **Operator ergonomics:** `eli5` (plain-language restatement of the last answer)
|
|
33
33
|
- **Visualization:** `tmux-layout` (opt-in operator side-channel — [ADR-0007](adr/0007-tmux-visualization-substrate.md))
|
|
34
34
|
|
|
35
|
-
## Commands (
|
|
35
|
+
## Commands (28)
|
|
36
36
|
|
|
37
|
-
A slash command has exactly ONE definition.
|
|
37
|
+
A slash command has exactly ONE definition. 26 are skills with explicit `user-invocable: true` in `skills/<name>/SKILL.md` (the same file the model dispatches; `argument-hint` and `disable-model-invocation` live there too), and 2 are plain `commands/*.md` files without a same-named skill. Claude Code registers both shapes as `/session-orchestrator:<name>`, so a name that exists as both a command file and a user-invocable skill is listed twice in the `/` picker — `tests/commands/headless-bare-command-availability.test.mjs` forbids that twin.
|
|
38
38
|
|
|
39
|
-
- **Skills (
|
|
39
|
+
- **Skills (26):** `/autopilot`, `/bootstrap`, `/brainstorm`, `/close`, `/debug`, `/discovery`, `/dispatcher`, `/eli5`, `/eval`, `/evolve`, `/go`, `/grill`, `/harness-audit`, `/memory-cleanup`, `/navigator`, `/persona-panel`, `/plan`, `/portfolio`, `/reconcile`, `/release`, `/repo-audit`, `/spinout`, `/sunset-review`, `/test`, `/test-audit`, `/ux-grill`.
|
|
40
40
|
- **Command files (2):** `/session` (its skill is `session-start`) and `/templates-ack` (an in-session hook bypass with no skill body).
|
|
41
41
|
|
|
42
42
|
Under `claude -p`, `/session` and `/plan` are reserved terminal built-ins; use `/session-orchestrator:session` and `/session-orchestrator:plan` there.
|
|
@@ -49,7 +49,7 @@ Custom agents live in `agents/` (plugin) or `.claude/agents/` (project) as Markd
|
|
|
49
49
|
|
|
50
50
|
## Hook event types (10)
|
|
51
51
|
|
|
52
|
-
The full Claude wiring uses: `SessionStart` (banner + init), `SessionEnd` (close events), `PreToolUse/Edit|Write` (scope enforcement), `PreToolUse/Bash` (destructive-command guard + enforce-commands + templates-first + staging-fence + memory-propose audit), `PostToolUse` (edit validation + opt-in frontend-slop detection + loop-guard), `Stop` (session events), `SubagentStop` (telemetry), `PostToolUseFailure` (corrective context), `PostToolBatch` (wave signal + operator-steer), `SubagentStart` (telemetry), `CwdChanged` (cwd-change record).
|
|
52
|
+
The full Claude wiring uses: `SessionStart` (banner + init), `SessionEnd` (close events), `PreToolUse/Edit|Write` (scope enforcement + config protection + sessions-ledger guard), `PreToolUse/Bash` (destructive-command guard + enforce-commands + templates-first + staging-fence + memory-propose audit), `PostToolUse` (edit validation + opt-in frontend-slop detection + loop-guard), `Stop` (session events), `SubagentStop` (telemetry), `PostToolUseFailure` (corrective context), `PostToolBatch` (wave signal + operator-steer), `SubagentStart` (telemetry), `CwdChanged` (cwd-change record).
|
|
53
53
|
|
|
54
54
|
Codex uses the curated six-event project subset `SessionStart`, `PreToolUse`, `PostToolUse`, `SubagentStart`, `SubagentStop`, and `Stop`. Claude-only events are not exposed there, and Claude Edit/Write handlers remain unwired until a real adapter translates Codex's canonical `apply_patch` payload. The manifest uses native `${PLUGIN_ROOT}` while exporting `CODEX_PLUGIN_ROOT` plus `SO_PLATFORM=codex` for shared compatibility code.
|
|
55
55
|
|
|
@@ -60,12 +60,12 @@ Codex uses the curated six-event project subset `SessionStart`, `PreToolUse`, `P
|
|
|
60
60
|
- **Destructive-command guard.** On Claude Code, the active `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json` in the main session and in subagent waves. The policy has 12 blocking rules (`git reset --hard`, `rm -rf`, `git push --force`, deletion of `.orchestrator/metrics/**`, and more) and 4 warning rules. Cursor and Pi use event bridges with documented limits; Codex does not enforce this guard. Scope `enforcement: warn` or `off` does not change the separate destructive-command policy — see [Platform support](../README.md#platform-support). Where the hook is active, bypass it per session only for intentional maintenance by setting `allow-destructive-ops: true` in Session Config. The rule source of truth is [`.claude/rules/parallel-sessions.md`](https://github.com/Kanevry/session-orchestrator/blob/main/.claude/rules/parallel-sessions.md) (PSA-003), vendored to consumer repos via `/bootstrap`.
|
|
61
61
|
- **Import probe.** `hooks/post-edit-import-probe.mjs` (PostToolUse on `Edit`/`Write`/`MultiEdit`) guards the other direction: a hook-reachable helper saved in a broken intermediate state makes *every* Bash/Edit/Write call fail with an internal hook error, host-wide, for every session sharing the working copy. Right after such a file is saved the probe runs ESLint `no-undef` on it (plus a child-process `import()` for `scripts/lib/**`) and reports the blast radius; it never blocks and always exits 0. It only fires for files listed in the committed allowlist [`hooks/_lib/hook-import-set.json`](../hooks/_lib/hook-import-set.json), regenerated by `node scripts/generate-hook-import-set.mjs`. Kill switch: `SO_DISABLED_HOOKS=post-edit-import-probe`.
|
|
62
62
|
- **Process-group kill + orphan reaper.** Both quality-gate paths spawn their commands `detached: true`, so the whole process GROUP is killed on timeout (SIGTERM → grace → SIGKILL, exit 124) instead of only the shell — the shell's own children used to be reparented to PID 1 and keep running. Every gate process this repo starts is recorded in `.orchestrator/runtime/gate-processes.jsonl`; `hooks/post-tool-batch-wave-signal.mjs` and `hooks/on-stop.mjs` trigger a throttled, detached orphan scan that reaps only what that register claims. Ships inert (`reaper.enabled: false`, `reaper.mode: report`) until its firing rate is measured — rule [`.claude/rules/host-resources.md`](../.claude/rules/host-resources.md) § HR-107, rationale [ADR-0015](adr/0015-process-group-kill-and-orphan-reaper.md), audit trail `.orchestrator/metrics/reaper-audit.jsonl`.
|
|
63
|
-
- **Plugin validators.** `scripts/validate-plugin.mjs` runs the validator set under `scripts/lib/validate/*.mjs` (
|
|
63
|
+
- **Plugin validators.** `scripts/validate-plugin.mjs` runs the validator set under `scripts/lib/validate/*.mjs` (49 modules on disk, measured 2026-09-24 with `ls scripts/lib/validate/*.mjs | wc -l`); a failing BLOCKING validator fails the run. Newest blocking member: `check-learnings-shell-writes.mjs` (#1446) — blocks instruction prose that tells an agent to write `learnings.jsonl` with a shell redirect. Before it: `check-hook-entry-guards.mjs` — an AST oracle over every hook registered in the four hook manifests, flagging a missing entry guard or a top-level profile exit, either of which makes an imported hook module execute its whole program.
|
|
64
64
|
- **Codex:** `.codex-plugin/plugin.json` (tracked `+codex.<UTC timestamp>` version), generated command and skill entrypoints under `.codex-plugin/skills/`, compatibility config, agent role definitions, and the public marketplace/add/list lifecycle implemented by `scripts/codex-install.mjs`. `scripts/generate-codex-skills.mjs` produces the name union with commands taking precedence; [Codex usage](codex-setup.md#usage) explains selection. Every installer run refreshes via `plugin add`; hook trust remains an operator decision in a fresh task through `/hooks`.
|
|
65
65
|
- **Cursor:** `.cursor-plugin/plugin.json` registers canonical skills and `.mcp.json` using Cursor's native manifest format. Additional native component discovery is explicitly disabled; `scripts/cursor-install.mjs` supplies the existing command and hook adapters. The former standard root manifest was moved to prevent [Codex manifest interception](codex-setup.md#manifest-compatibility).
|
|
66
66
|
- **Pi:** `package.json` `pi` manifest, `pi/extensions/session-orchestrator.ts` bridge, `hooks/hooks-pi.json`, `scripts/pi-install.mjs`.
|
|
67
67
|
- **Portable cross-harness surface (generated, never hand-edited):** root `AGENTS.md` (byte-identical copy of `CLAUDE.md`) and `.agents/skills/<name>/SKILL.md` — mirrors carrying only spec-legal frontmatter plus a pointer body. These two surfaces are written by `scripts/generate-agents-skills.mjs` and drift-checked via its `--check` form inside `scripts/validate-plugin.mjs`. Native plugin manifests are maintained separately, with versions updated by `scripts/release.mjs`.
|
|
68
|
-
- **Scripts:** deterministic CLI tools (parse-config, run-quality-gate, validate-wave-scope, validate-plugin, token-audit, autopilot, session-shape) plus shared lib under `scripts/lib/*.mjs` — e.g. `session-shape.mjs` (the one wave-shape resolver for `/session` mode + `--profile`), `maintenance-due-banner.mjs` (the single session-start probe for the whole maintenance loop), `session-end/tail-runner.mjs` (the mechanical apply-half of Phase 3.6.4's Expired-Learnings Sweep), `issue-budget-reconcile.mjs` (close-time recorded-vs-charged cross-check), `telemetry/pricing.mjs` (per-model USD-per-token rates for cost estimation), `process-group.mjs` (the one `spawnInGroup`/`killProcessGroup` primitive both quality-gate paths use) and `orphan-reaper.mjs` (the pure `decideReapCandidates` decision
|
|
68
|
+
- **Scripts:** deterministic CLI tools (parse-config, run-quality-gate, validate-wave-scope, validate-plugin, token-audit, autopilot, session-shape) plus shared lib under `scripts/lib/*.mjs` — e.g. `session-shape.mjs` (the one wave-shape resolver for `/session` mode + `--profile`), `maintenance-due-banner.mjs` (the single session-start probe for the whole maintenance loop), `session-end/tail-runner.mjs` (the mechanical apply-half of Phase 3.6.4's Expired-Learnings Sweep), `issue-budget-reconcile.mjs` (close-time recorded-vs-charged cross-check), `telemetry/pricing.mjs` (per-model USD-per-token rates for cost estimation), `process-group.mjs` (the one `spawnInGroup`/`killProcessGroup` primitive both quality-gate paths use) and `orphan-reaper.mjs` (stable entry + CLI over `orphan-reaper/`: the pure `decideReapCandidates` decision, `runOrphanScan`, the bounded B5 audit) — all covered by the vitest suite. Standalone CLIs added in the same cycle: `scripts/check-sessions-integrity.mjs` (validates the just-written `sessions.jsonl` record against the schema AND vault-mirror's real render path; exit 0/1/2). `scripts/apply-session-learnings.mjs` (#1446) is the only sanctioned session-end/retro write path for `learnings.jsonl`: confirm/contradict, strict validation of new records, passive decay, then `pruneLearnings()`; dry-run by default, exit 0/1/2. Config parsers live beside their block: `scripts/lib/config/reaper.mjs`, `scripts/lib/config/gate.mjs`.
|
|
69
69
|
|
|
70
70
|
## `/harness-audit` — Anthropic large-codebase rubric
|
|
71
71
|
|
|
@@ -82,7 +82,7 @@ Surface counts measured 2026-09-06 by this repo's 360° ecosystem probe (`docs/a
|
|
|
82
82
|
| Axis | session-orchestrator | `open-gsd/gsd-core` |
|
|
83
83
|
|---|---|---|
|
|
84
84
|
| Commands / skills / agents | 26 / 50 / 14 | 70 / 71 / 35 |
|
|
85
|
-
| Hook guards |
|
|
85
|
+
| Hook guards | 29 hook files, 10 event types | 28 hooks, incl. write / read / prompt / workflow / secret-read / agent-isolation / worktree-path guards |
|
|
86
86
|
| Cross-session learning | `/evolve` + confidence-scored `learnings.jsonl`; reconcile turns eligible learnings into PROPOSED rules an operator approves one by one | `gsd-extract-learnings`, `gsd-mempalace-*` |
|
|
87
87
|
| Harness coverage | Claude Code, Codex CLI, Cursor IDE, Pi (4) | 44 `capabilities/` directories (pi, hermes, kimi, windsurf, opencode, ollama, …) |
|
|
88
88
|
| Install | marketplace / clone + installer script per harness | `npx @opengsd/gsd-core@latest` |
|