Skip to content

Latest commit

 

History

History
879 lines (702 loc) · 182 KB

File metadata and controls

879 lines (702 loc) · 182 KB

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 — 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.
  • 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, SPARQ-authoritative for RDF + WAC, with Solid-OIDC/DPoP auth delegated to the pinned 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 @sparq-org/solid-server), and sparq-wac-oracle is the server-independent WAC/ACP decision test-vector corpus. Usage: skills/solid-lws-server/SKILL.md; design decisions: research/lws-design-records.md.
  • npm: @sparq-org/sparq — RDF/JS-typed API over the wasm build, zero runtime deps; [GPT-5.6] @sparq-org/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/ (the agentskills.io open format — name/description frontmatter + Markdown). Read the one that matches the surface you are integrating:

Read skills/SKILL.md first — it is the router skill that lists every surface and points you at the right one. The main entry points:

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 — 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 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, 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 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 (@sparq-org/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

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):
    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. 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

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/ 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.

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) , 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. 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). 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. 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. 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 §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

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 — 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

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)

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 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 (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 (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 (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.

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 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. 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

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.

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 and 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, and the XPath differential harness pins XPATH_TAG: "v0.3.0" (.github/workflows/xpath-differential.yml, zk/xpath/scripts/run_differential_harness.sh — bumped off v0.2.0 by #5456, so that lane's evidence is now about the current release) — 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

A hand-written parser (replacing or bypassing a reference implementation) ships only with ALL of (design record: 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.mds — 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)

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, 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. 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. 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). 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. 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. 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. 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. 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. 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). 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. 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 — 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; 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: — 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).

  • 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. 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. @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 sparq-org/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

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. 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 #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. 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

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 sparq-org/sparq. 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. 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. 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 --excludes; 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. 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). 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. 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. 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.

  1. 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). 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). 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 closed — 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.)
  2. 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.)
  3. 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). 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:

        • 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/).
  4. Drive bd ready (the engine) — dispatch FROM the launchable-bead frontier. 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 creates 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), MATERIALISED as the committed, re-runnable .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.
  5. 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.)
  6. 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.
  7. 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.)
  8. 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

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. 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. 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 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 §6.3)

    — 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 — 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 — 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 — 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 closed, 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, 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

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):

  • Transcripts live out-of-tree. Durable transcripts are appended to the orphan agent-logs branch via 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 TODOs 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 — 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 — 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.