AGENTS.md · diff

git:20260729.d038039 to git:20260729.22b77fb

1 added, 0 removed. Audit C to C.

# AGENTS.md — sparq
> A README for coding agents. If you are an AI agent working on or with this repo, read this first.
## [CONTEXT-ECONOMY] Worker-tier core available
**Codex worker tier:** The worker-essential rules (sub-agent shared contract, maintenance rule, task tracking, repository hygiene) are now available in **[AGENTS-worker-core.md](AGENTS-worker-core.md)** — a ≤32 KiB subset that fits the codex `project_doc_max_bytes` cap. The full AGENTS.md (this file, ~144 KB) remains the authoritative reference for the complete contract and orchestrator documentation; every rule in the worker core is reachable via linked sections below.
## What sparq is
sparq is a from-scratch **RDF triplestore and SPARQL 1.1 engine in Rust** — dictionary-encoded, six sorted permutation indexes, parallel + streaming execution, RDFS/OWL-RL/N3 inference, an out-of-core (mmap) mode with a compressed on-disk format, a WebAssembly build, and a W3C-conformant HTTP server. The engine is published across several surfaces:
- **Rust crates** (crates.io): `sparq-core`, `sparq-engine` (core), `sparq-cli`, `sparq-server`, plus opt-in capability crates (`sparq-reason`, `sparq-reason-el`, `sparq-shacl`, `sparq-geo`, `sparq-text`, `sparq-rsp`, `sparq-hdt`, `sparq-solid`, `sparq-arrow`, `sparq-mcp`, `sparq-vc`, ...). `sparq-reason-el` is a **separate** opt-in crate (depending on it is the opt-in): an OWL 2 EL consequence-based classifier that computes the **complete** `rdfs:subClassOf` subsumption lattice that OWL 2 RL (`sparq-reason`) is sound but silently incomplete for — see [`skills/inference/SKILL.md`](skills/inference/SKILL.md).
+ - **In-workspace, `publish = false` server estate** (NOT on crates.io): `sparq-lws-core` — an **EXPERIMENTAL** native Solid/LDP (Linked Web Storage) server core imported from [jeswr/solid-server-rs](https://github.com/jeswr/solid-server-rs), SPARQ-authoritative for RDF + WAC, with Solid-OIDC/DPoP auth delegated to the pinned [solid-oidc-verifier](https://github.com/jeswr/solid-oidc-verifier). It does **not** replace the TypeScript prod-solid-server and its default storage is ephemeral. `sparq-lws-wasm` is its opt-in wasm adapter (the local-development host behind `@jeswr/solid-server`), and `sparq-wac-oracle` is the server-independent WAC/ACP decision test-vector corpus. Usage: [`skills/solid-lws-server/SKILL.md`](skills/solid-lws-server/SKILL.md); design decisions: [`research/lws-design-records.md`](research/lws-design-records.md).
- **npm**: `@jeswr/sparq` — RDF/JS-typed API over the wasm build, zero runtime deps;
[GPT-5.6] `@jeswr/solid-server` — loopback-only Solid/LDP development host over the separate
in-memory wasm adapter, with fixed-owner default and opt-in Node-side Solid-OIDC verification.
- **PyPI**: `sparq-rdf` (import name `sparq`) — pyo3/maturin bindings.
Status: experimental research engine; the API is unstable.
## Skills — how to USE sparq from your code
Usage instructions for each public surface are packaged as Agent Skills under [`skills/`](skills/) (the [agentskills.io](https://agentskills.io) open format — `name`/`description` frontmatter + Markdown). Read the one that matches the surface you are integrating:
Read [`skills/SKILL.md`](skills/SKILL.md) first — it is the router skill that lists every surface and points you at the right one. The main entry points:
- [`skills/sparql-query/SKILL.md`](skills/sparql-query/SKILL.md) — run SPARQL from Rust (`sparq-core` + `sparq-engine`).
- [`skills/data-formats/SKILL.md`](skills/data-formats/SKILL.md) — parse/load RDF (Turtle/N-Triples/N-Quads/TriG, HDT) into a Graph.
- [`skills/rdf-wrapper/SKILL.md`](skills/rdf-wrapper/SKILL.md) — traverse RDF as Rust-native focus objects with the opt-in `sparq-wrapper` crate.
- [`skills/cli/SKILL.md`](skills/cli/SKILL.md) — the `sparq` CLI (query, mmap build/query, reason, bench).
- [`skills/http-server/SKILL.md`](skills/http-server/SKILL.md) — the SPARQL 1.1 Protocol HTTP server.
- [`skills/helm-deploy/SKILL.md`](skills/helm-deploy/SKILL.md) — [GPT-5.6] deploy either native server to Kubernetes with the secure-default Helm chart.
- [`skills/javascript-wasm/SKILL.md`](skills/javascript-wasm/SKILL.md) — the `@jeswr/sparq` npm package.
- [`skills/python/SKILL.md`](skills/python/SKILL.md) — the `sparq` Python package.
The capability surfaces (reasoning — RDFS/OWL-RL/N3 in `sparq-reason` plus the opt-in OWL 2 EL classifier in `sparq-reason-el`, both covered by [`skills/inference/SKILL.md`](skills/inference/SKILL.md) — SHACL, full-text, vector, GeoSPARQL, streaming RSP-QL, RDFC-1.0 dataset canonicalization, ZK query proofs, MPC, GenAI retrieval) each have their own `skills/<surface>/SKILL.md` — the router in [`skills/SKILL.md`](skills/SKILL.md) enumerates them.
If your agent runtime supports the Agent Skills standard, these load via progressive disclosure (name+description first, body on demand). If not, just read the SKILL.md files directly.
> Note: `.claude/skills/` (separate tree) holds INTERNAL skills for agents working *on* the engine's source (parsing perf, ZK circuits, etc.), not usage docs. Do not confuse the two.
## Working on this repo (contributor agents)
- Build: `cargo build --workspace`. Test: `cargo test --workspace`.
- Lint is enforcing (CI gates on it): `cargo clippy --workspace --exclude sparq-py --all-targets -- -D warnings` must pass. Run clippy over the **full workspace**, not a single crate — feature unification surfaces lints that an isolated-crate check misses. (`sparq-py` is excluded because it needs the Python/maturin toolchain.)
- **`cargo fmt --all --check` is NOT a gate — it is informational** (`continue-on-error: true` in `ci.yml`'s `clippy (gate) + fmt (non-blocking)` job). The one-time workspace reformat has never been run, so the check fails on files your change did not touch; do not treat that failure as your regression and do not fix it by running `cargo fmt --all` (a workspace-wide reformat is its own reviewed change — see `rustfmt.toml`). Format the code **you** touched, matching the surrounding committed style. The formatter *version* is no longer ambient: `rust-toolchain.toml` pins the channel and ships the `rustfmt` component, so CI and a local checkout run the same rustfmt (this closes the reproducibility half of issue #2360).
- The core crates (`sparq-core`, `sparq-engine`) must stay dependency-free of the opt-in capability crates, and the wasm build must not regress — both are enforced in CI.
- **New capabilities are opt-in by default** (a dedicated crate and/or a cargo feature that is OFF by default), so `sparq-core`/`sparq-engine` stay lean and the lean wasm bundle never grows. **One maintainer-directed exception ([OPUS-4.8] sq-oy1f.4 / sq-oy1f.20, user-prioritised epic [sq-oy1f]):** **JSON-LD is DEFAULT-ON in the native binaries + the Python wheel** — `sparq-cli` and `sparq-server` carry `jsonld` in their `default` feature set (the CLI parses + re-serialises JSON-LD, the server speaks `application/ld+json` in both directions), and `sparq-py` does too (`Graph.load(..., format="jsonld")` + a `.jsonld` path work out of the box; the wheel has no bundle-size floor). It stays toggleable (`--no-default-features` drops the `oxjsonld` parser in all three), and the lean wasm bundle keeps JSON-LD **opt-in** (`sparq-wasm/jsonld`, OFF by default — the `wasm_bundle_bytes` floor is unchanged). The `sparq-core`/`sparq-engine` *library* defaults also stay lean (oxjsonld enters only via the binaries' `jsonld` feature). What is default-on now: **JSON-LD parse + serialise (expanded/flattened/prefix-compacted) + full W3C 1.1 Compaction/Framing + server content-negotiation**; full conneg-conformance ratcheting is on the `sq-oy1f` roadmap.
- **Frontend optional-code policy ([GPT-5.6] sq-mrrn4):** any net-new site/GUI feature that is uncertain-value or rarely used and increases bundle size MUST load through a literal ESM dynamic `import()` only when the user invokes it; a feature flag or conditional render does not keep a static import out of the initial bundle. Use `next/dynamic`/`React.lazy` for components and invocation-path `import()` for libraries/codecs. Classify new frontend dependencies as core/shared or optional in the PR, and extend `site/scripts/check-bundle.mjs` for material optional chunks. See `.claude/skills/frontend-design/SKILL.md` for the decision rule, exceptions, and audit procedure.
- Conformance: the W3C SPARQL, inference, W3C SHACL (core + SPARQL), OGC GeoSPARQL and Solid WAC + ACP suites must stay green and are each **ratcheted** (the committed floor only goes up). All of them are indexed in ONE central scoreboard — `cargo run -p sparq-conformance --bin sparq-conformance-scoreboard` (registry: `crates/sparq-conformance/src/scoreboard.rs`) — so a single artifact reports every suite + its floor + the CI job that gates it. The per-suite detail reports are **generated** by that crate, not committed: the SPARQL report `conformance-report.md` is git-ignored and regenerated locally by `cargo run -p sparq-conformance` (the CI job re-runs it and publishes it as a build artifact); the inference report is committed at [`inference-conformance-report.md`](inference-conformance-report.md), and the SHACL/geo/Solid job scoreboards are emitted the same way. Performance is gated the same way against a best-ever floor (`bench/perf-baseline.json`).
- **Merge discipline:** the gate for landing any change is *full-workspace clippy + `cargo test` + the conformance/perf ratchets*, all green. When work is done in parallel git worktrees, gate and merge **one branch at a time** with a full re-gate between merges; never edit `.beads/` files inside a worktree (it conflicts at merge — `bd export` regenerates the JSONL).
### sparq-substrate (shared eval substrate)
`sparq-substrate` is a **leaf crate** (depends only on `sparq-core`) holding the shared evaluation substrate consumed by **BOTH `sparq-engine` AND the reasoners**, so neither consumer depends on the other and they share one eval body (epic [sq-6tykl]/[sq-qonbz]; design `research/shared-eval-substrate.md`). It holds, behind **default-OFF features**:
- `numeric` — the XSD numeric value tower (`Num`/`Dec` + `as_numeric` classification + the arithmetic ops and XSD lexical helpers), driving the engine's FILTER/BIND/ORDER BY.
- `join` — the **four id-tuple join kernels** (sorted merge-join, radix-partitioned hash-join, index-nested-loop bind-join, leapfrog trie-join/WCOJ) behind a **generic `JoinKeys` descriptor** (the row→key projection + combine layout) and a **generic `Budget`** cooperative-cancel hook.
- `compare` — the **SPARQL term total order** (`compare::compare_terms` — the engine's `compare_values`: error/unbound < blank < IRI < literal < triple, numeric-aware + strict typed/temporal + string fallback + recursive triple-term order) behind a **generic `CompareTerm` trait** the consumer implements for its term type. Drives the engine's `ORDER BY` / sort / `MIN`/`MAX` fallback.
- `rows` — the shared `Row`/`Key`/`Posting` id-tuple vocabulary both kernels operate on.
**INVARIANT (perf-neutrality — enforced):** the hot loops are **MONOMORPHISED** — **NO `Box<dyn>`/`&dyn`/`dyn` trait-object dispatch** on any per-row / per-key-group / per-distinct-value hot loop. Generic type parameters bounded by a trait (`fn merge_join<B: Budget>(…)`) are fine (they monomorphise + inline); a trait *object* inserts a vtable the optimiser cannot inline, which would make the substrate non-zero-overhead for its two consumers and risk regressing the deterministic byte ratchets (`wasm_bundle_bytes`, store/dict bytes). This is enforced structurally by **`scripts/check-no-dyn-dispatch.py`** (the `no-dyn-dispatch (substrate)` gate in `docs-quality.yml` — comment-aware, with a narrow `// perf-neutrality-allow: <reason>` per-line opt-out for a genuinely-cold path). A new hot-path module in the crate must be added to that script's scanned set.
The engine's `compare_values` total order **now lives in `compare` here** (bead [sq-vezew], Phase 4): the ALGORITHM moved as the generic `compare::compare_terms` over a tiny `CompareTerm` trait, and the engine implements that trait for its `Value` (zero-cost wrappers over `value_str` / `as_num` / `value_compare_strict`) and calls the substrate body — so the engine AND the reasoners share ONE total-order body with no `Box<dyn>` on the compare path. The seam deliberately leaves `Value` **engine-resident**: the engine's `Value` enum, its `LitKind` literal-family classifier and `value_compare_strict` typed/temporal comparison ALSO drive the relational `<`/`>`/`=` operators (not just ORDER BY) and are coupled to `oxrdf::Term`, so a wholesale `Value` relocation would be non-perf-neutral and sprawling; moving the algorithm while surfacing the term observations through the trait is the clean perf-neutral seam (mirrors how `join` keeps `Bindings` engine-private behind `JoinKeys`). The genuinely-deferred remainder — should a reasoner ever need the *full* `Value`/`LitKind` value-space (not just the ordering) shared — is captured as a follow-up bead, not faked complete.
#### Substrate boundary: what lives in sparq-substrate vs. what stays engine-private (durable architecture fact)
[HAIKU-4.5] sq-qonbz.7 — the substrate boundary is architecturally explicit: sparq-substrate holds **ONLY** the four generic hot-loop modules (rows / numeric / join—including join::delta / compare) and nothing else. These modules are consumed by BOTH the SPARQL engine and the reasoners, monomorphised and vtable-free.
**In sparq-substrate (generic, consumer-agnostic, monomorphised, shared):**
- `rows` — Row/Key/Posting id-tuple vocabulary.
- `numeric` — XSD numeric value tower + arithmetic ops (monomorphic over the concrete numeric tiers `i64`/`i128`/`f32`/`f64`; `#[inline]` accessors, no `Box<dyn>`/`&dyn`/vtable anywhere).
- `join` — four id-tuple join kernels (merge-join, hash-join, bind-join, trie-join) + `join::delta` (persistent extendable hash table for semi-naive Δ⋈full join). All generic over JoinKeys descriptor and Budget cooperative-cancel hook; no vtable on the hot path.
- `compare` — SPARQL term total order (compare_terms over CompareTerm trait). Generic over the trait; concrete consumer implements it for its term type; no vtable on the per-comparison hot loop.
**In sparq-engine (engine-private, NOT in sparq-substrate — never moved to the shared crate):**
- `Value` enum + `LitKind` literal-family classifier + `value_compare_strict` typed/temporal comparison (drives relational ops, ORDER BY, and all value semantics; coupled to oxrdf::Term).
- `Bindings` struct (engine's result binding representation; the join kernels expose Row/Key instead).
- `LocalVocab` interning (engine-specific; reasoners have their own vocab).
- `QueryBudget` thread-local cancellation (engine-specific; reasoners may supply their own Budget impl).
- `ScanCmp` pushdown filter logic (engine optimizer detail).
- `service.rs` (SPARQL SERVICE federation).
- Serializers and EXISTS/aggregation (engine executor details).
The seam is **clean and intentional**: generic algorithms flow outward to sparq-substrate; engine-private types and optimizations stay inward. This is verified structurally by the perf-neutrality gate (scripts/check-no-dyn-dispatch.py enumerates the four hot-loop modules — rows/numeric/join (including the join::delta submodule)/compare — and fails if any Box<dyn> / &dyn enters a hot path).
## MAINTENANCE RULE (REQUIRED — read before changing any public surface)
**When you change a public API, update the matching skill in the SAME change (same commit/PR).** A "public API" means any of:
- a `pub` item in a crate's public surface (a published crate's exported types, traits, functions, or their signatures);
- a CLI flag, subcommand, or its behavior in `sparq-cli`;
- an HTTP route, query/body parameter, or response shape in `sparq-server`;
- a Python binding (the `sparq` package) or a JS/RDF-JS binding (`@jeswr/sparq`).
Then edit the corresponding `skills/<surface>/SKILL.md` (sparql-query / data-formats / cli / http-server / python / javascript-wasm) so its instructions and examples still compile and run against the new surface. Do not split this across a follow-up PR — a skill that documents a removed flag or a changed signature is worse than no skill. If the change spans surfaces (e.g. a new query option exposed in both the CLI and the HTTP server), update every affected `SKILL.md`. Keep each `SKILL.md` body under ~500 lines; move long flag/route tables and runnable examples into that skill's `references/` and `scripts/`.
If you add a brand-new public surface, add a new `skills/<surface>/` (dir name == the skill's `name` frontmatter) and link it from the list above and from the README.
## STANDING RULE — proceed without waiting for the maintainer's greenlight (2026-06-21)
**Never stall the autonomous loop waiting for the maintainer to greenlight a design or make a decision.**
- **Blocked on greenlighting a DESIGN?** Proceed with implementation *without* the greenlight, then open a GitHub issue (🤖 SPARQ-agent self-id) so he can review and steer *after* it is built. (The same git-native issue channel now also carries *out-of-scope discoveries* — labelled `self-improvement`; see the shared-contract items 12–13.)
- **A DECISION you'd otherwise ask about?** Make the best-judgment choice, **document it** (PR body / bead / issue), and proceed. He corrects later if needed.
- **Drop "needs-user / awaiting greenlight" as a blocking state.** Convert it to "proceeded with default X — see issue #N." The only still-valid blocker is an **external credential/access an agent literally cannot obtain** (OS code-signing certs `sq-v286.8`, npm/PyPI publish tokens, the external cryptographer audit `sq-qhy4`).
- **EXCEPTION — this does NOT override honesty/soundness discipline.** Never label an unaudited ZK/MPC capability "sound/proven" (that is gated on `sq-qhy4`, a credential/access block, not a greenlight). Proceed on the *build*; keep the honest "not-yet-sound / research-grade / not externally audited" labels.
This rule binds the orchestrator, every sub-agent, and the autonomous-scheduler (`.claude/workflows/autonomous-scheduler.js`): impl/verify agents PROCEED on design/decision-blocked beads (best-judgment + document + a short feedback issue) rather than parking them for the maintainer. The reusable agent-facing procedure for this rule — the four steps (best-judgment choice → PR-body note → one-line bead note → SPARQ-self-id issue) plus the two hard exceptions — is the **`proceed-and-document` skill** (`.claude/skills/proceed-and-document/SKILL.md`); the scheduler's `implPrompt()` + Frontier brief and any hand-dispatched agent brief reference it **by name** instead of re-stating it, so the rule is inherited identically everywhere. This section stays the **authority**; the skill carries it.
## Fix a shared issue everywhere it applies — cross-crate/cross-surface parity
<!-- [OPUS-4.8] charter cross-poll from PSS #173 -->
When a bug or review finding describes a **class** of problem affecting more than one place — a parser edge case in Turtle that also hits TriG, an operator bug whose sibling operators share a code path, a `pub`-surface footgun repeated across the CLI / HTTP / Python / JS-WASM bindings — it must **eventually be addressed in every instance, not patched only where it surfaced.** **Prefer fixing the pattern ONCE in the shared place** (the common code path, or a `sparq-core` helper) so all surfaces inherit it; if a shared fix isn't feasible, fix each instance to the **same spec** and file a bead for the consolidation. Either way, when you fix one instance, **file a bead** (see *Task tracking* below) covering the other affected crates/surfaces so the parity work is tracked, not lost. This is the cross-crate analogue of the differential-fuzz philosophy (a finding in one path implies checking the others — see the *Post-batch re-evaluation checklist*).
## Task tracking — beads, not markdown TODOs
This repo tracks work in **beads** (`bd`, a git-native dependency-graph issue tracker; the committed source-of-record is `.beads/issues.jsonl`). Rules for any agent working here:
- **Do NOT write TODO/FIXME into markdown or leave them in `TODO.md` files.** Capture future work as a bead instead.
- **When you identify follow-up/future work, create a bead for it** (from the repo root, with `bd` on your PATH):
```sh
bd create "<imperative title>" -t <task|bug|feature|chore|spike> -p <0-4> -l <area:crate,kind:...> -d "<what + why + where>"
```
This writes the shared Dolt DB (exclusive-lock-serialized — safe across parallel agents). For the rationale behind a *deferred* task, put it in the bead's `-d` description or `--design` field so the bead is self-contained. **Never edit `.beads/issues.jsonl` (or any `.beads/` file) by hand** — it causes merge conflicts; `bd export` regenerates it.
- Run `bd ready` to see unblocked work; close with `bd close <id>`.
**Beads session-context hook.** `.claude/settings.json` registers a `SessionStart` hook (`scripts/bd-session-context.sh`) that injects a concise bead snapshot — the `bd ready` list + open count — at the start of every Claude Code session, so a new or post-compaction session recovers the task state automatically. It's a graceful no-op when `bd` isn't installed or `.beads/` is absent. We deliberately do **not** use beads' own `bd setup claude` / `bd prime` injection: that path ships generic rules ("do not use TaskCreate / MEMORY.md") that conflict with this harness's task tracker and auto-memory, and it duplicates the beads guidance already in this file. The hook ships only the useful, non-conflicting part. (A committed `.claude/settings.json` hook takes effect on the *next* session start / `/hooks` reload, not the current session.)
## Orchestration — delegate to sub-agents + run a continuous bead loop
If you are an ORCHESTRATING agent (driving multi-step work on this repo), three standing rules:
1. **Delegate every substantive task to a sub-agent in an isolated git worktree** — implementation, research, test/triage, merge-conflict resolution, doc writing, AND the heavy verification/gating of a change. The orchestrator keeps only cheap glue: sequencing, `git merge`/`push`, worktree add/remove, bead bookkeeping (`bd close`/`export`), and reading one-line gate results. Do NOT run builds, toolchain installs, CI-log spelunking, or end-to-end gate runs in the main thread when an agent can — keep the orchestrator context small. Parallelise independent agents; serialise only CPU-heavy wall-clock *measurements* (those need a quiet box).
2. **Continuous loop:** iteratively `bd ready` → spin up sub-agents (parallel, worktree-isolated, smallest context-independent briefs) to address the ready beads → gate + merge one at a time → re-check `bd ready` → repeat. Don't wait to be prompted bead-by-bead. Sequence beads that touch the same files; respect dependency edges (`bd dep`).
3. **Never idle while CI runs — keep MULTIPLE agents working in parallel.** <!-- [OPUS-4.8] consistent parallelisation --> The merge train drains *one branch at a time* via background CI watchers — automated jobs that poll each open PR, and once `ci-summary` is green and all review threads are resolved, squash-merge it, delete the branch, and watch main CI; that draining is the watchers' job, not the orchestrator's. While it drains, the orchestrator's job is to **drive new work** — fan out worktree sub-agents (and research/understand/review workflows) across independent, non-conflicting beads so progress never blocks on a green run. Partition by crate so concurrent agents touch distinct surfaces — `sparq-zk`, `sparq-mpc`, `bench/`, `research/`, engine internals can all run concurrently — and **reserve `sparq-server` for ONE server-touching branch at a time** (it is the contended surface). Lean hardest on multi-agent fan-out for the research / understand / review phases, where there is no merge contention at all. The CI watchers drive merges; the orchestrator drives new work.
Each sub-agent brief must: work in its own worktree, NOT push/merge (the orchestrator does), gate in-worktree (scope tests to affected crates; the orchestrator does the authoritative full-workspace gate at merge), create beads for any discovered work (`bd create`, never edit `.beads/`), and report a concise result. See the per-batch re-evaluation checklist below to decide which gates a given change must re-run. **The standing rules every sub-agent inherits are the *shared contract* below — a task brief states only what is task-specific.**
### The sub-agent shared contract — write TERSE task-only briefs
<!-- [OPUS-4.8] sq-or5m: single-source the sub-agent contract so role prompts carry it and task briefs stay terse (brief-discipline; design: research/agent-efficiency-tooling.md §2/§6/§10). -->
This is the **single source of truth for the standing rules every dispatched sub-agent follows** — the rules that used to be re-typed into every task brief. The role-agent system prompts under [`.claude/agents/`](.claude/agents/) each **carry this contract** and point here for the long form — the mutating impl/execution roles (`sparq-rust-feature`, `sparq-rust-impl`, `sparq-perf-engineer`, `sparq-site`, `sparq-ci-infra`, `sparq-docs`, `sparq-researcher`, `sparq-architect`, `sparq-merge-fixer`, `sparq-bench-ec2`, `sparq-issue-sweeper`) and the read-only verify/review roles (`sparq-verify-mechanical`, `sparq-reviewer`, `sparq-perf-reviewer`, plus the placement/monitor helpers). So a dispatcher does **not** repeat it: a task brief states the **task only** (the bead, the target crate/surface, any task-specific constraint) and otherwise says *"follow the shared contract."* If a role prompt and this section ever disagree, **this section wins** — fix the role prompt. <!-- [FABLE-5] roster refreshed: added perf-engineer (sq-7d3dj), bench-ec2, issue-sweeper (sq-x6pzo); merge mechanics now the GitHub merge queue (plain `--auto`, no `--squash`). -->
The contract (every mutating sub-agent, every task):
1. **Worktree + branch.** Run in your OWN isolated git worktree (`isolation: "worktree"`); never `cd` into the shared main checkout (`/home/ubuntu/sparq`). Branch from current main — `git fetch origin main && git checkout -b <kind>-<topic> origin/main` — and run all git from your cwd. (Full rationale: *Worktree isolation* below.)
2. **Staging.** Stage ONLY the files you change, by explicit path. NEVER `git add -A`; NEVER stage `.beads/` (re-export is the orchestrator's job on its own branch — *Merge discipline*); revert any beads churn that appears in your tree.
3. **No push / no merge.** Commit in-worktree and report; the **orchestrator** pushes, opens the PR, and merges. (Exception: `sparq-merge-fixer` pushes to an *existing* PR branch — see its prompt.)
4. **Gate in-worktree (HARD — never weaken a gate to pass).** Run your role's gates scoped to the affected crates/files; the orchestrator runs the authoritative full-workspace gate at merge. If a gate fails on real content, fix the content or report it as an honest finding — never disable, regex-weaken, or blanket-exclude a gate to go green.
5. **Model provenance (model-aware, derived from harness runtime).** Your model identity is supplied by the harness via the `--model` flag; derive the inline marker + `Co-Authored-By` trailer from THAT, never from the agent brief or your own assumptions. Stamp the model that ACTUALLY authored the change: an inline `[MODEL]` marker on new code/notes **+** a `Co-Authored-By: Claude MODEL <noreply@anthropic.com>` commit trailer (`MODEL` is the placeholder; the angle brackets around `noreply@anthropic.com` are literal git-trailer syntax). Use `[OPUS-5]` + `Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>` when running as **Opus 5** (`claude-opus-5`) — the primary top tier, replacing both the Fable 5 and Opus 4.8 heads (maintainer directive 2026-07-24); use `[FABLE-5]` / `[OPUS-4.8]` + the matching `Co-Authored-By` trailer when a session runs DOWNGRADED (Opus 5 unavailable — e.g. a Fable 5 or Opus 4.8 session), which flags the work for later re-review under Opus 5 (*Model provenance*); and `[SONNET-4.6]` / `[HAIKU-4.5]` + the matching `Co-Authored-By: Claude <that model>` when a cheap tier runs as that model (the `fable-architect-drain` workflow carries the per-tier marker/trailer table as the canonical reference). This keeps multi-model diffs from being mislabelled and lets each model's work be re-reviewed per-model. **Agent briefs MUST NOT hard-code a specific model marker** — they MUST use model-aware wording that instructs the agent to derive markers from the harness's actual runtime model. Existing `[OPUS-4.8]` stamps are accurate HISTORY — never rewrite them (*Model provenance*).
6. **Self-identify (🤖) in every comment.** Open a PR vs `main` whose body starts with the SPARQ-agent blockquote; begin every issue/PR/comment you author with it (*Cross-agent self-identification*). PRs default to the review queue — arm with plain **`gh pr merge <n> --auto`** (the repo now uses a GitHub **merge queue**, which CHOOSES the merge strategy, so **`--squash` is REJECTED**; `"already queued"` is success; after arming, verify ~20s later that `autoMergeRequest` latched or the PR is in the merge queue and retry once if neither — *Arming model*) <!-- [FABLE-5] merge-queue mechanics; verified via graphql mergeQueue(branch:"main") -->, only when the brief says so, and **never arm a STACKED PR whose base ≠ `main`** (it merges into the wrong base + the content misses `main`; retarget base to `main` first — *Arming model*, sq-u59rq).
7. **Honesty gates that bite — keep mentions safe.** The **privacy-claims** gate is LIVE: never write an unqualified ZK/MPC privacy/soundness claim (the v1 ZK verifier is remediated + internally re-audited but **external accredited-cryptographer sign-off is pending**, `sq-qhy4`, and `sparq-mpc` is honest-majority semi-honest only) — hedge/negate the wording or add an inline `privacy-claims-allow: <why>` marker (*outward ZK/MPC claim* row). **No hard-coded performance numbers** in markdown, and **work-box/EC2 timings are NON-canonical** (*No hard-coded performance numbers*; *Where this runs*). The **typos** gate flags ordinary doc words — reword `DELETEd`/`DROPped`/`invokable`/`ANDed` (e.g. "data removed by DELETE/DROP", "invocable", "AND-combined").
8. **Honest scoping, no empty PRs.** Be non-sycophantic: if the bead's premise is wrong, the thing already exists, or a claim lacks evidence, say so plainly — don't fabricate work, tests, or numbers, and don't open a no-op PR. If the bead is too large for one sound PR, ship a correct self-contained slice and capture the remainder. Capture genuinely-new discovered work as a clear LIST in your report (`bd` is not on PATH in a worktree — the orchestrator beads it) — that LIST is for *planned* follow-up the orchestrator will bead; an OUT-OF-SCOPE *discovery* (bug/tech-debt/doc-drift/footgun/better-approach) you instead self-file as a `self-improvement`-labelled GitHub issue (item 12).
9. **Maintenance rule.** When your change touches a public API (`pub` item / CLI flag / HTTP route / Py/JS binding) or a config key, update the matching `skills/<surface>/SKILL.md` (and crate README) in the SAME change (*MAINTENANCE RULE*).
10. **Heartbeat.** Print to stdout at least once per minute during long cargo/npm runs (a watchdog reaps silent agents at ~600 s).
11. **A permission denial is FINAL — never try to work around it, and `.claude/agents/*.md` is a PROTECTED surface.** <!-- [OPUS-4.8] sq-4fj0r: an auto-mode-blocked Edit was retried via a Python heredoc to circumvent the denial; the agent was KILLED. A denial is a hard stop, not an obstacle to route around. --> If the harness (the auto-mode classifier, a permission prompt, a `PreToolUse` hook) **denies** a tool call, that decision is final: do **NOT** retry the same change through a *different* tool to evade the block — no `python`/`node` heredoc, no `sed -i`/`awk`/`tee`/`>>`-redirect, no `cat <<EOF`, no `git apply`/`patch`. Trying to bypass a denial is itself a violation and **gets the agent killed** (observed 2026-06-21). **Surface the denial instead** — report it as an honest finding (what was blocked, why you believe it was needed) and stop that line of work. In particular, **`.claude/agents/*.md` (the role-agent configs) is a PROTECTED surface**: agent-config self-modification is **blocked by design** (an agent must not rewrite its own / a sibling's operating rules), so any task that genuinely needs a role config edited is **`needs:user`** — capture it as a bead/finding for the maintainer to apply, never attempt the edit yourself. **Every agent brief MUST use model-aware wording per item 5** (derive markers from the harness's `--model` flag, never hard-code `[OPUS-4.8]` or model-specific trailers). (This guardrail firing correctly is a *positive* datapoint, not a bug — do not "fix" it by routing around it.)
12. **Out-of-scope discoveries → a self-filed GitHub issue (not an inline fix, not a bead).** <!-- [OPUS-4.8] self-improvement discovery channel; authority: research/agent-observability-and-self-improvement.md --> When you spot something that should change but is OUTSIDE your task scope (a latent bug, tech-debt, doc drift, a footgun, a better approach), do NOT fix it in this PR — open a GitHub issue with `gh issue create --label self-improvement`, body led by the `> 🤖 SPARQ agent — <one line>` self-ID blockquote and one line of what/where/why, so the self-improvement triage lane actions it. Dedupe first (`gh issue list --state open --label self-improvement --search "<keywords>"`) and file ONLY genuine, actionable, out-of-scope findings — never a nit you could fix inline or a subjective style preference (SPAM guard). The self-filed issue is the git-native channel for *newly-discovered* work; `bd`/beads remain the *planned* task graph the orchestrator owns — you still report planned follow-ups as a LIST for the orchestrator to bead, but you self-file out-of-scope discoveries as an issue yourself.
13. **Never read agent transcripts / logs.** <!-- [OPUS-4.8] no-read-logs rule; see research/agent-observability-and-self-improvement.md --> Do NOT `Read`/`cat`/`grep`/`ast-grep` the ephemeral subagent transcripts (`/tmp/claude-*/**/tasks/*.output`), the `agent-logs` branch, or any saved transcript — a full transcript is a guaranteed context blowout and these are write-only from your side. If a log genuinely must be inspected, that is the job of the ONE explicitly-tasked debug/self-improvement agent, never a side-quest in an unrelated task. Durable transcripts are appended out-of-tree by `scripts/save-agent-log.sh` (orphan `agent-logs` branch) / uploaded as an Actions artifact — carry a one-line LINK, never the body.
14. **Pre-flight your own diff before reporting done.** <!-- [OPUS-5] --> Run *AUTHOR pre-flight* (§ under *Review lessons*) against your own change — the 12 checks that every first-review failure on 2026-07-27/28 came from. It is the author's half of the review-lessons list and is defined **once**, there; do not restate it in a brief. **What actually delivers it is `CLAUDE.md` → this file auto-loading, not the role-brief pointers** — of the 20 briefs in `.claude/agents/`, 12 carry the general "follow the shared contract" reference whose catch-all picks up a new item, and 8 (including three *authoring* roles: `compliance-engineer`, `compliance-auditor`, `compliance-orchestration`) cite the contract only through an HTML comment scoped to *items 12–13*, which does not. So do not rely on a brief pointer to propagate a contract change on this repo; the auto-load is the load-bearing path.
#### Brief discipline + cache hygiene (why terse briefs, and how to keep the cache warm)
Terse, contract-leaning briefs are not just tidy — they are the cheap, measured lever for cutting parallel-agent token/$ cost (the rationale and the cost model are in [`research/agent-efficiency-tooling.md`](research/agent-efficiency-tooling.md) §2/§6/§10; a prior independent study on a similar `AGENTS.md` consolidation reported a meaningful output-token + runtime reduction — that figure is **someone else's measurement, not a canonical sparq metric**, and is gated on our own Phase-1 before/after telemetry). Standing dispatch conventions:
- **Agent briefs MUST be model-aware (§ PROTECTED surface rule 11).** Every brief in `.claude/agents/*.md` MUST NOT hard-code a specific model marker or Co-Authored-By trailer. Instead, every brief MUST use wording that instructs the agent to derive its model identity from the harness's `--model` flag at dispatch time. The `fable-architect-drain` workflow carries the canonical per-tier marker/trailer table — briefs reference it, never replicate it. This ensures that a worker executing with the actual model (e.g., a downgraded Fable 5 session) produces correct attribution (e.g., `[FABLE-5]`, flagged for re-review under the Opus 5 primary), not false markers (e.g., `[SONNET-4.6]`) left over from an earlier brief generation.
- **State the task, not the contract.** The bead, the target crate/surface, and any task-specific constraint — then *"follow the shared contract"*. Re-typing worktree/staging/PR/self-ID/gate boilerplate into every brief wastes the tokens this section exists to save.
- **Keep the invariant prefix STABLE within a session.** `AGENTS.md` / `CLAUDE.md` and the role prompts sit in the cached prefix; front-load the invariant, put per-task variation last. Editing `AGENTS.md` or a role prompt mid-session does not take effect until `/clear`/`/compact`/restart **and** does not invalidate the running cache — so a contract/role-prompt edit is a *next-session* change (that is fine; it is why this work is doc/config-only).
- **Pin model + effort at session start.** Each mid-task model/effort switch is a full cache rebuild — avoid it.
- **Read-only agents may share the main checkout for a warm cache.** Worktrees miss each other's cache (per-directory scope), and each mutating agent rightly pays a cold first turn (*Worktree isolation* is non-negotiable for writers). But read-only search/analysis/review agents are *permitted* to share the main checkout (same rule) — co-locating serial read-only work there rides a warm cache instead of cutting a fresh cold worktree.
- **Defer MCP tool loading.** Prefer Tool Search / deferred tool definitions so MCP schemas sit behind the cache breakpoint rather than re-billing the prefix every turn.
### Worktree isolation — every MUTATING agent gets its own worktree+branch
<!-- [OPUS-4.8] worktree-isolation race rule -->
Rule #1 says "isolated git worktree"; this is the non-negotiable mechanics. **Any sub-agent that WRITES files, runs `git checkout -b`, or commits MUST work in an isolated git worktree** — give it `isolation: "worktree"` on the Agent tool, or `git worktree add` its own directory + branch — **NEVER the shared main checkout.** Read-only agents (search, analysis, review) may share the main checkout.
**This is a MANDATORY clause in EVERY sub-agent brief that does branch/PR work** <!-- [OPUS-4.8] worktree-isolation mandatory-in-brief --> — spell it out (or rely on the shared-contract reference, which carries it): the agent operates in its **own** isolated worktree+branch and **MUST NOT run `git checkout` / branch-switch on the shared `/home/ubuntu/sparq` main checkout.** Read-only investigation on the shared checkout is fine (`git grep`, `git log`, reading files); **branch switching is not** — it clobbers concurrent agents' uncommitted working-tree changes.
Why this is mandatory, not advisory: a git working tree has **one** branch, index, and working directory. Two mutating agents on the **same** checkout therefore race — one agent's `git checkout -b` switches the branch out from under the other, and uncommitted edits leak onto the wrong branch. **Observed 2026-06-20: two agents lost uncommitted work this way; one had to recover from a worktree.** A separate worktree gives each agent its own branch + index + working dir, so they cannot collide.
The **orchestrator keeps the main checkout for itself** — it is single-threaded glue: `bd` operations (the Dolt DB is branch-independent, so `bd` is safe from the main checkout regardless of which branch is out), bead re-export on a dedicated `chore-beads-resync-*` branch, and PR review/merge. Keep `.beads/*` and otherwise-unrelated files **out of feature PRs** — a `bd export` re-export lands on its own `chore-beads-resync-*` branch, never folded into a feature branch (it conflicts at merge; see *Merge discipline*).
### Worktree lifecycle — remove every worktree the moment its task is done
<!-- [OPUS-4.8] charter cross-poll from PSS: worktree disk hygiene -->
Worktrees and their build artifacts (`target/`) are a large disk sink and accumulate fast. Standing requirements (cross-pollinated from the PSS sibling charter, adapted to Rust):
- **Remove every worktree the moment its task is done** — once its branch has merged (or its work is captured/abandoned), `git worktree remove --force <path>`. The **branch persists in `.git`**, so removal loses nothing; only the working copy + its `target/` go. The orchestrator owns this — remove the worktree in the same step that closes the bead / lands the merge. Don't leave worktrees lying around "in case."
- **Don't spawn a worktree you don't need.** Read-only or single-stream work uses the main checkout (per the isolation rule above); a worktree is justified only for *concurrent* mutating work. Reuse one scratch worktree for serial tasks rather than churning fresh ones.
- **Periodic sweep:** `git worktree prune` + remove stale worktrees; if disk is tight this is the first lever (before launching EC2 — see *Maximise parallelism* — or asking the user). Safe to delete: `target/` dirs, and the *git-ignored* benchmark outputs (per `.gitignore`: `bench/native-qlever/`, `bench/competitor-results/`) plus generated datasets, which suites write **outside the tree** — e.g. `bench/bsbm/gen.sh` defaults its output to `/tmp/bsbm/…` — and are regenerable. But **most of `bench/` is tracked** (~300 files): generators/runners (`gen.sh`/`run.sh`), queries (`*.rq`), expected results (`*.tsv`) and baselines like `bench/perf-baseline.json` are committed — never delete tracked bench assets, scripts, or `.gitignore`. When in doubt, `git ls-files bench/` shows what's tracked.
## Measured agent operating configuration — how to spend tokens (each rule is MEASURED)
<!-- [OPUS-4.8] agent-effectiveness program. Standing operating rules distilled from the RUN agent-efficiency experiments. The EVIDENCE — tables, method, caveats — lives in the sanctioned measurement records cited inline; per the no-perf-numbers house rule NO figure is restated here. Each rule below is backed by a real experiment, not intuition. -->
These are not style preferences — each is a rule the **measured** experiments support. The figures stay in the sanctioned records (`bench/` is the only home for measured numbers, exempt from `check-no-perf-numbers.py`); cite them, never restate them in this file.
1. **Project-knowledge question → query the PKG, don't read whole docs.** When the question is "what does the repo *say* about X / what is the status or provenance of Y / what depends on (or is blocked by) bead Z" — i.e. a sourced, answer-sized fact that lives in `AGENTS.md`, a `SKILL.md`, or the `bd` backlog — **DELEGATE it as a natural-language tool call to a cheap-model (`model:haiku`) sub-agent running the [`query-pkg`](.claude/skills/query-pkg/SKILL.md) flow** (NL → SPARQL → run → NL), which returns the answer + the **exact executed SPARQL** + per-row provenance/confidence. This is the cheap path **at equal answer quality** — measured in the 3-arm A/B in [`bench/pkg-dogfood/RESULTS.md`](bench/pkg-dogfood/RESULTS.md) (the cheap-model NL-tool arm beats both "Opus reads the docs" and "Opus runs `pkg-query` itself" on model-price-weighted \$). Two load-bearing rules go with it:
- **Abstain → fall back.** The PKG is a head slice, so it **safely abstains** when it cannot answer (an empty result / `NOT_IN_PKG` — the honest "not in the slice / none outstanding" answer, *not* a guess). On an abstain, **fall back to reading the docs/code** for that question. The win is scoped to PKG-answerable questions **by construction** (the boundary the RESULTS.md caveats make explicit); a question whose fact is outside the slice forces the fallback on every arm.
- **Escalate a tier only when accuracy is critical.** Haiku is the default; escalate to a Sonnet/Opus `pkg-query` (arm B — still cheaper than read-docs) only for an accuracy-critical lookup where a cheap-model miss would be costly. Verify the returned SPARQL matches the question before trusting the answer (the soundness echo); if it does not, re-ask or fall back.
2. **`ast-grep` + outline: split the verdict by the SHAPE of the task — scoped lookup → just `Read`; whole-file shape or a cross-file structural edit/codemod over a large file → build a compacted-AST skeleton FIRST (measured saver).** Two firm real-token A/Bs settle this, in opposite directions, on two different question classes — cite them, never restate the figures:
- **Scoped code-structure LOOKUP → do NOT go structural-first; just `Read` the located span.** A firm A/B (`bench/pkg-dogfood/RESULTS-astgrep.md`) found that going *outline / `ast-grep`-first* on a "where / how is X" lookup was **MORE expensive end-to-end** than a scoped `Read`, across every question kind — the structural-tool install + the queries + the verification reads cost more than just reading the located span, and B beat A on only a minority of tasks. For a lookup, `ast-grep`/outline earn their place on **correctness/completeness**, not cost: reach for [`ast-grep`](.claude/skills/ast-grep/SKILL.md) (impl / call-pattern / code-shape / codemod) and the LSP/outline recipes when you must enumerate **all** impls / **all** call sites where a `Read`/`grep` might silently miss one, or to express a shape a line-regex cannot. It skips the same token inside comments/strings and matches shapes; in that A/B it bought a small quality nudge on call-site completeness, **not** a raw token cut.
- **Whole-file-SHAPE understanding, or a structural edit / codemod over a LARGE file → generate a compacted-AST skeleton first and work over it (measured token-saver).** A second firm A/B (`bench/ast-compact/RESULTS.md`) measured the maintainer's lever — produce a **compacted-AST representation** (a one-line-per-item structural skeleton; the `bench/ast-compact/compact_ast.sh` dump) that the agent **works over and manipulates**, reading raw bytes only for spans the skeleton can't resolve — against the plain `Grep`/`Read` baseline on **whole-file-understanding and structural-edit/codemod planning over large Rust files**. On that class the compacted-AST-first arm was **cheaper at equal quality on the large majority of tasks**, and the win **grows with file size / structural breadth** (summarise a multi-thousand-line file; add an enum variant with an exhaustive-match audit; change a trait signature across a crate). This is the **opposite direction** to the lookup verdict and does not contradict it — the file's *bytes* dwarf its *skeleton* only when the unit of work is the whole shape.
- **The decision rule that survives both A/Bs:** *scoped lookup answerable by a narrow read → just `Read`* (do not pay the structural-view setup tax — both A/Bs show it slightly **hurts** on a small, already-narrow task); *whole-file shape, or a cross-file structural edit/codemod over a large file → build the compacted-AST skeleton first.* (The earlier narrow "outline only a very large single file's skeleton to locate a span" lever is the lookup-side floor of this same rule.)
3. **Brief discipline + prompt-cache hygiene are the confirmed free levers.** Terse, task-only briefs that lean on the *sub-agent shared contract* (above) plus keeping the cached prefix warm are the cheapest, highest-ROI cost lever for parallel agents — the cost model and the `AGENTS.md`-consolidation study are in [`research/agent-efficiency-tooling.md`](research/agent-efficiency-tooling.md) (the sq-or5m single-source-the-contract design). Concretely: state the task only and say *"follow the shared contract"*; keep the invariant prefix stable within a session; don't cut a fresh worktree (a cold cache) or switch model/effort mid-task when you don't have to; let read-only agents share the warm main checkout. The mechanics live in the *Brief discipline + cache hygiene* subsection above; this entry records that they are **measured-backed**, not just tidy.
## Post-batch re-evaluation checklist — what to re-run after a change
**Run `python3 scripts/preflight.py` first.** [OPUS-5] It is the executable form of the
mechanical half of this table: one diff-scoped command that runs G1 + G2 + G6 +
`check-no-perf-numbers.py` + `check-readme-template.py` + `check-privacy-claims.sh`
against YOUR diff, plus a `guard-untested` check, and prints the two obligations no
script can decide (mutate your headline guard; read your own prose against your own
diff). It exists because a census of the 831 review verdicts on the registry `ledger`
branch found that 20 of the 317 blocking round-1 findings were violations of rules
this repo ALREADY OWNS A SCRIPT FOR — and none of those scripts was named in any
worker brief, so the gates only ever fired post-PR. Running them earlier lowers no
bar; every one of them already blocks the merge.
After a batch of changes, re-run only the evaluations whose inputs changed — on top of the base gate, which is always required. The base gate is full-workspace `clippy -D warnings` **plus full-workspace `cargo test`**: a sub-agent may scope its in-worktree test run to the affected crates for speed, but the orchestrator's authoritative pre-merge gate runs `cargo test` across the **whole workspace** (feature-unification and cross-crate regressions only surface workspace-wide). The gating `clippy (gate)` lane **also runs `cargo doc --workspace --no-deps --all-features` with `RUSTDOCFLAGS="-D warnings"`** (bundled into that lint job), so a feature-gated rustdoc link breakage — a public doc-comment that `[link]`s to a private/`pub(crate)` item inside a default-OFF feature module — is a **gate failure**, even though `cargo clippy` alone never surfaces it. Run that `cargo doc --all-features` pass in-worktree before opening a PR (it has bitten 4 feature PRs: #926/#936/#950/#954; it is the rustdoc half of the lint job, codified in the rust-feature/merge-fixer agent gate checklists). Map change → evaluation.
<!-- [OPUS-4.8] sq-ncvq.10: the "Enforced by" column codifies the §3 rule taxonomy of research/maintenance-flow-on-automation-design.md, so this prose index and the automation cannot silently diverge (the table's own "keep in sync" note now has teeth). -->
The **"Enforced by"** column names what *catches* a missed follow-up, so prose and automation cannot drift apart. Codes: **`Gn`** — a proactive merge-time gate (`scripts/gate-*.py` / `check-*.py`, wired in `.github/workflows/flow-on-gates.yml`, picked up by `ci-summary`); **`flow-on:<rule-id>`** — a reactive bead minted on PR-merge by `scripts/flow-on.py` from a `scripts/flow-on-rules.toml` rule (for follow-ups produced out-of-band, un-gateable); **`E`** — already enforced by an existing ratchet/test/lane (named in the Re-run cell); **`norm`** — honour-system prose with no machine gate (the row's own discipline). A row can carry more than one (e.g. `E` + `flow-on:` for the un-gateable remainder). The gates: **G1** new-crate-completeness (`gate-new-crate.py`), **G2** public-api→skill (`gate-api-skill.py`), **G3** new-bench→registry+dashboard (`check-new-bench-registered.py`), **G5** zk-circuit→gate-count snapshot (`crates/sparq-zk-compose/tests/gate_count.rs::snapshot_covers_top_level_circuits`), **G6** new-config/flag→docs (`check-config-documented.py`). (G4 new-unsafe→justification is **subsumed** — the `unsafe-gate.py` count ratchet + the `clippy::undocumented_unsafe_blocks` lint + the Miri lane already cover it, so there is no separate G4 script.)
| If the change touches… | Re-run | Enforced by |
|---|---|---|
| a parser (turtle/nt/nq/trig, `sparq-core` parse, `spargebra`) | W3C SPARQL + rdf-turtle conformance; the chunked-vs-serial parser oracle; `sparq-bench fuzz` (differential oracle); the **`fuzz` lane** (`.github/workflows/fuzz.yml`) — coverage-guided cargo-fuzz/libFuzzer targets `parse_rdf_str` / `load_reader_parallel` / `parse_sparql` over hostile bytes (T-PARSE-FUZZ; nightly toolchain). **Heavy-lane placement (sq-6vshe.6):** the per-PR / merge_group leg is now a DETERMINISTIC corpus-replay (`-runs=0` over the committed seeds + cached corpus — a reintroduced known crasher replays RED in seconds, no randomized-search pole); randomized fuzzing is the FULL form on push-to-main + nightly (with a `fuzz-full` PR label to opt a fuzz-sensitive PR back into per-PR randomized search), and a randomized-run finding auto-files a P1 bead via the demotion protocol (`scripts/ci-file-demoted-lane-failure.py`). Locally, replay: `cd fuzz && cargo +nightly fuzz run <target> corpus/<target> seeds/<target> -- -runs=0`; randomized: `… -- -max_total_time=15`. | **E** (conformance ratchets + oracle test + fuzz lane) |
| query execution / operators (`sparq-engine` exec/optimizer) | full conformance ratchet; the operator-coverage bench; per-builtin error table | **E** (conformance ratchet) |
| the reasoner (`sparq-reason`, rules, closure) | inference conformance ratchet; incremental==batch property tests; LUBM entailed tier | **E** (inference ratchet) |
| a public API (`pub` item / CLI flag / HTTP route / Py/JS binding) | update the matching `skills/<surface>/SKILL.md` (REQUIRED, same change); the surface's tests | **G2** (`gate-api-skill.py`) + `flow-on:changed-public-feature-docs` (mints a sync-SKILL bead if a CLI/HTTP/Py/WASM surface merged untouched); also the *MAINTENANCE RULE* norm |
| a public **config key / CLI flag / env var** | document it in the matching `SKILL.md` / crate README (the value, default, and effect) | **G6** (`check-config-documented.py`); escape `config-internal` label |
| `sparq-wasm` / the wasm graph | `scripts/wasm-deps-guard.sh`; `wasm-pack test --node`; the `wasm_bundle_bytes` size gate | **E** (deps-guard + node test + bundle-size gate) |
| Cargo dependencies (`Cargo.toml`/`Cargo.lock`) | `cargo audit` + `cargo deny check` + regenerate the SBOM (supply-chain gate) | **E** (`supply-chain.yml`) |
| the ZK verifier / circuits (`sparq-zk`, `sparq-zk-compose`) | `forge_gates` + `differential_fuzz`; the **gate-count snapshot** (`crates/sparq-zk-compose/tests/gate_count.rs` + `gate_count_snapshot.json`) — `gate_count_regression` recompiles each circuit under `nargo`+`bb` and fails on >tolerance bloat, while `snapshot_covers_every_member` (the `zk/compose/` family) and `snapshot_covers_top_level_circuits` (**Gate G5** — any `bin`-type Noir package elsewhere under `zk/`) fail without a toolchain if a new proving circuit lacks a `members` baseline or an `exempt_circuits` entry; re-baseline via `bench/zk-compose/scripts/gate_counts.sh`. **Adding/removing a `zk/compose/` circuit member** (a `bin` package under `zk/compose/` that uses the shared `compose_core` lib — e.g. `join_eq_na16_nb16`; NOT `compose_core` itself) has a [circuit-member checklist](#zk-circuit-member-checklist--adding-or-removing-a-zkcompose-member) below — `nargo test` + `clippy -p sparq-zk` alone do NOT catch a missing baseline. Also re-open the soundness audit; the **`zk-toolchain` lane** (`.github/workflows/zk-toolchain.yml`) — runs the `#[ignore]`d real-`bb` forge/anchor suite under the pinned Noir toolchain (nightly + `workflow_dispatch` + on ZK-path PRs). If you change the public-input serialization (`verifier.rs::reconstruct_public_inputs`) re-capture the empirical bb anchors via the `probe_*_public_inputs_hex` e2e probes | **E** (forge/fuzz/snapshot tests) + **G5** (`snapshot_covers_top_level_circuits`); `flow-on:new-zk-circuit-gatecount` mints a baseline bead for a new top-level `zk/` circuit; soundness re-audit is **norm** |
| SHACL (`sparq-shacl`) | the W3C SHACL conformance ratchet (core ≥98, sparql ≥5); the differential-fuzz nightly lane (`shacl-diff-fuzz.yml`, sparq-shacl vs pySHACL) for correctness drift | **E** (SHACL ratchet + diff-fuzz lane) |
| an **outward ZK/MPC claim** ([OPUS-4.8] sq-cuzr) — any root doc / README / `SKILL.md` / site copy / compliance-index string that describes the `sparq-zk*` / `sparq-mpc` estate's privacy or soundness | keep every assertion **hedged / negative / illustrative**: the v1 ZK verifier is pending external cryptographer sign-off and `sparq-mpc` is honest-majority semi-honest only (SECURITY.md; `research/zk-*-audit.md`), so no line may state a ZK/MPC privacy-or-soundness property as a *settled, achieved* fact without the not-yet-sound caveat. Run `bash scripts/check-privacy-claims.sh` (exit 0 = clean). A grep hit fails the build unless the line carries an inline `privacy-claims-allow: <why>` marker — and the **predicate-form** soundness tier ("the verifier **is** sound", "…are COMPLETE and SOUND") is also exempted by a same-line negator/hedge, so honest "NOT sound" / the canonical "SOUND as landed for the … threat model" verdict still pass. Weakening the regex or blanket-excluding a live doc surface to pass is itself an honesty defect (`research/` design records + the audit docs are already path-excluded). | **E** (`scripts/check-privacy-claims.sh`, the `privacy-claims` HARD job in `docs-quality.yml` + its both-direction self-test `scripts/tests/test_privacy_claims.sh`; gated by `ci-summary`) |
| storage/encoding (`sparq-core` store/dict/compress, mmap, dict-spill) | the deterministic perf-gate metrics; byte-identity differentials; coverage with `--features dict-spill`; the **`fuzz` lane**'s `graph_open` target (`.github/workflows/fuzz.yml`) — corrupts the on-disk store files (`perm*.bin` / `dict-meta.bin` / sidecars / `named.bin`) and asserts `Graph::open` returns `Err`, never a panic/OOM/UB (T-MMAP-FUZZ) | **E** (perf-gate + byte-diff + graph_open fuzz) |
| a new `unsafe` block / `unsafe fn` | a `// SAFETY:` justification (lint-required) + a row in `compliance/memsafety/unsafe-register.md`; re-seed `bench/unsafe-snapshot.json`; the crate in the Miri lane | **E** / **G4-subsumed** (`unsafe-gate.py` count ratchet + `clippy::undocumented_unsafe_blocks` + `miri.yml`) |
| an opt-in cargo feature, or a test behind a **default-OFF** feature ([OPUS-4.8] sq-vya1) | wire the suite into a **`feature-matrix.yml` leg** (`.github/workflows/feature-matrix.yml`, job `opt-in-features` — per-leg `cargo test -p <crate> --features <set>`, each leg a required `ci-summary` check). The `ci.yml` nextest archive carries **only** `approx-ann,filtered-ann,vec-predicate`; any other default-OFF feature's test compiles EMPTY there and runs silently-zero, so its coverage is the job of `feature-matrix.yml`. Prove a suite is reached: `cargo nextest list -p <crate> --features <set>` must SHOW its test names. **[SONNET-4.6] sq-qcnn.31 structural guard C1 (fail-closed):** the `setup` job in `feature-matrix.yml` now runs `python3 scripts/check-feature-test-execution.py --check` — a gated test with no CI executor and no allowlist entry **fails the gate immediately** rather than silently never running. To exempt a test temporarily, add an explicit entry (with a written reason) to `scripts/check-feature-test-execution.allowlist.json`; resolve it by adding the executor leg. **[OPUS-5] sq-p1ccp — the sq-vya1 rule is now MECHANIZED by the tier ratchet:** the same `setup` job runs `python3 scripts/feature-matrix-tiers.py --enforce`, which reds the gate when (1) a leg carries `tier: check` although the detector classifies one of its features **sensitive** (a `cfg(feature = "F")` hit in `tests/`/`benches/`, a `#[cfg(test)]`-adjacent hit in `src/`, or **any** `cfg(not(feature = "F"))` in the crate) with no reviewed `tier-reason:` override — the ratchet, so feature-gating a test under a demoted feature re-promotes it on the next PR — or (2) a sensitive **(crate, feature)** appears in **no `test: true` leg for that crate**, which is the sq-vya1 guard itself. Both invariants key on the **(crate, feature) PAIR**, never the bare name: cargo feature names are crate-LOCAL and 15 are shared across crates today (`arrow`, `service`, `templates`, …), so a `test: true` leg for `F` in crate A never stands in for a sensitive `F` in crate B (`scripts/tests/test_feature_matrix_tiers.py::TestCrossCrateFeatureNameCollision`). The only exit from (2) is a written **`test-reason:`** on the leg, for coverage that genuinely cannot be a `cargo test` leg — today exactly one: `sparq-py`'s `arrow`, whose surface is reachable only through the pymodule (`[lib] test = false`, so no `cargo test` harness covers it) and whose executor is the gating maturin `arrow` pytest job in `python.yml`. The detector is **fail-closed**: a parse/IO error classifies the leg sensitive (keep the full leg), never a silent demotion. A fragment leg's optional `tier:` selects the lane — `tier: test` (the default, and what a **missing** `tier:` means — a present-but-unrecognised value is a hard assembler error, never a silent demotion) keeps the full build+test+clippy leg; `tier: check` demotes it to the `check-tier` job, which on PR/merge_group runs **clippy only** (`-D warnings`, no `--all-targets`, no test execution) while push-to-main still runs it as a **full build+test leg** as the per-merge backstop. Nothing is demoted by omission — only by a reviewed fragment edit that `--enforce` holds to the evidence; as of 2026-07-26 no fragment carries `tier:`, so both `check-tier` shards are green-empty but still required | **E** (`feature-matrix.yml` legs + `scripts/check-feature-test-execution.py --check` + `scripts/feature-matrix-tiers.py --enforce` in the `setup` job, and the required `feature-matrix check-tier (<shard>)` job — gating on every PR) |
| a **new crate** (`crates/<x>/Cargo.toml`) | a `README.md` (template — concise: **≤120 lines**, or a **≤30-line** `publish = false` stub carrying the `<!-- internal-stub -->` directive; verbose API detail belongs in rustdoc/`SKILL.md`, not the README), a registered bench in `bench/benchmarks.toml` (or `publish = false` stub), and a `skills/<surface>/SKILL.md` if it is a public surface | **G1** (`gate-new-crate.py`); the README length/sections leg of `check-readme-template.py` (HARD in `docs-quality.yml`); escape `<!-- flow-on-exempt: reason -->` |
| a **new bench suite** (`bench/<suite>/`) | register it in `bench/benchmarks.toml` **and** add a `FEATURED_SUITES` row in `bench/dashboard/dashboard.js` (or flag `featured = false`) | **G3** (`check-new-bench-registered.py`) + `flow-on:new-bench-dashboard-row` (mints the dashboard-row bead — produced out-of-band) |
| a competitor-relevant engine/store path (with the `competitor-relevant` label) | refresh competitor baselines (`bench/` gather harness → `bench/competitors.json`) | `flow-on:competitor-feature-gather` (un-gateable — out-of-band gather) |
| a `research/*.md` design that is now shipped | graduate it: rewrite into an architecture doc or fold into the crate README / `SKILL.md`; convert any stale "not implemented" claim to a bead | **norm** (*Documents must stay current*) |
| anything merged | the per-crate coverage ratchet + test-presence gate (`scripts/coverage*.py`). The ratchet only ever **RISES**: `coverage-gate.py --check-monotonic` (sq-neq8) diffs the PR's `bench/coverage-floor.json` floors against `origin/main` and FAILS on any floor LOWERED or crate DROPPED without an explicit, reviewed `--allow-lower` (a floor LOWERING in a PR is otherwise invisible to `--check-robust`, which only compares measured-vs-floor — exactly how #661 silently re-seeded `sparq-serve` 92→83). **History note:** the monotonicity (`--check-monotonic`) and test-presence halves were always enforcing, but the **measured line-coverage-vs-floor** half was *vacuous* for a stretch — `cargo-llvm-cov` was not actually installing (the `install-action` step lacked an explicit `with: tool:`), so `--check-robust` never ran; **#680 restored real measured enforcement** by pinning `tool: cargo-llvm-cov`. <!-- [OPUS-4.8] de-overclaimed: measured-coverage half was vacuous until #680 restored it --> | **E** (`coverage-gate.py` + `coverage-presence.py`; measured-coverage half live again as of #680) |
| test QUALITY (a test that runs a line but never asserts on it) | the per-crate **mutation-testing ratchet** — a committed surviving-mutant *ceiling* (`bench/mutants-baseline.json`, `scripts/mutants-gate.py`) that only ever FALLS, the test-quality companion to the coverage floor. cargo-mutants is too slow for per-commit, so it runs in the **nightly** tier (`.github/workflows/ci.yml` `mutants-nightly-advisory`, schedule + `workflow_dispatch`) and is **advisory** while the baseline seeds across the workspace, then promotes to gating. Static exclusions (presence-gated / non-host crates) live in `.cargo/mutants.toml`. [OPUS-5] The nightly ratchet and the PR-time `changed-code mutation review (advisory)` lane are BOTH advisory, and the latter additionally carries `if: github.event.pull_request.draft == false` — so on the fleet's draft worker PRs there is no mutation signal at all until the un-draft moment. `scripts/preflight.py`'s `guard-untested` check covers only the STATICALLY decidable slice (a guard-shaped public symbol with NO test naming it anywhere); a test that exists but asserts a bound, a type, or a marker string instead of the behaviour is caught by neither, which is why the worker briefs make mutating the headline guard an explicit manual obligation. | **E** (`mutants-gate.py`, nightly; seeding → gating) + `preflight.py` `guard-untested` (static slice) + **norm** (the manual mutation obligation in every worker brief) |
| this `AGENTS.md` / any "how we work" convention | ask whether it's portable to a sibling repo's charter — if so, file it there (see *Cross-pollinate the charter with sibling repos*) | **norm** (*Cross-pollinate the charter*) |
(Keep this table in sync as gates are added — and keep each row's **Enforced by** code honest: when a gate is added, removed, or renamed, update both the cell here and the gate's own docstring, per sq-ncvq.10. A row whose follow-up has no gate or flow-on rule is **norm** by definition, not a silent gap.)
### ZK circuit-member checklist — adding or removing a `zk/compose/` member
<!-- [OPUS-4.8] sq-0x65: PR #170 added the `join_eq` member but ran only `nargo test` + `clippy -p sparq-zk`, not the Rust suite, so CI failed on the snapshot. This checklist makes the two required Rust-side steps explicit. -->
A `zk/compose/` **member** is a `bin`-type Noir package directory under `zk/compose/` that uses the shared `compose_core` library (e.g. `join_eq_na16_nb16`, `filter_int_d4`, `scan_k1_n16_r4`). It is NOT `compose_core` itself (the shared lib) and NOT `target`. The membership-and-count gate lives in the **Rust** test suite (`crates/sparq-zk-compose/tests/gate_count.rs`), not in `nargo`, so `nargo test` passing is not evidence the gate passes. When you **add or remove** a member:
1. **Update the gate-count snapshot.** Add (or delete) the member's entry in `crates/sparq-zk-compose/tests/gate_count_snapshot.json` (`members` map) with its real `circuit_size` from `bb gates -s ultra_honk`. Re-baseline by running `bench/zk-compose/scripts/gate_counts.sh` and copying the values into BOTH the snapshot and `bench/zk-compose/gate_counts_latest.json` — `snapshot_covers_every_member` fails if a compiled member has no baseline, and the snapshot↔bench parity test fails if the two views drift. (For a member that is intentionally not gate-count-baselined, add an `exempt_circuits` entry instead.)
2. **Run the Rust gate locally:** `cargo nextest run -p sparq-zk-compose` (NOT just `nargo test`). `snapshot_covers_every_member` runs WITHOUT the `nargo`/`bb` toolchain — it only reads the `zk/compose/` directory names against the snapshot — so this step catches a missing baseline even on a box without the ZK toolchain. The `gate_count_regression` bloat check additionally needs `nargo`+`bb` (it skips cleanly when absent).
This is **Gate G5** territory for top-level `zk/` `bin` circuits (`snapshot_covers_top_level_circuits`); the `zk/compose/`-family coverage is enforced by `snapshot_covers_every_member` in the same test file.
<!-- [OPUS-4.8] sq-5reoy (#1599): externalized Noir libs + toolchain-pin alignment. -->
**Externalized Noir dependencies (sq-5reoy / #1599).** The former in-tree `zk/ieee754` and `zk/xpath` trees were split out to the [`sparq-org/noir_IEEE754`](https://github.com/sparq-org/noir_IEEE754) and [`sparq-org/noir_XPath`](https://github.com/sparq-org/noir_XPath) face repos and removed from this repo. Their latest releases are **`v0.11.0`** (noir_IEEE754) and **`v0.3.0`** (noir_XPath, cut 2026-07-06 — see `research/zk-audit-readiness-dossier.md` §1.3). Those are the face repos' own release trains and are NOT automatically what this repo consumes: `zk/compose` pins `sparq_ieee754 @ v0.11.0`, while the XPath differential harness still pins `XPATH_TAG: "v0.2.0"` (`.github/workflows/xpath-differential.yml`, `zk/xpath/scripts/run_differential_harness.sh`) — read the pin, not this note, when you need to know what a lane actually verifies. `zk/compose` is the only in-tree Noir tree left; `zk/compose/compose_core/Nargo.toml` now consumes `sparq_ieee754` as a **pinned Nargo git dependency** (`{ git = "…/noir_IEEE754", tag = "v0.11.0" }`), exactly like its existing `poseidon` git dep. Two consequences for agents: (1) any `nargo compile` of `zk/compose` (the forge suite, `bench/zk-compose/scripts/gate_counts.sh`, the `sparq-zk-compose` test estate) now **fetches that git dep from GitHub** — a cold `~/.nargo` cache needs network access; a network-restricted runner must warm `~/.nargo` first. (2) **Toolchain-pin drift:** the `NARGO_VERSION`/`BB_VERSION` pins in `zk-toolchain.yml` and the face repos' pins are now maintained independently — when you bump the Noir toolchain here, confirm the released `sparq_ieee754` tag was cut on a compatible `nargo` (and re-run the forge suite, which compiles the git dep). Their nargo tests + the ieee754 differential oracle now run in the face repos' own CI, not in `zk-toolchain.yml`.
### New-parser correctness checklist — shipping a hand-written parser
<!-- [FABLE-5] sq-3dyje.13: codified from research/testing-strategy-assessment-2026-07.md §8 so every hand-written-parser bead (e.g. sq-jocpn) can cite it as its acceptance frame. -->
A hand-written parser (replacing or bypassing a reference implementation) ships only with **ALL** of (design record: [`research/testing-strategy-assessment-2026-07.md`](research/testing-strategy-assessment-2026-07.md), section 8):
1. **Round-trip property tests** — `parse ∘ serialize ∘ parse` fixpoint over generated inputs (proptest), plus `serialize ∘ parse` identity on canonical forms, in the parser's crate.
2. **Differential fuzz vs the reference it replaces** — a cargo-fuzz target feeding identical bytes to both (e.g. native tokenizer vs `oxttl`), asserting identical triple streams/errors-modulo-documented-divergences; wired into the auto-discovered `fuzz/` workspace so per-PR corpus replay + nightly randomized runs apply automatically.
3. **Conformance-suite tie** — the relevant W3C ratchet floor (e.g. TurtleTests) unchanged or raised in the same PR; byte/count-identical parse on the suite corpus.
4. **Robustness target** — hostile-input fuzz (never panic/OOB), separate from (2), if the grammar entry point is new.
5. **Feature/fallback discipline** — a fast-path parser that falls back to the reference on unrecognized shapes must differential-test the *dispatch decision* too (fallback taken ⇒ results identical).
6. **Honest perf claim** — measured on the canonical bench path, never a work-box number in docs (see *No hard-coded performance numbers*).
## Documents must stay current — research records become architecture docs
A document must never describe the code as it ISN'T. Concretely:
- **No "not implemented" / "TODO" / "future work" statements left standing in a doc when they describe a real gap** — that is a disguised markdown TODO. Convert it to a **bead** and edit the doc to either delete the claim or replace it with a forward reference to the bead id. (If the feature IS now implemented, the statement is stale — fix the doc.)
- **A `research/` design record is provisional.** Once its design is implemented, it should graduate: either rewrite it into an **architecture document** describing what the code actually does (and where), or fold the durable parts into the relevant crate `README.md` / `skills/<surface>/SKILL.md` and delete the speculative design. A research doc that still says "we will…" or "X is not implemented" about shipped code is a bug in the docs.
- When you touch code, check the docs that describe it; if your change makes a doc statement false (in either direction), update the doc in the SAME change.
- **Top-level human-facing docs (the root `README.md`) state capabilities and link the relevant standards + in-repo docs; they do NOT explain engine internals and do NOT enumerate the contents of a linked spec.** Engine internals (dictionary encoding, permutation indexes, join algorithms, planning, delta overlays, mmap/compression, closure maintenance, …) live in `research/` design docs and crate `README.md`s — link them, don't inline them. When a doc says it supports a standard, hyperlink the standard and stop; don't list what's in it (no "SELECT/ASK/CONSTRUCT/property-paths/…" after a SPARQL link).
## Upstream blockers — roll your own, then contribute back
When a feature or a performance goal is **blocked by an upstream dependency** (a parser that rejects valid input, a missing API, a slow hot path), do NOT just mark it "unsupported/blocked-upstream" and stop. Instead:
1. **Vendor a local copy** of the upstream code (under `vendor/` or as a forked crate via `[patch.crates-io]`, as already done for `spargebra`), implement the feature/fix there, and ship it so sparq is unblocked.
2. **Open an issue + PR upstream** offering the change. Record the upstream issue/PR URL in the relevant bead and in the vendored copy's `*-PATCHES.md` (as `vendor/spargebra/SPARQ-PATCHES.md` does).
3. **Keep the PR live:** if you later change that vendored code, update the open PR; if the upstream PR was already merged/closed, open a new one for the delta.
4. A `blocked-upstream` bead is therefore a signal to roll-your-own + contribute, NOT a dead end. When the local implementation lands, the bead is unblocked.
**Proactive upstreaming, not just unblocking.** The rule above triggers on a blocker, but it also runs the other direction: when a fix or feature built here against a vendored or forked upstream (`spargebra`, the `hdt` crate, any `[patch.crates-io]` dependency) would be useful to that upstream even though nothing here was blocked, proactively offer it upstream (an issue + PR) rather than siloing it in the vendored copy. Record the upstream URL in the relevant bead and in the vendored copy's `*-PATCHES.md`. Keeping vendored deltas flowing upstream shrinks the patch set we carry.
### Upstream contributions — how to open the PR (the N3.js practice)
<!-- [OPUS-4.8] sq-758 (B5): codify @jeswr's verbatim standing practice for agent-opened upstream PRs (stated on sparq issue #758). Applies to EVERY outbound PR an agent opens on an external repo — KonradHoeffner/hdt, rossanoventurini/qwt, etc. -->
Every upstream PR an agent opens on a third-party repo (`KonradHoeffner/hdt`, `rossanoventurini/qwt`, any external dependency) **must** follow @jeswr's standing N3.js upstream-contribution practice. This mirrors how he opens PRs against `rdfjs/N3.js`:
1. **Open it as a DRAFT.** Never a ready-for-review PR.
2. **Explicitly identify the author as an agent** — a 🤖 SPARQ-agent self-id line in the body, e.g. *"This PR was opened by an autonomous agent (a SPARQ agent) operating on @jeswr's behalf."*
3. **Assign @jeswr as reviewer** (`gh pr edit <n> --add-reviewer jeswr`, and/or `--add-assignee jeswr`). If GitHub rejects this — the PR author IS @jeswr and you lack write/triage on the upstream repo, so `RequestReviewsByLogin` / `ReplaceActorsForAssignable` fail — **@-mention @jeswr in the body as the review gate instead** (the only available mechanism on a fork-based PR you authored). Note in the bead which mechanism was used.
4. **Carry a "NOT yet ready for maintainer review" note** in the body — the PR is pending @jeswr's own review first; the upstream maintainers should not review/merge until he marks it ready. **Never mark a PR ready-for-maintainer-review yourself — that is @jeswr's call.**
5. **Include a clear, concise "Why" section** — why this PR exists and why *this* repo needs it (e.g. "sparq vendors its own decoder purely to read every triple on bulk import; this provides that upstream so the wrapper can delete the vendored copy"). Why first, then What/How.
6. **Keep it as minimal as possible**, and **split unrelated changes across separate PRs** — one self-contained feature/fix per PR. Do not bundle independent changes.
When asked to revise such a PR, **edit the existing PR on its existing branch** — never open a new upstream PR, and never destructively force-push without flagging it. If a PR bundles unrelated changes, *note* which way it should split rather than silently rewriting it.
## Proactively maintain this file (and the skills)
Do NOT wait to be told. Whenever you notice a **repeated behaviour, a standing rule, a convention, or a hard-won lesson** that future agents should follow, add it to this `AGENTS.md` (or the matching `skills/<surface>/SKILL.md`) as part of the same work — the same way you'd capture a follow-up as a bead. This file is the durable home for "how we work here"; keep it current without prompting.
## Cross-pollinate the charter with sibling repos
This charter is shared, adapted, with sibling repositories (e.g. **[prod-solid-server](https://github.com/jeswr/prod-solid-server)**, a production Solid server whose AGENTS.md was adapted from this one). Keep the charters **convergent on the shared *principles*** while each keeps its own domain-specific gates. The shared core that should stay aligned across siblings: *delegate to worktree sub-agents + the continuous bead loop*; *track work in beads, never markdown TODOs*; *no `HANDOVER`/`SESSION` scratch docs*; *docs must stay current*; *the `needs:user` queue*; *proactively maintain this file*. sparq's domain-specific gates (cargo/clippy `-D warnings`, the W3C-conformance + best-ever-perf ratchets, the crate/wasm/CLI/HTTP/Py/JS surfaces, roborev/codex review) stay sparq's; a sibling's HTTP/Solid-specific gates stay theirs.
The flow is **bidirectional**:
- **Inbound (pull).** Siblings file *portable-pattern* issues on this repo. Review them; for each, decide if it is **genuinely portable** to sparq (be conservative — do not copy repo-specific bits). If portable, fold it into this `AGENTS.md` **adapted to sparq's reality**, then close or comment the issue. Also, periodically read a sibling's full charter for portable **repeat-workflow behaviours** that were never filed as issues, and adopt the genuinely-shared ones. (Both are step 1 of the *Maintenance loop*.)
- **Outbound (push).** When YOU add a convention to this file that would help a sibling, **file an issue (or open a PR) on the sibling repo** so it can adopt it — the mirror of how it files here.
- **Watch your OWN outbound cross-repo threads for follow-ups — and answer them.** <!-- [OPUS-4.8] charter cross-poll from PSS: watch outbound threads --> An issue or PR you opened (or commented on) in a sibling repo is *threaded*: the sibling agent or the maintainer may reply downthread with a question, a clarification, or a decision. Periodically poll those threads (`gh issue view <n> --comments`, `gh pr view <n> --comments` on the siblings) and **respond promptly**, self-identifying as the SPARQ agent in each reply — exactly as you watch your own PRs' review threads here. A stale half-finished cross-repo conversation loses the portability work it was started for. (The mirror: the PSS agent runs the same "check my own upstream threads for follow-ups" standing item against the threads it opened on this repo.)
**Task-completion checklist item (the mirror of the siblings' rule):** before considering a change done, ask — *"did this change produce a convention worth upstreaming to a sibling repo's charter?"* If yes, file the outbound issue/PR as part of the same work.
## Orchestration cadence — background by default
When orchestrating, run agents and long shell commands (builds, gates, fetches, watches) **in the background** by default, and parallelise independent work. A foreground/blocking call stalls the loop and prevents picking up new instructions or other ready beads meanwhile. Reserve foreground for genuinely sequential, short glue. (Continues the delegation + continuous-loop rules above.)
**Dispatch sub-agents in the BACKGROUND — then keep going; do NOT block the turn on a batch.** <!-- [OPUS-4.8] background-dispatch discipline --> Launching agents and waiting (a synchronous Agent call, or a batch of them in one message) until the whole batch returns is an anti-pattern with two costs: (1) it **idles the orchestrator** for the wall-clock of the *slowest* agent — no new fan-out, no merges, no bead/glue work happens meanwhile; and (2) it makes the session **unresponsive** — the user cannot interject without force-backgrounding the running batch. Instead, fire each agent as a **background task** (`run_in_background`) and immediately continue: spin up the next unblocked agent, drain any `ci-summary`-green PR, close beads, answer the user. Each agent's completion arrives as a notification — act on that result when it lands, not by blocking for it. Only run an agent synchronously when its result is the *sole* possible next step AND there is genuinely nothing else to fan out or merge meanwhile (rare). The target steady state: many agents in flight, the orchestrator always free to fan more / merge / respond. (This is the sub-agent-level form of *background by default*; it is also what keeps the ≥3–4-parallel target from collapsing into serial bursts.)
**Reconcile finished-but-unnotified agents.** Completion notifications can occasionally not surface. Don't wait indefinitely on a background agent: if it's gone quiet, check the real state — `git worktree list`, the branch's last commit (`git log -1 <branch>`), and whether any of its processes are still alive (`pgrep -af cargo`). A committed branch + no live process = done; verify with your own gate and merge. (Trust ground truth over the notification stream.)
**But do NOT redispatch a backgrounded agent as "dead" on weak evidence — that causes a duplicate-agent collision** (two agents on one branch/worktree, one deleting the other's worktree mid-run). <!-- [OPUS-4.8] charter cross-poll from PSS: don't redispatch on weak evidence --> The reconcile rule above is for genuinely-finished agents, not a licence to assume death. Two signals that look like death but are NOT: (1) **an unchanged branch** — a sub-agent that commits only at the END leaves its branch untouched for its entire run, so "the branch hasn't moved" is not evidence it died; and (2) **an empty `pgrep`** — it does not reliably match a backgrounded agent's process. The only reliable done/dead signals are the agent's **completion or killed `<task-notification>`**, or a `TaskStop` you issued. Before redispatching a task you think stalled, prefer `TaskStop <id>` on the original first (so exactly one survives), or wait for its notification — never fire a second agent at the same branch/worktree just because the branch is static and `pgrep` came up empty.
**Graceful degradation on a model usage/quota limit — narrow to bookkeeping-only, don't idle or abandon.** <!-- [OPUS-4.8] charter cross-poll from PSS #928 --> If the orchestrator hits a model usage/quota limit, do **not** idle, crash-loop, or abandon in-flight branches. **Degrade to BOOKKEEPING-ONLY work** — the subset that runs on the no-model-quota `git`/`gh`/`bd` CLIs: merge already-verified-green PRs (those whose adversarial-verify — `ci-summary`-green **and** all roborev/Copilot threads resolved — completed *before* the limit), reap finished worktrees (the loop-step-0 reconcile + `scripts/worktree-gc.sh --apply`), reconcile the bead tracker, and triage incoming issues/PRs. Do **not** dispatch new sub-agents while degraded (they consume quota). Resume full fan-out the instant quota returns, and file a `needs:user` recording the reset time. The heartbeat keeps running throughout — only the task *subset* narrows. (The degraded tick is exactly the lightweight maintenance sweep below, minus step-3 dispatch.)
## Contribution workflow — PRs, reviews resolved, the `ci-summary` gate
Changes land on `main` via **pull requests**, not direct pushes (direct push is reserved for the rare hotfix the team agrees on). For every PR:
1. Branch → open a PR (`gh pr create`). Request review, **including GitHub Copilot** code review.
2. **Address and RESOLVE every review comment** before merge — especially Copilot's. "Resolve" means: make the change or reply with the reason it's declined, and mark the conversation resolved. An unresolved thread blocks merge.
3. CI must be green: a single **`ci-summary`** check aggregates every other workflow's result and passes ONLY when they all pass. It is the one required status check for branch protection.
4. Merge only when: `ci-summary` is green AND all review threads are resolved. Then squash/merge and delete the branch.
5. **Close the GitHub issue when the fix lands on `main`.** <!-- [OPUS-4.8] issue close-out --> Beads track *internal* work; the GitHub **issues** are how the owner and sibling agents see resolution — so when a merged PR resolves an issue, CLOSE that issue (with a comment linking the closing PR/commit) and close the corresponding bead (`bd close`). When a bead corresponds to a GitHub issue #N, prefer `Closes #N` in the PR body so the merge auto-closes the issue; otherwise close it manually on merge. Don't leave a resolved issue open — an open issue with a landed fix misleads everyone watching the repo.
**Arming model — adversarial-verify automated, PERFORMANCE retains discretion.** <!-- [OPUS-4.8] perf-discretion gate (.claude/agents/sparq-perf-reviewer.md + the PreToolUse hook in .claude/settings.json) --> The decision to *arm* a PR for the merge train (`gh pr merge … --auto`) is gated, but the gate is split by concern so the orchestrator only spends discretion where it matters:
- **Honesty / correctness / scope are AUTOMATED** by the adversarial-verify (the roborev cross-family review + `ci-summary`). A PR that is **verified-clean AND NOT performance-affecting** is **auto-armed** — no manual hold. "Verified-clean" means adversarial-verify passed; "not perf-affecting" means it touches no hot path, no benchmarked crate, no bench harness, and no canonical performance number, and makes no perf claim.
- **The verdict → label bridge is the ONLY automated writer of `review:pass`.** <!-- [OPUS-5] green-but-unqueued PRs, 2026-07-26 --> Every arming lane (`scripts/auto-arm.py`, `scripts/rearm-sweeper.py`, `scripts/batch-merge.py`) admits on the **`review:pass` label**, but a review's actual artefact is a **line-anchored `VERDICT: pass` comment** and review agents are forbidden from touching labels. Nothing in the repo used to ADD that label (`merge-queue-feedback.yml` and the fix lane only REMOVE it), so the hop lived only inside an orchestrator session — a review landing while nobody was watching was **lost**, and the PR sat green, `CLEAN` and unarmed indefinitely. `.github/workflows/verdict-bridge.yml` + `scripts/verdict-bridge.py` now perform that hop **event-driven with a cron backstop**, **fail-closed**: a verdict counts only when the comment's LAST non-blank line is exactly `VERDICT: pass`/`VERDICT: fail`, the body **contains the current head as a standalone full 40-hex SHA** (an *occurrence* test — a SHA quoted inside a commit URL or a `diff --git` line binds too; binding only PAIRS a comment to a head, the trusted author's trailing `VERDICT:` line is what makes it a verdict), and the author's `author_association` is OWNER/MEMBER/COLLABORATOR; among head-bound verdicts the **latest by immutable `created_at`** wins, so a retraction defeats an earlier pass. No missing, malformed or unreadable input can produce a pass **or leave an older one standing**: strictness that merely *discards* a malformed line is fail-closed alone and fail-**open** in composition, because a slightly-misformatted retraction (`VERDICT: FAIL`, `VERDICT: fail (retracting my pass)`) would leave the superseded pass as the newest surviving verdict — and a comment is a reviewer's *only* retraction channel. So a verdict-**shaped** final line that does not parse is `AMBIGUOUS`, not absent: it never promotes, it defeats an earlier pass at the same head, and it **removes** an existing `review:pass`. (A quoted / blockquoted / fenced / bulleted *mention* is not verdict-shaped, and an ambiguous line from an untrusted author is discarded entirely, so neither the instruction text nor a drive-by commenter can influence arming.) The hole is **channel-independent**, so PR **review bodies** are read too — on the same GraphQL page, at no extra API call — but strictly **withhold-only**: a review body can retract or suppress a pass, never grant one, so including them can only *shrink* the promote-set. It grants **no new arming authority** — it writes labels only; `auto-arm` still performs the arm with its own `expectedHeadOid` CAS. It also never retracts on *absence* of evidence, so a hand-applied `review:pass` is never fought. A second hole it closes: a green PR with no head-bound verdict carries no `review:*` label at all and is therefore invisible to every lane (auto-arm needs `review:pass`, the fast-fix ring only fires on FAILING CI, the dispatchers work off `review:needs`); the bridge marks those **`review:unreviewed`** — purely informational, consumed by no arming predicate, so it can never block or cause a merge.
- **A scheduled workflow is NOT a ten-minute guarantee on this repository — measure before you rely on one.** <!-- [OPUS-5] event-driven conversion, 2026-07-26 --> Over the 24h to 2026-07-26T22:10Z, `auto-arm`, `re-arm sweeper`, `promote-on-approval` and `batch-merge` each fired **11–16% of their scheduled cron ticks**, with observed inter-run gaps of **53–75 minutes** against a 10- or 15-minute `cron:`. GitHub coalesces and drops `schedule` events under load, and this repo is permanently under load. Two consequences. First, the offset-chain pattern (`verdict-bridge :01 → auto-arm :04 → batch-merge :07`) **does not hold in practice** — the ticks do not land where the crons say. Second, any stage whose input is carried by a webhook (`issue_comment`, `pull_request_review`, `check_suite`, `workflow_run`, `issues`) should be **event-triggered, with the cron KEPT as the reconciliation backstop** — never replaced by one, or a dropped webhook becomes lost work rather than late work. Stages whose trigger genuinely *is* the passage of time stay periodic and should say so in their header: `promote-on-approval` (GitHub has no reaction webhook), `re-arm sweeper` (it repairs arms GitHub silently drops, which emits no event), and every nightly verification lane. Adding an event trigger makes the event and the cron **race**: the converted job must be idempotent under a concurrent double-fire, and `concurrency:` alone is not sufficient — a concurrency group **cancels, it does not lock**, and a per-PR group is disjoint from the sweep's group by construction. `verdict-bridge` handles this by re-reading the single PR and re-deciding immediately before every write (`reconfirm`), which is a genuine compare-and-set on (head SHA, labels, newest verdict).
- **Performance-affecting PRs get a specialized perf review BEFORE arming.** A `PreToolUse` agent-hook on `Bash` (in `.claude/settings.json`) fires on the `gh pr merge … --auto` arming step and invokes the **`sparq-perf-reviewer`** agent (`.claude/agents/sparq-perf-reviewer.md`). It decides `perf_affecting`, and if so `perf_ok` (regression risk + whether every perf claim is evidenced) against the benchmark catalog (`bench/CATALOG.md` / `bench/benchmarks.toml`) and the honesty rules (work-box timings are NON-canonical; **no hard-coded perf numbers in markdown**; numbers must trace to real evidence; deterministic-floor regressions are real, timing wobble is advisory). If `perf_ok=false` (or perf-impact cannot be determined), the hook **denies the arm** with a clear reason and the maintainer reviews; otherwise it allows.
- **Canonical-number changes additionally surface to the maintainer.** When a PR edits canonical performance numbers — a `bench/perf-baseline.json` floor or a published-results artifact — the perf-reviewer flags it so the orchestrator surfaces it to the maintainer **even when `perf_ok=true`**: a floor move is a deliberate policy decision, not a routine pass. (This is the *arming* gate only; the actual merge is still gated independently by `ci-summary` + resolved review threads + the perf ratchet `scripts/perf-gate.py`.) A newly-edited `.claude/settings.json` hook is not necessarily hot-loaded mid-session — the maintainer opens `/hooks` once or restarts to pick it up.
- **NEVER arm auto-merge on a STACKED PR (base ≠ `main`).** <!-- [OPUS-4.8] sq-u59rq: the stacked-PR auto-merge trap, #1023 → #1028 --> When you chain PR *B* onto PR *A*'s branch to dodge a re-conflict — i.e. *B*'s base is *A*'s **head branch**, not `main` — do **not** arm auto-merge on *B* until its base is `main` again. GitHub squash-merges *B* into its **stacked base** (*A*'s branch), not `main`, the moment *A* merges first; it then marks *B* "merged" and **auto-deletes *B*'s head branch (unreopenable)** — yet *B*'s content never reaches `main`. That cost a re-land of identical content as a fresh PR (observed 2026-06-21: #1023 → #1028, GUI EXPLAIN reconciliation). The rule: stack strictly **sequentially**, or after the lower PR lands **retarget the upper PR's base to `main`** (`gh pr edit <n> --base main`) and only THEN arm it. This is enforced mechanically by a deterministic **`PreToolUse` command-hook** (`scripts/check-pr-arm-base.py`, sibling to the perf-reviewer in `.claude/settings.json`) that reads the arm command, calls `gh pr view <n> --json baseRefName`, and **denies** the `gh pr merge … --auto` when the base is not `main`. <!-- [OPUS-5] #1135 / PR #4192: failure disposition is per-AXIS. --> Its failure disposition differs by axis, deliberately: the **stacked-base** axis fails OPEN on its own lookup error so it never wedges the train, while the **release-PR** axis (issue #1135 — never arm the release-plz Release PR) fails CLOSED, because a crates.io version can never be unpublished. If the guard script cannot run at all, `.claude/settings.json`'s wrapper denies any `gh pr merge` and allows every other Bash call. Verify with `python3 scripts/check-pr-arm-base.py --self-test`. Relates *sparq merge mechanics* + the autonomous-scheduler.
Branch protection (owner-set, out-of-repo) enforces this: require `ci-summary`, require conversation resolution, require the Copilot/CodeQL review. The repo documents the required set in `docs/branch-protection.md`.
**Security & quality gating:** new security/quality regressions must not merge. CodeQL (SAST) + `cargo clippy -D warnings` + `cargo-deny` + the coverage/conformance ratchets all feed `ci-summary`. Keep the GitHub **code-scanning** alert count at zero — SHA-pin every action (`uses: owner/action@<full-sha> # vX.Y.Z`), and resolve/triage Scorecard + CodeQL alerts as they appear.
**Web + GUI E2E gating & flake-quarantine** <!-- [OPUS-4.8] sq-ymr2e.12 --> — the deterministic site (`site/e2e/`) and GUI (`gui/e2e-playwright/`) Playwright lanes are **advisory-first**: they gate `ci-summary` only after earning it on a probation bar (**50 consecutive green runs on `main` spanning ≥ 10 distinct PRs, OR two weeks — whichever is LONGER**), and a flaky test is quarantined (`test.fixme`) same-day with a P2 fix bead — never re-run to green. Promotion is a one-line flip (delete the lane's entry from `.github/advisory-registry.json` — since #3773 the aggregator excludes **only DECLARED** check names, never a name pattern), never a raw branch-protection edit. The checked-in policy + evidence ledger is [`.github/E2E-GATING-POLICY.md`](.github/E2E-GATING-POLICY.md); `tauri-driver` + nightlies never promote (design `research/web-gui-test-program.md` §6.3).
**Scorecard supply-chain + token conventions (born-compliant, so future config needs no clean-up):**
- **Pin published-artifact dependencies by digest.** Dockerfile base images are SHA-pinned (`FROM image:tag@sha256:… # image:tag`, keeping the readable tag as a trailing comment for legible bumps), same as CI action `uses:` pins. This covers everything in the **released** supply chain (the `ghcr.io` server image, the action graph).
- **SHA-pinned `taiki-e/install-action` MUST carry `with: tool:`** <!-- [OPUS-4.8] sq-ur7o --> — `taiki-e/install-action` selects which tool to install from its `@<tool>` git ref (`@cargo-llvm-cov`); the SHA-pin above **drops that selector**, so without an explicit `with: tool: <name>` the action installs **nothing** and the downstream `cargo <tool>` ENOENTs — silently making the gate **vacuous** (this is exactly the coverage-gate regression root-caused 2026-06-18). `scripts/check-install-action-tool.py` is a stdlib-only lint that scans `.github/workflows/*.yml` and **fails** on any SHA-pinned `install-action` step missing `with: tool:`; it runs (with a `--self-test`) in the `docs-quality` `ci-scripts` job. When adding a SHA-pinned `install-action`, always include the `with: tool:` input.
- **Ephemeral bench/bootstrap scripts are exempt from hash-pinning.** The throwaway self-terminating EC2 bench/hardware-run scripts (`scripts/aws-bootstrap.sh`, `hwrun/*.sh`, `bench/**/remote.sh`) `curl … | sh` rustup and best-effort `pip3 install` transient tools (e.g. `rapidgzip`); they're outside any released artifact, so their Scorecard `PinnedDependencies` alerts are **dismissed** (`won't fix`, with a per-file reason) rather than given a brittle fake pin. CI helpers that *are* part of the workflow graph (e.g. `python.yml`'s build-tool install) are pinned with `==` where it's a small fixed set.
- **Least-privilege workflow tokens.** Every CI workflow declares a top-level `permissions: contents: read`; any job that needs to write (push a branch, create a release/deployment, comment on a PR, assume an OIDC role) opts into the **narrowest** scope **per-job**, so every other job inherits read-only. A job-level `contents: write` that is genuinely required (e.g. the release job publishing a GitHub release) is the accepted least-privilege necessity — keep it scoped to that one job (dismiss its Scorecard `TokenPermissions` alert with that reason rather than removing the needed grant).
**Supply-chain attestation stack (cert epic sq-toze — GX-1/2/7).** <!-- [OPUS-4.8] -->
- **`cargo deny check advisories` is a GATING PR check** (`supply-chain.yml` `audit` job, no `continue-on-error`). The old CVSS-4.0 parse blocker (sq-q8de) is resolved; the policy is **fail-closed** (`deny.toml`: `yanked = "deny"`, advisories v2 ⇒ every unignored advisory fails). Every `deny.toml [advisories].ignore` entry carries a justification + a tracking bead, and the list only ever shrinks by REMOVING the dependency. Most recently [OPUS-5] sq-5ah3p retired `rustls-pemfile` (RUSTSEC-2025-0134) for good: the archived crate was the last PEM decoder in `sparq-lws-core/src/tls.rs` + `sparq-server/src/main.rs`, and both now call `rustls-pki-types`' `PemObject` through rustls' own `pki_types` re-export, so it is absent from Cargo.lock. A real vuln, a yanked crate, or any regression that reintroduces an unmaintained dep blocks the PR. Keep `deny.toml [advisories].ignore` and the VEX (below) **1:1 in sync**.
- **Per-release CycloneDX SBOM + VEX.** `scripts/gen-sbom-vex.sh` emits a CycloneDX SBOM per released binary (`sparq-cli`, `sparq-server`) + a version-stamped **VEX** (`supply-chain/vex.cdx.json` is the checked-in source of truth; it states `not_affected` + justification for every advisory `deny.toml` ignores). The `release.yml` `sbom` job runs it, SLSA-attests the outputs, and attaches them to the GitHub Release (covered by `SHA256SUMS`). Editing the ignore set ⇒ update `supply-chain/vex.cdx.json` to match. Each shipped SBOM is normalized through `scripts/sbom-normalize.jq` (a deterministic, idempotent `jq` transform, also applied in `supply-chain.yml#sbom`) so no host-revealing absolute build path leaks into a `bom-ref`/`purl`: `path+file://…#<ver>` refs become canonical `pkg:cargo/<name>@<version>` and the dependency graph is rewritten in lock-step (gap GS-6 / sq-toze.30).
- **`cargo-auditable`** wraps **every shipped-binary build path** — the `release.yml` `package` job, the `dist.yml` matrix, the `Dockerfile` builder, and the local `scripts/build-dist.sh` (sq-ytnq) — so the shipped binary/image **embeds its dependency manifest** (`cargo audit bin <file>` post-build). **`cargo-vet`** (`supply-chain/{config,audits.toml,imports.lock}`) is a **GATING** CI check (`supply-chain.yml` `vet` job, `cargo vet --locked`): every crate must be audited, covered by an imported trusted audit set (Mozilla/Google/Bytecode-Alliance/ISRG/Embark/Zcash), or hold an explicit `[[exemptions.*]]` entry. The bootstrap exemption set makes it pass today; the gate's value is the **ratchet** (a new unaudited/unexempted dep fails until audited/exempted). `cargo vet suggest` shows what to audit to shrink exemptions; the vendored `spargebra` patch is `audit-as-crates-io`.
- **Screen a NEW crate BEFORE you add it — not only at the post-hoc gate.** <!-- [OPUS-4.8] charter cross-poll from PSS: pre-add dependency screening --> The gates above (`cargo deny`, `cargo vet`, the SBOM/VEX) run *after* a dependency is in the tree; they catch advisories, bans, and unaudited deps, but they do not by themselves stop you reaching for a **typosquatted / slopsquatted name** (an LLM-hallucinated or look-alike crate), a **suspiciously brand-new or single-release** crate, or a **low-reputation / unmaintained** one in the first place. Before adding any new dependency, do a quick provenance check — confirm the crate name is the one you mean (not a homoglyph/typo of a popular crate), that it has a real release history + repository + non-trivial reverse-dep/download footprint, and that it is maintained — and prefer an already-vetted crate or the std/`sparq-core` path over pulling a new one. A *new* dependency is a supply-chain decision, so the bar is "is this crate trustworthy and necessary," not just "does the gate pass." (PSS runs this as a scripted `check-packages` pre-add check over npm; the portable principle is the **pre-add screen**, which on a cargo workspace is the provenance check above plus `cargo deny`/`cargo vet` as the recorded ratchet.)
**Perf gate — deterministic strict, timing advisory (`scripts/perf-gate.py`, sq-dzfu/sq-perf).** The perf ratchet hard-gates the **DETERMINISTIC** metrics (integer byte counts — `store_bytes_per_triple{,_small}`, `dict_bytes_per_term`, `wasm_bundle_bytes`) strictly against the committed best-ever floor in `bench/perf-baseline.json`: any value past its band fails (exit 2). The **TIMING** metric (`parse_ns_per_byte`, wall-clock-derived) is **ADVISORY / non-blocking** — it is still measured, still warned-on loudly (a band trip prints a prominent `WARNING (advisory, non-blocking)` with the reading + band), and still tracked/published on the dashboard, but a timing-only regression contributes **exit 0** and can **never block a merge**. Reason: shared GitHub-runner wall-clock variance exceeds any useful band even with the best-of-N re-measure — `parse_ns_per_byte` flapped the merge train repeatedly (it tripped on main, was "fixed" by raising the floor to the series median in #133, then flapped *again* on unrelated PRs like #130, an MPC-only change that touches zero parsing code) because the published `parse_ns_per_byte` series (the dashboard / `bench/perf-baseline.json` history) spans a band wider than any useful threshold. The best-of-N re-measure (`ci-bench.sh --parse-only`, up to K reads, keep the min) still runs to squeeze the *tracked* number toward the true cost, but its outcome is advisory. The deterministic-vs-timing split is data-driven from each metric's `mode` (`noise`=timing/advisory, else deterministic/hard), not a hard-coded name list; a mixed run exits 2 (the deterministic fail dominates) while still emitting the timing advisory. Result: CI-runner timing noise never blocks a merge (we removed the false-positive merge-block, not the visibility), real deterministic regressions are still caught.
**Contingency — if CI is genuinely unreachable.** The PR + `ci-summary` flow above is the standing rule and applies whenever CI can run. If GitHub Actions is genuinely unavailable (a platform/account outage — *not* a red run, which you fix), don't let the gate stall indefinitely: run the **full local gate** (full-workspace `clippy -D warnings` + `cargo test` + the conformance/perf ratchets) and treat **roborev's PASS verdict on the commit as the standing human-review substitute** (codex is non-Anthropic, so the substitute reviewer is still cross-family). This degrades the *review* and *gate-execution* to local; it does **not** authorise pushing to a protected `main` — that's a branch-protection change (a `needs:user` item), not something to bake in. Raise the outage itself as `needs:user`. The moment CI returns, revert to the normal flow and reconcile anything that landed during the outage against a green run.
**Cross-agent self-identification — identify as the SPARQ agent in every issue/PR/comment.** <!-- [OPUS-4.8] cross-agent self-id --> @jeswr runs multiple agents under one GitHub account, so a reader cannot tell *which* agent is speaking from the account alone. Therefore **every** issue, PR, and comment you author — orchestrator **and** sub-agents — must begin with a 🤖 self-identification blockquote naming the **SPARQ agent** (mirroring how the sibling PSS agent identifies itself in cross-repo threads). Issues and PRs are *threaded*, so identify in **each** comment you add, not just the first. The canonical header:
> 🤖 **SPARQ agent** — I am @jeswr's agent for the jeswr/sparq RDF/SPARQL engine. @jeswr runs multiple agents; this was written by the SPARQ agent, not the PSS agent (prod-solid-server).
The required header is **model-agnostic** — it names the *agent*, not the model, so it stays accurate whichever model is running. You MAY append the model name when it's relevant; the authoritative model record is the commit trailer + inline marker described under *Model provenance* below (don't duplicate it in the header by default). Carry this requirement into every sub-agent brief, so worktree-authored issues/PRs/comments self-identify too.
### Review lessons — checkable rules distilled from caught defects <!-- [OPUS-4.8] review-lessons subsection; 2026-07-06 verdict-gate catches -->
These are the review reflexes that caught **real** defects at the verdict gate (2026-07-06). Apply each as a PASS/FAIL check, never a vibe; each cites the PR whose defect motivated it. They are injected into the reviewer/verifier briefs — keep terse. The unifying failure mode: *green-and-configured is not the same as executed-and-gating* — trust the live evidence, not the shape of the config.
- **EFFECT-EVIDENCE RULE — never accept "the config looks right"; open the live check-run log and confirm the new tests EXECUTED and the enclosing JOB is required.** A green PR whose new tests never ran, ran in a non-required job, or exercised nothing the engine consumed is **vacuous**. Read the actual run (`gh run view <id> --log` / the check-run output), see the assertions print, and confirm the job feeds `ci-summary`. (Caught: EL abox tests compiled-but-ungated #1672; an ACP "coverage" benchmark the engine consumed **none** of the emitted policy shape #1650; a step "gating" from inside an advisory job #1679.)
- **DELETION-EVIDENCE RULE — a green suite after a REMOVAL proves nothing about what the removal took with it.** <!-- [OPUS-5] --> A guard nested inside the region it protects is deleted along with that region, and the diff looks like a coherent feature removal, so review and CI both miss it. The signature is a mutant moving **`KILLED` → `SURVIVED` when a feature is removed** (its target still exists, so it did not merely leave the spec) — i.e. **diff the mutation kill set across the deletion**, and report the transitions, not just the post-deletion total. For a contract crossing a module or repo boundary the mutant must instead be a **symmetric rename** (rename the symbol in production *and* its own tests together; if nothing reds, no assertion checks the name the CONSUMER resolves). (Caught: a `fetch_lanes` guard deleted with the queue-wait region it was nested inside, #4810 / registry #1031; an enrichment call site deletable with the suite green, same PRs; an inertness contract bound to the defining module instead of the one the consumer loads, #4823 / registry #1032.) Full class, detection method, worked instances and limits: `research/guard-mortality-and-kill-set-diffing.md`.
- **FEATURE-LEG PAIRING RULE — a new feature is not done until it has all three: (a) its own CI feature-matrix leg, (b) an `LC_ALL=C`-sorted golden-fixture line, and (c) an assemble/self-test that goes RED when the leg or the line is missing.** Compiling under `--all-features` is NOT execution; a feature with no dedicated leg is silently never run, and a golden fixture without the deterministic-locale sort flaps. (Caught: EL abox — missing matrix leg, then missing golden line, **TWICE**, #1672.)
- **ORACLE-STRENGTH RULE — a differential/conformance oracle compares term structure and full answer SETS, never row COUNTS.** A count-equal oracle silently passes a shared-blank-node cartesian product, a duplicated-binding blow-up, or a wrong-term / same-cardinality answer. Strengthen to set/term-structure equality before trusting any "matches the reference engine" claim. (Caught: a QL shared-blank-node cartesian product exposed **only** by strengthening the oracle to term-structure equality #1653.)
- **FAIL-CLOSED-BRANCH RULE — every multi-branch operator (UNION / OPTIONAL / FILTER combinations) must fail CLOSED, and the oracle must include a case where exactly ONE branch is permissive.** A filter dropped on a single UNION arm "fails open" and leaks rows; a single-branch happy-path test never sees it. (Caught: QL multi-branch UNION+FILTER fail-open #1647.)
- **GRADUATION-EVIDENCE RULE — a ratchet floor moves only with per-CASE oracle evidence for each newly-passing case; never force-pin or hand-edit a floor to turn a ratchet green.** When a feature genuinely graduates cases the floor rises **legitimately** — but prove it case-by-case; a pinned floor hides a regression behind a passing ratchet. (Caught: QL graduation-floor gap where the feature really did graduate cases yet the ratchet arm tripped #1653.)
- **DOCS-HONESTY RULE — a doc that asserts a soundness / correctness property IS itself a soundness surface; review it as adversarially as code.** "Feature X supports Y" in a README / SKILL / rustdoc is a claim that must trace to a passing test on the REAL path; if the code is unsound for that case the doc is a false soundness claim, not a cosmetic nit. (Caught: RIF docs claiming variable-equality works when it is unsound #1651.)
- **REACHABILITY-SEEDING RULE — a reachability-pruned / orphan-dropping validator must not seed its reach set from anything the validated data controls (e.g. declaration-typing).** Declaration-typing reachability lets an orphan node re-enter the reach set and bypass the validator; confirm the seed set is closed over TRUSTED roots only. (Caught: DL orphan-validator bypass via declaration-typing reachability seeding #1652.)
- **LANE-ISOLATION RULE — prove a per-PR lane and a nightly / heavy lane are disjoint by running the selector (`--list` / `testMatch`) in BOTH env-flag states and diffing the two lists.** An unfiltered `testMatch` leaks nightly visual specs into the per-PR lane (slow, flaky, wrong gate). Env-gate the selector and show the two lists differ by exactly the intended set. (Caught: nightly visual specs leaking into the per-PR lane via an unfiltered `testMatch` #1676.)
- **ADVISORY-vs-GATING RULE — "does it gate?" keys on the enclosing JOB's DECLARATION, not on any name.** Since sparq-org/sparq#3773, `ci-summary` excludes a check-run **iff its name is DECLARED in `.github/advisory-registry.json`**; everything undeclared GATES, whatever it is called. A load-bearing step inside a **declared-advisory** job still does NOT gate, however authoritative its `run:` reads — so verify the enclosing job has no registry entry before calling a check a gate, and put hermetic checks in their own undeclared job. Adding an `advisory` name token changes nothing on its own; *renaming* a declared job makes it GATE (fail-closed) and REDs `check-advisory-registry.py` C4. (Caught: a CI step that claimed to gate while running in an advisory job #1679; then #3773 — the name rule itself neutralising four real gates.)
- **MISSING-LEG-ATTRIBUTION RULE — a missing check is not a failing check: when one `opt-in *` leg looks red or blocking on MULTIPLE unrelated PRs at once, check the `assemble feature matrix` job FIRST.** <!-- [FABLE-5] sq-2wo5t --> If that `setup` job fails (golden leg-name drift in `scripts/tests/feature-matrix-legnames.golden.txt` after a branch adds a new opt-in leg, the C1 feature-gated-test-execution guard, the tier ratchet, or a malformed `.github/feature-matrix.d/` fragment), the whole opt-in matrix is never generated and **every** required `opt-in *` check goes expected-but-unreported — which masquerades as a single-leg cross-PR main regression. The job emits a loud `::error` + job-summary attribution on failure (pinned by `test_feature_matrix_assemble.py`); triage the assemble job's own failing step, never the phantom leg. (Caught: the false 'spqv-provenance regression' drain-blocker alarm — three unrelated PRs, zero actual leg failures, 2026-07-11.)
### AUTHOR pre-flight — run these on your OWN diff before you report done <!-- [OPUS-5] author-side pre-flight; 2026-07-28 first-review-failure classes -->
The rules above are the *reviewer's*. This list is the **author's**, and it exists because on 2026-07-27/28 essentially every PR that went through independent adversarial review here and in the sibling `jeswr/agent-account-registry` **failed its first review** — not on design, on this small repeating set. Running them yourself costs one pass and saves a whole review round. Each cites the PR that earned it (`reg #N` = the registry). Shared-contract item 14 points here.
⚠️ **This list exists in FULL in two repos with no shared owner** — here and in `jeswr/agent-account-registry`'s `AGENTS.md`, which has no `CLAUDE.md` and whose worker container is offline, so it cannot point at this file. **Treat this copy as canonical**: change a rule here and mirror it there in the same wave, or say why not. The two have already diverged on lane-specific detail (tooling, examples, emphasis). That is the `reg #958` shape applied to prose, and `reg #945` measured the cost of duplication directly — two copies of one guard make **each copy individually unkillable**.
1. **Line coverage FIRST — and read it LINE-granular, not function-granular.** Run the module's own `--self-test` / test binary under coverage (`python3 -m trace --count …`; stdlib, no install needed — or `cargo llvm-cov`) and list the **never-executed LINES** before you mutate anything. Four for four as a predictor of where mutants survive: `reg #756` (`cmd_record` + `_read_json` never executed → the shipped tree printed `planned_rows=4` where the mutant printed `0`), `reg #956` (the module's **only two write methods** had never executed anywhere), `reg #937` (`main` + `_gh_readers` at 0 % → **13 of 13** one-line edits there survived a **248**-check suite, including an `apply=false` "census-only preview" that writes real ledger records), `#4743` (**17 of 29** functions at 0 %; `main()` at 55 % with its whole `sweep` branch unexecuted, so `return 1 if …sweep() else 0` → `return 0` survived all **111** tests). Helpers get tested because they are easy to call; **entry points get skipped because the test has to construct the real world — which is exactly where a *fabricating* bug survives.** ⚠️ **"Nothing at 0 %" does NOT clear you.** The function-granular headline is the weak form and it misses the worst regions: `reg #956`'s `main` is at **8/18**, not 0 %, and a fresh sweep of exactly that region found **10 survivors out of 10**; `reg #941`'s `_escalate_two_head` had **1 of 3 call sites covered at 3/3 confidence**, which `reg #945` re-derived as **3 of 4 site lines never executed while the enclosing functions read 75 %**. ⚠️ **Validate the coverage instrument against a function you know is never called**: `reg #756`'s counted docstring lines as covered, scored a never-called function at 6.2 %, and printed *"no code unit is entirely unexecuted"*; `reg #956`'s reported zero uncovered lines from a mode that **cannot emit one**. An instrument that cannot fail has told you nothing.
2. **Ask FOUR independent questions of every assertion** — none subsumes another, and each found holes the others swept past (`reg #941`). (a) Does the **call site** recompute or re-wire this value? (`reg #937` `Z6`: dropping one argument at the single production call site bound the wrong issue with a 219-check suite green.) (b) Does my **expected** value come from the same place the code reads it? (`reg #958`: `review:parked` defined four times — every assertion compared what a module wrote against the constant it writes from, a tautology that cannot fail.) (c) Does my **input** derive from the same constant the code reads? (`reg #941`: every over-cap input derived from `STUCK_UNPARK_MAX`, so setting `STUCK_UNPARK_MAX = 999` left 76/76 green.) (d) Does this control ever **execute**, and does the check test the flag's **value** or merely its **presence**? (`reg #941`: `--stuck-grace-hours 6` → `100000` survived 76/76.)
3. **Two mutants per guard: DELETE it, and separately make it conditionally inert** — in a **non-crashing** form. They are different experiments. `reg #938`: deleting a census emission was caught; wrapping it in `if census.get("total")` was **not**, so it would have vanished on exactly the quiet tick an operator interrogates. ⚠️ **One-at-a-time is structurally blind to a DUPLICATED guard** — see item 4's fourth outcome; that experiment needs both copies gone at once.
4. **FIVE false mutation outcomes — say which one you have.** *False kill*: an exception raised **by the mutated line itself** is malformedness, not detection (`reg #956`: two mutants "died" to an `IndexError` that aborted the suite before any row printed). *Equivalent survivor*: declare it and show it unreachable (`reg #937` `D1-default`). *Value-identical survivor*: the substituted value collides with one the fixture already uses (`reg #941` pins a fixture head as `'b'*40`) — **choose mutant values that appear nowhere in the harness.** *Mutually-masking duplicates*: two copies of one guard make **each copy individually unkillable** — three of `reg #945`'s four survivors were a single `MIN_ARG_TOKEN` floor written at both the producer and the consumer, where *"removing either copy alone left the suite green."* Item 3's one-at-a-time protocol **cannot see this**: find it by asking whether the value is written twice, and by deleting **both** copies as one mutant. *Crash-after-partial-run*: a mutant that reds some rows and then **aborts** the suite records as KILLED while every check below it never ran (`reg #945`: an emission block raising `IndexError` after three named `FAIL` rows). Require the mutant run's **total check count** to equal the pristine run's before you call it a kill.
5. **Ask of every control: WHO can write the thing this reads?** Three arm-capable holes in one night, all from evidence read out of **author-controlled** text with no author filter: `reg #681` (per-provider review markers parsed from `pull["body"]` → the required-review count goes **2 → 1** and the surviving lone review arms it), `reg #937` (closing-reference declarations from title/body), `#4743` (a marker in **any** comment, with no `login` / `author_association` check, on a **public** repo → a drive-by comment forges `route=preserve` and re-arms). Evidence *about* a review must be written by the party that did it, filtered by author, and read with **quoted contexts stripped** — a marker inside a fenced block otherwise self-marks (`reg #937` `Z1`–`Z5`).
6. **The YAML seam is where the vacuity lives** — every uncaught mutant measured that night sat at a workflow `if:`, a step, or a call site, never in the module logic. **Pin exact-match, not containment**: `reg #956`'s `--apply-DROPPED` and `--reconcile-max-DROPPED` both survived a substring check; `reg #941`'s `if: false` on the resolver **step** and on the whole **job** each survived 76/76; `#4743`'s `route != 'preserve' && false` satisfies a substring assertion while killing the lane.
7. **Never substring-grep for a kill.** These suites print each row's name on the **pass** path too — extract kills line-anchored (`^FAIL:` / `^\s+FAIL\b`) or from the traceback frame. Measured on one real 6100-line gate log: **62** lines contained `FAIL` as a substring, **44 of them passing `ok` rows**; the anchored form extracted **1** (`reg #949`).
8. **A census must always emit, including a zero row.** Ask: *would this alarm fire if this branch took 100 % of the population?* (`reg #938`: the reservation census never zero-sealed.) And **a residual computed from rows that ENTERED a pipeline cannot see a loss that prevented entry** — `reg #756`'s `chain_unaccounted` read 0 in both the shipped and the mutant tree, so its own missing-edge detector was structurally blind to the break.
9. **Verify the marquee claim against the EVIDENCE path, not the object it names.** `reg #681`'s headline held for the *record* and failed for the *review-set evidence* it is actually enforced through. The feature in the **title** is disproportionately the one with no red test — mutate it first.
10. **Publish corrected counts.** Four headline numbers moved that night once someone asked a specific question of every row: `reg #941` 22/22 → **21/22** then 26/26 → **25/26**; `reg #937` 48/48 → **48/56**; `reg #956`'s "52 mutants, 52 killed, 0 survivors" → six reproducible survivors. **A downward correction is what makes the rest of the report trustworthy** — the counts that never moved are the ones a reviewer rejects.
11. **Check what the transition DELIVERS INTO.** A park exit that re-admits into an unchanged tree, a mint that yields no review, or a fix that lands one layer short of the binding layer has produced nothing (`reg #956`; `reg #937`'s root cause was swept only as far as `sweep()`'s call sites and stopped one layer short).
12. **Do not "re-run your sweep" — ask a NAMED question.** A re-sweep returns the same answer. One precise question — *"which of my assertions reads its expected value from the code under test?"* — is what turned up real defects in the same authors' own patches that night, including one author catching its **own fix** one layer short.
## Automated review — roborev on every commit, including in worktrees
Every commit is auto-reviewed by **roborev**: a `.git/hooks/post-commit` hook enqueues a review job to the local roborev daemon. The reviewer agent is **codex — a deliberately non-Anthropic model**, so the engine is never reviewed by the same model family that wrote it. Git worktrees share the main repo's `.git/hooks`, so commits made by worktree sub-agents are reviewed too. Verify the loop is live with `roborev list` (recent jobs, all `done`) and `~/.roborev/post-commit.log` (the per-repo enqueue trail, incl. `sparq-wt-*` worktrees).
**Install the hook in EVERY repo you commit to — not just `jeswr/sparq`.** <!-- [OPUS-4.8] charter cross-poll #122 --> Worktrees off this repo inherit its `.git/hooks` automatically, but a *separate* clone you commit to does not. So when the agent works in any other repo under the maintainer's namespace (e.g. it lands a fix on a sibling like `jeswr/fetch-rdf`, or scaffolds a new repo), run `roborev install-hook` there at first entry, so no commit in any namespace repo escapes the non-Anthropic reviewer. The hook is cheap and idempotent.
**Read the verdict ASYNC — never block on `--wait`.** <!-- [OPUS-4.8] charter cross-poll #76/#122 --> Because the post-commit hook already auto-enqueues the review, read the verdict asynchronously with `roborev show <sha>` (poll briefly with `roborev list` if the daemon hasn't finished) rather than sitting on a blocking `roborev review <sha> --local --wait`, which stalls the orchestrator loop. The verdict must still PASS before a branch merges (see below); you just don't hold a foreground call to get it. (Consistent with *Orchestration cadence — background by default*.)
Findings must be **addressed, not merely gathered.** A finding is resolved in exactly one of two ways — **never** by merging the branch:
1. **the flagged code changed** — the lines the reviewer objected to were rewritten or removed, so the finding no longer describes anything that exists on `main` (verify against current HEAD, *not* the WIP SHA the review was filed on); or
2. **explicit triage** — it is fixed, beaded as real-but-deferred work (`bd create`), or closed with a written reason (`roborev close <id>`) when it's a false positive.
Squashing a branch into `main` does **not** address its findings: if the flagged code survived the squash it is still live — just orphaned onto a SHA that no longer appears in `git log`, which is *worse* than an open finding on a current commit because it's invisible. So before merging a branch, reconcile its roborev findings against current `main` HEAD and dispose of each (fix / bead / close-with-reason). Periodically run `roborev list --open` and drain the backlog; don't let unaddressed findings accumulate.
**The diff-scoped-review trap — a later PASS does NOT clear an earlier finding.** <!-- [OPUS-4.8] charter cross-poll from PSS #927 --> codex reviews **per commit-diff**, so a finding raised on commit *N* and left unfixed, followed by commit *N+1* that touches *other* code, makes *N+1*'s review come back **clean** — its diff simply doesn't include *N*'s flagged lines — even though the finding is **still latent in HEAD**, now masked by a later "PASS." Never treat a later commit's diff-scoped PASS as clearing an earlier commit's finding. Corollary: a multi-round branch's final PASS only attests to its *last* diff, so before merging you must walk back each prior round's findings and confirm it is genuinely gone from HEAD (re-read the flagged lines), not infer it from the newest verdict. (This is the multi-commit form of the squash-orphaning rule above, and reinforces *reconcile against current `main` HEAD*.)
## Model provenance — tag downgraded-model work for re-review
**Opus 5 (`claude-opus-5`) is the primary top-tier model** (maintainer directive 2026-07-24): it replaces both **Fable 5** (`claude-fable-5`) and **Opus 4.8** (`claude-opus-4-8`) as the head model on every task previously routed to either. When work is instead authored under a **downgraded model** (Opus 5 temporarily unavailable — e.g. a Fable 5 or Opus 4.8 session), tag it so it can be deliberately re-reviewed or regenerated under Opus 5. This is provenance for re-review, not blame.
- **Commit trailer + co-author.** Add a `Co-Authored-By: <model> <noreply@anthropic.com>` trailer for the RUNNING model. Under the Opus 5 primary: `Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>`. Current standing downgrade instances (Opus 5 unavailable): a Fable 5 session authors as **Claude Fable 5** with `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`; an Opus 4.8 session authors as **Claude Opus 4.8** with `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
- **Inline marker on new code/notes.** Under a downgrade, mark substantive new source or design notes with the matching `[FABLE-5]` / `[OPUS-4.8]` inline marker (the downgrade tags), so downgrade-authored spans are greppable for re-review under Opus 5 without trawling `git blame`. (Opus-5-authored work stamps `[OPUS-5]` per the shared-contract provenance rule; it carries no re-review flag — it *is* the reviewing tier.)
- **Carry it into sub-agent briefs.** Every sub-agent brief inherits the same tagging requirement, so worktree-authored commits are tagged too.
The trailer + inline-marker pair *is* the ledger — git history is already greppable (`git log --grep`, `grep -rn '\[FABLE-5\]'`, `grep -rn '\[OPUS-4.8\]'`); do **not** add a separate tracked `MODEL-PROVENANCE.md` doc (it would duplicate git and drift — see Repository hygiene). Under Opus 5, sweep the `[FABLE-5]` / `[OPUS-4.8]` markers/trailers and re-review or regenerate as warranted. Existing markers are accurate HISTORY — never rewrite them.
## Monitor CI after every push to main — and fix red
A local gate is necessary but NOT sufficient: CI runs on a clean checkout with different toolchains/targets/feature-unification than your incrementally-built box, so it catches things your local gate cannot (e.g. a `cargo test --workspace` that includes a crate your local gate `--exclude`s; a wasm-target-only lint; an action container with an older cargo). Therefore: **after pushing to main, watch the CI runs to completion and fix any failure immediately** — a red main is a stop-the-line condition. `gh run watch <id> --exit-status` (background) or `gh run list --branch main`; on failure `gh run view <id> --log-failed`. Roll the fix into the next push and re-watch. Do not pile more pushes onto a red main.
## Maximise parallelism — many agents + extra compute
Aim to keep **as many sub-agents running in parallel as possible** without (a) conflicting work or (b) competing for the same resource. Partition by **file/area ownership** so no two concurrent agents edit the same file (give each a distinct crate/dir; wire shared files — `ci-bench.sh`, `benchmarks.toml`, workflows, `AGENTS.md` — centrally afterward). The only things to serialise: two agents that must touch the same file, and CPU-heavy **wall-clock measurements** (benchmarks need a quiet box — read-only/light analysis parallelises freely). Don't sit at one or two agents when the `bd ready` set has more independent work — fan out.
**The fan-out ceiling is not only the box — the model API rate-limits aggressive bursts too.** <!-- [OPUS-4.8] charter cross-poll from PSS: API-rate-limit fan-out ceiling --> Beyond the CPU/box constraints above, firing a large batch of agents at once can trip the **Anthropic API's** rate limit (a sibling observed ~10 concurrent agents → transient "Server is temporarily limiting requests"). So keep a **full-but-sustainable** steady fleet and **refill as agents land**, rather than launching a dozen in a single burst — a thundering herd backs off and is slower than a sustained pool. This is the API limit, distinct from the per-box CPU budget (and from the EC2 cost cap); both apply.
**Extra compute:** you MAY launch additional **EC2 instances** to run work in parallel (e.g. the heavy full-scale benchmark tiers, large fuzzing, coverage). Follow the EC2 rules in memory: orphan-proof self-terminate is MANDATORY (instance-initiated-shutdown=terminate + a user-data watchdog), ~$5/day cap, tag `purpose=sparq-bench`/`sparq-dev`, **never touch the prod or dev boxes**, and a fresh session must orphan-check first. **If AWS SSO has expired** (`aws sts get-caller-identity --profile pss` fails), you cannot launch instances — raise a `needs:user` item (below) asking the user to re-auth, and keep surfacing it until they do.
**Where this runs — the session executes ON an AWS EC2 instance (the persistent work box).** <!-- [OPUS-4.8] execution-environment clarification --> The orchestrator and every sub-agent run on an EC2 instance (confirm with `systemd-detect-virt --vm` → `amazon`, or query the EC2 instance-metadata service, e.g. `curl -s http://169.254.169.254/latest/meta-data/instance-id`; note `uname -r` showing `…-aws` only reflects the AWS-tuned kernel flavour and is not a reliable provider check across distros/AMIs), not a local laptop and not a throwaway bench instance. Three things to never get wrong: (1) **a wall-clock/throughput number measured on this box is NON-CANONICAL** — never bake it into markdown or into a test's expected values; gate only DETERMINISTIC metrics (byte/triple/gate counts), keep timings advisory, and treat a controlled quiet box or the CI runner as the authoritative perf source (this is *why* the timing perf-gate is advisory and *why* "no hard-coded performance numbers" exists). A benchmark/CI-suite agent must assert deterministic invariants (e.g. closure **triple counts**), not timings it measured here. (2) **This box is the work box, NOT a throwaway** — never self-terminate it; the orphan-proof self-terminate rule + orphan-checks apply only to bench instances you explicitly *launch* for measurement. (3) **It is not a CI runner** — benchmark/conformance suites run in GitHub Actions on clean ephemeral runners; running gen/run here is only quick local verification.
**Build-profile honesty guard.** <!-- [SONNET-4.6] sq-6vshe.10 --> The `release-fast` Cargo profile exists only for optimized correctness/smoke lanes and local iteration. Never use it for a measured lane: canonical EC2 benchmarks, perf-gate ratchets, published performance numbers, benchmark gatherers, and shipped artifacts must use the shipping `release` profile (or the explicitly shipping `release-wasm` / `python-release` profile). Results produced with `release-fast` are not valid performance claims.
**Deploy-image hygiene.** <!-- [OPUS-4.8] charter cross-poll #121 --> Build the published `sparq-server` container (`ghcr.io`, SHA-pinned base — see *Scorecard supply-chain* above) from a **minimal, artifact-only build context** — the compiled server binary plus its runtime config — never the repository root, so source and any local secrets cannot ride into the published image layers. (The release workflow's `docker buildx` job already scopes its context this way; keep it that way.)
## Inputs needed from the user — the `needs:user` bead queue
Anything blocked on a **human decision, credential, or out-of-repo action** (re-auth SSO, enable GitHub Pages, approve a destructive step, a product decision) is tracked as a **bead labelled `needs:user`**, with the exact ask in the description. This is the standard human-in-the-loop pattern (a dedicated review/blocked-on-human queue) mapped onto the existing tracker, so nothing waiting on the user is lost in the chat scroll. List them with `bd list -l needs:user`. An orchestrator should **surface the open `needs:user` items in its responses** (concise, at the end) until the user resolves them, so they can be actioned whenever the user is next available.
## Maintenance loop — the repeat-tasks sweep (the concrete instantiation of the above)
This is the standing orchestration loop that ties the sections above together. Run it as a sweep; it is **lightweight — a no-op pass when nothing is actionable.** Each step is the concrete form of a rule already stated above.
> **The loop is a SAFETY NET, not the cadence.** Any scheduled sweep (a `/loop`, a cron, a periodic tick) exists only to *guarantee forward progress when nothing else is driving* — it is the floor, not the clock. The **primary mode is event-driven and eager**: act the **instant** something is ready, never wait for the next tick.
> - A PR's `ci-summary` goes green + its review threads are resolved → **merge it now.**
> - A sub-agent finishes → gate + land it and **fan the next unblocked bead now.**
> - A bead unblocks (its blocker merged) → **pick it up now.**
> - CI on a push to `main` completes → **check/fix now.**
>
> To stay event-driven, **keep a CI watcher armed on every in-flight PR** (`gh run watch <run-id> --exit-status` in the background) so its completion notifies you and you can act immediately — do not let a green PR sit undiscovered until the next sweep. If you ever find yourself thinking "the next loop tick will merge this," that's the bug: merge it now. When a sweep does fire, it should usually find little to do because the event-driven path already handled it.
0. **Reconcile first (cheap, every pass).** `git worktree list` + `gh pr list`; reap finished-but-unnotified worktree agents (a committed branch + no live `cargo` process = done → open its PR); merge any PR that is `ci-summary`-green **and** has all review threads resolved (squash, delete branch, then watch main CI); rebase any PR gone stale/red. (See *Orchestration cadence* + *Contribution workflow*.)
- **Disk guard (run EVERY tick — `scripts/disk-guard.sh --apply`).** <!-- [OPUS-4.8] sq-4vo9m per-tick disk guard --> The autonomous loop spawns one worktree-isolated agent per bead, each accumulating a multi-GB `target/`, so disk re-fills every wave — left unguarded the work box hit 99% / ENOSPC and crashed a `--workspace` verify build mid-run. So each tick run `scripts/disk-guard.sh --apply`: it `df`-checks `/` (warns when **< 20 G** free, escalates **< 10 G**), then **delegates the worktree prune to `worktree-gc.sh`** (which only ever removes a worktree whose HEAD is in `origin/main` or whose branch is gone-on-origin, with no dirty/unpushed work — **never an active one**, never the main checkout — and, with the `--reclaim-completed` path the guard passes through by default (sq-h34dc), also a **completed-but-unmerged** workflow worktree that is clean, fully pushed, workflow-named, and not in use). When the disk is genuinely **CRITICAL** (< 10 G) and you opt in (`--reclaim-main-target`), it also drops the **orchestrator main checkout's `target/`** — regenerable, because impl agents build in their OWN worktrees — but only after confirming no live `cargo`/`rustc` build references the main checkout (so it can never abort an in-flight build). It is **non-fatal** (a guard tick never aborts the sweep) and **dry-run by default** (it only mutates with `--apply`); its advisory exit code encodes the disk state (0 OK / 10 WARN / 20 CRITICAL). The `autonomous-scheduler` runs this same guard before each wave and **backs off dispatch under pressure** (WARN → ≤ 1 new agent; CRITICAL → dispatch nothing that wave, let the prune/reclaim take effect, re-measure next tick). (See the script catalog below; supersedes the bare "run `worktree-gc.sh --apply` at idle" advice — the guard wraps it and adds the per-tick `df` check + escalation.)
- **Reconcile merged-but-still-open beads (run EVERY tick — `scripts/reconcile-merged-beads.sh`).** <!-- [OPUS-4.8] sq-13uyp reconcile merged beads --> push-frontier's in-flight exclusion (sq-7mwun) subtracts beads with an **OPEN** PR, but it misses a bead whose fix already **MERGED** and was simply never `bd close`d — that bead stays on the launchable frontier and an agent gets dispatched only to find the work done (2026-06-21 sq-bpoey: merged via #1017, left open, ~32k tokens wasted). This script is the **COMPLEMENT** to that exclusion: for each OPEN bead it greps the **MERGED** signal (merged-PR titles + head-branches via `gh pr list --state merged`, plus `origin/main` commit subjects) for the bead's **exact dotted id token** and reports the matches as close-candidates (bead id + the merging `#N`/commit). Together: sq-7mwun handles OPEN-PR (in-flight) beads, sq-13uyp handles MERGED-but-bead-still-open beads — both keep the frontier free of beads that must not be re-dispatched. It is **conservative** (exact-token match only — `sq-ixc3.1` never matches `sq-ixc3.11`; OPEN-status beads only) and **NEVER auto-closes an epic, an umbrella-parent (≥ 1 dependent), or a `needs:user`/`needs:maintainer`/decision bead** — those are reported for **manual review** instead. **Default is dry-run** (report only; mutates nothing); the orchestrator reviews the candidates and applies the closes separately (`--apply` closes matched non-gated beads with note `reconcile: fix merged via #N`). It is idempotent and **fail-safe** (a per-bead lookup error skips that bead; an empty merged signal closes nothing). Pinned by `scripts/tests/test_reconcile_merged_beads.sh`. (See the script catalog below.)
1. **Charter cross-pollination.** *Pull:* fetch each sibling charter (`gh api repos/<sibling>/contents/AGENTS.md --jq .content | base64 -d`) + the open cross-pollination issues on this repo; fold genuinely-portable conventions into THIS file — **adapted to sparq** (cargo/clippy `-D warnings` + the W3C-conformance + best-ever-perf ratchets as the gate; roborev/codex as the reviewer; beads; the crate/wasm/CLI/HTTP/Py/JS surfaces) — via a PR, conservatively; then close/comment the issue. *Push:* for any convention this charter gains that a sibling lacks, file an issue (or PR) on the sibling repo. *Watch:* poll the cross-repo threads YOU opened/commented on (the sibling's issues + PRs) for follow-up replies and answer them, self-identifying as the SPARQ agent. No-op when there is no charter drift and no open thread awaits a reply. (See *Cross-pollinate the charter with sibling repos*.)
2. **Screen + triage inbound work — open issues, code-scanning alerts, deps, roborev.**
- **Open issues** (`gh issue list`): screen every one. Many are filed by the **PSS agent** — the agent developing the private sibling `jeswr/prod-solid-server`, which consumes sparq as its triplestore/server; **"PSS" anywhere in an issue refers to that codebase.** For each actionable issue: capture it as a bead (`bd create` with the issue as `--external-ref`, priority by the issue's stated severity — a "SHOWSTOPPER" → P0/P1) and drive it through the loop; comment on the issue with the bead id + status; close it when the work lands (referencing the merged PR). If an issue is **unclear, do not guess — post a clarifying reply on the issue** (the PSS agent monitors and responds), and leave it open.
- **Issue-close policy (maintainer's standing rule).** <!-- [OPUS-4.8] standing issue-close policy --> **Close a GitHub issue once code resolving it is on `main`; ALWAYS comment a link to the resolving PR before closing.** Do not close on PR-open or on a green-but-unmerged PR (the fix is not yet on `main`), and never close silently — the linking comment is how the owner and sibling agents trace resolution. (This is the triage-side restatement of *Contribution workflow* step 5.)
- **Issue triage → beads is a RECURRING sweep, not a one-off.** Each maintenance pass, walk the *whole* open-issue list and **categorise every issue against the beads tracker + the live code**, then act on the category: <!-- [OPUS-4.8] codify recurring issue↔bead reconciliation -->
- *resolved-closeable* — already fixed on `main`: **close it, citing the merged PR/commit** (`git log`/`git grep` to confirm the fix actually landed before closing).
- *has-bead* — already tracked: comment the bead id + current status, leave open.
- *needs-new-bead* — a genuine gap with no bead: **`bd create` it** (issue as `--external-ref`, severity → priority) and link it back on the issue.
- *needs-user* — blocked on a human decision/credential/out-of-repo action: mint/route a `needs:user` bead and **surface it** (step 7), leave the issue open.
- *chore* — a small bookkeeping/doc task: bead it as a chore.
- *duplicate* — close with a pointer to the canonical issue/bead.
This keeps **issues and beads in sync** in both directions — no resolved issue lingers open, no real gap goes un-beaded. Do not silently skip an issue; every open issue lands in exactly one category each sweep.
- **Code-scanning alerts** (`gh api repos/<owner>/<repo>/code-scanning/alerts`): screen open CodeQL/Scorecard alerts and **resolve each** — fix the flagged code, or dismiss with a written reason when it is a genuine false positive (never leave one silently open). Keep the open-alert count at **zero** (the security/quality posture from *Contribution workflow*).
- Also scan **Dependabot PRs + open roborev findings + the siblings** for recurring or again-needed work → `bd create` (never hand-edit `.beads/`).
3. **Drive `bd ready` (the engine) — dispatch FROM the launchable-bead frontier.** <!-- [OPUS-4.8] codify bead-driven dispatch --> The standing answer to "use beads to kick off agents": each orchestration tick, consult the **launchable-bead frontier** via `scripts/push-frontier.sh` and **dispatch the READY beads off that frontier** — not only hand-picked work. push-frontier already subtracts in-flight beads and applies the **conflict-partition (≤ 1 bead per crate/surface; `site` and the sparq-server `server-auth` path → ≤ 1)**; pipe its output through the **`sparq-workload-triage` placement agent** for the local↔EC2 compute tiering (LOCAL packed onto the work box to the cargo-slot cap; HEAVY measurement beads bin-packed onto the EC2 build farm). Pick the largest set of unblocked beads on **disjoint file-areas**. **If you fall back to a raw `bd ready` list** (push-frontier unavailable, or you blend in extra beads), you MUST apply that same ≤ 1-per-crate / server+site→1 dedup to the **COMBINED** push-frontier + bd-ready set — never dispatch the bd-ready fallback un-deduped, or two beads on the same crate launch and conflict (sq-8rpq: this once dispatched two sparq-server beads at once). Delegate each survivor to a **background** worktree sub-agent (smallest context-independent brief; gates in-worktree scoped to its crates; `bd create`s discovered work; does **not** push). Maximise parallelism — serialise only (a) two beads touching the same file and (b) CPU-heavy perf/benchmark **measurements** (those need a quiet box). Cap concurrent *cargo-heavy* agents to the core budget; doc/research/config agents parallelise freely. (See *Maximise parallelism*.) **End-state:** the fully self-driving form of this step — a scheduler that runs the frontier + placement + dispatch with the orchestrator out of the per-agent loop — is epic **sq-sgu1** (design: [`research/autonomous-scheduler-design.md`](research/autonomous-scheduler-design.md)), MATERIALISED as the committed, re-runnable [`.claude/workflows/autonomous-scheduler.js`](.claude/workflows/autonomous-scheduler.js) Workflow (see its script-catalog entry below): run a tick with `Workflow({ name: "autonomous-scheduler" })`. **When you instead run the frontier by hand** (the scheduler is off, or you are hand-dispatching), apply the same dedup + the `proceed-and-document` skill per agent as described here.
4. **Land each finished agent via a PR — one through merge at a time.** The orchestrator pushes the branch, opens the PR, requests Copilot review. **`ci-summary` is the authoritative full gate** (workspace clippy `-D warnings` + `cargo test` + SPARQL/SHACL/inference ratchets + coverage ratchet — monotonicity + test-presence always enforced, measured line-coverage enforcement restored in #680 — + best-ever perf floor) — **read CI; do not re-run the heavy gate locally.** Address every Copilot **and** roborev finding (fix it, or reply with the decline reason) and resolve the thread; merge only when `ci-summary` is green **and** all threads resolved; squash; `bd close`; re-export beads in a bookkeeping PR. (See *Contribution workflow* + *Automated review*.)
5. **roborev hygiene.** Before merging a branch, reconcile its roborev (codex, non-Anthropic) findings against current `main` HEAD — never assume a merge cleared them; fix / bead / close-with-reason.
6. **Watch main CI after every push; a red main is stop-the-line** → fix it before any further merge. (See *Monitor CI after every push to main*.)
7. **Surface the `needs:user` queue at pass end** (`bd list -l needs:user`) — owner-only items (branch protection, SSO, Pages, upstream-PR filings); never block the loop on them.
**Done when** `bd ready` minus `needs:user` is empty and no PR / agent / CI is in flight.
### Orchestration automation (mostly manual-invoke scripts + one durable CI; mechanical substrate for the loop)
The deterministic, no-judgment parts of the loop above are factored into small shell
scripts under `scripts/`. They follow the mechanical-vs-judgment boundary set out in
`research/orchestration-automation-design.md` (PR #374): **automate the DETECTION and
the bookkeeping; never automate the DECISION.** The first three (PR #374 Phases A/C/F)
are shipped; `worktree-gc.sh` (sq-6xdr) is a later addition that follows the same
discipline (dry-run default, mutation behind `--apply`). These shell scripts are
**invoked manually**. The **one piece of auto-running, mutating automation that IS wired**
is the durable bead-autoclose CI (`.github/workflows/bead-autoclose.yml`, sq-84a8, below) —
it replaced an ephemeral session-scoped watcher (`b1kzhfxq5`) that did not persist across
sessions, so beads stayed `in_progress` after their PR merged and the orchestrator closed
them by hand each tick. The other deferred hooks (a `SessionStart` orphan-check hook, a
`PostToolUse` bead-export hook) remain a documented follow-up in the design doc's phased
plan (§5, Beads D/E + H), to be added only after the scripts are proven in manual use. Each
shell script is `bash -n`/`shellcheck` clean and carries a `--dry-run-self-test` (hermetic;
no network); the Python close-script carries a `--self-test`.
### Hermetic suites must be UNABLE to reach the network — poison the runners <!-- [OPUS-5] #4652 -->
A "hermetic" test suite that merely *intends* to inject fakes is not hermetic. `def __init__(self, ..., gh=run_gh)`
binds the runner **at definition time**, so patching `module.run_gh` from a test cannot reach it — and a suite
whose reads were faked while its writes were real posted **567 real comments to a production PR** across a
mutation sweep (#4652). Two rules follow:
- **Late-bind injected collaborators** (`gh=None` → resolve inside `__init__`), never a module-level default arg.
- **Poison the real runners at test-module import** so any path that forgets to inject raises instead of
reaching the network, and add a test asserting the poison is in place. Restore **every** patched name in
`finally` — a block that restored one of two leaked a stale fake into every later test.
**Never exercise a write path against a live production PR/issue.** Use a scratch object you own, or a fixture;
if a test needs a real remote value, *read* it. And when a probe reports hostile input, **verify the author
before reporting an exploit** — a false attack report costs twice: unwarranted alarm now, and a discounted
warning when the real thing arrives.
**Mutation harnesses need a preflight.** A greedy edit silently deleted 15 mutants and the harness reported a
clean total — a dropped mutant is indistinguishable from a killed one, so the number improves as coverage
disappears. Fail the sweep outright on a duplicate id or an anchor that no longer exists in the tree, count a
`SyntaxError` as a broken mutant rather than a kill, and prefer a test-file traceback frame over a crash frame
(the first failure is arbitrary).
- **`.github/workflows/merge-group-watchdog.yml` + `scripts/merge-group-watchdog.py`**
(#4652) — the **durable** zero-dispatch merge-group recovery. GitHub occasionally builds a
merge-group ref and then never dispatches the `merge_group` event for it: **zero
check-suites, zero check-runs, zero workflow runs**. Because the queue merges strictly in
order and each entry needs its **own** group's required check (a green *superset* group does
**not** substitute), that entry holds position 1 until the ruleset's 60-minute
`check_response_timeout_minutes` reaps it, and the rebuild discards every group stacked on
top. Observed three days running (#4331, #4534, #4709), ~60 min each. The sweep runs on a
5-minute cron (never on a PR head, so it can never gate), and after a **120 s** grace —
30× the measured maximum create→first-suite latency (N=209: p50 2 s, max 4 s; the failure is
categorical, not slow) — it dequeues and re-enqueues the entry. Bounded (1 per group head
SHA, 2 per PR per 6 h, 2 per run) and idempotent. On exhaustion it hands back to the platform
timeout, which **does demote to `review:changes`** — by then the only trusted observation names
a superseded group head, so preserving the verdict would be unsound, and three consecutive
zero-dispatch groups on one PR warrant a human look. It never escalates to a `needs:user` hold
and never permanently stalls. **Everything it cannot positively establish is a
refusal, never a recovery.** The marker it writes is **evidence**, so its AUTHOR is part of
the predicate: markers are honoured only from `github-actions[bot]` / `sparq-orchestrator[bot]`
(`TRUSTED_MARKER_AUTHORS`, enforced at the read), because sparq is public and a marker forged
by any commenter would otherwise carry `review:pass` through a dequeue and reach
`gh pr merge --auto`. The recovery also reads its own marker back before dequeuing and refuses
to act if it is not there — a runner whose identity is missing from that list cannot see its
own markers, which would silently disarm both caps. It emits one row per entry **every tick**, carrying the ref, the
suite count, the decision, and `stacked=`/`stacked_green=` — the count of groups built on top
of the dead ref, which is the real cost term and the watchdog's own effectiveness measure.
Companion routing split in `merge-queue-feedback.yml`: a `CI_TIMEOUT` whose group ref had
**zero suites** preserves `review:pass` instead of manufacturing a re-review for a platform
event drop, while a timeout that followed checks that genuinely ran keeps demoting — the two
are separated by the **suite count**, never by the reason alone. The same split makes an
INFRASTRUCTURE dequeue verdict-neutral: measured on #4709, `review:pass` was swapped for
`review:changes` **17 seconds** after a `MANUAL` dequeue, so a watchdog without this would
burn the verdict of every PR it rescued. `MANUAL` conflates "a reviewer withdrew this" with
"infrastructure moved it", so the discriminator is a fresh watchdog marker for that exact
group head — evidence, never the event name — and **a genuine human dequeue still strips the
verdict**. On every preserve route the verdict may only survive if the head has **not moved
since it was granted** (a commit, a force-push, or a revoked label after the grant forfeits
it): never restore a verdict onto a tree it was not given for.
**Runbook — resolving and clearing a dead ref by hand.** The group head is the queue entry's
`headCommit{oid}` (`MergeQueueEntry` has **no** `headOid` field), or read the live refs with
`git ls-remote origin 'refs/heads/gh-readonly-queue/main/*'`; the trailing sha in a ref name
is the group's **BASE**, and the ref is named for one *member*, so **absence of a ref named
for a PR is not evidence its CI never started**. Test `commits/<head>/check-suites` —
`total_count == 0` is the signal, and never `check-runs` (a `paths`-filtered workflow that
matches nothing still creates a suite). To clear it, `gh pr merge --disable-auto` does **not**
dequeue an already-queued PR; use `dequeuePullRequest`, whose input field is named `id` but
wants the **pull request** node id (`PR_…`), not the entry id (`MQE_…`).
- **`.github/workflows/bead-autoclose.yml` + `scripts/ci-close-merged-beads.py`**
(sq-84a8; issue-native since #2475) — the **durable** auto-close-on-merge. On a merged PR
(`pull_request_target: [closed]` gated on `merged == true` — base-repo context so the
`issues: write` token survives fork PRs; safe because the job checks out pinned `main`
and never executes PR-controlled code),
it extracts the `sq-XXXX(.NN)` bead token(s) from the PR title + merge-commit subject and
closes the **migrated GitHub issue** each bead maps to (resolved via the migration's
`<!-- bd-id:sq-… -->` body marker; `gh issue close --reason completed`, plain
`issues: write`). The original JSONL-commit-back design never persisted — the default
`GITHUB_TOKEN` cannot push to protected `main` (GH013 ruleset rejection, sq-roe3), and a
PR-based write from that token would hang forever because `GITHUB_TOKEN` events trigger
no workflows, so `ci-summary / gate` would never report. Epic (`kind:epic`) /
human-gated (`needs:*`) issues are never auto-closed; a bead with no marker-carrying
open issue (not yet migrated, or already closed) is a logged no-op, covered by the
manual `reconcile-merged-beads.sh` sweep. The script retains the minimal in-place
`.beads/issues.jsonl` edit mode for orchestrator use outside CI (CI passes
`--skip-jsonl`). It runs AFTER merge, so it is **NOT a gate** and never registers as a
required check (`ci-summary / gate` polls the PR head while the PR is OPEN; this
workflow does not trigger on the open-PR events). The merge is verified against the
GitHub API as defense-in-depth on top of the `merged == true` event gate.
- **`scripts/bead-close-on-merge.sh <pr> [--apply]`** (Phase A) — the **manual** sibling of
the bead-autoclose CI, for orchestrator use outside CI: closes the bead a PR
maps to, but **only after verifying the merge against the API** (`gh pr view --json
mergedAt` must be non-null; a parsed log/monitor line is never the source of truth).
Resolves the bead id from an `sq-XXXX` token in the PR title or in a linked issue's
title/body. **Default is dry-run** (prints what it *would* close); acts only with
`--apply`; idempotent (a bead already closed is a no-op). The guardrail makes the
dangerous case — closing a bead for a PR that did **not** merge — impossible.
- **`scripts/orphan-check-bench.sh [--apply] [--region r]`** (Phase C) — lists
running/pending EC2 instances carrying the **exact** tag `purpose=sparq-bench`
(allow-list semantics, not deny-list) and greps the local process table for in-flight
`gather-*` launchers. **Default is dry-run** (prints orphans only); `--apply`
terminates **only** tag-matched instances and **never** the prod (`i-090531b4ede8f2d3f`)
or dev (`i-00f76802f345b6b77`) box — those are a hard, unconditional exclusion list,
asserted by the self-test. Degrades to a graceful no-op when `aws` is unconfigured.
- **`scripts/refill-candidates.sh`** (Phase F) — **read-only, advisory only.** Lists
`bd ready` beads grouped by inferred crate/surface and flags surfaces that already have
an open PR or in-flight worktree (contention). It is the **substrate** for the refill
decision (loop step 3), **not** the decision: it never dispatches, closes, or mutates
anything. Surface inference and contention flags are heuristic (free-form branch names)
and advisory by design. **Contention is reserved by open PR + worktree branches with
UNPUSHED local commits only — not every git worktree branch.** The harness never
auto-removes a finished agent's worktree, so hundreds of stale branches accumulate;
reserving on *all* of them once reserved every crate and made the launchable frontier
spuriously empty (sq-8rpq). A pushed / squash-merged branch is ignored (we use the
UNPUSHED test, **not** "ancestor of `origin/main`" — squash-merged feature branches are
*not* ancestors of main yet *were* pushed, so an ancestor test would re-introduce the
bug). Run `worktree-gc.sh --apply` at idle so stale worktrees do not pile up.
- **`scripts/push-frontier.sh`** — the read-only **decision layer** on top of
`refill-candidates.sh`. Prints the beads SAFE TO LAUNCH NOW: `bd ready` **minus**
in-flight beads (open PR, or a worktree branch with unpushed commits — same signal as
refill, not every branch) **minus** conflict-collisions (the conflict-partition: at
most **one bead per crate/surface**, with `site` and the sparq-server `server-auth`
http.rs path serialised to ≤ 1) **minus** epics, then capped at the CPU ceiling
(`min(16, nproc-2)`). **The conflict-partition is canonical — it must be applied to the
COMBINED launch set, not bypassed by a raw `bd ready` fallback.** If you ever dispatch
from `bd ready` directly (push-frontier unavailable), you MUST still apply the same
≤ 1-per-crate / server+site→1 dedup over the COMBINED push-frontier + bd-ready set, or
two beads on the same crate launch and conflict (this gap once dispatched two
sparq-server beads at once — sq-8rpq). Carries an `--explain` (per-bead keep/drop
reasons) and a hermetic `--dry-run-self-test`.
- **`scripts/pr-area-labels.py`** + **`ci/area-labels.toml`** — the thing that makes the
conflict-partition *work on PRs*. <!-- [OPUS-5] reg#677 --> The scheduler partitions by
`area:<name>`: an in-flight PR **reserves** its areas and a ready issue **defers** while
any of its areas is reserved — so a PR with **no `area:` label** maps to the serializing
`__global__` partition and defers **every** issue, whatever crate it names. Measured
2026-07-26 (registry #677): **84 of 87 open PRs carried no `area:` label**, because
nothing in the pipeline ever applied one; the live chain was candidates 12 → frontier 3
→ lease 3 → max_concurrent 8 → account pool 28, i.e. **28 account slots idle while 3
workers ran**, and raising `max_concurrent`/`package_width` measured **net +0** workers
(registry #689). This deriver reads a PR's changed paths and applies the `area:` labels
the paths imply — `crates/<name>/…` → `area:<name>` implicitly, everything else from the
reviewable `[[map]]` table in `ci/area-labels.toml`. It is **additive only** (a
human-applied `area:` is never removed), **idempotent**, **never creates a label** (the
add-labels REST call silently would, so a typo'd area is dropped with a `::warning`
instead), and **fail-closed**: unresolvable paths or more than `[policy] max_areas`
distinct areas keep the PR on `__global__` — *unclassified* and *genuinely cross-cutting*
are now distinguishable, which is the actual bug. The changed-file list is the one input
attribution cannot check for itself, so it is enumerated with the **paginated REST**
endpoint *and* cross-checked against `changedFiles`; any disagreement is
`incomplete-paths` → `__global__`. (`gh pr view --json files` is GraphQL
`files(first: 100)` and **silently truncates** — measured on PR #3581: `changedFiles`
646, `--json files` 100. A truncated list derives a proper **subset** of the true areas,
and a too-narrow reservation puts two workers on one crate, where a too-broad one merely
delays. Renames consume `previous_filename`, so a cross-crate move implicates both
ends.) `--dry-run` is the default; `--apply`
mutates; `--backfill` sweeps every open PR (use `--pace` — each label add fires a
`pull_request: labeled` event that ci/bench/fuzz/feature-matrix all react to). Wired to
every PR by `.github/workflows/pr-area-label.yml` (least-privilege
`pull-requests: write`, checkout pinned to the **default branch** so a privileged token
never runs PR-authored code, explicit fork read-only-token path, and **no `if:` at any
level** — every skip decision is in Python, where a mutant dies). Guards:
`scripts/tests/test_pr_area_labels.py`, run by `routing-self-tests.yml`.
**When you add a top-level directory, add a `[[map]]` row** or every PR touching it
silently starves the frontier again — the totality test over `git ls-files` is what
catches this.
- **`.claude/workflows/autonomous-scheduler.js`** (epic sq-sgu1) — the **self-driving
bead-frontier loop**, MATERIALISED as a committed, re-runnable harness Workflow so it
survives a session restart. <!-- [OPUS-4.8] durable scheduler --> Each wave it reads the
launchable frontier (`scripts/push-frontier.sh`), dispatches one isolated-worktree impl
agent per dispatchable ready bead, adversarially verifies each PR, **arms only the clean
low-risk ones** (honesty/soundness-sensitive surfaces stay OPEN for the maintainer), then
re-reads the frontier (newly-unblocked beads appear) and repeats until the frontier is dry
or the per-run cap / token budget is hit. Run it any tick with
`Workflow({ name: "autonomous-scheduler" })`; override the per-run cap or focus list via
`args` — e.g. `Workflow({ name: "autonomous-scheduler", args: { maxBeads: 6 } })` (also
takes `only: ["sq-…", …]`). It **codifies** loop steps 3–4 (drive `bd ready` → land each
PR) so the orchestrator stays out of the per-agent dispatch loop; it is bounded by
`maxBeads` so it cannot flood, and impl agents never branch-switch the shared checkout.
**Before each wave it runs `scripts/disk-guard.sh --apply --reclaim-main-target`** (sq-4vo9m)
and **backs off dispatch under disk pressure** — WARN caps the wave to ≤ 1 new agent,
CRITICAL skips dispatch entirely that wave (the guard already pruned/reclaimed; it
re-measures next tick) — so the loop's own per-agent `target/` dirs cannot ENOSPC the box.
Its recurring per-agent briefs **delegate** their judgment rule to a committed skill rather
than re-stating it inline — the proceed-on-decision standing rule is the
[`proceed-and-document`](.claude/skills/proceed-and-document/SKILL.md) skill, referenced by
name from `implPrompt()` + the Frontier brief so a hand-dispatched agent inherits the same
rule. **Durability contract (the standing rule for EVERY durable workflow/skill — sq-lhwo.3):**
a durable workflow/skill is durable only if it has ALL THREE — (1) **committed** under
`.claude/`, (2) **linked from `AGENTS.md`** (this script catalog for a workflow; the rule it
codifies for a recurring procedure — e.g. the *STANDING RULE* section above links the
`proceed-and-document` skill), and (3) a **one-line description** (a workflow's `meta.description`;
a skill's frontmatter `description`) so it is discoverable by name. Missing any one and it
gets silently re-improvised. (This is why this entry, the *STANDING RULE* link, and the skill
frontmatter all exist — closing the old gap where the scheduler header claimed an AGENTS.md
link the Maintenance-loop section did not yet carry.)
- **The escalated-tier workflow trio** (`sq-sgu1.2`; design: [`research/fable-work-plan.md`](research/fable-work-plan.md) §6.3)
<!-- [OPUS-5] sq-sgu1.2: the durability contract needs the LINK. All three had a
meta.description but NO AGENTS.md link — fable-architect-drain was mentioned only by name
(rules 5/11) and the other two not at all — so they were re-improvised rather than re-run.
Catalog entry added; pinned by scripts/tests/test_workflow_dispatch_contract.py. -->
— three committed Workflows that spend the scarce escalated tier (Opus 5 primary) only where
judgment is the product, with the cheap fleet doing the mechanical middle. **Every producing
stage of each carries a schema** (the schema-guard trap), research fan-out is findings-only so
a recon stage never opens its own `research/` PR (the researcher-PR gotcha), and each arms
strictly by iterating the **verdict objects** — never a blind PR-number loop:
- [`.claude/workflows/fable-architect-drain.js`](.claude/workflows/fable-architect-drain.js)
— the flagship epic drain: escalated-tier architect decomposes an epic into N **disjoint**
beads (one call/epic) → cheap recon grounds each spec → the fleet implements one isolated
worktree per bead → cheap mechanical verify arms the clean ones → the escalated tier reviews
only the escalated diffs, including the rare `fable_implements` branch where it authors the
fix itself and re-enters mechanical verify. It also carries the **canonical per-tier
marker/trailer/dispatch TIER table** that rule 5 and every brief point at.
`Workflow({ name: "fable-architect-drain", args: { epic: "sq-…" } })`.
- [`.claude/workflows/fable-soundness-verdict.js`](.claude/workflows/fable-soundness-verdict.js)
— clears the pile of OPEN honesty/soundness PRs the cheap loop deliberately leaves for
stronger judgment: cheap recon assembles a compact per-PR evidence pack, the escalated tier
returns one verdict per PR, and the glue **holds** anything `honest=false` or touching the
PENDING external ZK audit (`sq-qhy4`) for the maintainer regardless of the other fields.
`Workflow({ name: "fable-soundness-verdict" })` (or `args: { prs: ["<url>", …] }`).
- [`.claude/workflows/fable-lens-review.js`](.claude/workflows/fable-lens-review.js)
— the cadenced review campaign, one **lens** at a time (privacy-claims, unsafe-sites,
perf-honesty, coverage-ratchet, …): cheap recon enumerates the lens's targets, the mid tier
builds claim→evidence→status tables, the escalated tier adjudicates ONE subtle question off
that tight table, and the fleet files the follow-up beads via `bd create`.
`Workflow({ name: "fable-lens-review", args: { lens: "privacy-claims", question: "…" } })`.
- **`scripts/worktree-gc.sh [--dry-run | --apply] [--reclaim-completed]`** (sq-6xdr, sq-h34dc) — a **manual / idle-time**
broom for the harness's `.claude/worktrees/` dirs. The harness creates one git worktree
per agent but never auto-removes a finished one, so they pile up (366+ this session) and
each carries a multi-GB `target/` build dir that fills the disk. The script enumerates
`git worktree list --porcelain` and classifies a worktree SAFE-to-remove **only** if ALL
hold: its HEAD is already an ancestor of `origin/main` (merged — nothing to lose) **or**
its branch is gone-on-origin with HEAD still reachable from a remote ref; `git status
--porcelain` is empty (no uncommitted/untracked work); it has **no** unpushed commits
(a never-pushed branch is treated as unpushed ⇒ kept); and it is **not** the main
checkout (`/home/ubuntu/sparq`, a hard unconditional exclusion, asserted by the
self-test). It is **allow-list by location** (only paths under `.claude/worktrees/` are
ever candidates). **Default is dry-run** — prints the safe set with per-worktree reasons
and a `du -sh` reclaimable-size estimate; `--apply` does `git worktree remove --force`
the safe set then `git worktree prune`. Run `--apply` **at idle, not while sibling agents
are building** (the predicate cannot misclassify a busy worktree, but removing one
mid-build aborts that build). When in doubt it KEEPS. **`--reclaim-completed`** (opt-in here,
ON when invoked via `disk-guard.sh`; sq-h34dc) widens the broom to also sweep a
**completed-but-unmerged** workflow worktree — CLEAN + no unpushed commits + workflow-named
(`wf_`/`agent-`/`worktree-wf_`/`worktree-agent-`; a human-named `feat/` branch is never swept)
+ not in use (a `/proc/<pid>/cwd` scan that KEEPs any tree a live process touches, and keeps
when it cannot tell); `--apply` re-verifies clean+not-in-use at the point of removal (TOCTOU
insurance). The default MERGED-or-GONE predicate is unchanged.
- **`scripts/disk-guard.sh [--dry-run | --apply] [--reclaim-main-target]`** (sq-4vo9m) — the
**per-tick disk guard**: the floor that stops the autonomous loop from filling the work box
(one worktree-isolated agent per bead, each a multi-GB `target/`, re-fills disk every wave;
unguarded it hit 99% / ENOSPC and crashed a verify build). Each invocation (1) reads
`df -BG /` and classifies **OK / WARN (< 20 G) / CRITICAL (< 10 G)**; (2) **delegates** the
worktree prune to `worktree-gc.sh` — it does **not** reimplement that safe predicate, so the
"never an active/unmerged/dirty worktree, never the main checkout" guarantees are the same; it
passes `--reclaim-completed` through **by default** (`--no-reclaim-completed` to disable, sq-h34dc)
so the broom also reclaims **completed-but-unmerged workflow worktrees** — a clean, fully-pushed,
workflow-named (`wf_`/`agent-`/…) tree whose PR has not yet merged and whose branch is not yet
deleted — which the bare MERGED-or-GONE predicate left to pile up (observed: ~229 G of them);
live worktrees stay doubly protected (the harness LOCK ⇒ structural keep, plus an in-use
`/proc/<pid>/cwd` probe that keeps any tree a live process is using); and (3) only when
**CRITICAL** *and* you pass `--reclaim-main-target` *and* no live
`cargo`/`rustc` build references the main checkout, drops the orchestrator main checkout's
regenerable `target/`. **Default is dry-run** (measures + dry-run-prunes + prints the plan);
`--apply` performs the prune (and, gated as above, the reclaim). It is **non-fatal** (runs
`set -uo pipefail`, never `-e`-aborts its caller — an absent `df`/`worktree-gc.sh` or a busy
main `target/` degrades to a logged skip); its **advisory exit code encodes the disk state**
(0 OK / 10 WARN / 20 CRITICAL) for a scheduler to read. Carries a hermetic
`--dry-run-self-test`; the end-to-end behaviour (gated escalation, the busy-build guard, the
state→exit mapping, non-fatal delegation) is pinned by `scripts/tests/test_disk_guard.sh`.
The maintenance loop runs `--apply` each tick (step 0) and the `autonomous-scheduler` runs it
before every wave (`--apply --reclaim-main-target`) and backs off dispatch under pressure.
- **`scripts/reconcile-merged-beads.sh [--dry-run | --apply] [--json]`** (sq-13uyp) — the
**per-tick merged-bead reconcile**: it closes the gap where a bead's fix already **MERGED**
but the bead was never `bd close`d, so it lingered on `push-frontier.sh`'s launchable frontier
and an agent was dispatched only to find it done (sq-bpoey, merged via #1017, ~32k tokens
wasted). It **COMPLEMENTS** push-frontier's open-PR exclusion (sq-7mwun): that exclusion drops
beads with an **OPEN** PR (in flight); this script catches beads whose PR is already **MERGED**.
For each OPEN bead it tests the **merged blob** — merged-PR titles + head-branches
(`gh pr list --state merged`) **and** `origin/main` commit subjects (squash-merge stamps
`(sq-XXXX)` there) — for the bead's **EXACT dotted id token** (a right/left boundary excluding
`[A-Za-z0-9.]`, so `sq-ixc3.1` never matches `sq-ixc3.11` and a base id never matches a `.N`
molecule). A match on an OPEN, non-gated bead is a **close-candidate** (reported with its
merging `#N`/commit). It **NEVER auto-closes** an epic (`issue_type==epic`, or an
`[epic]`/`EPIC:`-titled bead), an **umbrella-parent** (≥ 1 dependent — a single merged child PR
must not close the parent), or a `needs:user`/`needs:maintainer`/decision bead — those are
reported in a separate **manual-review** list. **Default is dry-run** (report only); `--apply`
closes the matched non-gated beads with reason `reconcile: fix merged via #N`. **Idempotent**
(a closed bead drops out of the open set) and **fail-safe** (a per-bead `bd show` error skips
that bead, never mass-closes; an empty merged signal — gh + git both unavailable — closes
nothing). `--json` emits a machine-readable `{candidates,count,gated,gated_count}` for a
scheduler. Carries a hermetic `--dry-run-self-test`; end-to-end behaviour (exact-match, the
`.1`-vs-`.11` guard, open-PR/unrelated exclusion, epic/umbrella/needs gating, apply/idempotency/
fail-safe) is pinned by `scripts/tests/test_reconcile_merged_beads.sh`. The maintenance loop
runs it (dry-run) each tick (step 0); the orchestrator reviews + applies the closes separately.
- **`scripts/render-start-here.py [--dry-run | --self-test | --if-ref owner/repo#N]`** (#4145) —
renders the **maintainer front door**, issue
[#1135](https://github.com/sparq-org/sparq/issues/1135), from
**`orchestration/start-here.toml`** (the curated 🔴/🟡 copy — one `[[entry]]` per ask, with
`ref` / `bucket` / `ask` / `short`) **plus live GitHub state**. It exists because #1135 said
"auto-maintained" and never was: on 2026-07-26 it was 13 days stale and **seven of its entries
were already resolved** while still being shown to the maintainer as work needing them.
**The rule: the renderer may only DROP, ANNOTATE and ORDER what the TOML says — it never invents
an ask, so a new 🔴/🟡 item arrives as a PR to that file, and a hand edit of the issue is
overwritten by the next refresh.** It **drops any entry whose issue/PR is closed or merged**
(listing them under *Removed as resolved*), renders the maintainer-gated **PR table from live
`mergeable`/`mergeStateStatus`** so a PR that has rotted to `CONFLICTING` reads "needs rebase"
instead of "say arm it", and renders the `needs:user`/`needs:area`/`needs:ec2` populations and
the held-PR list from the **LIST API** (the search index lags label writes, so it under-reports).
**Fail-closed:** an entry is dropped only on positive evidence of resolution — an unreadable ref
is KEPT and marked *state unknown* while the process exits non-zero (sticky), and an **aggregate**
failure (a label count, the held-PR list) **aborts without publishing**. The issue is edited only
when the rendered text **differs** (no notification churn); 🟢 truncates before the
65 536-char body limit, 🔴 never does. `.github/workflows/refresh-start-here.yml` runs it on a
cron plus `issues`/`pull_request` **closed** (with `--if-ref` as the API-budget guard) and
`workflow_dispatch`; `routing-self-tests` runs `--self-test` +
`scripts/tests/test_start_here_render.py` so a broken renderer or a malformed TOML fails on ITS
PR.
See `research/orchestration-automation-design.md` §1.3 (the five judgment behaviours that
must stay with the orchestrator), §6 (failure modes + the guardrail behind each), and §5
(the full phased stand-up plan with per-phase rollback).
### Agent logs + the self-improvement lane
<!-- [OPUS-4.8] agent observability + self-improvement lane; authority: research/agent-observability-and-self-improvement.md -->
Two conventions keep agent output OUT of every working agent's context and route discovered work back into the loop (the authority is [`research/agent-observability-and-self-improvement.md`](research/agent-observability-and-self-improvement.md)):
- **Transcripts live out-of-tree.** Durable transcripts are appended to the **orphan `agent-logs` branch** via [`scripts/save-agent-log.sh`](scripts/save-agent-log.sh) — git-plumbing only, **never merged to `main`, never checked out into a worktree**, so a working agent's broad grep/ast-grep can never load one. The Actions-worker path uploads the same JSON as an `actions/upload-artifact@v4` (30-day retention). PRs/issues/docs carry only a one-line LINK (`agent-logs:<id>` or an artifact URL), never the body. Working agents do **NOT** read transcripts (shared-contract item 13); log inspection is only the one explicitly-tasked debug/self-improvement agent's job.
- **Discovered work → a `self-improvement` issue (NO new agent).** An out-of-scope discovery is self-filed as a `self-improvement`-labelled GitHub issue (shared-contract item 12). Actioning reuses the existing dispatch chain: `scripts/ready-issues.py` surfaces open, unblocked, non-in-flight issues; the `sparq-issue-sweeper` (model: sonnet) sweep is extended to also triage `self-improvement` issues each tick (verify vs `origin/main`, dedupe, add the missing `role:`/`priority:`/`area:` labels or close-satisfied); the actual fix routes to whatever role agent the `role:` label maps to at its own tier. This adds zero new agents.
## No hard-coded performance numbers
Do not bake benchmark numbers (MB/s, ×-faster, recall, gate counts, latencies) into markdown. Reference the **generated structured data** instead (the benchmark harnesses emit JSON; CI publishes results). If you cite a number, cite where it was generated.
## Repository hygiene — where things live (READ THIS; it keeps the repo clean by default)
Everything you produce has exactly **one** correct home. Putting it anywhere else creates the cruft that forces periodic "clean-up runs" — so don't create it in the first place.
- **Tasks / TODOs / follow-ups / "future work" → a bead.** Never a `TODO`/`FIXME`/`XXX` marker in a markdown file, never a `TODO.md`, never a `- [ ]` checklist of pending work in a tracked doc. If you catch yourself writing "we should later…", run `bd create` (see the beads section above) and move on. Code-comment `TODO`s are discouraged too — prefer a bead and reference its id.
- **Durable knowledge → `AGENTS.md` / `CLAUDE.md`, a `skills/<surface>/SKILL.md`, a crate `README.md`, or a `research/` design record — whichever fits.** Workspace-wide conventions and contributor rules go here in `AGENTS.md` (Claude Code also auto-reads `CLAUDE.md`, which just points here). Usage knowledge goes in the matching skill. Per-crate caveats go in that crate's `README.md`. Design rationale and measured verdicts go in `research/` — or, for the rationale behind a specific deferred task, in that bead's description / `--design` field.
- **Do NOT commit narrative scratch docs.** No `HANDOVER*.md`, no `SESSION*.md`, no "current state" / "what I'm doing now" / progress-log markdown in the repo. Session and orchestration state belongs in beads (for work) or in your own un-tracked notes — never in a tracked file. The only living operational markdown allowed is **genuine reference** (a runbook, the benchmark catalog) and **generated reports** (the CI-published perf/conformance data) — not a story about a session.
- **No hard-coded performance numbers in markdown** (restated; see the section above): cite the generated structured data, not a baked-in figure.
- **RDF/SPARQL terminology → match the W3C specs.** Before writing or editing any doc that names an RDF/SPARQL feature, check [`skills/terminology/SKILL.md`](./skills/terminology/SKILL.md) — the single source of truth for preferred wording (say **RDF 1.2** / **SPARQL 1.2** and **triple term** / **reifier** / **reified triple**, never the community-era "RDF-star" / "RDF\*" / "SPARQL-star" / "quoted triple" / "embedded triple"). Enforced by the `terminology` HARD gate (`scripts/check-terminology.py` in `docs-quality.yml`); a hit fails the build unless the line is a legitimate proper-noun / paper-title / third-party-doc / URL mention or carries an inline `terminology-allow: <why>` marker.
- **Banned terms are DATA, and they are checked in CODE + VOCABULARY too.** The banned list lives in [`scripts/banned-terminology.json`](./scripts/banned-terminology.json) — adding a maintainer-banned term is **one object**, not a code change — and each term declares its own file surface (`.rs` / `.ttl` / `.md` / `.typ` / manifests / workflows, not just markdown). This exists because a banned term once reached a merge-ready PR as a `pub` Rust type **and** a published `rdfs:comment` with a fully green `ci-summary / gate`, since the gate then scanned `*.md` only (issue #3811). Escapes are narrow and reviewed: the inline `terminology-allow: <why>` marker, per-term proper-noun patterns, and an **enumerated** `exemptPaths` list where every entry carries a `why`. Widening a path exclusion to make a violation pass defeats the gate — reword instead. `scripts/tests/test_banned_terminology.py` pins all of it (fixtures + the workflow wiring).
Honour these homes and the repo never accumulates stale TODO lists or handover docs — no clean-up pass is ever needed.
## Public-API → SKILL.md maintenance rule
Important enough to state twice: see **MAINTENANCE RULE (REQUIRED)** near the top. In short — when you change any public API (`pub` item, CLI flag, HTTP route, Python/JS binding), update the corresponding `skills/<surface>/SKILL.md` in the SAME change. The surface→skill map is in [`skills/SKILL.md`](./skills/SKILL.md).