session-orchestrator
Kanevry/session-orchestrator/CLAUDE.md
Project-instruction file resolution: this is CLAUDE.md on Claude Code / Cursor IDE; the equivalent file on Codex CLI is AGENTS.md. See skills/_shared/instruction-file-resolution.md. Lean root by design (Anthropic large-codebase best-practice: root = pointers + critical gotchas). Delegated docs: - Install, CLI usage, architecture, component inventory → README.md (full inventory: docs/components.md) - Plugin ↔ private baseline relationship (what degrades without it, the public fallback) → docs/baseline.md - Sub-agent authoring spec (frontmatter, body, sandbox-tier, output-schema) → docs/agent-authoring.md (moved out of agents/ in 4.0.0…
- Reads credentials
- Deletes or force-pushes
# Session Orchestrator Plugin
> Project-instruction file resolution: this is `CLAUDE.md` on Claude Code / Cursor IDE; the equivalent file on Codex CLI is `AGENTS.md`. See [skills/_shared/instruction-file-resolution.md](skills/_shared/instruction-file-resolution.md).
> Lean root by design (Anthropic large-codebase best-practice: root = pointers + critical gotchas). Delegated docs:
> - **Install, CLI usage, architecture, component inventory** → [`README.md`](./README.md) (full inventory: [`docs/components.md`](./docs/components.md))
> - **Plugin ↔ private baseline relationship** (what degrades without it, the public fallback) → [`docs/baseline.md`](./docs/baseline.md)
> - **Sub-agent authoring spec** (frontmatter, body, `sandbox-tier`, `output-schema`) → [`docs/agent-authoring.md`](./docs/agent-authoring.md) (moved out of `agents/` in 4.0.0 — the plugin loader registers every `agents/*.md` by directory convention, and the spec was dispatched as an agent 5× fleet-wide)
> - **Stable product/tech/structure context** → [`.orchestrator/steering/`](./.orchestrator/steering/) (injected at session-start Phase 2.6)
> - **Always-on + path-scoped rules** → [`.claude/rules/`](./.claude/rules/) (per-wave via `rule-loader.mjs`, #336/#694); authoring spec → [`docs/rule-authoring.md`](./docs/rule-authoring.md)
> - **Decisions log** (hand-authored) → [[01-projects/session-orchestrator/decisions]] in the Meta-Vault
> - **Session narrative** (machine-written by session-end Phase 3.7 via `scripts/lib/vault-status/narrative-mirror.mjs`) → `01-projects/session-orchestrator/_session-narrative.md`
> - **Operator tmux side-channel** → `/tmux-layout` (4-pane: STATE.md/CI-watch/events; [ADR-0007](docs/adr/0007-tmux-visualization-substrate.md))
>
> Additive instruction-file layering: this root for the big picture, nested files (`.claude/rules/*.md`) for local conventions; the authoring spec lives in `docs/agent-authoring.md`.
>
> **Root `AGENTS.md` is GENERATED byte-identical from this file** (`node scripts/generate-agents-skills.mjs`) for harnesses that read only `AGENTS.md` (Codex CLI, OpenCode, Kiro, Cursor's AGENTS path). Never edit it; edit here and regenerate. `validate-plugin` fails on drift.
## Current State <!-- consistency:exempt:lean-root-pointer-section -->
State-free by design (see the **Live state is not in this file** gotcha below). The live version, test/coverage counts, backlog, and component inventory live in their SSOTs — never inline here, because they drift faster than this file is edited:
- **Version + test/coverage** → README badges
- **Per-session metrics & history** → [`.orchestrator/metrics/sessions.jsonl`](./.orchestrator/metrics/sessions.jsonl)
- **Decisions** (hand-authored) → [[01-projects/session-orchestrator/decisions]] (Meta-Vault) · **Session narrative** (generated) → `_session-narrative.md` beside it
## Critical Gotchas <!-- consistency:exempt:lean-root-pointer-section -->
These are the non-obvious, mistake-causing facts that must load every session. Everything else is delegated above.
- **CI status is the source of truth at session-start.** Local-only test runs are insufficient evidence of CI green.
Phase 4 of session-start invokes `scripts/lib/ci-status-banner.mjs` via `checkCiStatus({ repoRoot })` to render a 🚨 banner when CI is red on HEAD.
Never claim CI green from `npm test` alone — the 8-pipeline silent regression (2026-05-09 → 2026-05-10, fixed in deep-2) is the cautionary tale. <!-- consistency:exempt:runtime-only -->
- **Destructive-Command Guard is active in main + subagent waves on harnesses with a compatible command hook.** On Claude Code, and through the supported Cursor and Pi bridges, `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json`:
**12 of its 16 rules are `severity: block`, the other 4 only warn** (measured 2026-09-19: `jq '[.rules[].severity]|group_by(.)|map({(.[0]):length})|add' .orchestrator/policy/blocked-commands.json`).
The 16th is `ledger-delete-protected` (#1401): `rm`/`mv`/`unlink` on `.orchestrator/metrics/**` — added after a wave agent deleted a rotated 53.896-line ledger archive that no rule covered, because `rm` existed only as the pattern `rm -rf` and the redirect denylist's `*.jsonl` did not match `events.jsonl.1`.
On Codex, the destructive-command rules are instructions only because no compatible `PreToolUse` command handler is wired. Rule source of truth: [`.claude/rules/parallel-sessions.md`](.claude/rules/parallel-sessions.md) (PSA-003). Per-session bypass via Session Config: `allow-destructive-ops: true` (intentional maintenance only).
- **Session Config below is runtime-critical.** `scripts/parse-config.mjs` parses the `## Session Config` block; `claude-md-drift-check` Check 6 enforces top-level-key parity against `docs/session-config-template.md`. Edit it like code, not prose — a dropped key changes runtime behaviour.
- **Live state is not in this file.** Stack: Node 24+, vitest, ESLint 10 (`npm ci` after clone). Test counts, backlog, version, component inventory drift fast — the SSOT is README badges + `.orchestrator/metrics/sessions.jsonl`. Per-session detail lives in the Meta-Vault decisions log (linked above), not here.
- **`memory.propose` requires `SO_WAVE_AGENT=1`.** `scripts/memory-propose.mjs` exits `3` (`rejected-wrong-context`) unless `process.env.SO_WAVE_AGENT === '1'`.
The wave-executor boilerplate (see `skills/wave-executor/SKILL.md`) sets this env-var for every dispatched agent automatically. Direct invocation from the coordinator thread will always be rejected — use `/evolve` there instead.
Full status dict: `queued` (0), `dry-run-ok` (0, validate-only via `--dry-run` — validates + prints, never writes proposals.jsonl; #741.3), `quota-exceeded` (1), `rejected-low-confidence` (2), `rejected-wrong-context` (3), `error` (4).
See `docs/session-config-reference.md` § Memory Proposals. <!-- consistency:exempt:runtime-only -->
- **Auto-promoted worktree cleanup is Hybrid Pattern (Anthropic-style) — and detection keys on a marker, not on session-id coincidence (#1069).** When a session ran in a sibling worktree created via `enterWorktree()` (Phase 0.5 PROMOTION_OFFER outcome), `/close` Phase 4a detects this via `detectAutoPromotedWorktree()`.
Since #1069 the worktree-promotion is a PROCESS BOUNDARY, not a live migration: the session that ends up RUNNING in the promoted worktree is a brand-new session with its own id (see ADR-0013), so the CURRENT session's id never equals the worktree's directory suffix or branch.
Detection therefore tries the `.orchestrator/promoted-from.json` marker `enterWorktree()` writes at creation time FIRST (`source: 'marker'` — carries the source session id + branch, survives the process boundary), falling back to the legacy `path.basename(repoRoot) === <repo-name>-<sessionId>` basename match (`source: 'basename'`) only for worktrees created before the marker existed.
Clean worktree → auto-remove with WARN. Dirty (uncommitted/untracked/unpushed) → AUQ `[Behalten/Löschen/Manuell]` (`isWorktreeClean()` discounts exactly the marker's own untracked line, never operator work).
The Phase 4a cleanup runs AFTER Phase 4 commit+push, not before — this respects #490 durableCommit ordering so sessions.jsonl + STATE.md are persisted to origin BEFORE worktree-removal.
PSA-003 compliance enforced. Implementation: `skills/session-end/references/phase-4a-worktree-cleanup.md`; process-boundary rationale: `docs/adr/0013-worktree-promotion-process-boundary.md`. <!-- consistency:exempt:runtime-only -->
- **`allowedPaths` is COMPUTED, and the scope declaration has TWO shapes — writing only one is the silent failure (#1020).**
(a) **Per agent**, `<state-dir>/filescopes/wave-<N>/<agent-id>.json`, a JSON array of path STRINGS. This is `$AGENT_FILESCOPE_JSON` — what the `FILE-SCOPE` prompt injection, the Learnings-Index and `--assert-subset` read.
(b) **Per wave**, ONE aggregate sidecar holding an array of `{id, files}` RECORDS, built from every file in (a) plus the coordinator's own. This is the only thing `--assert-disjoint` and `--union` accept: pass them the wave directory and you get `Cannot read --union file`; pass them a single (a)-file and you get `must be a JSON array of {id, files} records`.
`node scripts/materialize-wave-scope.mjs --state-dir <state-dir> --wave <N>` is the canonical writer: it receives that one record array on stdin, writes every (a) file first, then (b) last. Neither is a `$TMPDIR` copy, and neither substitutes for the other.
Skipping (a) degrades the chain to signal-free ALLOW: no `FILE-SCOPE` block reaches the prompt, so `hooks/pre-task-scope-disjoint.mjs` extracts nothing, permits the dispatch, and writes no ledger entry — the absence looks identical to a clean run.
**Measured 2026-08-19:** a coordinator that wrote only (b) ran SIX waves and ~27 dispatches with zero injection; `.orchestrator/wave-dispatch-scopes.json` still carried a `|w2|` entry from two days earlier. Only `wave-scope.json` `allowedPaths` (global Gate 7) was live, which is why blocks still fired and the gap stayed invisible.
The coordinator's OWN direct edits go in a `coordinator.json` under (a) **and must be folded into (b)** — a `coordinator.json` that is never folded in is read by nothing. 2 of the 5 recorded divergences were coordinator-direct edits, which no agent scope file covers by construction.
Mechanism, error-class matrix and named limits: [`docs/scope-collision-guard.md`](docs/scope-collision-guard.md); the authoritative two-stage procedure is `skills/wave-executor/references/wave-loop-scope-manifest.md` § 3.1/3.2. <!-- consistency:exempt:runtime-only -->
- **`vault-dir` resolves HOST-LOCALLY — a hand-run vault probe writes into the operator's REAL vault.** Precedence is `SO_VAULT_DIR` > `owner.yaml` `paths.vault-dir` > the committed `vault-integration.vault-dir` below (`scripts/lib/config/host-paths.mjs`); the literal value in this file LOSES against both.
So a throwaway invocation of a vault writer (`mirrorNarrative()`, `vault-mirror.mjs`, the board writer) with a synthetic `repoRoot` does **not** land in a sandbox — the fake repo path only fakes the SOURCE, never the DESTINATION. A reviewer in this repo walked into exactly that.
Export `SO_VAULT_DIR=<tmpdir>` before ANY manual invocation of vault-writing code. <!-- consistency:exempt:runtime-only -->
## Session Config
persistence: true
enforcement: warn
agents-per-wave: 6 (deep: 18)
waves: 5
recent-commits: 20
test-command: npm test
typecheck-command: npm run typecheck
lint-command: npm run lint
stale-branch-days: 7
plugin-freshness-days: 30
plan-baseline-path: OVERRIDE-IN-owner.yaml # placeholder, NOT a real path — resolve host-locally
plan-prd-location: docs/prd
plan-retro-location: docs/retro
plan-default-visibility: internal
vcs: gitlab
auto-skill-dispatch: false
vault-integration:
enabled: true
vault-dir: ~/Projects/vault
mode: warn
vault-name:
docs-orchestrator:
enabled: false
audiences: [user, dev, vault]
mode: warn
vault-staleness:
enabled: false
thresholds:
top: 30
active: 60
archived: 180
mode: warn
docs-staleness:
enabled: false
thresholds:
living: 90
mode: warn
moc-staleness:
enabled: false
thresholds:
moc: 90
mode: warn
context-coverage:
enabled: false
mode: warn
worktree-orphans:
enabled: false
base-branch: main
mode: warn
drift-check:
enabled: true
mode: warn
check-docs-parity: true
wave-reviewers:
enabled: false
reviewers: []
mode: warn
memory-cleanup-soft-limit: 180
vault-mirror:
quality:
min-narrative-chars: 400
min-confidence: 0.5
memory:
banner:
enabled: true
proposals:
enabled: true
quota-per-wave: 5
confidence-floor: 0.5
auto-dream:
min-confidence: 0.5
cold-start:
enabled: true
nudge-after-hours: 1
silence-after-sessions: 1
state-md-lock:
enabled: true
timeout-ms: 10000
handover-gate:
enabled: true
max-open-questions: 3
issue-budget:
max-per-session: 12
mode: strict
overflow: collect-issue
broken-window-budget:
enabled: false
due-days: 7
slopcheck:
enabled: false
sources: [plan, discovery]
templates-first:
enabled: true
hosts: [github, gitlab]
verification-auto-fix:
enabled: false
max-retries: 2
discovery-validator:
enabled: false
frontend-slop-hook:
enabled: false
loop-guard:
enabled: true
threshold: 3
window: 5
instruction-budget:
enabled: true
ceiling: 480
mode: warn
config-protection:
enabled: true
mode: warn
allow-config-weakening: false
custom-phases:
- name: archive-closed-prds
when: both
command: node scripts/archive-closed-prds.mjs --apply
mode: warn
- name: archive-closed-plans
when: both
command: node scripts/archive-closed-prds.mjs --apply --prd-dir docs/plans --vault-subdir 01-projects/session-orchestrator/plans
mode: warn
remote-hosts:
- alias: m5
roles-allowed: [test, ui, perf]
evolve:
extra-sources: []
dialectic:
cadence: 5
model: haiku
budget-tokens: 32000
eval:
enabled: true
mode: warn
judge: off
report: html
handle:
reconcile:
enabled: false
mode: warn
targets: [repo-local]
rule-expiry-days: null
confidence-floor: 0.5
min-rule-days: 7
min-insight-chars: 24
## Skill Evolution <!-- consistency:exempt:parity-exempt-skill-evolution-block -->
> Opt-in self-evolution autonomy gate (Epic #643). A DISTINCT top-level block from the `evolve:` Session Config key above — `scripts/lib/config/skill-evolution.mjs` parses it independently of the `## Session Config` boundary, so `claude-md-drift-check` Check 6 (session-config-parity) never flags it.
> See `docs/session-config-reference.md` § Skill Evolution for the #652 activation history, the C2/H1-guard detail, and the quadruple-gate + MR-only rules.
skill-evolution:
autonomy: autonomous-gated # off | advisory | autonomous-gated — armed (#652)
evidence-floor: 0.5 # float 0.0..1.0 — min evidence before an autonomous-gated repair acts
judge: off # opt-in session-end LLM-judge (advisory only); default off
## Dispatcher Autonomy <!-- consistency:exempt:parity-exempt-dispatcher-autonomy-block -->
> **Parity-exempt section** (Epic #673, #679). Intentionally OUTSIDE `## Session Config` so `claude-md-drift-check` Check-6 (session-config-parity) does not flag repos that have not adopted this feature — `scripts/lib/config/dispatcher-autonomy.mjs` parses it independently.
> This block's committed presence is itself the never-re-ask marker (session-start Phase 1.1 migration trigger). Effective autonomy resolves host-locally: env > `owner.yaml` > committed > `off`.
> See `docs/session-config-reference.md` § Dispatcher Autonomy for the #681 capture history and the full precedence chain.
dispatcher-autonomy:
autonomy: off # off | advisory | autonomous-gated — default off (fail-closed)
confidence-floor: 0.5 # float 0.0..1.0
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
No one has posted yet. Be the first.

