Technical description
The plain-language version is in README.md. This file is the engineering account: what runs, what is enforced, what was measured, and why each rule exists.
Everything here is empirical. Each rule exists because it failed at least once on a real run. Nothing was inherited from a vendor's documentation without being reproduced.
1. Architecture
Section titled “1. Architecture”One brief, every configured reviewer, in parallel, followed by verification of each answer.
The table shows the two transport shapes — an HTTPS API and a local CLI subprocess — through the
channels that established them. The live roster is python routing.py; it has changed most weeks.
| Channel | What it is | Transport | Typical time | Billed against |
|---|---|---|---|---|
spark (http) |
Muse Spark 1.1 | Messages API over HTTPS | 1–5 min | metered API key |
| codex | OpenAI Codex CLI | local subprocess | 7–35 min | your subscription's heaviest tier |
| agy | Antigravity CLI (Gemini 3.8 Flash; Gemini 4 Argon on the standard panel) | local subprocess | ~1 min (Argon: 17.5 min on one release review) | your Google subscription |
Threads, not asyncio: the CLI channels are blocking subprocesses, and on Windows asyncio subprocess support depends on the event-loop policy. Threads just work.
The point is not redundancy, it is disagreement. Measured across two rounds on real work the ordering inverted: once the 55-second channel found the item both slower ones missed, once the 25-minute one did. Which channel wins is not predictable from cost, from speed, or from the previous round. That is the entire argument for running the whole panel.
SKILL.md operating manual, loaded on demand by Claude Codeorchestrate.py the harness: dispatch, verification, gates, diagnosticsrouting.py resolves registry + flags + free text into a planchannels.json THE registry - every model name lives here and nowhere elsedoctor.py "is this machine set up?" - probes, never assertsselftest.py "does the code still behave?" - behavioural checks; prints its own countcitecheck.py citation grounding and existence checksupgrade.py install/update in one path; migrates settings out of the treeupdate_check.py the update cycle: daily release check, the notice, `--apply` self-updatepatch_agy_permissions.py mandatory post-install step for the agy channelechocheck.py proves a depth knob from the counter the vendor returnsVERSION the release this tree is; generated at build timechannels.shipped.json reference copy of the registry, so edits to it are named field by field (an edit you FORGOT, not one someone is hiding: it sits under the same write permission as the file it describes - see §5b)references/*.md detail read on demandsystems/*.md system-prompt presetspremium/*.py + *.json the premium batch/flex panel - a SEPARATE dispatcher on purpose (batch mechanics never enter orchestrate.py; --panel premium prints the pointer; premium/premium_panel.py --mode dry is the free start)Everything above is REPLACED by an update. Your own configuration is therefore not in it — see §5b.
2. What is enforced in code, not in prose
Section titled “2. What is enforced in code, not in prose”This kit assumes prose restrains nothing, because that was measured: an instruction in a model's own system prompt failed 5 times out of 5 to stop it calling a tool it had been told not to call. Only permission rules worked. So everything that must not happen is a check, not a sentence.
Outbound payload gate
Section titled “Outbound payload gate”- A payload containing a key, token or private key is never sent. There is no override flag.
The detectors are
SECRET_PATTERNSinorchestrate.py: private-key blocks, vendor key formats, labelled assignments, bearer tokens. - A payload containing personal identifiers is reported, itemised by kind and line, and sent — pass
--strict-piito refuse instead. Secrets are refused always, with no override deliberately. The detectors arePII_PATTERNSin the same file: national ID numbers, case/receipt numbers, SSNs, emails, phone numbers, labelled dates of birth, labelled passport numbers. - The gate reports kind and line number, never the value. Printing the value would leak it into the transcript, which is the same mistake one step earlier.
- Both the brief and the system-prompt file are scanned. A hand-written preset is just as capable of carrying a name or a key as the brief is.
- It runs under
--dry-run, so checking costs nothing.
Once a payload is sent it cannot be recalled. It is at every vendor the round reached.
Inbound / logging gate
Section titled “Inbound / logging gate”Everything written to the console, run.log or diagnostics.json passes through a substitution
that replaces secret- and PII-shaped runs with [REDACTED:KIND].
It is a substitution, never a truncation. A "masking" expression that kept the first 60 characters of a 48-character key kept all of it, and that is how a real key once reached a transcript. A substitution cannot fail that way: either the pattern matched and the text is gone, or it did not match and nothing claimed otherwise.
Scrubbing happens in the single logging choke point rather than at each call site — found while testing the crash handler, where an exception whose message contained a key printed it in full because only the diagnostics file was being scrubbed.
Answer verification
Section titled “Answer verification”- A refusal cannot pass as a review. A model that declines still obeys the formatting instruction, appends your end marker, and clears every mechanical check. Measured: a 162-byte refusal was reported as a successful channel. Detection combines a refusal tell in the opening with a short body; a long review that merely mentions such a phrase does not trip it.
- The end marker is checked, and now also instructed. The harness verified the marker for
months without ever asking the model to emit one — the brief's author was silently expected to
know. A brief written by anyone who had not read the docs came back
PROBLEMon every channel of the three then configured with a perfectly good review inside. The instruction is now appended automatically when the brief does not already contain the marker. - Citations are cross-checked against pages actually opened. One channel fabricated four real-looking document numbers in a single session, each under the correct article slug — and one was even fetched successfully, because the site resolves by number and ignores the slug. Grounding varied 1/6 → 4/4 → 5/5 across runs of the same brief, so this is checked every run.
- Never trust an exit code or a status field on these channels. Observed, on the same channel
in the same week:
SUCCESSwith an empty answer,ERRORwith a complete one, and exit0on a hard HTTP 400 returned by the vendor's own server.
3. Citation checking: two different questions
Section titled “3. Citation checking: two different questions”These are constantly conflated, and they need different evidence.
| Question | Name | What it needs |
|---|---|---|
| Did the model actually open this page? | grounding | the channel's event log |
| Does this page exist at all? | existence | just fetch it |
citecheck.py --answer FILE --resolve-urls answers the second without an event log, which
makes it the only mechanical citation check possible on the Codex channel, which exposes no
telemetry whatsoever.
Each cited URL is reported as LIVE / MOVED / DEAD / BLOCKED / UNKNOWN.
BLOCKED and UNKNOWN are never reported as fabrication. Inferring "fake" from "could not
check" is exactly the move this harness forbids the models; it would be worse coming from the
harness. Non-public hosts are refused rather than fetched, because a cited URL is model-generated
text and should be treated as untrusted input.
Measured on one round: spark 22 URLs / 0 dead; codex 32 / 1 (a deliberate negative probe — the 404 was the answer); agy 11 / 3 dead, having opened zero pages.
Automatic re-ask on zero grounding
Section titled “Automatic re-ask on zero grounding”When the agy channel cites sources and opened none of them, the harness re-runs it once with an explicit instruction naming that specific failure. Measured, same brief, back to back:
| attempt 1 | escalated re-run | |
|---|---|---|
| citations grounded | 0 of 3 | 8 of 8 |
| dead URLs | 3 of 11 | 0 of 8 |
| tool calls / search+fetch | 14 / 10 | 72 / 43 |
| thinking tokens | 4 387 | 9 257 |
Design constraints worth keeping: exactly one extra attempt; the cost announced before it is spent; both transcripts kept; and if the re-run also grounds nothing, the first answer is returned — cleaner conditions, no nagging — with both flagged unverified, because two ungrounded runs say something about the brief, not just the model.
This is the fourth data point on one asymmetry: prose cannot restrict what tools a channel may touch, but prose reliably improves how it uses them. Do not collapse either half into "prompts work" or "prompts don't".
4. Routing
Section titled “4. Routing”Every model name lives in channels.json and nowhere else. Adding, disabling or re-pointing a
channel is an edit to that file, never to orchestrate.py.
--only spark codex restrict to named channels (any alias works)--skip gemini exclude--set codex=gpt-5.4 pin a specific model--route "<free text>" parse an instruction as written, in Russian or English--panel cheap|standard WHO is in the room; filters down only, never enables--tier max ONE tier, and it is every vendor's ceiling. strategic|deep are aliases kept so older commands keep working--system legal-research system-prompt preset--dry-run complete preflight, spends nothingThere is ONE axis you choose: who is in the room. Membership is declared per channel
("panel": "cheap") and the ladder in the panels object, so standard INCLUDES everything
cheap has: «standard» has to mean "what normally runs", and it does — the default is
bit-for-bit the behaviour that shipped before panels existed.
🔴 A panel filters DOWN and never enables anything — and since 1.22.0 neither does a GROUP.
--only openrouter used to resurrect every disabled member; it now runs only the members that
are already on. That change closed a real cost: on a machine holding a direct vendor key, the
group word woke the OpenRouter twin of a channel it was already buying directly — two bills for
one voice. enabled is exactly the field the packaging step flips per distribution, so
resurrecting on a group word means calling a vendor whose key the user may not have. Naming a
channel still works and is now the only way in: --only <name>, or «включая
🔴 A channel may declare explicit_only. Then even naming its group is not enough — it runs
only when something names it, because enabled: false alone was not a lock: --only
overrides it by design, and --only <group> reached it through two different vendor words. Four
channels ship that way: agy31pro (off since 1.115.0: its model is gone from agy), and since 1.113.0 the three Muse Wonder transports
(mcwonder10cont, wonder10, orwonder). One narrow exception keeps a model reachable by its own
name: a group whose channels are exactly one cascade (one voice, several transports — today
wonder) names that model, so «вондер» / "only wonder" starts ONE of its explicit_only members,
the first that is ready here, and stops the run instead of paying when only a paid member could
start. A broader group — a vendor, cli, openrouter, a panel, «все» — still starts none of them,
and a member that is also enabled: false still needs its own name.
🔴 What the cheap panel really costs is vendor diversity, not depth. The plan prints the vendor tally of whatever resolves and warns when one vendor holds half the seats, because six channels reaching one company's weights that agree with each other are one opinion reported six times. Read that line before treating convergence as corroboration.
Why ONE tier and not two. There were four, then two, now one. The pair that survived
differed, at the end, in a timeout and two multipliers — depth was already identical on Spark
(xhigh; max returns 400), on the Gemini CLI (high is the model's ceiling), on the direct
Gemini API (top of thinking_levels) and on the xAI model (which refuses the field at every
value and placement). A knob whose range shrinks to a point every time the underlying values
improve is not a knob, so the second tier was removed rather than re-justified.
Every channel now runs at the maximum depth its own vendor accepts, always. Each one's
ceiling is declared beside it — supported_efforts for the OpenRouter models, thinking_levels
for the direct Gemini ones — and the self-test asserts the configured value is the top of that
list, so a vendor adding a rung shows up as a failing test rather than as a silent shortfall.
Two channels were found below their ceiling when this was first checked: one running an effort
one rung down, and one whose "reasoning budget" was being converted by the gateway into roughly
medium on a model whose own default is max.
A panel may never change depth. That is asserted mechanically: every panel is resolved and every surviving channel's depth fields are compared field-by-field against the unfiltered plan. Only the number of reviewers may differ between modes.
--tier quick is still refused by name (invalid choice). strategic and deep resolve to
max and the plan says which word it honoured — accepted-and-ignored is the failure this
project has recorded most often, and a rename is exactly when it happens.
🔴 max_tokens covers reasoning AND the answer on these protocols. If a channel returns
nothing, the run reports OUTPUT BUDGET EXHAUSTED BY REASONING with both numbers: that is a
truncation, not an empty response, and the fix is a larger ceiling or a smaller brief. Measured:
766 seconds, 60,002 reasoning tokens against a 60,000 cap, zero bytes of answer — while the
diagnosis at the time said "the provider gave no reason" and the reason sat three fields away in
the same record.
--route is deterministic rule-based parsing, not a model call. It handles negation
(«не использовать», «без», «кроме», "don't use"), substitution («вместо», including the «вместо
нее» anaphora referring back to the just-refused model), and «только»/"only".
Design rules, each earned:
- The resolved plan is printed before anything is spent, with a reason per channel and an
EXPENSIVEmarker. A router that silently picks a costly model defeats its own purpose. - An unparseable route is a hard stop listing known aliases — never a guess.
- A flag contradicting the route is also a hard stop, not a precedence rule.
--only codexcombined with a route excluding codex used to run codex, printing "excluded by name" on the line directly above[RUN] codex. Neither source outranks the other; a genuine contradiction names both sides and stops. A safety printout that contradicts itself is worse than none. - Colliding aliases are rejected at registry load time, where they are a config typo, rather than at spend time, where they are the wrong model.
- Effort is clamped to what each model actually exposes, and ties break upward — an under-thought review is confidently wrong, which costs more than the quota.
- Precedence: registry < environment variables < flags/route.
A refused free allowance is skipped until 00:00 UTC (1.113.7; edges fixed in 1.115.1)
Section titled “A refused free allowance is skipped until 00:00 UTC (1.113.7; edges fixed in 1.115.1)”opencode's free MiMo v2.6 Flash, GLM 5.3 Flash and DeepSeek V4.1 Flash (and the retired free Spark)
carry one quota_group in channels.json, opencode-free: they behave as one daily allowance of
requests per network address (measured 2026-10-18/19; the vendor's limiter is not public). The kit
starts one of them, MiMo Flash. Its fallback is GLM 5.3 Flash: run once after the round when
MiMo ends without an answer, never into a chain, never after provider.quota, because a member of
the same group would be refused too, and since 1.115.1 never into a group that any record of the
round was refused on; retryable_stream_death does not auto-retry a provider.quota either. After
the round record_quota_refusals writes a member's provider.quota to
~/.claude/model-orchestration.quota-state.json ({"groups": {"opencode-free": {"date": "YYYYMMDD", "at": "HH:MM:SSZ", "channel": ..., "why": "provider.quota", "recorded": "YYYY-MM-DDTHH:MM:SSZ"}}}). date and at are the start of the run phase in which channel was
refused (the round, the auto-retry or the fallback phase; with several members refused, the latest
such start and its member - channels lists them all): never later than that refusal. A refusal in
a phase that began before 00:00 UTC is not written at all (the allowance has reset since), while a
phase that began after 00:00 UTC arms the new day - the worst error left is one start that is
refused again, never a lost day. recorded is when the round wrote the entry, in whole seconds: a
member's answer clears the group unless the same round was refused too, or the refusal was recorded
in a second that began at or after that round's start (then it is newer than the answer and is
kept). Within the second the round began, recorded cannot tell before from after, and the entry
clears - one more start is the cheap error (an entry written before 1.115.1 has no recorded and
is read by date + at). The folder
is created when missing, and every note and QUOTA: line is written from the write's result («NOT
remembered», «could NOT be cleared … delete --ask and python routing.py included, leaves the group's channels out ([skip], with the
time to 00:00 UTC, «under 1 min» in the last minute) unless the human named the channel: its name,
an alias, its model id or a --set pin, in --only, a route or --ask-channel
(routing.explicitly_named); a group word ("opencode") never counts. A date other than today's is
ignored, and an unreadable file is an empty one. MODEL_ORCH_QUOTA_STATE points it elsewhere (off
or empty: never read or written). Every write goes through its own temporary file beside the state
file and os.replace. There is no lock: two rounds writing at the same moment can lose one update -
with one quota group, as in this registry, that is one extra start at worst; with two groups a
stale write can also bring back a refusal another round had just cleared, and that group then
skips until 00:00 UTC.
What 1.120.1 changed in a run
Section titled “What 1.120.1 changed in a run”- Route aliases (R178; measured on the 1.120.0 kit tree: "only mimo pro" ->
mimo25pro, «только мимо про» ->ocmimo26flashfree, «только мимо флеш» ->mimov26flash, "only mimo flash" refused, "only glm 5.3 flash" and «только глм 5.3 флеш» ->orglm53).ormimopro's model gained "mimo pro" and «мимо про» (mimo25prolost "mimo pro"). A new groupmimoflash("mimo flash", "mimo 2.6 flash", "mimo v2.6 flash", «мимо флеш», «мимо 2.6 флеш» and the «флэш» spellings) holds exactly the MiMo v2.6 Flash cascade, so the cascade picks the transport as in the cheap panel;mimov26flashlost «мимо флеш».routing.one_voice_groupsderivesmimoflashas a one-voice group; it holds noexplicit_onlymember, and the selftest pin of the one-voice groups (wonder, kimi, mimoflash) checks that.ocglm53flashfree's model gained "glm 5.3 flash", "glm-5.3-flash", "glm flash", «глм 5.3 флеш», «глм флеш» (and «флэш»).codexgained "codex cli" and «кодекс кли»: the longer name wins over "codex" + thecligroup word. _detect_silent_dropreads the words after a MODEL alias (1.119.3 read them after a group word only): a word the registry spells another model with stops the route ("only glm-5.3 flash"); the model's own words, effort words and «через» stay legal. Cyrillic words after a group word or a channel's name are read too and count only when the registry spells models with them: «только квен про» is refused (1.120.0 ran Qwen 3.8 Max), «только глм про», refused before by the family rule, now names «про» as the unknown word, and the Latin "only qwen pro" still runs Qwen 3.8 Max (a short word after a channel's name stays tolerated).- Codex effort ladders = Codex's own catalog (
~/.codex/models_cache.jsonof codex-cli 0.167.2, fieldsupported_reasoning_levels):gpt-5.6-solnone to xhigh -> low to max,gpt-6-solnone to max -> low to max. orchestrate.py passes tier max by default, so a pin's effort is clamped to the model's ladder: with"effort": "max"in the settings "only codex 5.6 sol" sent xhigh in 1.120.0 and sends max now; with the kit's defaultxhighnothing changes. Codex also listsultra(«Maximum reasoning with automatic task delegation») for gpt-6.1-sol, gpt-6-astra, gpt-6-sol and gpt-5.6-sol; it is in noeffortslist and is never sent (_ultra_r178).gpt-5.5andgpt-5.4are absent from that catalog (_not_in_codex_catalog_r178); their ladders are unchanged and their pins unmeasured. - New selftest suite
suite_r178_route_residuals(24 checks).
What 1.120.0 changed in a run
Section titled “What 1.120.0 changed in a run”- Two Spark voices in the cheap panel (R179, the owner's rule of 2026-11-09). Muse Spark 1.2 Contributor and Muse Spark 1.3 Contributor both run, each on the first route the machine has: the OpenCode Go plan (
ocspark12go,ocspark13go), then, for 1.2 only, a Meta key (spark12cont), then OpenRouter (orspark12cont,orspark13cont, now on in the kit). The cheap panel has 7 voices (was 6), «CLI only» 5 (was 3), the standard panel 13 (was 12), CLI + standard 7 (was 5).--skip ocspark12go ocspark13gomoves the two voices to the next route;--skip sparkdrops them. - An
opencode-go/channel is ready only where opencode lists its model (_oc_listed_models, oneopencode modelslisting per plan). The binary alone made it ready before, so a machine with opencode but no Go plan would have lost both Spark voices instead of falling to OpenRouter. «CLI only» has no OpenRouter twin, so such a machine still starts the two Go channels there;channel_preflightnow says the plan is missing and how to leave them out. - No API Gemini 3.8 Flash without agy (the owner's rule of 2026-11-09).
orgemini38flashisdistribution: local: the kit build switches it off with its own reason and it starts only when named. Without agy the Gemini voice ends at once («binary not found (agy)»). - An OpenCode Go refusal names the owner's action. Two stock diagnoses (
KNOWN_FAILURES): «This Go model trains on request data ...» (Account.TrainingNotAllowed) and «This Go model requires Global regions ...»; each fix is a change a workspace owner or admin makes in the OpenCode Console, Settings › Privacy.owner_action()(OWNER_ACTIONS) gives the one-line action, the round's problem record carries it asowner_action,REPORT.md's «What each channel actually did» prints «🔴 FAILED —» in that channel's row, and HANDOFF.mdadds «🔑 Needs the account's owner» with it. New selftest suitessuite_r179_kit_defaultsandsuite_r179_i3_go_owner_action.
What 1.119.3 changed in a run
Section titled “What 1.119.3 changed in a run”- Route model words (R177; a 2026-11-08 round asked for «mimo 2.6 pro» and got Gemini 3.1 Pro).
agy31prolost the model aliasproandagy36flashthe aliasflash: a model alias counts as naming its channel directly (routing.explicitly_named), which overrides bothenabled: falseandexplicit_only, so any route holding the word started those two channels ("mimo 2.6 pro", "qwen pro", "gemini 4 pro" -> Gemini 3.1 Pro; "mimo 2.6 flash", "glm 5.3 flash" -> Gemini 3.6 Flash).ormimopro's model gained the aliases "mimo 2.6 pro", "mimo v2.6 pro" and "mimo-v2.6-pro" («мимо 2.6 про» already named it)._detect_silent_dropnow reads the words after a GROUP word too: the members the scanner put at the group word's position are walked as one unit, a word that any member's aliases contain is accepted, function words are skipped, and an unknown model-looking word refuses the route with the group's members and the names they answer to. Before, the check ran only after a channel's own alias, so a model name after a group word was dropped and the group's default members ran.suite_r177_route_model_words(21 checks) pins the phrases and runs a registry census: a one-word alias of a channel that is off orexplicit_onlymay not be a word of another channel's id, label, model id or model label unless it starts its owner's id. Not changed: a short word after a channel's alias is let through ("qwen pro" runs Qwen 3.8 Max), a word after a MODEL alias is not checked ("glm 5.3 flash" runs GLM 5.3), and Cyrillic words after a group word are not checked (SUPERSEDED in 1.120.1: both are checked, and "glm 5.3 flash" names GLM 5.3 Flash, see «What 1.120.1 changed in a run»).
What 1.119.2 changed in a run
Section titled “What 1.119.2 changed in a run”- Grok's inner watchdog waits 300 s for every tool (R175; the operator chose one limit for every tool on 2026-11-08).
GROK_TOOL_SILENCEis 300 s (90 s since 1.117.0;ORCH_GROK_TOOL_SILENCEstill overrides), andGROK_TOOL_SILENCE_BY_TOOL({"web_search": 300}) is a per-tool floor that neither the default nor the override lowers (_grok_tool_silence(tool)). The tracker judges each pending tool against its own limit and names the one furthest along it. After a watchdog kill_grok_stop_racedre-reads Grok's session record: a tool that completed as the watchdog fired giveswatchdog_raced: true, aWATCHDOG RACEDwarning, «completed as the watchdog fired» in the error and its owndiagnose()entry (the limit was too low; nothing hung). Measured on the author's machine before the change: 64 Grok web searches, median 58.7 s, p90 85.8 s, the longest 172.3 s, 5 past 90 s; in a 2026-11-09 round the 90 s watchdog fired as a search completed (94.6 s) and the review was lost under «stayed pending». Suite:suite_r175_grok_tool_limits. - A free API voice re-sends one request that got no response headers (R176; the operator's choice 2026-11-08). In
call_oai_reviewer, anAPIReadCut("headers")(no response headers withinAPI_HEADER_LIMIT_S, 300 s) inside_stream_with_retrysends the same request once more; the request body grows only after a successful round, so the copy is verbatim and carries the pages already read. Once per call (headers_resends), passed by the dispatcher as_headers_resends(reg, cname): 1 when the registry'scostisfree, else 0, because a paid request with no headers may still be queued and billed upstream.deadlineandstallcuts are never re-sent. The record gainsheaders_resent(after_rounds,at_s,answered) andheaders_waits_s(each streamed request's wait for its headers);retriescounts the re-send; a failed copy's error says it was the re-send, anddiagnose()has twoNO RESPONSE HEADERS withinentries (plain, and after the re-send). 1.119.0's free-voice in-round rule (below) is unchanged: a copy that fails too is still astream_cut, i.e. silence: no in-round retry, no fallback. Measured:nvdeepseekv41flash, 24 rounds since 2026-10-31, 11 answered, 10 ended on «no response headers within 300 s» (6 on the first request, 4 mid-review); a live probe on 2026-11-09 got HTTP 504 from NVIDIA's own gateway after ~302 s for a small and a large request alike, so a longer header limit would not help, and in such an hour the copy fails too;headers_resent[].answeredshows how often the re-send rescues a round. Suite:suite_r176_headers_resend.
What 1.119.0 changed in a run
Section titled “What 1.119.0 changed in a run”- Free-voice IN-ROUND retry+fallback rule (R172 И-2; operator's rule 2026-10-31).
orchestrate.mainusesconcurrent.futures.wait(FIRST_COMPLETED)inside a nested in-roundThreadPoolExecutor; a free primary's failure is classified by_classify_free_failure(r, elapsed_s, 1500 s)intopast_window,silence,transient+ delay,quota, orother. A known transient (429 → 60 s delay, 5xx → 30 s) retries the primary once in-round, bounded by the remaining window; a known refusal or an unknown-but-real failure dispatches the fallback at once in-round. Silence (no error, no answer) and failure past the 25-minute window end the voice without a retry and without a fallback — the operator's rule verbatim «silence or past 25 min: no fallback». The markerfree_inroundon the primary record blocks the after-round AUTO-RETRY and the after-round fallback from re-firing. Paid and subscription voices keep the after-the-round fallback, unchanged. The 1.118.0 bullet below said «a stream that dies without an answer is first retried once with the full budget (the AUTO-RETRY rule, unchanged)»; in 1.119.0 the AUTO-RETRY is still unchanged for paid/subscription voices, but a free voice's silent stream death inside the window is now treated as unknown and NOT retried. Env overrides for CI and probe worlds:ORCH_FREE_INROUND_WINDOW(window seconds, default 1500),ORCH_FREE_INROUND_RETRY_429(default 60),ORCH_FREE_INROUND_RETRY_5XX(default 30). To see which branch fired, grepin-round:inrun.logor read thefree_inroundfield indiagnostics.json. Suitesuite_r172_i2_free_inroundhas 29 checks a-k; the rule was live-verified on a cheap panel on 2026-11-02 (r172i2-panel): 8/8 behaviours matched the expected branch and 0/4 false positives on paid voices. - Chat panel seats scaffold (R173 И-3). Four channels —
claudeaiseatandcgptseat(kindchatseat, CDP, parallel),cldesktopseatandcgptdeskseat(kinduiaseat, UI Automation, serial foreground) — are present withdistribution: localandenabled: false; an overlay enables them.--panel chatrefuses to run without--chat claude|chatgpt|both. Drivers live in the author'sseats/directory and are not shipped — a chat seat is operated through the author's own browser or desktop app and is not a general-purpose channel. The kill-switch envR173_SEAT_FORBID_LIVEleaveschat_slot_spent: Falseso a refusal does not count a chat slot. The chat panel is exempt from the free-voice in-round rule above. Suites:suite_r173_i2_chat_panel_scaffold(wired),suite_r173_i3_seats_topology(a-d3). valid_throughself-expiry in routing (R174 И-1).routing._valid_through_expired(ch, today=None)returns the ISO date when today >valid_through(lexical YYYY-MM-DD compare), None otherwise (shape guard rejects non-string, wrong length, wrong separator, non-digit components).initial_planAND-s expiry intoenabled, writesvalid_through+expired_oninto the plan, and prepends awhyline naming the date.default_enabledis not touched by expiry — a spend-gate can still tell "off by default" from "on but expired". Overlay channels withvalid_throughrevert to off automatically the day after the date; the mechanism IS the reminder, no code change, no manual step. Suite:suite_r174_valid_through(18 checks a-s).
What 1.118.0 changed in a run
Section titled “What 1.118.0 changed in a run”- DeepSeek V4.1 Flash takes Step 5 Preview's seat in the cheap panel:
nvdeepseekv41flash(oai, NVIDIA's free endpoint,NVIDIA_NIM_API_KEY), effortmax(NVIDIA accepts none..max; n=3 high against max overlapped, so no depth verdict),max_tokens131072,timeout61m (one live review was still reasoning at 20 minutes), the page-fetch tool on, no web search. Its fallback is GLM 5.3 Flash on the same key (nvglm53flash: off as a voice, effortmax, 20m), started once after the round when DeepSeek ends with under 2000 bytes of text - a timeout counts, and a silent stream death is first retried once with the full budget (the AUTO-RETRY rule, unchanged). Without the key the plan names it and prints the fallbackOFF in this run.ocstep5previewnv/nvstep5previeware off and start by name. Kit counts: cheap 6, standard 12, «CLI only» 3 (2 on a refused day),--ask4 (3). - Family words:
deepseek/дипсикandglm/глм/zaiare groups over every transport (askimiandgrok): «не используй glm» also turns DeepSeek's fallback off;--only glmon the cheap panel stops, because its only startable member there is the paidorglm53. - The page-fetch fence:
citecheck._resolve_publicrefuses every address that is notis_global(Python calls 100.64.0.0/10, the range Tailscale uses, neither private nor global, so it used to pass) and every*.ts.netname before it is resolved (this includes Tailscale Funnel pages, which are public by design but still resolve to tailnet addresses). One function serves the citation check and the model-facing page-fetch tool; before it, a tailnet admin page counted as public and a panel's API reviewer fetched it. - Two more causes in
diagnose(): Qwen Code's «403 Access to model denied. Please make sure you are eligible» (the account's Token Plan lapsed or its monthly credits are spent: renewing is the owner's step, and re-running the brief on a paid channel is a spending decision, not a repair) and OpenRouter's «would exceed your available credits» (402; tokens generated before the refusal are billed). - CI green again (red since 1.116.0): the Python 3.9 leg runs on
ubuntu-24.04(setup-python has no 3.9 for Ubuntu 26.04, today'subuntu-latest), two Muse Code env checks admit 1.116.0's utf-8 trio, R138 re-pins the grok bypass prompt for 1.116.0's utf-8 sentence (the only change in it), and SKILL.md is back under its 5,000-token re-attach budget. - Not in this release: a free voice's fallback still starts only after the whole round, a timeout included.
What 1.117.0 changed in a run
Section titled “What 1.117.0 changed in a run”- The inner watchdog (R166): a grok whose turn has not ended but one of whose tools stays pending longer than
GROK_TOOL_SILENCE(90 s; SUPERSEDED in 1.119.2: 300 s for every tool, see «What 1.119.2 changed in a run») is stopped at once — whole tree,info["watchdog"]with the tool and its age.call_grokclirecords it failed-with-partial (no text, no outfile,WATCHDOGwarning, the session note and the stuck tool in the error,watchdog_tool/watchdog_stuck_sfields), so an armed fallback may still run the seat.diagnose()answersWATCHDOG afterwith the stalled-tool cause (a longer timeout would not help). Other CLIs keep the channel-timeout tree kill only. - Source-tree repair (no run-behaviour change, the registry is back to its 1.116.2 intent): five local channels back on (
mcspark13,ocspark12go,mcwonder10,qwen38maxcli,kimik3free— the last off at runtime via the local overlay), 18prompt_suffixblocks and 9 manglednotesrestored from the 2026-10-24 snapshot, the build-stamped_kit_excluded_channelsdropped from the source registry, the EN README credit back to theIgor Saevetsplaceholder.
What 1.116.1 changed in a run
Section titled “What 1.116.1 changed in a run”ocnemotron3ultrafreeships off:opencode/nemotron-3-ultra-freeis gone fromopencode models(onlynemotron-3.5-*-freeleft) andopencode runanswersModel unavailablefor it. The cascade group is unchanged, so the cheap panel still seats ONE Nemotron (NVIDIA's free endpoint, then the OpenRouter twin); the kit cheap panel still runs 6 voices, the standard one 12.
What 1.116.0 changed in a run
Section titled “What 1.116.0 changed in a run”- A free opencode voice that ends without an answer hands the brief to the paid copy of the SAME model on the OpenCode Go plan (
ocmimo26flashgo,ocdeepseekv41flashgo,ocglm53flashgo: subscription,enabled: false, noquota_group, deliberately outside the cascade groups). One fallback, never a chain; the cheap panel's line-up is unchanged. - The HANDOFF.md fallback line names the models as well as the channels:
`X` (modelA) answered for `Y` (modelB), from the records'modelfields; without them the line reads as before. - Every Python child process inherits
PYTHONIOENCODING=utf-8+PYTHONUTF8=1+PYTHONUNBUFFERED=1(process-widesetdefaultplus every CLI child env): the 2026-10-26 grok 4.6 hang (cp1251 + non-ASCII print in a PDF script →UnicodeEncodeError→ pipe deadlock → 40m TIMEOUT) cannot recur through the harness's own CLIs. TheNO_REPObypass preamble carries the same rule to the model (temp.pyover multilinepython -c,pdftotext -layoutfirst).
What 1.115.2 changed in a run
Section titled “What 1.115.2 changed in a run”HANDOFF.mdlists anever_readanswer (every Nemotron channel) withneverin itsreadcolumn and counts it nowhere: not in the totals line, the resume block («прочитай каждый файл … КРОМЕ …»), the launch log'sHandoff:line, the answer-cap lists orread_order_files, and the fallback line marks anever_readfallback's file (Nemotron's own fallback);stamp_reading_fields(moved out ofmain) stampsnever_readbesideread_order,reading_noteandmust_read.- The free-allowance memory: an answer clears a refusal recorded in the second its run began (1.115.1 kept it all day);
channelnames the member whose run phase setat, and the plan reason says «in a part of a run (the round, its retry or its fallback) that began at»; every write uses its own temporary file (tempfile.mkstemp), not a shared<file>.tmp. - The
provider.quotahint no longer quotes opencode's FAQ figure, which opencode removed on 2026-10-21.
What 1.115.1 changed in a run
Section titled “What 1.115.1 changed in a run”- The free-allowance memory, as the section above describes: the stamp is the start of the refused call's run phase, a stamp on the previous UTC day is not written, the new field
recorded, an answer clears only a refusal recorded before its round began, the folder is created, everyQUOTA:line says what the write did, a note on every refused record, «under 1 min»; no fallback into a group refused in the round; no auto-retry ofprovider.quota;python routing.pyapplies today's skip. --ask: the line «--ask goes to N channel(s)» is built from the final plan (_ask_plan_line, after the cascade and the quota skip), with «not started:( )» and the voices whose answer is not printed. A channel with never_read: trueinchannels.json(nvnemotron3ultra,ocnemotron3ultrafree,ornemotron3ultra: the rule for every Nemotron channel, run it, never read its answer) still runs, but--askprints one line with its answer file instead of the body (_print_ask_answers);reading_notestays the human text.channels.json:ocnemotron3ultrafreecarries a stated exemption from «every free opencode channel has aquota_group» (it was measured outside the group); a selftest holds that invariant.
What 1.115.0 changed in a run
Section titled “What 1.115.0 changed in a run”--panel standardaddsagy4argon: Gemini 4 Argon through the agy CLI,--model gemini-4-argon --effort high(agy 1.4.7 offers it low, medium and high;xhighandmaxexit 1 at the start althoughagy --helplists them for the flag). agy's own log records «gemini-4-argon» resolved to «gemini-4-argon-high». The cheap panel does not run it; "gemini 4 cli", «gemini 4 аргон» andagy4argonstart it alone. Its weekly limit and the AI credits after it: README, the standard panel.agy31proships off: agy 1.4.7 does not offergemini-3.1-proon our account (agy models;agy -p /model --model gemini-3.1-proexits 1, «not recognized as a known model»). Naming it still starts it; the standard plan no longer prints its «NOT started» line.--skip openrouterand--only openrouterincludeorspark12cont: theopenroutergroup holds every channel of kindopenrouter(a selftest derives it from the kinds).- agy's print-mode slash commands answer without an agent turn and spend nothing (
/usageread before and after them was equal):agy -p /usage --output-format json(quota buckets: the fraction left and the reset time),agy -p /model --model <slug>andagy -p /effort --model <slug>(the efforts a model offers). The facts above were measured that way.
What 1.114.0 changed in a run
Section titled “What 1.114.0 changed in a run”- An API voice's
timeoutbounds the whole call, inside the read: streamed response headers must arrive within 300 s, the per-read limit is re-armed to the time left before every read, NVIDIA's channels stop after 300 s without data (stall_s; SSE comment lines do not reset it), and the forced final answer after a page-opening stop gets one moretimeout. A read cut this way keeps its text, graded partial, and is never retried automatically (SUPERSEDED in part in 1.119.2: on afreechannel a request that got no response headers is re-sent once inside the call, see «What 1.119.2 changed in a run»). 1.113.8 named this as planned (one read ran 60 minutes under a 20-minute timeout). - A CLI agent's
timeoutstops its whole process tree (opencode, mimo, Claude Code):taskkill /Ton Windows, the process group elsewhere. Until 1.113.9 only the program's launcher was stopped (on Windows theopencode.CMDshim), the agent below it kept working, and one round waited 3,980 s for a channel with a 1,200 s timeout. What the agent wrote before the stop is kept. - One voice per model, the first transport that can run here (
_cascade_groups): DeepSeek V4.1 Flash =nvdeepseekv41flash(NVIDIA's API), thenocdeepseekv41flashfree/ocdeepseekv41flashgowhere switched on; Step 5 Preview =ocstep5previewnv,nvstep5preview(off since 1.118.0, by name); Nemotron 3 Ultra =nvnemotron3ultra,ocnemotron3ultrafree,ornemotron3ultra(the last on the standard panel); MiMo v2.6 Flash =ocmimo26flashfree,ormimo26flash. When no member can run, the plan names what each one lacks («opencode CLI not found», «NVIDIA_NIM_API_KEY not set (a free key: build.nvidia.com)», «OPENROUTER_API_KEY not set») and the first member's preflight says it again. - A PAID copy that stands in for a free one is
twin_only(ormimo26flash): it takes the voice only where no free or subscription member before it in its group can run on this machine, or when it is named. Skipping the free copy,--only openrouter, "without opencode" or a day opencode's free allowance is spent never moves the voice to the paid copy where opencode is installed (routing._twin_only_gate; a route that leaves nothing else to run stops with an error). - opencode on NVIDIA: the provider reads
NVIDIA_API_KEY; the harness copiesNVIDIA_NIM_API_KEYinto that name in the opencode child's environment only (yours, if set, wins) and runs it--standalone; without the key the channel is refused before it starts. Its billing line names opencode's own provider and the key.
What 1.113.8 changed in a run
Section titled “What 1.113.8 changed in a run”- What an opencode channel costs, in words. The plan, the preflight, the call log and the
--askheader say how the channel's provider bills: opencode Zen's «free model», the OpenCode Go plan, «PAID per call (cost metered)» with the provider's name for a provider your own opencode config adds, or «billing UNKNOWN» (routing.opencode_billing). Until 1.113.7 every opencode channel printed «free model, no API key». - A channel your settings file adds prints a short block (2-4 lines), not one plan line per
field; whether it needs
--accept-settingsis unchanged. - An API channel that opens pages itself stops opening them at its
timeout.call_oai_revieweropens no new page once the channel'stimeoutis spent and answers from what it has; the record marks ittime_stopped. Until 1.113.7 the timeout was an idle limit: on the author's machine one channel ran 75 minutes under a 20-minute timeout. Thetimeoutdoes not yet bound ONE API read: a streamed read waits per socket receive, so a connection that stays alive can outlast it (one read in the panel on 1.113.8 ran 60 minutes under a 20-minute timeout); a deadline inside the read is planned. - Grok: an ended turn keeps its answer. When Grok's own session says the turn ended but the
grokprocess does not exit, the harness stops it 30 s later and keeps the answer (from the output, else from Grok's session record); the note saysLINGERED. An HTTP 402 from xAI readsGROK SUBSCRIPTION LIMIT, and each run records the CLI's version (cli_version). - Muse Code calls pass
--contributor-modefrom the model id (INSTALL.md, Muse Code CLI). - Key files follow
channel_may_read_keys(SECURITY.md, «Key and credential files»). - The default line-up. Grok Build plans
grok-4.6; MiMo v2.6 Pro (ormimopro,mimov26pro) and Spark 1.3 Contributor (spark13cont,orspark13cont,ocspark13go) are off and start only by name; the Spark voice is Spark 1.2 Contributor (spark12cont, ororspark12conton OpenRouter). A channel meant to be off in the kit isdistribution: both(orlocal) withenabled: false:package.pyswitches adistribution: kitchannel that is off back on.
5. Partial installs are a supported state
Section titled “5. Partial installs are a supported state”A missing API key or a missing CLI disables exactly one channel and nothing else. The preflight warns but never blocks: a stale check that vetoes a working channel is worse than a warning.
Verified by selftest.py, which covers: each CLI missing individually, both missing at once, and
the API key absent from both the environment and the Windows registry fallback — the trap being
that emptying the environment variable is not enough, because the code reads HKCU\Environment
next and would find the real key.
5b. Configuration lives outside the tree, because updates replace the tree
Section titled “5b. Configuration lives outside the tree, because updates replace the tree”Until 1.7.0 the documented way to enable a channel was to edit channels.json — a file inside the
folder that every update replaces. All four install methods destroyed that edit, and none of them
mentioned it. The plugin path was the worst of them precisely because it is the one recommended:
Claude Code replaces the whole folder when it updates the plugin, so the loss happened with nobody
running a command.
The fix is not a merge algorithm. It is a location — with one honest exception, below:
~/.claude/model-orchestration.local.json # yours; nothing can reach it<skill>/channels.json # shipped; replaced wholesaleThe overlay is merged over the registry before validation, so anything it changes faces exactly the same checks as anything shipped — a validated-then-mutated registry is how a config file becomes an unchecked code path.
🔴 1.7.0 was default-deny on fields. 1.8.0 keys trust on PROVENANCE instead, and the difference is not a relaxation — it is a correction. One of the review channels had named a real consequence of the first design: a file that survives every update, is merged before validation, and can name a transport hands anything able to write one file in a home directory a persistent, update-proof redirection of where your documents are sent — "and the per-run print of what the overlay changed only helps if a human reads it, which the plugin path specifically removes."
The finding was right; the remedy was aimed at the wrong axis, and looking rather than reasoning showed why:
~/.claude/model-orchestration.local.jsonand<skill>/channels.jsonhave identical write permissions. Anything that can write the first can write the second. Refusingmodelin the overlay never stopped an attacker; it sent them one file to the left.- And that file was the quiet one. The plan printed every overlay change on every run, while
an in-place edit of
channels.jsonwas fingerprinted only bydoctor.py, which nobody runs before a round. The gate was steering the most sensitive class of change into the least visible place. 1.8.0 closes that half: the plan reports registry drift by field, every run. - Meanwhile it fired on correct use — and the hand on the keyboard is usually an AI assistant helping its owner configure their own machine. A gate that fires on the intended workflow is one people learn to switch off.
So: at the home path, anything. Channels and tiers, edited or added ("_new": true required
for an addition, so a typo cannot become a second channel). Under MODEL_ORCH_LOCAL, only the
"how hard does it work" knobs — enabled, effort, reasoning, thinking_level, max_tokens,
fetch_tool, web, timeout, label, notes — because a project's own .claude/settings.json
can set environment variables for sessions run inside it, so a repository you cloned can choose
that path, and it cannot choose your home directory. There is deliberately no environment variable
to turn that off: an escape hatch that can be set once and forgotten is the same defect as a
registry that documents the way around its own gate.
🔴🔴 And then three reviewers of that design, independently, found what it had missed — the
permission-equivalence argument is true for a RESIDENT attacker and false for a ONE-SHOT one.
channels.json is self-healing: the next update replaces it. That is the defect this release
fixed and, at the same time, a security property nobody had named. The home settings file is
update-proof by construction. So opening it up handed the permanent file the powers the
ephemeral one had. In their words: a single compromised assistant session writes model into
the home overlay once, and every future run — including after the tool updates — silently
re-points a channel. That threat is not hypothetical for this product, whose own premise is that
an AI assistant edits the configuration on the user's behalf.
So a transport-affecting change is applied but not SPENT against until it is accepted once, by
a command the user runs (routing.py --accept-settings). The acknowledgement stores a digest of
the sharp section only: re-order the JSON, reformat it, or edit a quiet field beside a sharp one
and it still matches; change what is sent or where, and the refusal returns naming the change. A
file write is no longer sufficient — the attacker would also have to make a human type a second
command after reading a refusal that describes the redirect.
--dry-run deliberately still works before acceptance: seeing what would happen must never
require accepting it first.
cost is not a quiet field, despite looking cosmetic: it drives the "EXPENSIVE channel"
warning and the set --ask fans out to. That one was found by taking a reviewer's general frame
seriously and then checking his example, which turned out to be harmless — verify the finding,
discard the proof.
🔴 What the registry-drift report is NOT. A fourth reviewer pointed out the circularity: the
reference copy lives under exactly the write permission the drift check exists to monitor, so
anyone who can edit channels.json can update the reference and silence the detector. That is
true, and no location fixes it — a signature would need a key on the same disk. So the claim is
scoped honestly: it detects an edit you forgot, not an edit someone is hiding. Its job is to
stop your own change dying at the next update, which is the failure this project actually
measured. The gate against a hostile write is the acceptance step above, and that one is not a
detector — it stops the spend.
Four properties make it safe to have a second source of truth at all, and each exists because the alternative was measured or reasoned to be worse:
| property | why |
|---|---|
| The resolved plan prints the file's path and every field it changed, every run, even when it changed nothing | A settings file mentioned only when it acts is one people forget they wrote. The question it must answer is "why is this channel not running", asked by someone reading the wrong file |
| An unknown channel name is refused, with the list of real ones | Ignoring it would look identical to a channel that is off for another reason. Silence is a config overlay's characteristic failure |
A renamed channel still resolves, through the same alias table --only uses |
Strict rejection plus a rename upstream is a hard startup failure on upgrade day, for people who did nothing wrong. All four original channels here were renamed once already |
doctor.py fingerprints channels.json against a hash shipped beside it |
An in-place edit is otherwise invisible right up to the update that erases it |
upgrade.py performs the one-time migration and reports it. Three-way where a pristine baseline
exists (written from 1.7.0 onward, outside the tree — a baseline stored inside it is destroyed
by exactly the thing it exists to survive), two-way and labelled INFERRED where it does not:
comparing an installed file against an incoming one cannot distinguish a user's edit from a default
the release changed on purpose, so in that mode it restricts itself to the one field the docs ever
told anyone to change, and says so rather than pretending to know.
🔴 The exception: the hop INTO 1.7.0, on a path that never runs the script
Section titled “🔴 The exception: the hop INTO 1.7.0, on a path that never runs the script”The guarantee above is about updates from 1.7.0. The one-time migration still needs something to
run, and on the recommended install path — the marketplace plugin — Claude Code replaces the folder
with nobody running anything (on claude plugin update, or by itself where the marketplace's
auto-update is switched on). A reviewer of this release put it plainly: it "makes future updates correct only after
the state has already been relocated", and upgrade.py "cannot rescue a path that never invoked
it."
There is a documented window. Claude Code copies marketplace plugins into ~/.claude/plugins/cache,
keeps each installed version in its own directory, and orphans the previous one for 14 days
before deleting it (plugins reference, read
2026-08-08; the layout <cache>/<marketplace>/<plugin>/<version>/ was also confirmed on a real
machine). So upgrade.py scans that cache for an older copy of this plugin, compares its enabled
flags with the incoming release, and offers to carry the difference into the overlay. It is
best-effort by construction and silent when it finds nothing — a rescue that crashes is worse than
one that misses.
How an install learns about a release, and updates (1.62.0)
Section titled “How an install learns about a release, and updates (1.62.0)”Measured 2026-09-11 (claude 2.1.268, an isolated CLAUDE_CONFIG_DIR against the real GitHub
marketplace): Claude Code's own plugin auto-update is off by default for third-party
marketplaces — a stale plugin stayed stale across four sessions and -p --maintenance until
claude plugin update was run — and the CLI has no "newer available" signal (claude plugin list --available prints an empty list). Script and manual installs had no network check at all except
doctor.py by hand. So the kit carries its own cycle, in update_check.py:
| step | how |
|---|---|
| detect | GET api.github.com/repos/igorsaevets/ai-second-opinion/tags at most every 24 h (R136; was 168 h) (stamp outside the tree, ETag, 3 s timeout, backoff 1→16 h, kill switches MODEL_ORCH_UPDATE_CHECK=0 / NO_UPDATE_NOTIFIER / CI); highest version tuple wins, never /releases/latest |
| notify | at session start (the plugin's SessionStart hook, or the same entry --install-hook writes for other installs) and at the end of a real orchestrate.py round — never on --dry-run. The notice quotes the release notes (the release object by tag, else the CHANGELOG at the tag's commit) and names one command |
| apply | update_check.py --apply: the archive at github.com/igorsaevets/ai-second-opinion/archive/<commit>.zip, pinned to the commit the tags API named; refused unless it has one top-level folder, the skill subtree, every required file, VERSION == tag, no path escaping the folder, no symlink; then the incoming release's upgrade.py --from <extracted> --to <install> (backup, settings carried, doctor). A plugin install runs claude plugin marketplace update + claude plugin update instead; a git checkout and the development tree are refused with the right command printed |
No signature is checked — the trust is TLS to github.com plus the commit pin, the same as git clone. --show-what-would-be-sent prints every request; the User-Agent carries no version.
6. Diagnostics
Section titled “6. Diagnostics”Every run writes run.log and diagnostics.json into the output directory.
run.log is appended per line as it happens, not buffered. If the run is killed or the
machine dies, a buffered log is empty at exactly the moment it is needed.
diagnostics.json carries schema, how_to_read_this, the invocation, the environment (Python,
OS, both CLI versions probed live, key presence and length only), the resolved plan, the
preflight, per-channel results and telemetry, the full console transcript, and — when the tool
itself crashes — the traceback.
problems[] is the part to act on: each entry pairs the raw detail with a plain-language
likely_cause and a suggested_fix, matched from a table of known failure signatures.
An unexpected exception is turned into this file rather than a bare traceback. A traceback on a terminal is lost when the window closes, and it is the one thing a user cannot usefully relay ("it says something about line 812").
Both files are scrubbed by construction (secret and personal-data shapes; since 1.113.9 the home
folder is written as ~), which is what makes the intended workflow safe: hand
diagnostics.json to an AI assistant and ask it to fix the cause.
7. System-prompt presets
Section titled “7. System-prompt presets”systems/base-depth.md is the default: maximum depth, first intuition may be wrong, enumerate
alternatives, check for contradictions, unofficial-but-lawful routes named alongside official
ones, no output-length cap, escalate fetch tools, never reconstruct a citation.
systems/legal-research.md is the regulated-domain variant. Three layers: role (research
assistant under professional supervision, explicitly not your lawyer); status of the output
(internal work product reviewed before reliance); boundaries stated as the task (verify claims and
find errors; do not decide what to file, do not choose an answer for a government form, do not
opine on a named person's eligibility).
⚠️ Do not merge the two presets. base-depth asks for "unofficial, grey routes alongside the
official one", which is right for engineering and wrong for a regulated domain, where it reads as
suggest a way around the rule — the exact trigger that gets a legal brief refused. The omission
is deliberate and is documented inside both files.
Measured, same six claims: framed as filing strategy, the strongest channel returned a 162-byte refusal. Framed as source verification with the preset, the identical claims were answered 6/6 with correct citations from official domains only. On another channel the same reframing moved citation grounding from 1/6 to 6/6 and tripled the tool calls.
All presets force English output: the report is machine-read by the orchestrating model, and Cyrillic costs roughly twice the tokens. A Russian brief still returns an English answer.
Every preset also carries the rule that stops a failed search from becoming a false negative: if your search finds nothing, say so, and do not conclude the thing does not exist. A non-existence claim requires positive evidence of absence, with a URL.
8. Cost discipline
Section titled “8. Cost discipline”- Spark first for anything a competent researcher with a search engine would settle.
- agy when that is inconclusive, for large-context reading, and as the fast sanity check.
- Codex last. It is for judgement calls and line-level audits of a finished artifact. Sending it a lookup wastes half an hour and the most expensive quota you have.
That ladder governs ordinary work. It does not apply to a commissioned second opinion: there, run every available channel on the identical packet, because the disagreement is the product.
Do not answer an exhausted subscription limit by opening a metered API path. That is usually several times more expensive than waiting or switching channel. Re-point the round in prose instead — that is what the routing layer is for.
9. Deep research
Section titled “9. Deep research”Not available, and the reason matters more than the answer.
At both vendors "deep research" is a separate agent product behind a metered API key, not a depth switch on a chat model — OpenAI's is on the Responses API, Google's on the Interactions API. The decisive evidence is not documentation but the vendor's own server:
$ codex exec -m o3-deep-research{"type":"error","status":400,"error":{"type":"invalid_request_error", "message":"The 'o3-deep-research' model is not supported when using Codex with a ChatGPT account."}}So the block is the subscription, not the CLI. codex exec returned exit 0 on that hard 400,
which is one more reason never to gate on an exit code.
What this harness does instead is the same shape — agentic multi-step web research at maximum effort with a source-discipline system prompt. Honest differences: shorter autonomous horizon, no built-in clarification loop, no vendor report formatting. Honest advantage, and not a consolation prize: several independent models that disagree, plus a mechanical audit of whether the citations were opened and whether they exist. Deep-research products emit citations with no audit trail.
If you want a vendor's deep research, run it by hand in the browser, save the report, and pass it to this harness as an input document to be attacked. That costs nothing extra and plays to what these channels are actually good at.
10. Verification practice
Section titled “10. Verification practice”Plant a deliberately false claim in every verification brief. It costs nothing, and a channel that "confirms" it has priced all of its other confirmations. In the three-channel round behind this document, all three refused both planted claims — which is the only reason to credit what they did confirm.
Demand a live web search, with a URL and a date for every dated claim. A review citing no URLs has verified nothing, whatever its exit code said.
Know which channel you can audit. Only the agy channel exposes per-call telemetry
(--output-format stream-json → every tool call and a cited-vs-opened check). Spark reports a
search count. Codex reports nothing at all — for that channel the answer and its URLs are the only
evidence there is, which is exactly why --resolve-urls accepts an answer with no event log.
11. Extending it
Section titled “11. Extending it”Add a channel by editing channels.json: a default model, a models map with aliases, a cost
class, and the valid efforts per model. Aliases in that file are what free-text routing matches
against.
The live channel list is python routing.py; your own additions go in the local overlay file
described in README.md, where no update can touch them. (An earlier revision of this
paragraph listed planned additions — Kimi, and OpenRouter as the one-credential route to many
models. Both shipped, and the sentence kept promising them as future work for weeks afterwards,
breaking the first rule of §12 below in the same file that states it.)
12. Two habits that outlive any specific bug
Section titled “12. Two habits that outlive any specific bug”Never assert a mutable value in a document. Both command-line tools changed version inside one
week. A document that asserts a version does not look stale — it looks like documentation.
Replace the assertion with a probe: that is what doctor.py is for, and why its last line is your
run command with the real path filled in.
When two files cover one subject, one holds the decision and the other holds a pointer — never a second summary. Measured here: two skills documented the same CLI, and the copy that was only read drifted to four wrong facts while reading as authoritative, because the copy that is executed gets corrected by failure and the copy that is only read has no error signal at all. That is also why the distributable is generated from the working skill rather than forked from it.