A README for coding agents. If you are an AI agent working on or with this repo, read this first.
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.
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-elis a separate opt-in crate (depending on it is the opt-in): an OWL 2 EL consequence-based classifier that computes the completerdfs:subClassOfsubsumption lattice that OWL 2 RL (sparq-reason) is sound but silently incomplete for — seeskills/inference/SKILL.md. - In-workspace,
publish = falseserver 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-wasmis its opt-in wasm adapter (the local-development host behind@sparq-org/solid-server), andsparq-wac-oracleis 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 namesparq) — pyo3/maturin bindings.
Status: experimental research engine; the API is unstable.
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:
skills/sparql-query/SKILL.md— run SPARQL from Rust (sparq-core+sparq-engine).skills/data-formats/SKILL.md— parse/load RDF (Turtle/N-Triples/N-Quads/TriG, HDT) into a Graph.skills/rdf-wrapper/SKILL.md— traverse RDF as Rust-native focus objects with the opt-insparq-wrappercrate.skills/cli/SKILL.md— thesparqCLI (query, mmap build/query, reason, bench).skills/http-server/SKILL.md— the SPARQL 1.1 Protocol HTTP server.skills/helm-deploy/SKILL.md— [GPT-5.6] deploy either native server to Kubernetes with the secure-default Helm chart.skills/javascript-wasm/SKILL.md— the@sparq-org/sparqnpm package.skills/python/SKILL.md— thesparqPython package.
The capability surfaces (reasoning — RDFS/OWL-RL/N3 in sparq-reason plus the opt-in OWL 2 EL classifier in sparq-reason-el, both covered by skills/inference/SKILL.md — 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.
- Build:
cargo build --workspace. Test:cargo test --workspace. - Lint is enforcing (CI gates on it):
cargo clippy --workspace --exclude sparq-py --all-targets -- -D warningsmust pass. Run clippy over the full workspace, not a single crate — feature unification surfaces lints that an isolated-crate check misses. (sparq-pyis excluded because it needs the Python/maturin toolchain.) cargo fmt --all --checkis NOT a gate — it is informational (continue-on-error: trueinci.yml'sclippy (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 runningcargo fmt --all(a workspace-wide reformat is its own reviewed change — seerustfmt.toml). Format the code you touched, matching the surrounding committed style. The formatter version is no longer ambient:rust-toolchain.tomlpins the channel and ships therustfmtcomponent, 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-enginestay 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-cliandsparq-servercarryjsonldin theirdefaultfeature set (the CLI parses + re-serialises JSON-LD, the server speaksapplication/ld+jsonin both directions), andsparq-pydoes too (Graph.load(..., format="jsonld")+ a.jsonldpath work out of the box; the wheel has no bundle-size floor). It stays toggleable (--no-default-featuresdrops theoxjsonldparser in all three), and the lean wasm bundle keeps JSON-LD opt-in (sparq-wasm/jsonld, OFF by default — thewasm_bundle_bytesfloor is unchanged). Thesparq-core/sparq-enginelibrary defaults also stay lean (oxjsonld enters only via the binaries'jsonldfeature). 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 thesq-oy1froadmap. - 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. Usenext/dynamic/React.lazyfor components and invocation-pathimport()for libraries/codecs. Classify new frontend dependencies as core/shared or optional in the PR, and extendsite/scripts/check-bundle.mjsfor material optional chunks. See.claude/skills/frontend-design/SKILL.mdfor 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 reportconformance-report.mdis git-ignored and regenerated locally bycargo run -p sparq-conformance(the CI job re-runs it and publishes it as a build artifact); the inference report is committed atinference-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 exportregenerates the JSONL).
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_numericclassification + 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 genericJoinKeysdescriptor (the row→key projection + combine layout) and a genericBudgetcooperative-cancel hook.compare— the SPARQL term total order (compare::compare_terms— the engine'scompare_values: error/unbound < blank < IRI < literal < triple, numeric-aware + strict typed/temporal + string fallback + recursive triple-term order) behind a genericCompareTermtrait the consumer implements for its term type. Drives the engine'sORDER BY/ sort /MIN/MAXfallback.rows— the sharedRow/Key/Postingid-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 tiersi64/i128/f32/f64;#[inline]accessors, noBox<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):
Valueenum +LitKindliteral-family classifier +value_compare_stricttyped/temporal comparison (drives relational ops, ORDER BY, and all value semantics; coupled to oxrdf::Term).Bindingsstruct (engine's result binding representation; the join kernels expose Row/Key instead).LocalVocabinterning (engine-specific; reasoners have their own vocab).QueryBudgetthread-local cancellation (engine-specific; reasoners may supply their own Budget impl).ScanCmppushdown 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).
When you change a public API, update the matching skill in the SAME change (same commit/PR). A "public API" means any of:
- a
pubitem 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
sparqpackage) 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.
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 auditsq-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.
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).
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.mdfiles. Capture future work as a bead instead. - When you identify follow-up/future work, create a bead for it (from the repo root, with
bdon your PATH):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'sbd create "<imperative title>" -t <task|bug|feature|chore|spike> -p <0-4> -l <area:crate,kind:...> -d "<what + why + where>"
-ddescription or--designfield so the bead is self-contained. Never edit.beads/issues.jsonl(or any.beads/file) by hand — it causes merge conflicts;bd exportregenerates it. - Run
bd readyto see unblocked work; close withbd 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.)
If you are an ORCHESTRATING agent (driving multi-step work on this repo), three standing rules:
- 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). - 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-checkbd ready→ repeat. Don't wait to be prompted bead-by-bead. Sequence beads that touch the same files; respect dependency edges (bd dep). - 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-summaryis 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 reservesparq-serverfor 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.
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):
- Worktree + branch. Run in your OWN isolated git worktree (
isolation: "worktree"); nevercdinto 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.) - 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. - No push / no merge. Commit in-worktree and report; the orchestrator pushes, opens the PR, and merges. (Exception:
sparq-merge-fixerpushes to an existing PR branch — see its prompt.) - 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.
- Model provenance (model-aware, derived from harness runtime). Your model identity is supplied by the harness via the
--modelflag; derive the inline marker +Co-Authored-Bytrailer 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 + aCo-Authored-By: Claude MODEL <noreply@anthropic.com>commit trailer (MODELis the placeholder; the angle brackets aroundnoreply@anthropic.comare 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 matchingCo-Authored-Bytrailer 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 matchingCo-Authored-By: Claude <that model>when a cheap tier runs as that model (thefable-architect-drainworkflow 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). - Self-identify (🤖) in every comment. Open a PR vs
mainwhose 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 plaingh pr merge <n> --auto(the repo now uses a GitHub merge queue, which CHOOSES the merge strategy, so--squashis REJECTED;"already queued"is success; after arming, verify ~20s later thatautoMergeRequestlatched 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 missesmain; retarget base tomainfirst — Arming model, sq-u59rq). - 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, andsparq-mpcis honest-majority semi-honest only) — hedge/negate the wording or add an inlineprivacy-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 — rewordDELETEd/DROPped/invokable/ANDed(e.g. "data removed by DELETE/DROP", "invocable", "AND-combined"). - 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 (
bdis 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 aself-improvement-labelled GitHub issue (item 12). - Maintenance rule. When your change touches a public API (
pubitem / CLI flag / HTTP route / Py/JS binding) or a config key, update the matchingskills/<surface>/SKILL.md(and crate README) in the SAME change (MAINTENANCE RULE). - Heartbeat. Print to stdout at least once per minute during long cargo/npm runs (a watchdog reaps silent agents at ~600 s).
- A permission denial is FINAL — never try to work around it, and
.claude/agents/*.mdis a PROTECTED surface. If the harness (the auto-mode classifier, a permission prompt, aPreToolUsehook) denies a tool call, that decision is final: do NOT retry the same change through a different tool to evade the block — nopython/nodeheredoc, nosed -i/awk/tee/>>-redirect, nocat <<EOF, nogit 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 isneeds: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--modelflag, 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.) - 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. - Never read agent transcripts / logs. Do NOT
Read/cat/grep/ast-grepthe ephemeral subagent transcripts (/tmp/claude-*/**/tasks/*.output), theagent-logsbranch, 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 byscripts/save-agent-log.sh(orphanagent-logsbranch) / uploaded as an Actions artifact — carry a one-line LINK, never the body. - 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.
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/*.mdMUST 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--modelflag at dispatch time. Thefable-architect-drainworkflow 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.mdand the role prompts sit in the cached prefix; front-load the invariant, put per-task variation last. EditingAGENTS.mdor 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.
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).
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 + itstarget/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.shdefaults its output to/tmp/bsbm/…— and are regenerable. But most ofbench/is tracked (~300 files): generators/runners (gen.sh/run.sh), queries (*.rq), expected results (*.tsv) and baselines likebench/perf-baseline.jsonare committed — never delete tracked bench assets, scripts, or.gitignore. When in doubt,git ls-files bench/shows what's tracked.
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.
-
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, aSKILL.md, or thebdbacklog — DELEGATE it as a natural-language tool call to a cheap-model (model:haiku) sub-agent running thequery-pkgflow (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 inbench/pkg-dogfood/RESULTS.md(the cheap-model NL-tool arm beats both "Opus reads the docs" and "Opus runspkg-queryitself" 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.
- Abstain → fall back. The PKG is a head slice, so it safely abstains when it cannot answer (an empty result /
-
ast-grep+ outline: split the verdict by the SHAPE of the task — scoped lookup → justRead; 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
Readthe 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 scopedRead, 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 forast-grep(impl / call-pattern / code-shape / codemod) and the LSP/outline recipes when you must enumerate all impls / all call sites where aRead/grepmight 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; thebench/ast-compact/compact_ast.shdump) that the agent works over and manipulates, reading raw bytes only for spans the skeleton can't resolve — against the plainGrep/Readbaseline 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.)
- Scoped code-structure LOOKUP → do NOT go structural-first; just
-
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 inresearch/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.
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.)
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:
- Update the gate-count snapshot. Add (or delete) the member's entry in
crates/sparq-zk-compose/tests/gate_count_snapshot.json(membersmap) with its realcircuit_sizefrombb gates -s ultra_honk. Re-baseline by runningbench/zk-compose/scripts/gate_counts.shand copying the values into BOTH the snapshot andbench/zk-compose/gate_counts_latest.json—snapshot_covers_every_memberfails 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 anexempt_circuitsentry instead.) - Run the Rust gate locally:
cargo nextest run -p sparq-zk-compose(NOT justnargo test).snapshot_covers_every_memberruns WITHOUT thenargo/bbtoolchain — it only reads thezk/compose/directory names against the snapshot — so this step catches a missing baseline even on a box without the ZK toolchain. Thegate_count_regressionbloat check additionally needsnargo+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.
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):
- Round-trip property tests —
parse ∘ serialize ∘ parsefixpoint over generated inputs (proptest), plusserialize ∘ parseidentity on canonical forms, in the parser's crate. - 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-discoveredfuzz/workspace so per-PR corpus replay + nightly randomized runs apply automatically. - 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.
- Robustness target — hostile-input fuzz (never panic/OOB), separate from (2), if the grammar entry point is new.
- 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).
- Honest perf claim — measured on the canonical bench path, never a work-box number in docs (see No hard-coded performance numbers).
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 crateREADME.md/skills/<surface>/SKILL.mdand 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 inresearch/design docs and crateREADME.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).
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:
- Vendor a local copy of the upstream code (under
vendor/or as a forked crate via[patch.crates-io], as already done forspargebra), implement the feature/fix there, and ship it so sparq is unblocked. - 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(asvendor/spargebra/SPARQ-PATCHES.mddoes). - 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.
- A
blocked-upstreambead 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.
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:
- Open it as a DRAFT. Never a ready-for-review PR.
- 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."
- 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, soRequestReviewsByLogin/ReplaceActorsForAssignablefail — @-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. - 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.
- 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.
- 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.
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.
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.mdadapted 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> --commentson 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.
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.)
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:
- Branch → open a PR (
gh pr create). Request review, including GitHub Copilot code review. - 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.
- CI must be green: a single
ci-summarycheck aggregates every other workflow's result and passes ONLY when they all pass. It is the one required status check for branch protection. - Merge only when:
ci-summaryis green AND all review threads are resolved. Then squash/merge and delete the branch. - 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, preferCloses #Nin 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 thereview:passlabel, but a review's actual artefact is a line-anchoredVERDICT: passcomment and review agents are forbidden from touching labels. Nothing in the repo used to ADD that label (merge-queue-feedback.ymland 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,CLEANand unarmed indefinitely..github/workflows/verdict-bridge.yml+scripts/verdict-bridge.pynow perform that hop event-driven with a cron backstop, fail-closed: a verdict counts only when the comment's LAST non-blank line is exactlyVERDICT: 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 adiff --gitline binds too; binding only PAIRS a comment to a head, the trusted author's trailingVERDICT:line is what makes it a verdict), and the author'sauthor_associationis OWNER/MEMBER/COLLABORATOR; among head-bound verdicts the latest by immutablecreated_atwins, 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 isAMBIGUOUS, not absent: it never promotes, it defeats an earlier pass at the same head, and it removes an existingreview: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-armstill performs the arm with its ownexpectedHeadOidCAS. It also never retracts on absence of evidence, so a hand-appliedreview:passis never fought. A second hole it closes: a green PR with no head-bound verdict carries noreview:*label at all and is therefore invisible to every lane (auto-arm needsreview:pass, the fast-fix ring only fires on FAILING CI, the dispatchers work offreview:needs); the bridge marks thosereview: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-approvalandbatch-mergeeach fired 11–16% of their scheduled cron ticks, with observed inter-run gaps of 53–75 minutes against a 10- or 15-minutecron:. GitHub coalesces and dropsscheduleevents 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, andconcurrency: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-bridgehandles 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
PreToolUseagent-hook onBash(in.claude/settings.json) fires on thegh pr merge … --autoarming step and invokes thesparq-perf-revieweragent (.claude/agents/sparq-perf-reviewer.md). It decidesperf_affecting, and if soperf_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). Ifperf_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.jsonfloor or a published-results artifact — the perf-reviewer flags it so the orchestrator surfaces it to the maintainer even whenperf_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 byci-summary+ resolved review threads + the perf ratchetscripts/perf-gate.py.) A newly-edited.claude/settings.jsonhook is not necessarily hot-loaded mid-session — the maintainer opens/hooksonce 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, notmain— do not arm auto-merge on B until its base ismainagain. GitHub squash-merges B into its stacked base (A's branch), notmain, the moment A merges first; it then marks B "merged" and auto-deletes B's head branch (unreopenable) — yet B's content never reachesmain. 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 tomain(gh pr edit <n> --base main) and only THEN arm it. This is enforced mechanically by a deterministicPreToolUsecommand-hook (scripts/check-pr-arm-base.py, sibling to the perf-reviewer in.claude/settings.json) that reads the arm command, callsgh pr view <n> --json baseRefName, and denies thegh pr merge … --autowhen the base is notmain. 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 anygh pr mergeand allows every other Bash call. Verify withpython3 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 actionuses:pins. This covers everything in the released supply chain (theghcr.ioserver image, the action graph). - SHA-pinned
taiki-e/install-actionMUST carrywith: tool:—taiki-e/install-actionselects which tool to install from its@<tool>git ref (@cargo-llvm-cov); the SHA-pin above drops that selector, so without an explicitwith: tool: <name>the action installs nothing and the downstreamcargo <tool>ENOENTs — silently making the gate vacuous (this is exactly the coverage-gate regression root-caused 2026-06-18).scripts/check-install-action-tool.pyis a stdlib-only lint that scans.github/workflows/*.ymland fails on any SHA-pinnedinstall-actionstep missingwith: tool:; it runs (with a--self-test) in thedocs-qualityci-scriptsjob. When adding a SHA-pinnedinstall-action, always include thewith: 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 … | shrustup and best-effortpip3 installtransient tools (e.g.rapidgzip); they're outside any released artifact, so their ScorecardPinnedDependenciesalerts 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-levelcontents: writethat 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 ScorecardTokenPermissionsalert with that reason rather than removing the needed grant).
Supply-chain attestation stack (cert epic sq-toze — GX-1/2/7).
cargo deny check advisoriesis a GATING PR check (supply-chain.ymlauditjob, nocontinue-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). Everydeny.toml [advisories].ignoreentry carries a justification + a tracking bead, and the list only ever shrinks by REMOVING the dependency. Most recently [OPUS-5] sq-5ah3p retiredrustls-pemfile(RUSTSEC-2025-0134) for good: the archived crate was the last PEM decoder insparq-lws-core/src/tls.rs+sparq-server/src/main.rs, and both now callrustls-pki-types'PemObjectthrough rustls' ownpki_typesre-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. Keepdeny.toml [advisories].ignoreand the VEX (below) 1:1 in sync.- Per-release CycloneDX SBOM + VEX.
scripts/gen-sbom-vex.shemits a CycloneDX SBOM per released binary (sparq-cli,sparq-server) + a version-stamped VEX (supply-chain/vex.cdx.jsonis the checked-in source of truth; it statesnot_affected+ justification for every advisorydeny.tomlignores). Therelease.ymlsbomjob runs it, SLSA-attests the outputs, and attaches them to the GitHub Release (covered bySHA256SUMS). Editing the ignore set ⇒ updatesupply-chain/vex.cdx.jsonto match. Each shipped SBOM is normalized throughscripts/sbom-normalize.jq(a deterministic, idempotentjqtransform, also applied insupply-chain.yml#sbom) so no host-revealing absolute build path leaks into abom-ref/purl:path+file://…#<ver>refs become canonicalpkg:cargo/<name>@<version>and the dependency graph is rewritten in lock-step (gap GS-6 / sq-toze.30). cargo-auditablewraps every shipped-binary build path — therelease.ymlpackagejob, thedist.ymlmatrix, theDockerfilebuilder, and the localscripts/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.ymlvetjob,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 suggestshows what to audit to shrink exemptions; the vendoredspargebrapatch isaudit-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-corepath 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 scriptedcheck-packagespre-add check over npm; the portable principle is the pre-add screen, which on a cargo workspace is the provenance check above pluscargo deny/cargo vetas 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.
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 feedsci-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→SURVIVEDwhen 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: afetch_lanesguard 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-featuresis 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 unfilteredtestMatchleaks 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 unfilteredtestMatch#1676.) - ADVISORY-vs-GATING RULE — "does it gate?" keys on the enclosing JOB's DECLARATION, not on any name. Since #3773,
ci-summaryexcludes 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 itsrun: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 anadvisoryname token changes nothing on its own; renaming a declared job makes it GATE (fail-closed) and REDscheck-advisory-registry.pyC4. (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 theassemble feature matrixjob FIRST. If thatsetupjob fails (golden leg-name drift inscripts/tests/feature-matrix-legnames.golden.txtafter 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 requiredopt-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 bytest_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.)
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.
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.
- 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 — orcargo 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_jsonnever executed → the shipped tree printedplanned_rows=4where the mutant printed0),reg #956(the module's only two write methods had never executed anywhere),reg #937(main+_gh_readersat 0 % → 13 of 13 one-line edits there survived a 248-check suite, including anapply=false"census-only preview" that writes real ledger records),#4743(17 of 29 functions at 0 %;main()at 55 % with its wholesweepbranch unexecuted, soreturn 1 if …sweep() else 0→return 0survived 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'smainis at 8/18, not 0 %, and a fresh sweep of exactly that region found 10 survivors out of 10;reg #941's_escalate_two_headhad 1 of 3 call sites covered at 3/3 confidence, whichreg #945re-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. - 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 #937Z6: 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:parkeddefined 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 fromSTUCK_UNPARK_MAX, so settingSTUCK_UNPARK_MAX = 999left 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→100000survived 76/76.) - 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 inif 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. - 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 anIndexErrorthat aborted the suite before any row printed). Equivalent survivor: declare it and show it unreachable (reg #937D1-default). Value-identical survivor: the substituted value collides with one the fixture already uses (reg #941pins 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 ofreg #945's four survivors were a singleMIN_ARG_TOKENfloor 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 raisingIndexErrorafter three namedFAILrows). Require the mutant run's total check count to equal the pristine run's before you call it a kill. - 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 frompull["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 nologin/author_associationcheck, on a public repo → a drive-by comment forgesroute=preserveand 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 #937Z1–Z5). - 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-DROPPEDand--reconcile-max-DROPPEDboth survived a substring check;reg #941'sif: falseon the resolver step and on the whole job each survived 76/76;#4743'sroute != 'preserve' && falsesatisfies a substring assertion while killing the lane. - 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 containedFAILas a substring, 44 of them passingokrows; the anchored form extracted 1 (reg #949). - 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'schain_unaccountedread 0 in both the shipped and the mutant tree, so its own missing-edge detector was structurally blind to the break. - 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. - Publish corrected counts. Four headline numbers moved that night once someone asked a specific question of every row:
reg #94122/22 → 21/22 then 26/26 → 25/26;reg #93748/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. - 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 assweep()'s call sites and stopped one layer short). - 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.
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:
- 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 - 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.)
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 withCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>; an Opus 4.8 session authors as Claude Opus 4.8 withCo-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 trawlinggit 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.
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.
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.)
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.
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-summarygoes 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
maincompletes → check/fix now.To stay event-driven, keep a CI watcher armed on every in-flight PR (
gh run watch <run-id> --exit-statusin 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.
- Reconcile first (cheap, every pass).
git worktree list+gh pr list; reap finished-but-unnotified worktree agents (a committed branch + no livecargoprocess = done → open its PR); merge any PR that isci-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-GBtarget/, so disk re-fills every wave — left unguarded the work box hit 99% / ENOSPC and crashed a--workspaceverify build mid-run. So each tick runscripts/disk-guard.sh --apply: itdf-checks/(warns when < 20 G free, escalates < 10 G), then delegates the worktree prune toworktree-gc.sh(which only ever removes a worktree whose HEAD is inorigin/mainor whose branch is gone-on-origin, with no dirty/unpushed work — never an active one, never the main checkout — and, with the--reclaim-completedpath 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'starget/— regenerable, because impl agents build in their OWN worktrees — but only after confirming no livecargo/rustcbuild 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). Theautonomous-schedulerruns 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 "runworktree-gc.sh --applyat idle" advice — the guard wraps it and adds the per-tickdfcheck + 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 neverbd 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 viagh pr list --state merged, plusorigin/maincommit 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.1never matchessq-ixc3.11; OPEN-status beads only) and NEVER auto-closes an epic, an umbrella-parent (≥ 1 dependent), or aneeds: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 (--applycloses matched non-gated beads with notereconcile: 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 byscripts/tests/test_reconcile_merged_beads.sh. (See the script catalog below.)
- Disk guard (run EVERY tick —
- 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.) - 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 siblingjeswr/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 createwith 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 onmain), 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 grepto 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 createit (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:userbead 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.
- resolved-closeable — already fixed on
-
- 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/).
- Open issues (
- 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 viascripts/push-frontier.shand 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;siteand the sparq-serverserver-authpath → ≤ 1); pipe its output through thesparq-workload-triageplacement 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 rawbd readylist (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.jsWorkflow (see its script-catalog entry below): run a tick withWorkflow({ name: "autonomous-scheduler" }). When you instead run the frontier by hand (the scheduler is off, or you are hand-dispatching), apply the same dedup + theproceed-and-documentskill per agent as described here. - 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-summaryis 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 whenci-summaryis green and all threads resolved; squash;bd close; re-export beads in a bookkeeping PR. (See Contribution workflow + Automated review.) - roborev hygiene. Before merging a branch, reconcile its roborev (codex, non-Anthropic) findings against current
mainHEAD — never assume a merge cleared them; fix / bead / close-with-reason. - 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.)
- Surface the
needs:userqueue 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.
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 themerge_groupevent 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-minutecheck_response_timeout_minutesreaps 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 toreview: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 aneeds:userhold 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 fromgithub-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 carryreview:passthrough a dequeue and reachgh 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, andstacked=/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 inmerge-queue-feedback.yml: aCI_TIMEOUTwhose group ref had zero suites preservesreview:passinstead 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:passwas swapped forreview:changes17 seconds after aMANUALdequeue, so a watchdog without this would burn the verdict of every PR it rescued.MANUALconflates "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'sheadCommit{oid}(MergeQueueEntryhas noheadOidfield), or read the live refs withgit 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. Testcommits/<head>/check-suites—total_count == 0is the signal, and nevercheck-runs(apaths-filtered workflow that matches nothing still creates a suite). To clear it,gh pr merge --disable-autodoes not dequeue an already-queued PR; usedequeuePullRequest, whose input field is namedidbut 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 onmerged == true— base-repo context so theissues: writetoken survives fork PRs; safe because the job checks out pinnedmainand never executes PR-controlled code), it extracts thesq-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, plainissues: write). The original JSONL-commit-back design never persisted — the defaultGITHUB_TOKENcannot push to protectedmain(GH013 ruleset rejection, sq-roe3), and a PR-based write from that token would hang forever becauseGITHUB_TOKENevents trigger no workflows, soci-summary / gatewould 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 manualreconcile-merged-beads.shsweep. The script retains the minimal in-place.beads/issues.jsonledit 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 / gatepolls 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 themerged == trueevent 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 mergedAtmust be non-null; a parsed log/monitor line is never the source of truth). Resolves the bead id from ansq-XXXXtoken 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 tagpurpose=sparq-bench(allow-list semantics, not deny-list) and greps the local process table for in-flightgather-*launchers. Default is dry-run (prints orphans only);--applyterminates 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 whenawsis unconfigured. -
scripts/refill-candidates.sh(Phase F) — read-only, advisory only. Listsbd readybeads 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 oforigin/main" — squash-merged feature branches are not ancestors of main yet were pushed, so an ancestor test would re-introduce the bug). Runworktree-gc.sh --applyat idle so stale worktrees do not pile up. -
scripts/push-frontier.sh— the read-only decision layer on top ofrefill-candidates.sh. Prints the beads SAFE TO LAUNCH NOW:bd readyminus 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, withsiteand the sparq-serverserver-authhttp.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 rawbd readyfallback. If you ever dispatch frombd readydirectly (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 byarea:<name>: an in-flight PR reserves its areas and a ready issue defers while any of its areas is reserved — so a PR with noarea: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 noarea: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 raisingmax_concurrent/package_widthmeasured net +0 workers (registry #689). This deriver reads a PR's changed paths and applies thearea:labels the paths imply —crates/<name>/…→area:<name>implicitly, everything else from the reviewable[[map]]table inci/area-labels.toml. It is additive only (a human-appliedarea:is never removed), idempotent, never creates a label (the add-labels REST call silently would, so a typo'd area is dropped with a::warninginstead), and fail-closed: unresolvable paths or more than[policy] max_areasdistinct 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 againstchangedFiles; any disagreement isincomplete-paths→__global__. (gh pr view --json filesis GraphQLfiles(first: 100)and silently truncates — measured on PR #3581:changedFiles646,--json files100. 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 consumeprevious_filename, so a cross-crate move implicates both ends.)--dry-runis the default;--applymutates;--backfillsweeps every open PR (use--pace— each label add fires apull_request: labeledevent that ci/bench/fuzz/feature-matrix all react to). Wired to every PR by.github/workflows/pr-area-label.yml(least-privilegepull-requests: write, checkout pinned to the default branch so a privileged token never runs PR-authored code, explicit fork read-only-token path, and noif:at any level — every skip decision is in Python, where a mutant dies). Guards:scripts/tests/test_pr_area_labels.py, run byrouting-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 overgit ls-filesis 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 withWorkflow({ name: "autonomous-scheduler" }); override the per-run cap or focus list viaargs— e.g.Workflow({ name: "autonomous-scheduler", args: { maxBeads: 6 } })(also takesonly: ["sq-…", …]). It codifies loop steps 3–4 (drivebd ready→ land each PR) so the orchestrator stays out of the per-agent dispatch loop; it is bounded bymaxBeadsso it cannot flood, and impl agents never branch-switch the shared checkout. Before each wave it runsscripts/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-agenttarget/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 theproceed-and-documentskill, referenced by name fromimplPrompt()+ 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 fromAGENTS.md(this script catalog for a workflow; the rule it codifies for a recurring procedure — e.g. the STANDING RULE section above links theproceed-and-documentskill), and (3) a one-line description (a workflow'smeta.description; a skill's frontmatterdescription) 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 rarefable_implementsbranch 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 anythinghonest=falseor touching the PENDING external ZK audit (sq-qhy4) for the maintainer regardless of the other fields.Workflow({ name: "fable-soundness-verdict" })(orargs: { 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 viabd 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-GBtarget/build dir that fills the disk. The script enumeratesgit worktree list --porcelainand classifies a worktree SAFE-to-remove only if ALL hold: its HEAD is already an ancestor oforigin/main(merged — nothing to lose) or its branch is gone-on-origin with HEAD still reachable from a remote ref;git status --porcelainis 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 adu -shreclaimable-size estimate;--applydoesgit worktree remove --forcethe safe set thengit worktree prune. Run--applyat 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 viadisk-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-namedfeat/branch is never swept)- not in use (a
/proc/<pid>/cwdscan that KEEPs any tree a live process touches, and keeps when it cannot tell);--applyre-verifies clean+not-in-use at the point of removal (TOCTOU insurance). The default MERGED-or-GONE predicate is unchanged.
- not in use (a
-
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-GBtarget/, re-fills disk every wave; unguarded it hit 99% / ENOSPC and crashed a verify build). Each invocation (1) readsdf -BG /and classifies OK / WARN (< 20 G) / CRITICAL (< 10 G); (2) delegates the worktree prune toworktree-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-completedthrough by default (--no-reclaim-completedto 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>/cwdprobe that keeps any tree a live process is using); and (3) only when CRITICAL and you pass--reclaim-main-targetand no livecargo/rustcbuild references the main checkout, drops the orchestrator main checkout's regenerabletarget/. Default is dry-run (measures + dry-run-prunes + prints the plan);--applyperforms the prune (and, gated as above, the reclaim). It is non-fatal (runsset -uo pipefail, never-e-aborts its caller — an absentdf/worktree-gc.shor a busy maintarget/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 byscripts/tests/test_disk_guard.sh. The maintenance loop runs--applyeach tick (step 0) and theautonomous-schedulerruns 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 neverbd closed, so it lingered onpush-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) andorigin/maincommit subjects (squash-merge stamps(sq-XXXX)there) — for the bead's EXACT dotted id token (a right/left boundary excluding[A-Za-z0-9.], sosq-ixc3.1never matchessq-ixc3.11and a base id never matches a.Nmolecule). 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 aneeds:user/needs:maintainer/decision bead — those are reported in a separate manual-review list. Default is dry-run (report only);--applycloses the matched non-gated beads with reasonreconcile: fix merged via #N. Idempotent (a closed bead drops out of the open set) and fail-safe (a per-beadbd showerror skips that bead, never mass-closes; an empty merged signal — gh + git both unavailable — closes nothing).--jsonemits 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-.11guard, open-PR/unrelated exclusion, epic/umbrella/needs gating, apply/idempotency/ fail-safe) is pinned byscripts/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, fromorchestration/start-here.toml(the curated 🔴/🟡 copy — one[[entry]]per ask, withref/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 livemergeable/mergeStateStatusso a PR that has rotted toCONFLICTINGreads "needs rebase" instead of "say arm it", and renders theneeds:user/needs:area/needs:ec2populations 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.ymlruns it on a cron plusissues/pull_requestclosed (with--if-refas the API-budget guard) andworkflow_dispatch;routing-self-testsruns--self-test+scripts/tests/test_start_here_render.pyso 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).
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-logsbranch viascripts/save-agent-log.sh— git-plumbing only, never merged tomain, 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 anactions/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-improvementissue (NO new agent). An out-of-scope discovery is self-filed as aself-improvement-labelled GitHub issue (shared-contract item 12). Actioning reuses the existing dispatch chain:scripts/ready-issues.pysurfaces open, unblocked, non-in-flight issues; thesparq-issue-sweeper(model: sonnet) sweep is extended to also triageself-improvementissues each tick (verify vsorigin/main, dedupe, add the missingrole:/priority:/area:labels or close-satisfied); the actual fix routes to whatever role agent therole:label maps to at its own tier. This adds zero new agents.
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.
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/XXXmarker in a markdown file, never aTODO.md, never a- [ ]checklist of pending work in a tracked doc. If you catch yourself writing "we should later…", runbd create(see the beads section above) and move on. Code-commentTODOs are discouraged too — prefer a bead and reference its id. - Durable knowledge →
AGENTS.md/CLAUDE.md, askills/<surface>/SKILL.md, a crateREADME.md, or aresearch/design record — whichever fits. Workspace-wide conventions and contributor rules go here inAGENTS.md(Claude Code also auto-readsCLAUDE.md, which just points here). Usage knowledge goes in the matching skill. Per-crate caveats go in that crate'sREADME.md. Design rationale and measured verdicts go inresearch/— or, for the rationale behind a specific deferred task, in that bead's description /--designfield. - Do NOT commit narrative scratch docs. No
HANDOVER*.md, noSESSION*.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 theterminologyHARD gate (scripts/check-terminology.pyindocs-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 inlineterminology-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 apubRust type and a publishedrdfs:commentwith a fully greenci-summary / gate, since the gate then scanned*.mdonly (issue #3811). Escapes are narrow and reviewed: the inlineterminology-allow: <why>marker, per-term proper-noun patterns, and an enumeratedexemptPathslist where every entry carries awhy. Widening a path exclusion to make a violation pass defeats the gate — reword instead.scripts/tests/test_banned_terminology.pypins 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.
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.