session-orchestrator 5.3.0 → 5.5.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/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/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- 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 +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -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 +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- 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 +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -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 +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- 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/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -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/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- 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 +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- 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 +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- 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 +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- 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/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- 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 +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- 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 +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- 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 +10 -3
- 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/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -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,168 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.5.0] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`events-retention` session-start probe** (`scripts/lib/events-retention-banner.mjs`, #1401 part 3, variant a). Each reader of the rotated events ledger declares `REQUIRED_EVENTS_WINDOW_DAYS` next to its call (`telemetry/sync` = 1 day, a chosen ceiling; the eval engine and `telemetry-stats` = `null`, they have no finite window). The probe warns only when the archive ring is full and covers less than the largest finite window, judged on archive start times; an unmeasured or unlistable source never records as clean. When the oldest archive's rotation record names a predecessor that was pruned (a `max-backups` raised after a prune), coverage starts at that archive, and a future-dated archive is unmeasurable rather than a negative coverage. No new config key. The probe family has 20 members.
|
|
15
|
+
- **`excludedScope.shell` in the tests:src envelope** (`scripts/lib/tests-src-ratio.mjs`, #1488, variant B). Shell code (`.sh`/`.bash`/`.bats`) still counts in neither bucket, so the ratio is unchanged, but its volume (split into test and src shell) is now reported. Additive; the schema stays `tests-src-ratio/2`.
|
|
16
|
+
- **`orchestrator.scope.refused_grant_allowed`** (`hooks/enforce-scope.mjs`, #1485 point 1): the first Gate-5b WARN of a wave (an absolute grant the wave-scope validator would refuse) emits one event, deduplicated per session and wave.
|
|
17
|
+
- **`isDeny` predicate** in `tests/_helpers/hook-decision.mjs` (#1027 Nachtrag 8), sharing one contract with `expectDeny` / `expectAllow` (the `isAllow` twin was removed again in the same cycle: zero consumers, #1497).
|
|
18
|
+
- **Probe telemetry `measure`** (`scripts/lib/session-start-probes.mjs`, #1401): a probe may declare `telemetry(result)`; its output is persisted on that probe's entry in `orchestrator.probes.completed` (at most 8 snake_case keys, finite numbers or null — the event travels the webhook unredacted). `events-retention` persists `{coverage_days, archives, max_backups, required_days}` on every measured answer, so a probe that never fires stays falsifiable (HR-105).
|
|
19
|
+
- **`reaper_trigger` on Stop / SubagentStop records** (`hooks/on-stop.mjs`, #1489): the orphan-scan trigger outcome is recorded as an optional key on the existing records — absent while the reaper is disabled, no new event type.
|
|
20
|
+
- **`reclaimed` on `orchestrator.session.lock.acquired`** (`hooks/_lib/lock-bootstrap.mjs`): `stale-heartbeat` or `corrupt` when a SessionStart took over a FOREIGN stale lock or an unparseable one (a session re-acquiring its own stale lock is a refresh and carries no key) — the bootstrap's stderr WARN is discarded by the harness, so takeovers were uncountable.
|
|
21
|
+
- **`navigator_adresse` in `scripts/fleet-checkin.mjs` output** (#1491): the validated lease address (or `null`) that session-start Phase 7.6 sends its optional hint to.
|
|
22
|
+
- **`tokens_unknown_sessions`** in the autopilot loop state, return value and `autopilot.jsonl` (#1487 item 4): an iteration whose record carries no token figure is counted instead of reading as a measured 0. The token-budget kill-switch still fires on the known lower bound.
|
|
23
|
+
- **`redactHomeDir(text)`** in `scripts/lib/common.mjs` (#1490): replaces the home directory with `~` in free text, path-boundary aware (`/Users/bob` never touches `/Users/bobby`).
|
|
24
|
+
- **`npm run update:local` — one command updates every local install** (`scripts/self-update.mjs`). Fast-forwards the clone (`git merge --ff-only origin/main`, `npm ci` when HEAD moved), updates the Claude Code marketplace and plugin, reads `claude plugin list --json` back and runs `npm ci --omit=dev` in the reported `installPath` when runtime dependencies are missing, and re-runs `scripts/codex-install.mjs` only when the plugin is already installed in Codex. `--dry-run` prints the mutating commands, `--skip-pull` leaves the clone alone, `--json` reports per step; old cache folders are never deleted because the running session loads them until the restart. The `release.mjs --publish` checklist names it as step 3, the Claude Code update banner points to it.
|
|
25
|
+
- **`vault-mirror --repo-root`** (`scripts/vault-mirror.mjs`, #1503): names the repo whose ledger is mirrored; without it the root is `<X>` of a `<X>/.orchestrator/metrics/<file>` source, else the git repo containing `--source` (realpath'd). A root whose namespace differs from the source's aborts before writing (exit 2, `aborted: source-repo-mismatch`). Notes gain a quoted `source-record` frontmatter field (learning id; `raw_session_id`, else `started_at`).
|
|
26
|
+
- **`sweep-expired-learnings --drop-malformed --line N`** (#1500): the sanctioned way to remove an unparseable learnings line. Dry run by default; `--apply` needs the store `--generation` and `--repo-root`, re-reads under the store lock, refuses a changed store or a line that is not malformed, refuses a line carrying a readable record (BOM or fused after a torn prefix) unless `--accept-embedded`, snapshots the store first and emits `orchestrator.learnings.malformed_dropped`.
|
|
27
|
+
- **`fallback_due_at` in `scripts/fleet-checkin.mjs` output** (#1501): check-in `zeit` + `CHECKIN_FALLBACK_MIN` while a navigator lease is active, else `zeit`; session-start Phase 7.6 reads it instead of a hard-coded 10 minutes. The check-in file schema is unchanged.
|
|
28
|
+
- **`commitlint.ci.config.mjs`** (#1477 item 4): the CI lint uses the repo config with `defaultIgnores: false` and first-line-only ignores for the merge and revert subjects git or GitLab write themselves, so `fixup!`, semver-only headers and body-line merges no longer slip through. The husky hook keeps the permissive config; `config-protection` warns on both files.
|
|
29
|
+
- **`history_gaps` in the `backfill-abandoned-sessions` summary** (#1498): counts pruned archives, so a backfill over a pruned history says it planned from an incomplete record.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Subagent worktrees branch from the session HEAD** (`.claude/settings.json` `worktree.baseRef: "head"`, #1485 point 2). Per the Claude Code docs this setting also governs `--worktree` and background sessions in this repo.
|
|
34
|
+
- **The worktree base check denies only a measured stale base** (`hooks/pre-task-scope-disjoint.mjs`, #1485). It reads `worktree.baseRef` in precedence order (local → project → user) at the session root, records the deciding layer as `base_ref_source`, denies an `isolation: "worktree"` dispatch only when `git merge-base --is-ancestor HEAD origin/HEAD` exits 1, skips shallow clones, unknown bases and a cached remote ref the harness would refresh first (`FETCH_HEAD` older than 24 h or absent, `skipped: 'stale-remote-ref'`), and writes no scope-ledger claim for a denied dispatch. The STATE.md `session-start-ref` prediction it replaced was wrong per the docs.
|
|
35
|
+
- **The `/evolve` prune flow snapshots first** (`scripts/sweep-expired-learnings.mjs --prune --snapshot`, `skills/evolve/**`, `.cursor/rules/060-evolve.mdc`, #1486). The sidecar's first line carries the store generation and path; `--entries` needs that header.
|
|
36
|
+
- **One orphan-scan trigger** (`scripts/lib/orphan-reaper/trigger.mjs`, #1489): `hooks/on-stop.mjs` and `hooks/post-tool-batch-wave-signal.mjs` import it instead of carrying byte-identical copies; the hardened no-follow append is shared too (`no-follow-append.mjs`).
|
|
37
|
+
- **Every bootstrap tier ignores the store-lock artefacts** (`skills/bootstrap/_shared-template.md` § store-lock-ignore, #1489 item 17, #1487 item 11): one idempotent snippet runs from Fast Step 3 (inherited by Standard and Deep), Upgrade Flow and Refresh-Lock Flow, so repos bootstrapped before 5.4.0 get the lines on their next refresh. It adds a missing trailing newline first (an append onto `.env` without one had un-ignored it), treats `pattern\r` in a CRLF file as present, and skips without a repository root or with an unreadable `.gitignore`.
|
|
38
|
+
- **The MR pipeline lints the MR title when the merge request will squash** (`.gitlab-ci.yml` `commitlint`, #1477 item 1): only when `CI_MERGE_REQUEST_SQUASH_ON_MERGE` is `"true"`, after stripping GitLab's Draft prefixes. Merge-commit MRs keep free-form titles. `docs/ci-setup.md` lists the remaining gaps.
|
|
39
|
+
- **`vault-backfill` reads `vault-integration` through the canonical parser and resolves `vault-dir` host-locally** (`scripts/vault-backfill.mjs`, `scripts/lib/config/vault-integration.mjs`, #1094): the inline-object, dash-bullet and bold-bullet forms are now read (19 of 56 instruction files on the measuring host returned no vault-dir before), a block inside an HTML comment is ignored, `SO_VAULT_DIR` and `owner.yaml` win over the committed path, `~` is expanded, and `gitlab-groups: null` means unset.
|
|
40
|
+
- **Scope drift counts uncommitted and untracked files** (`scripts/lib/scope-baseline.mjs`, #1027 item 2): the numerator is the committed range ∪ the working tree vs HEAD ∪ untracked files. The plugin's own writes are excluded (`.orchestrator/metrics/**`, untracked paths under `.orchestrator/`, `<state-dir>/filescopes/**` and `<state-dir>/worktrees/**`); tracked deliverables such as `.orchestrator/steering/` and `bootstrap.lock` count. Each query is bounded (10 s, 64 MiB); a failing query skips the measurement instead of reporting a partial numerator. Files already dirty at session start, or a same-checkout peer's files, can over-report (named ceiling).
|
|
41
|
+
- **A session spanning the v1/v2 token-schema boundary is reported as unpriced** — the rollup returns `total_cost_usd: null` and `sessions.jsonl` omits the field (`scripts/lib/session-token-rollup.mjs`, #1487 item 1, owner decision 2026-10-02): a v1 record is an unpriced cost candidate, so a partial sum no longer reads as the whole bill (`cost_records_priced < cost_records_total` says why).
|
|
42
|
+
- **Scope hooks read the session's own working copy** (`scripts/lib/platform.mjs` `resolveSessionRoot`, #1492): `enforce-scope`, `enforce-commands`, `post-bash-write-verify` and `pre-task-scope-disjoint` resolve their control files from the repo root of the payload cwd instead of `CLAUDE_PROJECT_DIR`, so a session that entered a worktree enforces that worktree's manifest. The root never climbs above the launch directory (`CLAUDE_PROJECT_DIR`; on the Cursor/Pi bridges their `*_PROJECT_DIR`, i.e. the payload cwd as before #1492), so a session launched in a monorepo package keeps its manifest; harness `agent-<hex>` worktrees lift to their parent repo, and the auto-memory carve-out still keys on the launch directory. Named ceiling: `isolation: worktree` agents of a session that entered a worktree lift to the launch checkout and stay unenforced.
|
|
43
|
+
- **One dispatch-scope ledger per session** (`hooks/pre-task-scope-disjoint.mjs`, #1493): `.orchestrator/wave-dispatch-scopes.<session>.json` (+ `.lock`, path-safe id) replaces the shared file, so two sessions in one checkout no longer erase each other's claims; a legacy shared ledger is adopted read-only when its wave key names this session. Wave step 7 and session-end 3.0 delete only this session's file.
|
|
44
|
+
- **The commit guard reads ownership, enforcement and role** (`hooks/wave-scope-commit-guard.mjs`, #1493): it stands down on a foreign session's manifest, skips under `enforcement: off`, notes a Discovery manifest, reports a stale one, and treats an empty writing-role union as an incomplete `--union` (warn reports, strict blocks). Out-of-scope staged paths under a non-empty manifest keep blocking at every other level.
|
|
45
|
+
- **Session-end diffs use `resolveBaselineRange`** (`skills/session-end/plan-verification.md`, docs-orchestrator, #1493): `SESSION_RANGE` is `<pinned sha>..HEAD`, else `<resolved base>...HEAD`, with explicit `no-baseline-ref` / `unresolvable-ref` skips instead of a hard two-dot `origin/main..HEAD`; the pinned ref must be hex.
|
|
46
|
+
- **Consumer repos ignore every plugin runtime artefact** (`skills/bootstrap/_shared-template.md` § store-lock-ignore, #1495): the snippet appends the session lock and its temps, state/rules/dispatch/transcript-tail locks, `current-session.json`, `host.json`, the staging fence, `.orchestrator/tmp/`, rewrite backups (`*.jsonl.bak-*`, legacy `*.jsonl.bak.*`) and 16 more paths a census found missing — among them `pending-dream.md` (a private MEMORY.md draft), `metrics/test-runs/` and `metrics/ux-grill/` (screenshots) and `metrics/verification-failures/`. A census test checks every `.orchestrator/` line of this repo's own `.gitignore` against the snippet; ledgers, `bootstrap.lock`, `policy/`, `steering/` and `peers/` stay versioned.
|
|
47
|
+
- **The SessionStart backfill bounds its archive reads** (`scripts/lib/session-close-backfill.mjs`, `scripts/backfill-abandoned-sessions.mjs`, #1498): a session whose start rotated into `_archive/` is read from the rotated ledger, under one shared 500 ms deadline per SessionStart; a candidate left unread is deferred (`skipped-history-unread`) instead of being written with now/now times. CLI and SessionEnd stay unbounded.
|
|
48
|
+
- **Telemetry answers that read "clean" while unmeasured** (#1498): `events-retention` reports a stampless ledger as unmeasurable (`ran-warn`); the autopilot run summary prints the token sum and `tokens_unknown_sessions` as a lower-bound note; `readGateProcessLedger` returns `read` / `absent` / `unreadable`, so a refused or dangling-symlink ledger is no longer silent; the orphan-scan trigger reads an unresolvable project dir as `disabled`.
|
|
49
|
+
- **B5 date-bomb scanner** (`scripts/lib/validate/check-test-value-bans.mjs`, #1499): all-caps clock constants (`NOW`, `FIXED_NOW`, `NOW_MS` …) count as clock names; only `setSystemTime(<date>)` or `useFakeTimers({ now: <date> })` freeze the date (a bare `useFakeTimers()` does not); a real-clock name bound at collection time is no longer exempted by a freeze. Corpus: 0 findings in 722 files before and after.
|
|
50
|
+
- **`cursor-install` prunes dead plugin links** (`scripts/cursor-install.mjs`, #1501): a `.cursor/{rules,commands,skills}` entry is removed only when it is a dangling symlink resolving inside the plugin's own `.cursor/<kind>/`; a broken link is reported as dangling instead of "symlink exists".
|
|
51
|
+
- **The commitlint job survives force-pushed branches and says how to repair main** (`.gitlab-ci.yml`, #1477, #1502): a non-default-branch push whose before-sha is gone lints from the merge-base with the default branch (fail-closed); a red default-branch run prints the revert remedy; the squash-title step names why a title is not linted. `docs/ci-setup.md` documents the owner settings (`suggestion_commit_message` template, `squash_option: never`) and the cancelled-pipeline ceiling.
|
|
52
|
+
- **Test fakes are no longer freshly written executables** (`tests/_helpers/executable-fixture.mjs` `installScriptCli`, `tests/fixtures/bin/node-script-launcher`, #1497): macOS's first-launch check flaked the suite under load (fresh node-shebang fake median 207 ms vs 34 ms).
|
|
53
|
+
|
|
54
|
+
### Deprecated
|
|
55
|
+
|
|
56
|
+
- **`findScopeFile()`** (`scripts/lib/scope-gate.mjs`, re-exported by `scripts/lib/hardening.mjs`): it returns the first `wave-scope.json` without checking whose it is, which is the #1504 item 6 defect. No production caller is left; use `findOwnScopeFile()`. Kept for one minor cycle (`.claude/rules/development.md` § Package Lifecycle).
|
|
57
|
+
|
|
58
|
+
### Removed
|
|
59
|
+
|
|
60
|
+
- **`/session-orchestrator:navigator` skill and command** (#1491, owner decision): the 5.4.0 package shipped fleet internals (a private host alias, internal references, calls to a CLI other users do not have). `skills/_shared/fleet-protocol.md` remains as a generic file contract; `scripts/fleet-checkin.mjs`, `scripts/lib/fleet-protocol.mjs` and the SessionStart navigator banner stay. **Not a breaking change (owner decision 2026-10-02):** a skill or slash command is not an API export under `.claude/rules/development.md` § Package Lifecycle, and this command only worked with a private CLI that other users do not have — so it ships in a minor release without a migration guide.
|
|
61
|
+
- **The v1 session-lock fallback** (`scripts/lib/session-lock.mjs`, harness-audit category 4, #595; sunset target was 2026-08-25): a lock without `last_heartbeat` is still visible but never live: `acquire()` reports it as `stale-heartbeat` and the SessionStart bootstrap reclaims it. Measured 2026-10-02: 8 of 8 locks and 10 of 10 registry entries on the measuring host carry the field.
|
|
62
|
+
- **The pre-#1404 staging-fence fallback** (`hooks/wave-scope-commit-guard.mjs`, #1487 item 16): its own revisit trigger named 5.4.0. The only fence writer, `hooks/pre-bash-staging-fence.mjs`, has written `paths` + `command_hash` and never `command` since 5.3.0, and no other hook or bridge writes fence files; an entry without `paths` is now skipped.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **vault-mirror no longer overwrites another project's notes on a slug collision** (`scripts/vault-mirror.mjs`, `scripts/lib/vault-mirror/`, #1503): a run started from another repo's checkout took the namespace from the cwd and overwrote three tracked notes of a different project. The namespace now follows the `--source` ledger (see Added; a monorepo package keeps its own namespace via the `<X>/.orchestrator/metrics/` convention), and an existing note whose `source-record` is not a key of the writing ledger is `skipped-foreign-owner` — which also stops a different-id session note from being overwritten and reported as `created`. A re-learned learning goes to the `<slug>-<uuid8>` disambiguation instead of freezing, and a note that still leaks a secret is healed over the guard.
|
|
67
|
+
- **Only one session wins a stale-lock takeover** (`scripts/lib/session-lock.mjs`, `hooks/_lib/lock-bootstrap.mjs`, #1494): `forceAcquire({ onlyIfNotLive })` moves the old lock to a unique tombstone and replaces it via create-or-fail, so two SessionStarts can no longer both believe they hold it; the winner keeps its enrichment through a racer's window. A `last_heartbeat` more than 5 minutes in the future is not live (mirrored in harness-audit category 4); an unreadable lock path is reported (`reason: 'unreadable'`, stderr WARN, `read_anomaly`); the `active` result carries `ageHours`.
|
|
68
|
+
- **vault-backfill and archive-closed-prds honour the vault-integration off switch** (#1496): neither writes into a vault while `vault-integration` is off (env `SO_VAULT_INTEGRATION`, `owner.yaml` or committed config) — an explicit `vault-backfill --vault-dir` still wins, as the run's own request; `gitlab-groups: "null"` is the group name in block and inline form alike; scheme-less strings containing `://` are redacted in logs.
|
|
69
|
+
- **A valid JSON line that is not an object no longer becomes an empty learning** (`scripts/lib/learnings/io.mjs`, #1500): `null`, numbers, arrays and strings count as malformed and are kept verbatim.
|
|
70
|
+
- **Scope bookkeeping false alarms** (#1493, #1027): `scope-echo --verify` no longer reports `injection-missing` for `coordinator.json`, `agent_count` excludes it, and `post-bash-write-verify` no longer calls `<state-dir>/filescopes/` writes violations. In a session launched from a repo subdirectory the hook rebases porcelain paths onto the session root, and a `../` path is never in scope or a "peer write".
|
|
71
|
+
- **A prune no longer archives learnings appended after the snapshot** (#1486). `--prune --entries --apply` compares the store generation inside the store lock and refuses on drift (exit 3, the current token is not printed); applying a sidecar to a different store, a sidecar without a header, and a `--snapshot` onto the store or archive (also via a symlinked directory or a different letter case) all exit 1. A store with unparseable lines is snapshotted with a WARN (the lines are kept verbatim on every rewrite). The Cursor `/evolve` rule had not been migrated and is now.
|
|
72
|
+
- **The Agent-dispatch guard keys its state on the repository the dispatch runs in** (`hooks/pre-task-scope-disjoint.mjs`, #1489): ledger, lock, wave key and events follow the git toplevel of the payload cwd, then the nearest `.git` ancestor, then `$CLAUDE_PROJECT_DIR` — after a `cd` into a subdirectory its records had landed in `<root>/sub/.orchestrator/`. All its subprocess calls share one 1500 ms budget (one hanging call had killed the hook and lost the verdict; an exhausted budget allows and records why); a stale-base deny is recorded as `deny-stale-base`, with the value it replaces in `collision_result`.
|
|
73
|
+
- **A pruned events history no longer reads as "never"** (`scripts/lib/events.mjs`, #1401, #1489): every reader of the rotated ledger declares its retention window (census over `scripts/`, `hooks/`, `skills/`), `scanEventsBackwards` reports gaps from a rotation record on any line, and maintenance-due answers *undeterminable*.
|
|
74
|
+
- **The orphan reaper's gate process ledger refuses FIFOs and symlinks** (`scripts/lib/process-group.mjs`, #1489): a FIFO planted at the ledger path no longer hangs the gate, and an inode replaced mid-append is reported as unlinked, not as a hard link.
|
|
75
|
+
- **Unparseable learnings-store lines survive every rewrite** (`scripts/lib/learnings/`, #1489 item 4): the expiry sweep, the prune and `export-hw-learnings` (which deleted them) write them back verbatim — `rewriteLearnings` keeps the store's own malformed lines by default — and count them as `malformed: N` (also on `orchestrator.learnings.sweep_applied`). `apply-session-learnings --apply` no longer exits 2 on such a store (it blocked the learnings write on every `/close` of that repo); it WARNs with line numbers and proceeds. A record appended after a torn last line is readable, and whitespace-only or CRLF blank lines no longer count as malformed.
|
|
76
|
+
- **A corrupt `.orchestrator/session.lock` no longer blocks every new session** (`scripts/lib/session-lock.mjs`, `hooks/_lib/lock-bootstrap.mjs`): `acquire()` reports it as `corrupt` and the SessionStart bootstrap reclaims it with one stderr WARN; an absent or unreadable lock keeps the conservative `active`. The lock reaper, the autopilot stall sampler and the vault board no longer show `started_at` as a heartbeat. `buildLock` always writes the lock shape (`mode` falls back to `unknown`, a non-positive or non-numeric `ttl_hours` to 4), so a lock from a caller passing `mode: undefined` is never mistaken for a corrupt one. Two SessionStarts forcing the same stale or corrupt lock no longer stamp one session's label and owner proof onto the other's lock.
|
|
77
|
+
- **The session-start probe runner never rejects on a throwing probe** (`scripts/lib/session-start-probes.mjs`): a throwing `severityOf`/`render` or an unprintable thrown value records the probe as `error` (`severity-threw` / `render-threw` / `probe-threw-unprintable`) instead of losing every banner line and the `orchestrator.probes.completed` record of that start; a throwing telemetry read drops only `measure`.
|
|
78
|
+
- **pseudonym-map warnings no longer print the home directory or map content** (`scripts/lib/vault-mirror/pseudonym-map.mjs`, #1490): V8's JSON parse error quoted the map's content.
|
|
79
|
+
- **Vault and home paths in logs show as `~`, and `ssh://` logins are redacted like the scp form** (`scripts/lib/vault-mirror.mjs`, `auto-commit.mjs`, `archive-closed-prds.mjs`, the `/close` metrics diagnostics, `export-hw-learnings` errors and the learnings-store WARN, #1490, #1487 items 9–10).
|
|
80
|
+
- **B5 reads clock bindings and fake timers from the shared setup** (`scripts/lib/validate/check-test-value-bans.mjs`, #1489 item 16): a module-level `const now = Date.now()` passed as control is caught, a `setSystemTime` in `beforeEach` no longer raises a false finding. A setup fake timer exempts only the blocks of its own or an enclosing `describe`, and only installers (`useFakeTimers`, `setSystemTime`) count.
|
|
81
|
+
- **The ci-status banner resolves its SHAs inside the probe budget** (`scripts/lib/session-start-probes.mjs`, #1396); the renderer no longer spawns git.
|
|
82
|
+
- **Orphan reaper file safety** (`scripts/lib/orphan-reaper/`, `scripts/lib/tail-window.mjs`, #1487 items 5–7): the audit append refuses symlinks and hard links; the reader opens with `O_NOFOLLOW|O_NONBLOCK` (a planted FIFO no longer hangs scan children) and is capped at 1 MiB; the throttle marker is read without following symlinks, a future mtime fails toward a scan, and a symlinked marker is replaced by a regular file on the next stamp (tmp + rename) instead of switching the throttle off for good (#1489 item 9); the false-alarm window reads N decisions, not N lines.
|
|
83
|
+
- **Vault logs no longer print absolute vault paths or scp login users** (`scripts/lib/vault-mirror/process.mjs`, `scripts/lib/named-vault-resolver.mjs`, #1487 items 9–10), including bracketed IPv6 remotes; the vault-relative helper matches the root by whole path segment.
|
|
84
|
+
- **B5 no longer counts a real-clock value as clock control** (`scripts/lib/validate/check-test-value-bans.mjs`, #1487 item 8).
|
|
85
|
+
- **The vault learnings backfill checks and appends under the store lock** (`scripts/backfill-learnings-from-vault.mjs`, #1487 item 12); bootstrap Step 3b ignores the lock artefacts in consumer repos (#1487 item 11).
|
|
86
|
+
- **`codex-install.mjs` failed with `ENOBUFS` on hosts with several Codex marketplaces** — `codex plugin list --available --json` returned 2.56 MB against `spawnSync`'s 1 MiB default (measured 2026-10-02); the buffer is now 64 MiB.
|
|
87
|
+
- **Claude Code skipped the plugin's dependency install** — Claude Code 2.1.287 refuses to install dependencies for a package that sets `overrides` and reports it only as `noteDetails: dependencies-refused`, leaving the 5.4.0 cache without `node_modules` (hooks still ran; skill scripts and the MCP server did not). The `overrides` block (vite, fast-uri, brace-expansion) is removed: without it npm resolves the same or newer versions and `npm audit` reports 0 vulnerabilities, full and `--omit=dev` (measured 2026-10-02); the lockfile is unchanged. `docs/install.md` § Upgrade documents the check and the manual fallback.
|
|
88
|
+
- **A peer session's wave manifest no longer switches this session's scope guards off** (`scripts/lib/scope-gate.mjs`, #1504 item 6): the guards took the first `wave-scope.json` in harness order (`.pi` > `.cursor` > `.codex` > `.claude`); when that one belonged to another session, `enforce-scope`, `enforce-commands`, `post-bash-write-verify` and `wave-scope-commit-guard` stood down and never read this session's own `.claude` manifest. `findOwnScopeFile()` checks ownership per candidate and skips only a provably foreign one; unbound, corrupt or unidentifiable manifests still decide. The wave-number readers (`post-edit-validate`, `post-tool-batch-wave-signal`, `pre-bash-memory-propose-audit`, `on-stop`, `run-quality-gate`) use it too, and so does the dispatch-ledger key of `pre-task-scope-disjoint` — a peer's manifest no longer supplies this session's wave number, splits its in-flight dispatch records, or toggles `gates.post-edit-validate`.
|
|
89
|
+
- **Wave readers, the staging fence and the orphan reaper resolve the session root** (`hooks/`, `scripts/lib/platform.mjs`, #1504 items 4 and 8, #1492 item 2): they used the launch dir while the scope guards use the session root, so a worktree session read its wave and reaped in the wrong tree. The fence is written at the git toplevel of the hook's cwd, where the commit guard reads it. The launch-dir clamp compares native realpaths, so a lowercase `/users/…` spelling on APFS no longer skips it. A nested repo or entered worktree that hides an active wave manifest records `orchestrator.scope.manifest_hidden_by_session_root` once per wave (#1504 item 3; the write is still allowed).
|
|
90
|
+
- **`blockedCommands` carries only command patterns** (`scripts/lib/blocked-commands-policy.mjs`, `skills/wave-executor/references/wave-loop-scope-manifest.md`, #1509): the manifest recipe copied every block rule, including the typed `redirect-truncate` rules (pattern `>`) and the `path-delete` label, so `enforce-commands` denied redirects inside `bash -c`/`sh -c`/`eval` payloads. `blockedCommandPatterns()` keeps only untyped rules; the destructive guard still enforces the typed ones.
|
|
91
|
+
- **Scope entries compare and grant in one spelling** (`scripts/lib/scope-gate.mjs`, `scripts/validate-wave-scope.mjs`, #1026 items 3–5): `./x`, `a//b` and `a/./b` collide with `x`/`a/b` in the disjointness check, the `allowedPaths` union is written in that canonical spelling (a `./x` entry used to be unmatchable, so the agent's own write was denied), and a schema-invalid manifest no longer hides a collision report.
|
|
92
|
+
- **A future-stamped registry entry or lock is never "young"** (`scripts/lib/session-registry.mjs`, `scripts/lib/lock-reaper.mjs`, #1505 items 1–2): it counted as a fresh peer forever, was never swept, and the reaper emitted a negative `age_hours`. Beyond the shared 5-minute tolerance the age is unmeasurable (`null`, never fresh); where an age only decides deletion, the file's mtime decides.
|
|
93
|
+
- **Lock leftovers of a killed reclaim are swept** (`scripts/lib/session-lock.mjs`, #1505 item 3): `acquire()` removes `.session.lock.tmp.*`, `.session.lock.create.tmp.*` and `.session.lock.reclaim.*` files older than one hour (regular files only; a reclaim tombstone only when it is not a live lock).
|
|
94
|
+
- **The close backfill no longer treats an unreadable events archive as a clean scan** (`scripts/lib/session-close-backfill.mjs`, #1505 item 5): such a candidate is deferred as `skipped-history-unread`.
|
|
95
|
+
- **`run-migrate-v2-cross-repo` exits 2 on read/write errors** as its help documents (#1487 item 15).
|
|
96
|
+
- **vault-mirror checks the generator marker on every write path** (`scripts/lib/vault-mirror/process.mjs`, #1506 item 3): the disambiguation branch overwrote a note another generator owned.
|
|
97
|
+
- **`export-hw-learnings` honours the vault-integration off switch** for its default output (#1506 item 4); `apply-session-learnings` resolves symlinks before pinning its write event to the repo.
|
|
98
|
+
- **The vault staleness probes expand `~`** (`skills/discovery/probes/vault-staleness.mjs`, `vault-narrative-staleness.mjs`): the committed `~/Projects/vault` resolved to `<cwd>/~/…` and reported a false all-clear. `ssot-code-diff` counts all 27 slash commands (`commands/*.md` + user-invocable skills) instead of 2.
|
|
99
|
+
- **commitlint in CI accepts merge and revert headers only from real merges and reverts** (`commitlint.ci.config.mjs`, `.gitlab-ci.yml`, #1507 item 1): an ordinary commit titled `Merge branch …` or `Revert "…"` skipped every rule. The job lints `--no-merges` and `--merges` in two passes; a revert needs git's or GitLab's `This reverts …` body line.
|
|
100
|
+
- **Docs match the code**: the memory-proposal quota is per wave, shared by all agents of that wave (#1027); `docs/ci-setup.md` records the GitLab settings `squash_option: never` and the conventional `suggestion_commit_message` applied 2026-10-03 (#1507 item 5); the evolve docs name the `--drop-malformed` repair.
|
|
101
|
+
|
|
102
|
+
## [5.4.0] - 2026-10-02
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **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).
|
|
107
|
+
- **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.
|
|
108
|
+
- **`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.
|
|
109
|
+
- **`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.
|
|
110
|
+
- **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.
|
|
111
|
+
- **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.
|
|
112
|
+
- **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).
|
|
113
|
+
- **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`.
|
|
114
|
+
- **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, the configured offload alias 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 (tracked in the navigator repository): 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.
|
|
115
|
+
- **`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).
|
|
116
|
+
- **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.
|
|
117
|
+
|
|
118
|
+
- **`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.**
|
|
119
|
+
- **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.
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
|
|
123
|
+
- **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`).
|
|
124
|
+
- **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.
|
|
125
|
+
- **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.
|
|
126
|
+
- **The offload threshold has one source** (`skills/_shared/fleet-protocol.md`, `skills/navigator/SKILL.md`, #1482): the navigator CLI's offload check and its exit contract replace the prose numbers (< 12, `last_max`).
|
|
127
|
+
- **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.
|
|
128
|
+
- **`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.
|
|
129
|
+
- **`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.
|
|
130
|
+
- **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.
|
|
131
|
+
- **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.
|
|
132
|
+
- **`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.
|
|
133
|
+
- **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.
|
|
134
|
+
- **`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.
|
|
135
|
+
- **`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.
|
|
136
|
+
- **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.
|
|
137
|
+
- **`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`).
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
|
|
141
|
+
- **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.
|
|
142
|
+
- **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.
|
|
143
|
+
- **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`.
|
|
144
|
+
- **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).
|
|
145
|
+
- **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.
|
|
146
|
+
- **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`.
|
|
147
|
+
- **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.
|
|
148
|
+
- **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`.
|
|
149
|
+
- **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.
|
|
150
|
+
- **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.
|
|
151
|
+
- **`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.
|
|
152
|
+
- **`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.
|
|
153
|
+
- **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.
|
|
154
|
+
- **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.
|
|
155
|
+
- **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.
|
|
156
|
+
- **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`.
|
|
157
|
+
|
|
158
|
+
- **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.
|
|
159
|
+
|
|
160
|
+
- **`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.
|
|
161
|
+
- **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.
|
|
162
|
+
- **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.
|
|
163
|
+
- **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.
|
|
164
|
+
- **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.
|
|
165
|
+
- **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.
|
|
166
|
+
- **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.
|
|
167
|
+
- **`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.
|
|
168
|
+
- **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.
|
|
169
|
+
- **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).
|
|
170
|
+
- **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).
|
|
171
|
+
|
|
10
172
|
## [5.3.0] - 2026-09-22
|
|
11
173
|
|
|
12
174
|
### 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 **51 skills, 27 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` (25 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 27 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.5.0)
|
|
125
127
|
|
|
126
|
-
Highlights of the v5.
|
|
128
|
+
Highlights of the v5.5.0 line:
|
|
127
129
|
|
|
128
|
-
- **
|
|
129
|
-
- **
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
130
|
+
- **Scope guards hold in worktrees, subdirectory launches and beside peer sessions.** The guards read the session's own working copy, find this session's own `wave-scope.json` even when a peer session's manifest ranks higher, and the wave readers, staging fence and orphan reaper follow the same session root. `blockedCommands` carries only command patterns, so redirects inside `bash -c` payloads are no longer denied (#1492, #1493, #1504, #1509).
|
|
131
|
+
- **Session locks are taken over by exactly one session.** A stale or corrupt lock is reclaimed through a tombstone, a future-stamped lock or registry entry never counts as young, and leftovers of a killed reclaim are swept (#1494, #1505).
|
|
132
|
+
- **vault-mirror never overwrites a note it does not own.** The namespace follows the mirrored ledger, a note of another project or generator is skipped, and the vault writers honour the `vault-integration` off switch (#1496, #1503, #1506).
|
|
133
|
+
- **The learnings store keeps every line.** Unparseable lines survive each rewrite, `sweep-expired-learnings --drop-malformed` removes one on purpose, and a prune snapshots first and refuses on drift (#1486, #1489, #1500).
|
|
134
|
+
- **Conventional commits on every path to `main`.** CI lints each commit range and the squash title, and accepts merge or revert headers only from real merges and reverts (#1477, #1502, #1507).
|
|
135
|
+
- **`npm run update:local` updates every local install**, and Claude Code installs the plugin's dependencies again (the `overrides` block it refused is gone). The `/session-orchestrator:navigator` skill left the package; the fleet file contract stays (#1491).
|
|
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,143 @@ 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` over the commits a pipeline introduces, with the CI-only rule
|
|
425
|
+
set `commitlint.ci.config.mjs` (= `commitlint.config.mjs`, Conventional Commits
|
|
426
|
+
with `header-max-length 120`, plus stricter ignores). CI is needed in addition to
|
|
427
|
+
the husky `commit-msg` hook because the hook is inactive in a fresh clone until
|
|
428
|
+
`npx husky` has run, absent for Codex / web edits, and skipped by `--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, so only
|
|
433
|
+
its tip is linted until its MR pipeline lints the whole range). The job logs
|
|
434
|
+
the range and the commits it lints.
|
|
435
|
+
- **Force-pushed branch without an open MR (#1477 item 3, closed).** The old
|
|
436
|
+
before-sha is not in the fresh clone. On a branch other than
|
|
437
|
+
`$CI_DEFAULT_BRANCH` the job fetches `origin/$CI_DEFAULT_BRANCH` and lints from
|
|
438
|
+
the merge-base instead, and says so in the log. Only plain branch pipelines can
|
|
439
|
+
hit this: a branch with an open MR runs MR pipelines only, and `main` is
|
|
440
|
+
protected against force-push, so a missing base on `main` stays a red job.
|
|
441
|
+
The all-zero before-sha of a new branch deliberately keeps the tip-only range:
|
|
442
|
+
a branch created at `main`'s tip (GitLab UI, `git push origin main:x`) would
|
|
443
|
+
otherwise lint an empty range and go red.
|
|
444
|
+
- **Fail-closed.** `commitlint --from X --to Y` exits 0 over an *empty* range
|
|
445
|
+
(measured 2026-09-29, `@commitlint/cli` 19.8.1), so the script itself fails
|
|
446
|
+
when the range is empty, its base is missing from the clone, or the fallback
|
|
447
|
+
cannot fetch `origin/$CI_DEFAULT_BRANCH` or find a merge-base. "Nothing was
|
|
448
|
+
checked" is never a pass.
|
|
449
|
+
- **`GIT_DEPTH: "0"`.** The project default clone depth is 20, so the range base
|
|
450
|
+
can be absent from a shallow clone. `semgrep` uses the same setting for its
|
|
451
|
+
baseline commit.
|
|
452
|
+
- **Stricter ignores in CI (#1477 item 4, closed).** commitlint's built-in default
|
|
453
|
+
ignores (`@commitlint/is-ignored` 19.8.1) skip far more than git's own merge and
|
|
454
|
+
revert messages: any message with a `Merge …` line *anywhere* (the pattern is
|
|
455
|
+
multiline, so one body line exempts the whole message), `revert <anything>`,
|
|
456
|
+
`Reapply <anything>`, `amend!`/`fixup!`/`squash!`, version headers (`v1.2.3`)
|
|
457
|
+
and `Merged …` / `Automatic merge…` / `Auto-merged … into …`. The CI config sets
|
|
458
|
+
`defaultIgnores: false` and accepts only first lines git or GitLab generate:
|
|
459
|
+
merges (`Merge branch 'x' into 'main'` from GitLab, and git's
|
|
460
|
+
`Merge branch 'main' into x`, `Merge remote-tracking branch 'origin/main' into x`,
|
|
461
|
+
`Merge branch 'main' of <remote>`), and `Revert "…"` / `Reapply "…"`. Both CI
|
|
462
|
+
call sites (range and squash title) pass `-g commitlint.ci.config.mjs`.
|
|
463
|
+
The first line alone does not prove git or GitLab wrote it (#1507 item 1): a
|
|
464
|
+
normal commit titled `Merge branch 'a' into b` or `Revert "x"` used to skip
|
|
465
|
+
every rule. So the job lints the range in two passes that together cover it
|
|
466
|
+
exactly: `--git-log-args=--no-merges` with `COMMITLINT_MERGE_COMMITS=0`, where
|
|
467
|
+
a merge-shaped subject is linted like any other, and `--git-log-args=--merges`
|
|
468
|
+
with `COMMITLINT_MERGE_COMMITS=1`, where git has counted two or more parents
|
|
469
|
+
and the merge shape is accepted. A hand-written message on a real merge is
|
|
470
|
+
still linted in the second pass. A revert or reapply shape is accepted only
|
|
471
|
+
with the body line git or GitLab writes under it: `This reverts commit <sha>`
|
|
472
|
+
(git, and GitLab's revert of a single commit) or `This reverts merge request
|
|
473
|
+
!<iid>` (GitLab's revert of a merged MR; taken from GitLab's source, not
|
|
474
|
+
measured on this instance). The squash-title call sets
|
|
475
|
+
`COMMITLINT_MERGE_COMMITS=0`: a squash commit has one parent, so a
|
|
476
|
+
merge-shaped or bare `Revert "…"` MR title fails. The
|
|
477
|
+
husky hook keeps the lenient defaults on purpose, so a local `git revert`,
|
|
478
|
+
`git commit --fixup` or `git pull` merge is not blocked mid-work; a `fixup!`
|
|
479
|
+
commit has to be autosquashed before its MR pipeline goes green.
|
|
480
|
+
History check (2026-10-02 @ `ff3e43b5`): the 200 most recent `main` commits
|
|
481
|
+
all pass the CI config (0 failures, as under the husky config). Over the full
|
|
482
|
+
history (1277 commits) it fails 10, 6 of which already failed under the old
|
|
483
|
+
config; the 4 new ones are hand-written merge subjects from 2026-04-30 to
|
|
484
|
+
2026-06-11 (e.g. `Merge branch 'x' (MR !21, …)`). The job only lints new
|
|
485
|
+
ranges, so old history cannot turn a pipeline red.
|
|
486
|
+
- **MR title on a squash merge (#1477 item 1, closed).** A squash merge commits
|
|
487
|
+
the MR title (the project's squash commit template is unset, so GitLab uses
|
|
488
|
+
`%{title}`), which no linted range contains. When
|
|
489
|
+
`CI_MERGE_REQUEST_SQUASH_ON_MERGE` is `"true"` (GitLab renders
|
|
490
|
+
`squash_on_merge?`, so the project's `always`/`never` option is included),
|
|
491
|
+
the job also lints `CI_MERGE_REQUEST_TITLE`, after stripping GitLab's Draft
|
|
492
|
+
prefixes (`Draft:`, `[Draft]`, `(Draft)`, any case, repeated). An MR that will
|
|
493
|
+
not squash keeps a free-form title, because its commits are what lands. A red
|
|
494
|
+
title blocks the merge, since the project only merges with a green pipeline.
|
|
495
|
+
After fixing a title, run a new pipeline for the MR. When the title is not
|
|
496
|
+
linted the log says why (#1502): not a merge-request pipeline, the variable is
|
|
497
|
+
unset (GitLab did not provide it), or the MR does not squash.
|
|
498
|
+
- **A non-conventional commit that reaches `main` (#1502).** Two cases slip past
|
|
499
|
+
every MR pipeline: the squash box ticked after the last pipeline (the variable
|
|
500
|
+
is fixed when the pipeline is created, and neither ticking squash nor editing
|
|
501
|
+
the title starts a new one), and a squash or merge commit message edited in
|
|
502
|
+
the merge dialog. `main` push pipelines run this job over `before-sha..sha`,
|
|
503
|
+
which contains the squash commit as the merge's second parent (measured
|
|
504
|
+
2026-10-02: job 152074, pipeline 16004, range `84c107a1..ff3e43b5`), so the
|
|
505
|
+
commit is caught after the fact. `main` cannot be force-pushed, so on the
|
|
506
|
+
default branch a red job prints the remedy: revert what landed via an MR
|
|
507
|
+
(`git revert -m 1 <sha>` for a merge commit, `git revert <from>..<sha>` for
|
|
508
|
+
directly pushed commits), then re-land the change from a new branch with a
|
|
509
|
+
conventional message (`git revert --no-commit <the revert commit>`, then
|
|
510
|
+
`git commit -m 'fix: …'`).
|
|
511
|
+
Closing these before the merge was an **owner decision** (project settings,
|
|
512
|
+
not changed by code):
|
|
513
|
+
- `squash_option: never` removes the squash case entirely. Cost: low,
|
|
514
|
+
measured 2026-10-02: 3 of 71 merged MRs were squashed, the newest on
|
|
515
|
+
2026-05-22. **Set by the owner on 2026-10-03** (was `default_off`;
|
|
516
|
+
verified via `glab api projects/:id` at 2026-10-03T06:47Z, #1507 item 5).
|
|
517
|
+
The title lint above stays as the guard should the setting ever change.
|
|
518
|
+
- A push rule (`commit_message_regex`) would reject an edited message at
|
|
519
|
+
merge time, but push rules are not available on this instance's tier
|
|
520
|
+
(the push-rule API answers 404).
|
|
521
|
+
A merge-commit message hand-edited in the dialog stays detectable only after
|
|
522
|
+
the fact, as above.
|
|
523
|
+
- **Suggestion commits (#1477 item 2, owner setting, set).** With
|
|
524
|
+
`suggestion_commit_message` unset, GitLab commits "Apply 1 suggestion(s) to
|
|
525
|
+
1 file(s)", which fails `type-empty` / `subject-empty`. A template that passes
|
|
526
|
+
(measured exit 0 against the CI config, 2026-10-02):
|
|
527
|
+
`chore: Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)`.
|
|
528
|
+
**Set to exactly this template by the owner on 2026-10-03** (project setting
|
|
529
|
+
`suggestion_commit_message`, under the merge-request settings; verified via
|
|
530
|
+
`glab api projects/:id` at 2026-10-03T06:47Z, #1507 item 5). Keep
|
|
531
|
+
`%{file_paths}` out of it if it is ever edited: the template is the header,
|
|
532
|
+
and a few paths break `header-max-length 120`.
|
|
533
|
+
- **Cancelled push pipelines (#1477 item 5, accepted ceiling).** An MR pipeline
|
|
534
|
+
re-lints the whole MR range on every push. A direct push is linted per push
|
|
535
|
+
only (`before-sha..sha`), and every job here is `interruptible` with
|
|
536
|
+
`auto_cancel_pending_pipelines` enabled, so a push pipeline auto-cancelled by
|
|
537
|
+
the next push could leave its commits unlinted if that happened before the
|
|
538
|
+
validate stage finished. Measured: of the last 100 `main` pipelines, 11 were
|
|
539
|
+
cancelled (7 with direct-push commits), and in all 11 every security and
|
|
540
|
+
validate job had already finished (2026-09-29); re-measured 0 of 11 affected
|
|
541
|
+
on 2026-10-02. No CI change: making the job non-interruptible on `main` would
|
|
542
|
+
stop auto-cancel for every merge and cost a full 30–40 minute pipeline each on
|
|
543
|
+
the constrained runner. Remedy if it recurs: lint `main` from the previous
|
|
544
|
+
successful `main` pipeline's SHA (needs a token that can read the pipeline
|
|
545
|
+
list) or from the last release tag, instead of the before-sha, which
|
|
546
|
+
re-covers a cancelled predecessor.
|
|
547
|
+
**Revisit trigger:** a non-conventional commit is found on `main` that no
|
|
548
|
+
pipeline linted because its pipeline was cancelled. Check with
|
|
549
|
+
`npx --no -- commitlint -g commitlint.ci.config.mjs --from <sha> --to origin/main`.
|
|
550
|
+
|
|
551
|
+
Two non-conforming commits reached `main` after `commitlint.config.mjs` was
|
|
552
|
+
added and before this job (`c99f57d9`, a 126-character header; `c5252e68`, a
|
|
553
|
+
`merge:` type). The same command from the commit that added this job shows
|
|
554
|
+
whether anything has slipped through since.
|
|
555
|
+
- Hard-`needs`-ed by `pipeline-gate` and on the shared gate rules, so it runs on
|
|
556
|
+
every non-scheduled pipeline. Behavioural tests: `tests/ci/commitlint.test.mjs`
|
|
557
|
+
(they execute the committed script block against temp git repos).
|
|
558
|
+
|
|
422
559
|
## `pipeline-gate` — the fan-in job
|
|
423
560
|
|
|
424
561
|
The last stage holds one job that depends on every blocking gate. It exists
|
|
@@ -443,10 +580,23 @@ nothing" are different failures:
|
|
|
443
580
|
`allow_failure` job never blocks a dependent job, so `needs:` alone is blind
|
|
444
581
|
to it.
|
|
445
582
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
583
|
+
`coverage` runs only on the default branch (`main`). It is the most expensive
|
|
584
|
+
job and re-runs a suite `test` has already run: across 14 MR pipelines
|
|
585
|
+
(2026-09-27..29) it took 5.0 to 12.9 min, median 6.6 min, 29% of the MR job
|
|
586
|
+
minutes (measured 2026-09-29 13:24 CEST). `pipeline-gate` requires
|
|
587
|
+
`.ci-markers/coverage.ok` when `CI_COMMIT_BRANCH` is the default branch (push,
|
|
588
|
+
web and api pipelines alike); on every MR and branch pipeline it prints
|
|
589
|
+
`coverage: NOT MEASURED here` — absent, but never silently absent.
|
|
590
|
+
|
|
591
|
+
Consequence: an MR no longer gets the thresholds (70/70/70/60, set in the
|
|
592
|
+
vitest configuration) nor a second, instrumented full-suite run; both fire
|
|
593
|
+
after the merge, so a coverage regression turns `main` red, not the MR. The
|
|
594
|
+
MR page also loses its coverage percentage. The second run has caught things
|
|
595
|
+
before: in the failed-job history (measured 2026-09-29, oldest 2026-05-11) 3 of
|
|
596
|
+
14 failed `coverage` jobs were in MR pipelines, each with every `test` shard
|
|
597
|
+
green. Revisit trigger: a `main` coverage failure whose MR pipeline was green,
|
|
598
|
+
or a switch to measuring coverage inside the three `test` shards with
|
|
599
|
+
`vitest --merge-reports`.
|
|
450
600
|
|
|
451
601
|
## Local pre-push gate
|
|
452
602
|
|