# RigorQuant architecture — the grilled decision record ## Sources studied - **Shanmu Jin's Crouzeix run** (2026-07-30): full prompt at https://github.com/jinshanmu/CrouzeixConjecture/blob/main/crouzeix_conjecture_prompt.txt — epistemic isolation, diverse multiagent portfolio with an approach-family registry, adversarial counterexample-only audit, concrete-output discipline, persistence, terminal honesty. Verification gate: Lean 4 + pinned Mathlib, axiom audit (trust boundary `propext`/`Classical.choice`/`Quot.sound`, no `sorry`/`admit`), manuscript pinned by SHA-256 and mapped line-by-line to Lean declarations. - **Terence Tao**: Blueprint + Lean formalization (PFR); the Equational Theories project (SAT solvers + Vampire/EProver grinding edges, humans/AI on the interesting nodes); frontier models as proposal generators on Erdős problems, humans verifying. ## Decisions (grilled with the user) 1. **Deliverable** — a framework (preset + skill + compute lanes) enabling any model to run long, difficult mathematical tasks **unattended within a single live session**; problems are empirical/computational (econ/finance/portfolio/simulation), not abstract proof. Crossing a session boundary disarms the goal and needs one human turn to re-arm (decision 10). 2. **Rigor gate** — hybrid: falsification by default; escalation to exact/ formal verification when correctness hinges on an unproven claim. 3. **Check battery** — (A) closed-form equality, (B) exact invariants, (C) analytic bounds, (D) staged statistical hardening; on simplified/special cases BEFORE numerical implementation. The battery is a **reference-case sanity gate**, not a general-validity proof; general validity is established in explicit stages (see the skill's references/lifecycle.md). 4. **Trust** — two tracks (method open / ground-truth re-derived twice by different means) + adversarial audit; counterexample-only elimination; "a producer cannot certify its own output". Context isolation between the root and its subagents is harness-enforced; web access, filesystem scope, and recursion are **procedural** (per-role delegation tools), not a "wall". 5. **Compute substrate** — pinned uv Python lane (sympy/mpmath/cvxpy/hypothesis/ jax) as default; **jacobian MCP as the independent escalation verifier** — dual verification, jacobian kept as escalation only. The `mcp-jacobian` row ships **disabled** and the command **pinned** (`jacobian@0.12.0`) so nothing runs at mount time; provisioning is approval-gated. 6. **Stochastic convention** — fixed seed + LLN: sampling error against the analytic mean must shrink (≈ C/√N) as N grows. Seeded, environment-pinned replay — not portable bit-identity. 7. **Isolation** — track-split: method track open (existing results allowed), ground-truth track re-derives; the orchestrator-detected off-grid toggle hands the route to the OffGridThinker, a separate agent whose only boundary is other people's results (no web, no prior context, no local files; the compute lane stays). These are procedural separations; only context isolation is harness-enforced. 8. **Multi-agent mechanism** — DSH-native: per-role delegation tools (`subagent_explorer` explorer, `subagent_offgrid` OffGridThinker and `subagent_double_checker` DoubleChecker, both web-denied, `subagent_adversary`; each `maxDepth: 1`, which permits exactly one level of delegation — a child is always at depth ≥ 1, so `maxDepth: 0` would block delegation entirely) + `workflow` fan-out with JSON schemas + goal-round driver; registry/journal files are the cross-round memory. `subagent_fork` is not used for track work. *(Amended on `feature/subagent-context`: the `subagent_fork`, `workflow`, and `ralph` rows are disabled outright in this preset — their children carry no role tag, and workflow `agent()` calls express neither a per-child persona nor a per-child toolFilter, so the preset declares them out of scope rather than minting unscoped, root-persona'd agents. Fan-out remains the goal-round driver plus the per-role delegation tools.)* *(Amended by Decision 24: the per-role delegation rows, the subagent control/list rows and the fork row are gone; teammates are created by `spawn_teammate` as `-`, awaited with `wait_agent`, and coordinated as a task DAG on the Team board. No delegation row remains enabled, so no row sets a `maxDepth` and depth one holds by construction: no teammate can create teammates. The goal-round driver was never this preset's to mount, either: `goal`, `goal-round-driver` and the goal session driver are host-plane rows of the shipped base bundle (`@deepseek-ai/dsh-base`), and the preset re-mounts only the human `/goal` command and the model-facing goal tool, which the web bundle disables at the host plane. The 0.1.5 study's "mount it" is superseded by `docs/upgrade-0.1.6.md` §5 N1.)* 9. **Model routing** — one model everywhere (user's choice); reasoning-effort knob available per role; independence comes from context separation. *(Superseded by Decision 16: routing is per-role through the rq-model-router plugin — the DoubleChecker and adversary ship `deepseek-v4-pro`@high natively, every other role inherits, and the user overlays choices per role.)* 10. **Lifecycle** — PASS → auto-implement under branch/worktree + frozen write scope + rollback, then proceed; BLOCKED → same exact gap 3 consecutive rounds → deliver strongest derivation + exact gap; UNKNOWN → recorded when neither proof nor counterexample lands; BUDGET → 5 orchestrator rounds → checkpoint + report. One study goal (no per-sub-problem goals); budget fields (`max_cost_usd`, `max_wall_minutes`) may be set. Resuming across a session needs one human turn. *(Amended by Decision 17: the BUDGET trip moved from 5 to **3** orchestrator rounds, and the auto-implement safety mechanics live in the skill's references/lifecycle.md — frozen write scope, target tests, rollback path.)* 11. **Publishing** — repo distributes a bundle (package.json `dsh.bundle.patch` + cordis.patch.yml registering the skill), an agent preset + bundled skill (install.sh), MIT, `dsh-plugin` GitHub topic — compliant with the awesome-list `dsh plugin add` convention. The npm bundle ships `env/` and `mcp/`; `install.sh` anchors the compute lane at `$DSH_HOME/share/rigorquant`. 12. **Workspace** — a **study** is the self-contained work unit: the user's assignment, one question worked in one directory with an identical internal structure everywhere. Two modes, implied by location, no config flag: **Mode A — one study per repo** (`study.json` at repo root) and **Mode B — multiple studies per repo** (`studies//study.json`, roster derived from `studies/*/study.json`). Durable deliverables (study.json, STUDY.md, registry.json, journal, derivations/, audits/, artifacts/) are committed; ALL scratch lives in `interim/` (explorer-reports, gt-scripts, tmp), gitignored via a study-local `.gitignore`. Intake resolves the study root by detection first (bounded at the git root, rejecting symlinks) and asks the user at most ONE question (mode + slug) when creating a study — resumed studies ask nothing, keeping runs unattended within the session. Supersedes the legacy hidden `.rigorquant/` layout, which conflated deliverables with scratch and could not hold multiple studies. ## Review amendments The four-pass repository review ([docs/repository-review.md](repository-review.md)) recorded accepted verdicts that amend decisions 1 (unattended scope), 3/6 (sanity gate + statistical criteria), 4/7/8 (isolation language + per-role tools), 5 (opt-in, pinned jacobian), 10 (one goal, auto-implement safety), and 11/12 (bundle contents + workspace). Those verdicts are the source of truth for the wording above; the checker CLI and JSON Schemas they require ship inside the skill, at `skills/rigorquant/scripts/` and `skills/rigorquant/schemas/`. ## Decision 13 — the checker is the honesty boundary A later review demonstrated that a study with an empty `derivations/`, empty stage `outputs`, a one-line adversary report and a paper reading "This paper says nothing" was certified `PASS -- complete evidence`. Three rules follow, and they bind every future change to the checker: 1. **A study may not vouch for itself.** Every evidence check reads `audits/`, `derivations/`, `artifacts/` — never `study.json`. A declaration states what was promised; only the record states what was done. 2. **Parse, never grep.** Registry state is read as JSON and traversed; `"passed"` appearing somewhere in the file is not a passed route. 3. **One validator, one schema, both tested.** A second copy of either is how the two silently diverged into different programs. `tests/` enforces this, and the schemas are what the validator actually loads. ## Decision 14 — literature lane A new literature-research lane answers "what is settled / impossible / open / current" before compute is spent, and a membrane exports ONLY verified negatives (proven impossibilities) to the off-grid lane so it stays un-anchored. The lane is a grad-student-style citation-graph traversal (backward references + forward citations + related work + surveys), walled per line, with an independent literature adversary that re-retrieves each load-bearing claim and certifies validity + freshness (version/venue/retraction/supersession). Tooling is two vendored skills (arxiv and academic-paper-search — both MIT; each skill's own vendoring record carries its provenance) plus a tiered retriever (author page → open repos/Unpaywall → preprint → OpenAlex/CORE → user-supplied mirrors, disabled by default). Thoroughness outranks speed (10+ hr runs welcome); the completeness gate — not the budget ceiling — is the finish line. The field procedure lives in the `literature` skill; this record owns the decisions. **Locked constraints.** These are the user decisions the implementation is bound by, and they outlive any particular gate: | # | Constraint | |---|---| | C1 | The blind lane keeps `bash`. Blindness is **tool-enforced** for web_search/web_fetch and delegation, **procedural + audited** for no-curl and no-cross-lane-read. Never described as a "wall"; the residual holes are named below. | | C2 | The blind lane gets **delegation denied outright**, not merely capped by depth. Blind = the DoubleChecker (always) + the OffGridThinker (the off-grid toggle). A per-role network sandbox in DSH core is the future upgrade path. | | C3 | A **verified negative** is a *mathematically proven* impossibility, falsehood, or known-intractability. Expert opinion, "big names think it unlikely", and the absence of a known result are NOT negatives. | | C4 | The literature lane briefs the orchestrator; the orchestrator passes the off-grid lane **negatives only — never hints, never semi-positives**. | | C5 | A fully-settled sub-problem gets no off-grid lane (the answer is a citation). A fully-impossible one gets no off-grid lane either (the answer is the impossibility, recorded as `status: "impossible"` with its math-lane escalation). | | C6 | Paywall bypass is permitted and author-hosted copies are first-class. Retrieval is tiered; mirrors are **user-supplied and disabled by default**, with the legal basis recorded by the user. | | C7 | Thoroughness beats speed; 10+ hours per run is acceptable. The budget is a resume-able safety ceiling, never a finish target. *(Amended by Decision 17: budgets are now **finish targets**, bounded by default — a line concludes at the budget with the strongest completed dossier and its remaining checklist items open; exceeding it is an explicit, recorded escalation.)* | **The membrane, as edges.** This is the complete set of crossings; anything not listed does not cross: ``` orchestrator ──line hypotheses──▶ lit line-agent (walled) lit line-agent ──dossier──▶ orchestrator (interim/, never read by the off-grid lane) orchestrator ──claims list──▶ lit adversary (NOT the dossier prose) lit adversary ──verdict──▶ orchestrator orchestrator ──verified negatives (provenance-stripped)──▶ off-grid lane ``` Open status never crosses (transmitting "this is open" is a hint), settled results for other sub-problems never cross, and no source, survey, or related-work framing crosses. The lane certifies "the literature says X, and X is current" — never "X is true"; that stays with the math lane, and a negative the study's *conclusion* rests on escalates there before the study may rely on it. What is **enforced**, and where: the blind deny lists live in the composition (`tests/test_blind_deny_list.py`); the known-mark, routed-away-impossible + math-lane escalation, negative-export subset, completeness-checklist, refs-seed and fabricated-citation gates live in `rq_check.py` (`tests/test_literature_gate.py`, `tests/test_integration.py`). The lane's boundary to the outside world (arXiv, Semantic Scholar, Crossref) is a live network dependency; `tests/test_retrieval_boundary.py` marks it unverified rather than reporting "not run" as "passed". **Residual holes, named.** What stays procedural and audited, because no per-role network or filesystem scope exists in the spawn provider: - **Bash-curl.** The blind lane keeps `bash`; nothing prevents it from curling `export.arxiv.org`. The math adversary audits for it — a blind output that cites or recalls an external result it could not have derived is flagged. - **Mounted checker-lane search** (named with Decision 23). The OffGridThinker must not fetch known results, but a mounted jacobian/Lean lane reaches every child (plugin-optional tools cannot appear in a static deny list — `tools.restrict` throws on unmounted names). Procedural rule, stated in the persona: a checker lane may verify a derivation the agent made itself; its find/search operations are off-limits, and the agent must say which checks it ran. Same audit as bash-curl — an off-grid output that leans on a lemma it could not have derived is flagged. - **Cross-lane filesystem read.** A blind child could read `literature/` or a sibling's `interim/lit/`. Same class as the ground-truth hole the repository review named; per-line directories and a root-only merge are conventions, not enforcement. - **Paywalled full text.** Where no tier yields full text, the lit adversary records `unverifiable` (abstract-only): a lower confidence tier, never `verified-current`, never load-bearing. - **Mirror rot.** Mirrors rotate domains and break unattended runs; the user-supplied, empty-by-default tier keeps those URLs out of this repository and makes the legal basis a user-owned fact. - **Hallucinated dossiers.** The lit adversary's independent re-retrieval plus the provenance gate is the defense; a dossier may never vouch for itself. - **Rate limits and resume drift.** Semantic Scholar and Crossref throttle; retrieval must back off and record partial sweeps honestly rather than fabricate coverage, and a resumed walk re-anchors on the checkpointed frontier, with the completeness checklist carrying what was already swept. - **Cost.** Thoroughness-first runs burn tokens; `max_cost_usd` / `max_wall_minutes` remain available as ceilings. Deferred on purpose: OCR of PDFs (revisit only if abstract-only verification proves insufficient in practice), and any mirror URL hardcoded in-repo. ## Decision 15 — the retrieval skills install globally `arxiv` (MIT, vendored verbatim from NousResearch/hermes-agent) and `academic-paper-search` (user-authored SKILL.md, MIT, author-confirmed 2026-08-16) are useful to any preset, not just this one, and the literature roles load them by name. `install.sh` therefore copies both to `$DSH_HOME/skills/` in both install modes and removes them on `--uninstall`, while the preset keeps its own copies under `agent-presets/rigorquant/skills/` so a checkout is self-contained. The blind roles deny `skill` outright, so a global install never widens what the off-grid lane can reach. ## Decision 16 — role-routed models (the rq-model-router plugin) The preset used to run every agent on one model: children inherit the parent's route, so the session default (flash@high) powered the DoubleChecker and the adversary too. The economics point both ways — most of the agent volume is divergent exploration and retrieval where flash@high is the right price, while the two proof-critical roles are exactly where a weak model burns the most rounds (a wrong derivation or a missed counterexample triggers the whole BLOCKED loop). Decision: keep **dynamic per-role policy** in a plugin, while fixed child defaults may use the native tool-row configuration in the composition. - **Mechanism.** The `dsh-rigorquant` package ships a host half that listens on the `agent/request` waterfall. It mounts at profile boot, so its listener can overlay an explicit user choice or an active fallback after the native child route has resolved. The per-role choice — provider, model, AND reasoning effort — wins over both the chatbox picker (root) and parent inheritance (children) only when the user configured an override; otherwise the fixed-tier tool row's native `agentOptions` remains authoritative. DSH 0.1.2 now carries `reasoningEffort` in that native channel, so it is no longer necessary to rewrite the shipped DoubleChecker/adversary primary on every request. - **Role identity.** Every role persona carries a machine-readable tag `[[rq:role=]]`. Continuable children persist the persona in their first `subagent/descriptor` event; one-shot (foreground) children carry it only in the live prompt, so the router probes the child's assembled persona section once. Children without a tag — fork, workflow workers, ralph rounds — and sessions on other presets are never touched. The `root` role applies only to sessions without a `parentSession` (a workflow worker also runs under this preset, but it is not the root). - **Root follows the chatbox.** The root role has no primary by default: the picker stays the master switch for the root and for every role left on "inherit". Pinning root is a one-select action in the card. - **One fallback per role.** On a terminal primary failure (no adapter, a model its provider does not declare or cannot resolve — `UNKNOWN_MODEL` or `INVALID_CONFIG`, codes with no status — or an HTTP 4xx the route cannot recover from) the router degrades that session+role to the role's own fallback and forces exactly one retry. A successful assistant step on the fallback — or the TTL (10 min) — restores the primary; a failing fallback is never retried again by the router. Each degrade and each route-fatal give-up (the fallback failed too, or the role has no fallback) logs one warning naming the role, the `provider/model`, and the settings key it came from (issue #22): the teammate's own failure surfaces only as an unaccepted initial prompt. - **Effort fallback to the model default.** A stored choice may carry a reasoning effort the exact route's model refuses — a model with no reasoning surface at all, or one that does not list the saved level. The LLM service rejects such a request before any provider I/O (`UNSUPPORTED_REASONING_EFFORT`) and the turn dies with it — the failure never reaches `agent/request-error` — so the router consults the model's real effort metadata while routing and drops a refused effort: the model's default level governs and the model override stays intact. The card offers only the catalog's real effort surfaces in the first place (a model with no surfaces offers "Default" only; a stored unsupported level renders disabled), so this lane mostly guards stale saved choices. - **Persistence and UI.** Choices live in the `rigorquant-models` settings namespace (user layer of `settings.yaml`); the browser half renders the form on the bundle's own page under Plugins (`plugins.bundle.config`, keyed by the package name — Decision 20's 0.1.6 amendment), with model and effort dropdowns from the live provider catalog. Only a save writes. - **Shipped defaults.** DoubleChecker and adversary: `deepseek-v4-pro`@high with a `deepseek-flash`@low fallback (a fallback is a degrade lane, not a second full-price route; `deepseek-flash` is DeepSeek-V41-Flash, the flash tier the 0.1.6 default catalog lists — the V4 flash id it replaced is unlisted there, and an unlisted id passes through to the wire and fails only when the lane is entered). Every other role: inherit. Defaults assume the `deepseek-official` catalog; a deployment without it overrides the row config or the card, and a default that cannot route degrades through the same fallback lane (or fails loudly if the fallback cannot either). - **0.1.2 native defaults.** `tool-subagent-double-checker` and `tool-subagent-adversary` now declare those fixed primary choices through the builtin `agentOptions` channel, including `reasoningEffort`. The router reads the raw user layer so a reset returns to that native route instead of running a redundant rewrite. `maxTokens` remains omitted deliberately: the provider's own output ceiling is safer than imposing an arbitrary cap on proof-heavy reports. The new `modelSelectionSettings` allow-list is not enabled for these role tools because caller-selected routes would undermine the forced tier matrix; the routing card remains the explicit override surface. Guarded by tests: every role persona must keep its tag, the router's ROLES list must equal the tagged roles plus `root`, and the fixed-tier rows must retain matching native `agentOptions` defaults. A persona that loses its tag silently falls back to the session model, which is exactly the failure class this closes; `tests/router_probe.cjs` separately pins native pass-through and fallback recovery. ## Decision 17 — budgets are finish targets, with explicit escalation Measured on real runs, the previous framing — "the budget is a resume-able safety ceiling, never a finish target; 10+ hour runs are expected" (literature lane), `max_orchestrator_rounds: 5`, fan-out 2–4, and an unconditional dual ground-truth track — let a single study bill into the hundreds of millions of metered tokens, dominated by per-step re-sends of a ~9k-token header plus open-ended traversal. Decision: **bounded by default, escalation is an explicit recorded act.** - `max_orchestrator_rounds` default 5 → **3**; BUDGET fires at 3 rounds. - Fan-out default 2–4 explorers → **1–2**; the second ground-truth track is mandatory only for **load-bearing** claims (the whole study rests on them), never one agent doing both "independent" derivations. - Literature budget default 8/4/80/8 → **4/3/20/4**. A line concludes at the budget with the strongest completed dossier and remaining checklist items recorded open. Exceeding the budget requires an explicit user escalation recorded in `study.json` `literature.budget` (raising numbers or setting `max_cost_usd`); a silent overrun is a defect. `max_wall_minutes` stays unset by default (the field remains nullable). - Compaction fires at **60%** of the routed context window (was 80%) and the tool-result pruner retains **4 KiB** per result (was 8 KiB): an earlier, smaller compaction shrinks the cached prefix re-sent on every step. The model-facing tool catalog itself is host-plane — the heavy tools (`workflow`, `ralph`, `ask_user_question`, …) are registered by the profile composition, not this preset, so they cannot be trimmed from this repository; a session's catalog is the union of host tools + preset rows. - Journal stays append-only (a rolling journal was considered and rejected: the append-only record is the study's audit trail). ## Decision 18 — claim-keyed blocking (narrow before patch) The 20260820 var-expected-return-term run burned rounds 2–6 of an 8-round budget re-certifying one stage-3 claim: every round found a genuine new defect (interval-independence → input-truth hypothesis → funding → factor-map hypothesis → log-vs-simple funding rate), so the "same gap 3 rounds → BLOCKED" rule never fired and patching never had to stop. Repeated independent failures on one claim are evidence that the claim's scope exceeds what is provable. Decision: **BLOCKED is keyed on the claim, not the gap** — after two consecutive NEEDS-EDITS on the same claim or section, the next round must either narrow the declared scope to what is certified or declare BLOCKED with the exact gap; a re-patch that changes only the mechanism, not the scope, is a defect. Guarded by procedure (lifecycle.md, SKILL.md Step 3). ## Decision 19 — verdict data is the deliverable; status waits for it The same run's other loops were all process failures: six delegated agents produced complete verdict JSON (36/36 cells) without ever writing the report files; queued follow-up messages to settled agents produced re-audits of dead documents; concurrent edits invalidated audit hashes; `status` certified the orchestrator's own repair before the ruling landed; and the orchestrator's own generator emitted a wrong table cell that went unaudited. Decision: - A delegated verdict is the **report with the `VERDICT:` line**; the JSON is a side effect. A run that settles without the verdict line is a failed run: read the results JSON once, record the verdict, never re-dispatch for prose. The orchestrator may transcribe an independent agent's structured verdict; transcription is not certification. - An artifact under adversarial audit is **frozen until the verdict lands**, and the verdict records the audited snapshot's SHA-256. No follow-up messages to in-flight or settled agents. - **Status is written from verdicts.** `rq_check.py` refuses a status that asserts a certification outcome without referencing an existing verdict file or frozen hash (`status.verdict-reference`). - The stage-3 claim's digest (`claim_sha256`) is recorded at certification; `rq_check.py` flags an edit after certification as needs-re-certification (`stage3.claim-digest`). - Schema and validator digests are pinned at intake (`intake_pins`); `rq_check.py` flags a reissue mismatch as a re-intake event (`intake.schema-pin` / `intake.validator-pin`). - Orchestrator-produced numbers are audited like agent output (second instrument); document-adversary passes are capped at two per deliverable. Guarded by `tests/test_procedural_gates.py`. ## Decision 20 — follow the harness surface across 0.1.1 → 0.1.6 Originally studied against deepseek-harness 0.1.1-rc.2 (then the newest release; the running harness was 0.1.0-rc.7), where the host half and preset were byte-compatible and the browser half hit one breaking change: `@deepseek-ai/dsh-client-schema-form` was deleted in rc.2 and its helpers folded into the `settingsSchema` service (`rehydrate`/`validate`; path helpers unchanged). **Re-surveyed for 0.1.5** (`docs/upgrade-0.1.5.md`): the dual-version client is gone and the floor moved to `0.1.5-alpha.2` (0.1.6 raised it again — see the amendment below). 0.1.3-alpha.2 split the persona row's single `text` key into a required `prefix` plus a `suffix`, and a row whose config fails validation rejects the WHOLE preset mount; 0.1.2-rc.1 removed the child-scoped `report` tool; 0.1.5-alpha.2 is where the deliverables tool (`present`) and the right Sidebar land. Decision: - **Take the draft model from `settingsSchema` — only.** `dsh/client.js` resolves the draft model from `ctx.settingsSchema` (@deepseek-ai/dsh-client-ui-settings). The legacy `@deepseek-ai/dsh-client-schema-form` fallback was removed in the 0.1.5 port: that package is gone from the harness and from the browser's frozen module table, so the `require` could only ever throw inside the controller's construction and take the whole bundle entry down. The probe's module table refuses to answer the deleted name rather than providing it, so a residual require fails the mount (`tests/client_bundle_probe.cjs`, `test_card_uses_the_settings_draft_model`). - **Deliver verdicts as the final assistant message.** Through DSH 0.1.1 the L2 contract was the child-scoped `report` tool; 0.1.2-rc.1 removed it, and a continuable child's **final assistant message** is now what the runtime hands to the agent that started it (and what wakes it). Every role persona and the protocol/SKILL documents say exactly that, and the seven child `toolFilter` deny lists no longer name `send_message` — a depth-1 child may message its direct parent (`interrupt_agent`/`list_agents` stay denied). See `docs/upgrade-0.1.5.md` §4.3. - **One persona section, split.** 0.1.3-alpha.2 replaced the single `deployment:persona` section with `deployment:persona-prefix` and `deployment:persona-suffix`, and the `dsh-persona` row now takes `prefix` (required) plus `suffix`. The router's `PERSONA_SECTION` constant follows the rename; the preset's identity prose is the prefix and the working-directory sentence is the suffix. - **`present` is a preset row.** No bundle mounts `@deepseek-ai/dsh-tool-present`; every preset that wants model-declared deliverables carries the row. RigorQuant now does, which is what the deliverables skill's closing `present` call depends on. - **Parallel `web_search`.** rc.8 added a `queries` array (default 4, merged); the literature lane batches independent queries. - **Named external-agent bundles.** Claude Code / Codex are installable as profile bundles (named instances, per-role permission modes: plan/never default, bypass reserved for the approval-gated escalation lane). The preset documents the modes on the disabled rows; bundles install at the profile, not in this preset. - **Multimodal.** rc.8/rc.1/rc.2 add native DeepSeek image requests, the `deepseek-v4-flash-vision-exp` model, Files API upload+reuse, and a model-facing `read_image` tool. The router card renders the live catalog, so the vision model appears on rc.2 without config; roles that read figures may use `read_image` when the route supports image input. - **Watch (not wired):** the experimental `agent-team` domain (shared task DAG, `spawn_teammate`/`wait_agent`) is the closest native match to the round-loop fan-out; adopt only when it stabilizes. **Amended for 0.1.6 (0.4.2, the last classic release; `docs/upgrade-0.1.6.md` §3):** two browser seams broke silently and are followed, not worked around, and the **required floor is now `DSH ≥ 0.1.6-alpha.2`** — enforced by `install.sh`, documented in both READMEs and the preset header, and pinned by `test_repo_consistency.py`. The floor is above every row's own requirement because the two seams below are browser-side: on 0.1.5 the preset still mounts while the card and the floater render nothing at all. - **The routing card is the bundle's configuration entry on the Plugins page.** 0.1.6 retired `settings.plugin.item` (plugin configuration moved out of Settings); a registration into a slot nothing renders fails silently. The card registers on `plugins.bundle.config`, keyed by the package name, and renders the two views the page asks for — `summary`, one line; `page`, the form with its own Save. It follows the page's form contract: only a save writes, so Discard and the unsaved marker are gone and leaving the page drops staged edits. The draft model, the `settings.describe` seam and the model-catalog seam are unchanged. - **The floater finds the main-view session by retain info.** Client sessions are references and the list has no `current` field. The floater scans the client session list for the id whose `sessions.retainInfo(id).retainedBy.mainView` is positive — the check the harness's own team UI makes — and re-scans on every list publish. Nothing reads a `current` field, and the client-bundle probe pins that on the source as well as on behaviour. - **Disabled rows still have to be honest.** The workflow engine row follows 0.1.6's rename to `workflow-ptc` (`@deepseek-ai/dsh-workflow-worker-thread` no longer exists) and the disabled external-agent rows move from `enableRunInBackground: false` to `backgroundMode: one-shot`, matching the shipped `standard` preset. All three stay **disabled** for the reasons that disabled them (untagged, unscopeable children). Nothing mounts either way — a disabled row is never imported — but the harness probe reported the stale package UNRESOLVED, and a row that cannot resolve reads as a typo rather than a decision. `modelSelectionSettings`, which `standard` turned on for `tool-subagent`, stays **off**: a caller-chosen model overrides the role's routed tier (Decision 16). - **Fan-out is bounded by the host's pool.** `maxActiveSubagents` (default 8) caps live children per root; over it a spawn raises `ACTIVATION_LIMIT_REACHED`, which nothing but a child settling clears. The skill and the protocol say to batch the round and wait, never to retry in a loop; raising the setting is the operator's change in Plugins, not the orchestrator's. - **The installer detects Agent Teams; it does not enable it.** A full install reads `dsh.profile.bundles` in the target profile's `package.json` and reports whether both optional Team bundles are on, printing where to toggle them when they are not. 0.4.2 is the classic release and needs neither; enabling a bundle appends a layer to the profile's stack, which changes what every session in that profile composes, so it stays the operator's decision (Decision 24 is where 0.5.0 enables them, under a marker it can find again). An unreadable manifest is reported as unknown, never as "off". The floor itself is enforced only on this path: the bundle install of Decision 22 runs after the profile has composed, so `dsh/sync.js` states the floor and cannot check it. - **The deprecated synchronous history reads stay, with the deferral note.** 0.1.6 deprecated `ownEvents()`/`snapshotEvents()` under the policy "existing logic may remain unmigrated for now, but new calls are prohibited". The router and the activity half each keep one helper carrying that note; the consistency suite pins the call count, so the deferral cannot quietly grow a third caller. Both disappear under Decision 24, where a role comes from the teammate's name. Storage note: rc.8 changed the SQLite backend format (no migration), but it is opt-in; rigorquant sessions persist as JSONL, which is byte-compatible across the upgrade. ## Decision 21 — reproducibility is the record; junk is derived state Core philosophy of the plugin, hard-won from the 20260820 var-expected-return-term run: the study tree ended at ~729 MB of venvs and caches, its deliverables promised "reproduction" through gitignored scratch paths, and a fresh clone had zero working reproduction commands. Two obligations: - **Perfect reproducibility.** The committed study record is self-contained: a fresh clone plus the pinned uv lane regenerates every piece of evidence. Every command a deliverable prints, and every path the record cites, resolves to a tracked file — never to `interim/` scratch. Study-generating scripts live in a tracked `code/` directory; record-cited data files live in `audits/` / `literature/`; the pinned lane's `pyproject.toml` + `uv.lock` make the environment reproducible by declaration, not by presence. - **Minimal junk.** Derived state (venvs, uv caches, bytecode, OS metadata) is never committed and is deleted at close-out; `interim/` is the one designated scratch home, gitignored at intake. Cleanup and reproducibility are the same close-out pass. Enforcement is in `rq_check.py` at PASS time: `evidence.junk` (derived state on the committed surface), `deliverables.scratch-refs` (a deliverable citing `interim/`), `deliverables.repro-paths` (a deliverable citing a missing `code/|derivations/|audits/|literature/` file), plus the pre-existing registry-outputs-exist check. Full statement, operational rules R1-R7, junk taxonomy and the close-out sweep protocol: `agent-presets/rigorquant/skills/rigorquant/references/reproducibility.md`. ## Decision 22 — the bundle self-installs the preset and the lane The distribution previously had two doors: `./install.sh` (everything) and `dsh plugin add` (a router with nothing to route, because only install.sh could land the preset and the lane). The asymmetry was mechanical, not stylistic: - **Presets are outside the patch plane.** Discovery is a filesystem scan of `$DSH_HOME/.agent-presets` (plus configured and shipped roots), and the harness's own profile overlay pins the `agent-presets` row's `roots` to the shipped root after every bundle layer composes — no `cordis.patch.yml` row can point the roster at node_modules. - **node_modules cannot host the compute lane.** A venv is derived state with absolute paths; pnpm's virtual store is version-pathed (an upgrade churns every recorded absolute `env_lane` in existing studies' `study.json`) and volatile (`pnpm remove/update` deletes a provisioned environment mid-study). Decision: ship a second host half, `rq-preset-sync` (`dsh/sync.js`, exported as `./sync`, mounted by a bundle row). Once per profile boot it lands files — the same work `install.sh` does, executed inside the host process: ``` agent-presets/rigorquant → $DSH_HOME/.agent-presets/rigorquant env/ mcp/ docs/ → $DSH_HOME/share/rigorquant/ ``` Contract, each property pinned by tests: - **Idempotent byte-compare** — identical trees are left untouched; no mtime churn on files a watcher may be serving. - **Replace on install/upgrade** — changed or missing files are copied and target-only entries pruned, EXCEPT derived state (`.venv`, `__pycache__`, `*.pyc`, `.DS_Store`), which is never copied out of a source and never pruned from a target. A lazily provisioned venv at the lane anchor survives every boot and every package update. - **Local-edit preservation** — a target stamped with the CURRENT version whose shipped files all still exist is kept (`kept-local`). The preset is meant to be edited in place (the escalation lane enables `mcp-jacobian` by flipping a row in the INSTALLED composition); boot-sync must not revert that. An upgrade moves the stamp and legitimately replaces shipped files — the same contract as re-running `install.sh`. - **Soft failure** — a sync error logs a warning; it never blocks the profile or the router sharing this package. Lifecycle honesty: DSH's plugin CLI has **no uninstall hook** — `dsh plugin remove` is pnpm delete plus a manifest reconcile, and code that no longer exists cannot run — so "remove the preset when the plugin is removed" is not implementable from inside this package. Each install/boot instead REPLACES the managed trees wholesale, every managed root carries an ownership marker (`.rq-sync.json`: manager + version + syncedAt) so ownership stays discoverable after removal, removal stays explicit (`./install.sh --uninstall`), and orphaning is benign: the synced preset is self-contained (skills travel inside its directory) and merely routes nothing without the router. Tests: `tests/test_preset_sync.py` executes the engine over real directories via `tests/preset_sync_probe.cjs` (first sync, quiet rerun, replace-on-bump, venv-survival, no venv leakage, kept-local edits, damage restore); `test_repo_consistency.py` pins the wiring (row mounted, export resolves, exclusions named). ## Decision 23 — role identity: DoubleChecker, OffGridThinker, `subagent_explorer` Three renames landed together because they are one decision about what a role name must carry: the name is protocol. Every delegation tool is model-facing, and the model reads the tool name as part of the brief. *(Amended by Decision 24: none of the names below is the identity any more. A teammate's role comes from its `-` roster name, and no delegation tool carries one — the tool, row, tag and settings keys this decision renamed are the record of how identity was carried before the Team layer.)* - **Oracle → DoubleChecker.** "Oracle" claims an authority the role does not have — an oracle vouches, and nothing in this framework vouches. The role's contract is narrower and its name now states it: re-derive the load-bearing claim **twice, by genuinely different means**, in epistemic isolation. Identity moves with it: tool `subagent_double_checker`, row `tool-subagent-double-checker`, tag `[[rq:role=doublechecker]]`, settings keys `doublechecker{Primary,Fallback}` (the native `agentOptions` primary and the router's `NATIVE_PRIMARY` map move too). The "ground-truth track" KEEPS its name — it names what the track produces (independently derived reference targets for the check gates), not the agent that runs it. - **The novelty toggle becomes its own agent: OffGridThinker** (was "explorer in full Jin isolation", sharing the explorer's tool label and portrait). The coupling understated the role: its boundary is not "an explorer told to ignore the web" but **other people's results** — no web, no literature, no other agents' drafts — while raw model intelligence plus the full compute lane (sympy/numpy/mpmath/cvxpy/hypothesis/jax) is not merely allowed but expected in every derivation. Identity: tool `subagent_offgrid`, tag `[[rq:role=offgrid]]`, settings keys `offgrid{Primary,Fallback}`, own persona, own portrait. One residual hole joins the Decision 14 list, same class as bash-curl: a mounted checker lane (jacobian/Lean) reaches this child too (plugin-optional tools cannot be named in a static deny list — `tools.restrict` throws on unmounted names), so the persona states the rule procedurally — a checker lane may VERIFY a derivation the agent made itself; its find/search operations (fetching known results) are off-limits, and it must say which checks it ran. - **`subagent` → `subagent_explorer`.** The open explorer's delegation tool carried no role name, so a persona drift or a copy-pasted deny entry could hide behind the generic name. Every delegation tool now names its role: `subagent_explorer`, `subagent_offgrid`, `subagent_double_checker`, `subagent_adversary`, `subagent_lit_line`, `subagent_lit_adversary`, `subagent_document_adversary`. - **Migration.** Role-key renames change the `rigorquant-models` settings fields; old `oracle*`/`novel*` user-layer entries are ignored (a stale field is a dead setting, not a wrong route — the native/inherit defaults govern until re-entered). The activity monitor's best-effort `labelRole` keeps mapping legacy `gt-`/`ground truth` one-shot labels to the DoubleChecker, so runs started before the rename still light up correctly. *(Amended by Decision 24: the monitor is deleted in 0.5.0, so the legacy labels have no reader left; the settings keys above are unchanged, and the role prefix — not a tool name — is the identity the router resolves.)* - **The activity pillbox is a hub-and-spoke map.** The previous five-layer DAG drew handoffs that do not exist — `explorer → oracle`, `lit-adversary → adversary` — implying role-to-role channels that would break producer≠checker if they were real. The topology actually enforced is a hub: the orchestrator is the only role that sees every brief and every report, and no two children ever exchange anything. The map now renders that (root at the center, seven spokes in call order), and a consistency test pins the structure and forbids the retired DAG constants from returning. *(Amended by Decision 24: the map is a static figure now, and the topology is enforced by `dsh/team.js`'s per-call guard rather than drawn — the pin follows the guard.)* Tests: `test_repo_consistency.py` pins the hub-and-spoke topology and the absence of caller-selectable models, and the tag↔row identity this decision pinned went with the `[[rq:role=…]]` tag — replaced by the role-name↔persona-file identity; `conftest.py`'s `BLIND_TOOLS`/`DELEGATION` sets and `test_role_tool_budgets.py`'s budgets follow the role tiers; the router and client-bundle probes exercise the renamed settings keys end to end. ## Decision 24 — RigorQuant runs on Agent Teams (0.5.0) Recorded as an ADR: `docs/adr/0001-rigorquant-on-agent-teams.md`. From 0.5.0 the multi-agent mechanism is DSH's experimental Agent Teams, consumed as the shipped optional bundle and never imported: **identity by name** (`-` teammate names replace the `[[rq:role=…]]` persona tag), **enforcement by scope** (a host plugin applies each role's persona, tool budget and bash network denial on the teammate's `agent/created`), **topology by guard** (a teammate may message only the orchestrator; every teammate is roster-blind). Amends 8 (mechanism: `spawn_teammate`/`wait_agent` and the task board replace the per-role delegation tools), 14 (the bash-curl hole is denied at the call for web-denied roles), 16 (the router carries the tier matrix; no native `agentOptions` rows remain), 19 (L3 keeps freeze-and-hash; a reused teammate receives only new hash-bound briefs), 20 (floor `0.1.6-alpha.2`; the activity monitor retires for the native team view plus a move pill) and 23 (role identity moves from the tool name to the teammate name; the hub-and-spoke map is now what the guards enforce). **Enforcement-by-scope's composition half shipped under issue #9** (`docs/upgrade-0.1.6.md` §3.9): `dsh/team.js` applies each teammate's persona and global tool-tier budget by name, and registers the orchestrator's "RigorQuant team guard: armed" line as a runtime context (not a section — the harness's own `CONTEXT_ORDERS` family, distinct from a persona slot). **Topology-by-guard shipped under issue #10** (`docs/upgrade-0.1.6.md` §3.10): a `tools.guard` per composed member enforces what `tools.restrict` cannot mask on the scoped Team tools — hub-and-spoke messaging, roster/board blindness, own-task-only board access, the bash network-verb denial for web-denied roles, and the orchestrator's `spawn_teammate` name/fork refusal. **A Lead-only composition gap, found live and fixed** (`docs/upgrade-0.1.6.md` §3.11): a session composed as `rigorquant` only *after* its own `agent/created` already ran (the ordinary "New Session, then pick a preset" UI flow) left the Lead's guard-armed context and `spawn_teammate` guard uninstalled for the session's entire life — teammates are unaffected, since a teammate's preset is already settled at spawn time. `dsh/team.js` now also re-triggers composition on the harness's `agent-preset/selected` event. **Router role-resolution by membership shipped under issue #11** (`docs/upgrade-0.1.6.md` §3.12): `dsh/index.js` resolves a routed agent's role the same way `dsh/team.js` resolves composition — through `agentTeams.tryMembership(agent)`, the Lead as `root`, every other member by its name — and carries the shipped tier matrix itself, since no native per-role model row exists once a teammate is created by `spawn_teammate`. The persona-tag regex, the persona-assembly probe, and the deprecated synchronous session-event reads are gone from the router; the classic per-role delegation rows still use the persona tag until a later issue removes them (`dsh/activity.js`, the tag's other reader, is deleted under issue #13). **The installer enables Agent Teams under issue #12** (`docs/upgrade-0.1.6.md` §3.13): a full install adds whichever of the two optional bundles are missing from the profile's `dsh.profile.bundles` and appends the `maxMembers: 64` cap override into that profile's `cordis.patch.yml` under a `dsh-rigorquant` marker that also records which bundles it enabled, printing every line written; idempotent on re-run. `--uninstall` removes the marker and disables only the bundles the marker records the installer having enabled — a bundle the operator already had on stays on. With no `dsh` on the path the whole step warns and is skipped, keeping CI's install smoke test green. The identical override row ships in this package's own bundle patch as a consistency pin, effective only when the Team layer precedes `dsh-rigorquant` in a profile's bundle order. **The browser goes native under issue #13** (`docs/upgrade-0.1.6.md` §3.14): the activity monitor host module (`dsh/activity.js`), its HTTP routes, its probe and its tests are deleted outright, along with the client bundle's floater/panel/geometry code — the deprecated synchronous session-event reads this repo carried a deferral note for have no caller left anywhere. In their place, a move pill registers on the conversation's per-session `conversation.session.header.utilities` slot (the sibling seat of the Team package's own roster action, which sits in `conversation.session.header.actions`): it reads the Lead's live roster and board through the Team namespace's `remote.agentTeams.view(leadSessionId)` request — the only team read the installed `0.1.6-alpha.2` browser half serves. That namespace is injected optionally, so a profile with no Team bundle never registers the pill at all and renders nothing. The read is addressed to the Lead (a teammate's own header resolves back through `subagent.address.parentSessionId`), and re-read on a short interval, because that namespace answers requests rather than subscriptions. The move is derived structurally from the task board's `blockedBy` DAG (a task's layer is one past its deepest blocker; the shallowest layer with incomplete work is the move), never from task text, so it needs no naming convention the orchestrator side has not been given yet. Rendering nothing while the projection is absent covers "no team running" and "the Team bundle is not mounted" identically. The static hub-and-spoke topology figure in the docs is untouched by that change. **Team-only preset and procedure under issue #14**: the seven per-role delegation rows, `tool-subagent-control`, `tool-subagent-list-agents` and the disabled fork row leave the preset (the external-agent rows and the checker lane stay disabled, `present` stays), so the Team tools are the only delegation mechanism and the `[[rq:role=…]]` tag has no carrier left. The orchestrator persona's ISOLATION paragraph now says role by name, tool budget by scope, topology by guard — still not a network wall — and states that a RigorQuant study is the explicit request the Team policy asks for, spawning only while the "guard armed" line is present. SKILL.md Step 3 runs the round as five moves on `spawn_teammate` / `wait_agent` / `send_message` over a layered task DAG (explore → ground-truth → attack → certify, one `blocked_by` layer per move, which is exactly what the move pill reads); protocol.md carries the brief contract, the roster policy, the rewritten L3, the pool rule and the lifetime-cap BUDGET rule. **The documents speak the glossary under issue #15**: both READMEs' Install sections state the floor, the alpha-plus-experimental dependency, the Beta toggle (the *Agent Teams* and *Agent Teams Web UI* cards under **Plugins → Official**) and the installer's enable/override/uninstall behaviour; "The team, live" is rewritten around the native roster, the task board, opening a teammate and the move pill, with the hub-and-spoke figure kept as the picture of what the guards enforce; a **Deployment notes** section covers the DeepSeek session log (`session-log-deepseek` with `enabled: false` in the profile's user patch, `$DSH_HOME/profiles//cordis.patch.yml`), the host-mounted goal-round driver (this decision's Decision 8 amendment) and the `workspace-changes` turn card as the human-visible witness of an edit after certification (Decision 19). `test_repo_consistency.py` sweeps the tracked documents for the two collisions `CONTEXT.md` lists under *Avoid* — a move named as a stage, a study named as a task, in English and Chinese — and pins both READMEs' team, install and deployment sections by heading. **Enforcement by scope has to follow the plugin out, found at release under issue #16** (`docs/upgrade-0.1.6.md` §3.15): every persona, context, restriction and guard `dsh/team.js` installs goes through the agent's own `agent.ctx`, so it belongs to the agent's scope, not the plugin's, and survives the plugin unloading. Toggling the `rq-team` row off in the Plugins page therefore left a live orchestrator armed and guarded, and toggling it back on could not take effect: the backfill re-registered the still-live armed context by name, which the harness refuses. The plugin now disposes every composition it installed from a `ctx.effect` of its own fiber, so a row toggle disarms live agents and a reload recomposes them exactly once. ## Repo map ``` agent-presets/rigorquant/ the preset: composition + persona + rigorquant skill skills/rigorquant/ SKILL.md, references/, scripts/rq_check.py, schemas/ dsh/ host halves: rq-model-router + rq-team (composition and per-call guard) + rq-preset-sync, dsh/personas/ (one role persona per file), and the client bundle (routing card + move pill) cordis.patch.yml bundle patch: skill layer + router + team + boot-sync env/ pinned uv compute lane (pyproject + lockfile) mcp/jacobian.md escalation lane wiring docs/architecture.md this record tests/ the validator's suite; a forged study must FAIL install.sh installs the preset (or --skill-only) into $DSH_HOME ``` A study folder (`studies//` in Mode B, the repo root in Mode A) lives in the *research* repo, not here: `interim/` inside each is gitignored, everything else commits.