# 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 `<role>-<n>`,
   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 Decision 24.)*
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/<slug>/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 (retired in 0.6.0 under Decision 25; it
stays readable at tag `v0.5.0`) 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=<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** (0.4.1): 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).
- **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):** two browser seams
broke silently and are followed, not worked around, and the floor rose to
`DSH ≥ 0.1.6-alpha.2` (Decision 25 raises it again) — 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-R8, junk
taxonomy and the close-out sweep protocol:
`agent-presets/rigorquant/skills/rigorquant/references/reproducibility.md`.

**Amended for 0.6.0 (issue #36, found in the rc.2 live run): the study
carries its own lane.** "A fresh clone plus the pinned lane" had a hole: the
lane was RigorQuant's copy under `$DSH_HOME`, which every release replaces,
and a study needing another package had no committed place to declare it.
On rc.2 the `workspace-write` sandbox also refuses a venv there. Now Step 2
copies the shipped lane's `pyproject.toml` + `uv.lock` into the study's
tracked `env/` (`env_lane: "env"`), builds the venv and uv cache under
`interim/`, and teammates run offline against it; extra packages go into
the study's lane with `uv add --project env`. `$DSH_HOME/share/rigorquant/env`
is only the template. `rq_check.py` enforces it at PASS (`evidence.lane`,
rule R8). Cost: studies cannot share a uv cache under the sandbox, about
1 GB per live study, deleted at close-out.

## 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/<same>
```

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 `<role>-<n>` 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**
(`<role>-<n>` 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**: `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**: 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**: 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**: `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** (Decision 25 cuts
it to one bundle): 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**:
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/<profile>/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**: 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.

## Decision 25 — declared preset on DSH 0.1.7; the escalation lane mounts at runtime (0.6.0)

Recorded as an ADR: `docs/adr/0002-declared-preset-on-dsh-0.1.7.md`.

**Harness.** From 0.6.0 the harness range was `>=0.1.7-rc.2 <0.1.8`, moving the
**required floor to `DSH ≥ 0.1.7-rc.2`**, enforced by `peerDependencies`
and by `install.sh`, stated in both READMEs, and pinned by
`test_repo_consistency.py`. There is no compatibility
with 0.1.6, and 0.5.0 is the last release for alpha.2. The release is one
cutover, installed together with the harness upgrade.

*(Superseded by Decision 26: the floor is now `DSH ≥ 0.2.0-rc.2`, and 0.6.1 is
the last release for this line.)*

**Preset.** `rigorquant` is a declared `@deepseek-ai/dsh-agent-preset` row in
the bundle's own patch file. The directory copy is gone, and so is the
preset's child `skill-filesystem` row: the host skill row serves the skill
to every agent.

**Escalation lane.** It is no longer a preset row. `rq_escalate` can be
called only by the orchestrator, and mounts the jacobian MCP client into
the orchestrator or into a teammate it names. It fails loudly and lives
until the session ends. The orchestrator switches the lane on without
asking, but installing jacobian and setting up Lean still ask the user.

**Routing.** The router's settings move into its profile config. With no
routable `deepseek-official`, the defaults fall back to the same model ids
on `deepseek-account`. The installer ports saved overrides.

**Browser.** The move pill is deleted. The routing card is kept only if
the harness's generated form cannot pick every route from the catalog.

**Leaner by rule.** Whatever the migration makes dead is deleted in the
same release, and so are the tests that pin it.

Amends:
- **5:** jacobian stays opt-in and pinned, but is mounted at runtime.
- **14:** a mounted lane reaches only the agents it is mounted into.
- **20:** the harness range.
- **21:** the study carries its own lane (`env/`, enforced as
  `evidence.lane`); the shared lane is only the template (issue #36).
- **22:** the bundle no longer copies a preset; the sync keeps the compute
  lane only.
- **24:** one Team bundle; the move pill retires; the floor is raised.

## Decision 26 — DSH 0.2.0-rc.2: the same architecture, four native surfaces taken up (0.7.0)

Recorded as an ADR: `docs/adr/0003-rigorquant-on-dsh-0.2.0.md`.

**Harness.** From 0.7.0 the harness range is `>=0.2.0-rc.2 <0.2.1`, and the
**required floor is now `DSH ≥ 0.2.0-rc.2`**, enforced by `peerDependencies`
and by `install.sh` — which refuses a core at EITHER end of the range, because
the loader skips an out-of-range bundle at both — restated declaratively under
`engines.dsh`, stated in both READMEs, and pinned by
`test_repo_consistency.py`. This is not cosmetic: the
CLI's compatibility gate reads a bundle's `peerDependencies` *before* it loads
it, and **skips a bundle the running core does not satisfy**, so on 0.2.0 the
old range would have mounted the plugin nowhere, silently. 0.6.1 is the last
release for the 0.1.7-rc.2 line; nothing is backported.

**What moved.** Almost nothing RigorQuant uses. 0.2.0-rc.2's package inventory
is the 0.1.7-rc.2 one plus telemetry, desktop analytics, a session-log settings
page and an opt-in schedule bundle; the declared-preset loader, persona,
tools, system prompt, settings forms, Agent Teams service, MCP client and home
paths are byte-identical between the two cores. The migration is therefore one
cutover to the new range, not a rewrite.

**Native surfaces taken up.** Four, each replacing work the plugin would
otherwise do itself or leaving a native surface bare:

- **Timed `ask_user_question`.** `dsh-tool-ask-user` grew a `Config`
  (`mode: legacy | timed`, `timeout`). The preset selects `timed` with an
  explicit wait, so the unattended intake ask returns `pending` instead of
  parking the round, and the answer arrives later as an
  `answer_to_pending_question` user message. The persona and the skill state
  the contract: pending is neither an answer nor permission; `timeout: -1` is
  for a step that cannot proceed at all without the answer.
- **Display metadata read without activation.** `locale/en.json`,
  `locale/zh.json` and `icon.svg` give the Plugins page, bundle details and the
  Settings plugin inventory a title, description and artwork, resolved from the
  manifest before the plugin runs. No host code and no client bundle is
  involved.
- **Declarative manifest metadata.** `dsh.manifestVersion: 1` and
  `engines.dsh` state the manifest dialect and the runtime range beside the
  enforced `peerDependencies`, so a reader learns the contract from the
  manifest rather than from a crash. This core enforces neither field yet:
  they are documentation with a machine-readable shape, and the range they
  describe is enforced by `peerDependencies` and by `install.sh`.
- **Field descriptions for the generated settings page, on the fields that
  page keeps.** `SettingsForms` projects a row's schema through `volatileForm`,
  which keeps only `meta.volatile` subtrees, so the sixteen route fields carry
  one description each — naming the role and the slot — and a non-volatile
  field carries none. `description()` clones the schema, so every route field
  keeps `meta.volatile` and a saved route still lives in the profile layer;
  the card stays the rich editor.

**Kept as they are.** The declared preset, the routing card on the harness's
own config forms and settings-draft helpers, the `rq_escalate` lane that mounts
`dsh-mcp-client` into one agent scope, the Team guard, `present`, the goal
tools, the harness's spill and the base bundle's `llm-retry` were already the
native mechanisms for their jobs; 0.2.0 changes none of them.

**Considered and rejected.** Keeping 0.1.7 support alongside 0.2.0 (two harness
generations for an experimental dependency, and no period in which both are
tested); dropping the routing card for the harness's auto-generated Settings
page (that form cannot pick from the model catalog or express "inherit", which
is the card's whole job); adopting the opt-in experimental schedule bundle to
restart rounds (it would replace the explicit human rearm the unattended
contract is built on); and a native per-agent MCP mount helper or a
plugin-registered settings namespace (neither exists on this core).

Amends:
- **20:** the harness range.
- **25:** the floor and the last-release statement; the architecture itself
  stands.

## Repo map

```
package.json                dsh.bundle manifest; dsh.manifestVersion + engines.dsh
                            and peerDependencies pin the harness range
                            (>=0.2.0-rc.2 <0.2.1); icon + locale/ are the
                            bundle's native display metadata
agent-presets/rigorquant.patch.yml  the declared preset (persona + child rows)
agent-presets/rigorquant/   the rigorquant skill and its sibling skills
  skills/rigorquant/        SKILL.md, references/, scripts/rq_check.py, schemas/
dsh/                        host halves: rq-model-router (routes as volatile
                            profile config) + rq-team (composition, per-call
                            guard, and the rq_escalate lane in dsh/lane.js) +
                            rq-lane-sync, dsh/personas/ (one role persona per
                            file), and the client bundle (routing card on
                            configForms)
cordis.patch.yml            bundle patch: skill layer + router + team + lane sync
env/                        pinned uv compute lane (pyproject + lockfile)
mcp/jacobian.md             escalation lane wiring (mounted by rq_escalate)
locale/en.json, locale/zh.json  native bundle display metadata (title, description)
icon.svg                    native bundle artwork, read from the manifest
docs/architecture.md        this record
docs/adr/                   ADR 0001 (Agent Teams), ADR 0002 (declared preset
                            on DSH 0.1.7), ADR 0003 (DSH 0.2.0)
docs/hard-lessons-…md       the run record the skill cites
docs/showcase.html          the showcase page
tests/                      the validator's suite; a forged study must FAIL
install.sh                  installs the compute lane and the plugin, enables
                            the one Team bundle, removes the retired web bundle,
                            ports saved routes (or --skill-only: the skills)
```

A study folder (`studies/<slug>/` in Mode B, the repo root in Mode A) lives in
the *research* repo, not here: `interim/` inside each is gitignored, everything
else commits.
