Skip to content

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.


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 Code
orchestrate.py the harness: dispatch, verification, gates, diagnostics
routing.py resolves registry + flags + free text into a plan
channels.json THE registry - every model name lives here and nowhere else
doctor.py "is this machine set up?" - probes, never asserts
selftest.py "does the code still behave?" - behavioural checks; prints its own count
citecheck.py citation grounding and existence checks
upgrade.py install/update in one path; migrates settings out of the tree
update_check.py the update cycle: daily release check, the notice, `--apply` self-update
patch_agy_permissions.py mandatory post-install step for the agy channel
echocheck.py proves a depth knob from the counter the vendor returns
VERSION the release this tree is; generated at build time
channels.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 demand
systems/*.md system-prompt presets
premium/*.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.


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.

  • A payload containing a key, token or private key is never sent. There is no override flag. The detectors are SECRET_PATTERNS in orchestrate.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-pii to refuse instead. Secrets are refused always, with no override deliberately. The detectors are PII_PATTERNS in 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.

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.

  • 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 PROBLEM on 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: SUCCESS with an empty answer, ERROR with a complete one, and exit 0 on 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.

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


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 nothing

There 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 «включая » in prose.

🔴 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 EXPENSIVE marker. 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 codex combined 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 », «… is kept»). Every later plan on that UTC date, --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.

  • 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 «мимо про» (mimo25pro lost "mimo pro"). A new group mimoflash ("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; mimov26flash lost «мимо флеш». routing.one_voice_groups derives mimoflash as a one-voice group; it holds no explicit_only member, 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 «флэш»). codex gained "codex cli" and «кодекс кли»: the longer name wins over "codex" + the cli group word.
  • _detect_silent_drop reads 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.json of codex-cli 0.167.2, field supported_reasoning_levels): gpt-5.6-sol none to xhigh -> low to max, gpt-6-sol none 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 default xhigh nothing changes. Codex also lists ultra («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 no efforts list and is never sent (_ultra_r178). gpt-5.5 and gpt-5.4 are 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).
  • 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 ocspark13go moves the two voices to the next route; --skip spark drops them.
  • An opencode-go/ channel is ready only where opencode lists its model (_oc_listed_models, one opencode models listing 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_preflight now 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). orgemini38flash is distribution: 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 as owner_action, REPORT.md's «What each channel actually did» prints «🔴 FAILED — » in that channel's row, and HANDOFF.md adds «🔑 Needs the account's owner» with it. New selftest suites suite_r179_kit_defaults and suite_r179_i3_go_owner_action.
  • Route model words (R177; a 2026-11-08 round asked for «mimo 2.6 pro» and got Gemini 3.1 Pro). agy31pro lost the model alias pro and agy36flash the alias flash: a model alias counts as naming its channel directly (routing.explicitly_named), which overrides both enabled: false and explicit_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_drop now 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 or explicit_only may 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»).
  • 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_SILENCE is 300 s (90 s since 1.117.0; ORCH_GROK_TOOL_SILENCE still overrides), and GROK_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_raced re-reads Grok's session record: a tool that completed as the watchdog fired gives watchdog_raced: true, a WATCHDOG RACED warning, «completed as the watchdog fired» in the error and its own diagnose() 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, an APIReadCut("headers") (no response headers within API_HEADER_LIMIT_S, 300 s) inside _stream_with_retry sends 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's cost is free, else 0, because a paid request with no headers may still be queued and billed upstream. deadline and stall cuts are never re-sent. The record gains headers_resent (after_rounds, at_s, answered) and headers_waits_s (each streamed request's wait for its headers); retries counts the re-send; a failed copy's error says it was the re-send, and diagnose() has two NO RESPONSE HEADERS within entries (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 a stream_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[].answered shows how often the re-send rescues a round. Suite: suite_r176_headers_resend.
  • Free-voice IN-ROUND retry+fallback rule (R172 И-2; operator's rule 2026-10-31). orchestrate.main uses concurrent.futures.wait(FIRST_COMPLETED) inside a nested in-round ThreadPoolExecutor; a free primary's failure is classified by _classify_free_failure(r, elapsed_s, 1500 s) into past_window, silence, transient + delay, quota, or other. 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 marker free_inround on 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, grep in-round: in run.log or read the free_inround field in diagnostics.json. Suite suite_r172_i2_free_inround has 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 — claudeaiseat and cgptseat (kind chatseat, CDP, parallel), cldesktopseat and cgptdeskseat (kind uiaseat, UI Automation, serial foreground) — are present with distribution: local and enabled: false; an overlay enables them. --panel chat refuses to run without --chat claude|chatgpt|both. Drivers live in the author's seats/ 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 env R173_SEAT_FORBID_LIVE leaves chat_slot_spent: False so 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_through self-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_plan AND-s expiry into enabled, writes valid_through + expired_on into the plan, and prepends a why line naming the date. default_enabled is not touched by expiry — a spend-gate can still tell "off by default" from "on but expired". Overlay channels with valid_through revert 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).
  • DeepSeek V4.1 Flash takes Step 5 Preview's seat in the cheap panel: nvdeepseekv41flash (oai, NVIDIA's free endpoint, NVIDIA_NIM_API_KEY), effort max (NVIDIA accepts none..max; n=3 high against max overlapped, so no depth verdict), max_tokens 131072, timeout 61m (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, effort max, 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 fallback OFF in this run. ocstep5previewnv / nvstep5preview are off and start by name. Kit counts: cheap 6, standard 12, «CLI only» 3 (2 on a refused day), --ask 4 (3).
  • Family words: deepseek / дипсик and glm / глм / zai are groups over every transport (as kimi and grok): «не используй glm» also turns DeepSeek's fallback off; --only glm on the cheap panel stops, because its only startable member there is the paid orglm53.
  • The page-fetch fence: citecheck._resolve_public refuses every address that is not is_global (Python calls 100.64.0.0/10, the range Tailscale uses, neither private nor global, so it used to pass) and every *.ts.net name 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's ubuntu-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.
  • 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_grokcli records it failed-with-partial (no text, no outfile, WATCHDOG warning, the session note and the stuck tool in the error, watchdog_tool / watchdog_stuck_s fields), so an armed fallback may still run the seat. diagnose() answers WATCHDOG after with 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), 18 prompt_suffix blocks and 9 mangled notes restored from the 2026-10-24 snapshot, the build-stamped _kit_excluded_channels dropped from the source registry, the EN README credit back to the Igor Saevets placeholder.
  • ocnemotron3ultrafree ships off: opencode/nemotron-3-ultra-free is gone from opencode models (only nemotron-3.5-*-free left) and opencode run answers Model unavailable for 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.
  • 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, no quota_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' model fields; without them the line reads as before.
  • Every Python child process inherits PYTHONIOENCODING=utf-8 + PYTHONUTF8=1 + PYTHONUNBUFFERED=1 (process-wide setdefault plus 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. The NO_REPO bypass preamble carries the same rule to the model (temp .py over multiline python -c, pdftotext -layout first).
  • HANDOFF.md lists a never_read answer (every Nemotron channel) with never in its read column and counts it nowhere: not in the totals line, the resume block («прочитай каждый файл … КРОМЕ …»), the launch log's Handoff: line, the answer-cap lists or read_order_files, and the fallback line marks a never_read fallback's file (Nemotron's own fallback); stamp_reading_fields (moved out of main) stamps never_read beside read_order, reading_note and must_read.
  • The free-allowance memory: an answer clears a refusal recorded in the second its run began (1.115.1 kept it all day); channel names the member whose run phase set at, 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.quota hint no longer quotes opencode's FAQ figure, which opencode removed on 2026-10-21.
  • 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, every QUOTA: 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 of provider.quota; python routing.py applies 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: true in channels.json (nvnemotron3ultra, ocnemotron3ultrafree, ornemotron3ultra: the rule for every Nemotron channel, run it, never read its answer) still runs, but --ask prints one line with its answer file instead of the body (_print_ask_answers); reading_note stays the human text.
  • channels.json: ocnemotron3ultrafree carries a stated exemption from «every free opencode channel has a quota_group» (it was measured outside the group); a selftest holds that invariant.
  • --panel standard adds agy4argon: Gemini 4 Argon through the agy CLI, --model gemini-4-argon --effort high (agy 1.4.7 offers it low, medium and high; xhigh and max exit 1 at the start although agy --help lists 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 аргон» and agy4argon start it alone. Its weekly limit and the AI credits after it: README, the standard panel.
  • agy31pro ships off: agy 1.4.7 does not offer gemini-3.1-pro on our account (agy models; agy -p /model --model gemini-3.1-pro exits 1, «not recognized as a known model»). Naming it still starts it; the standard plan no longer prints its «NOT started» line.
  • --skip openrouter and --only openrouter include orspark12cont: the openrouter group holds every channel of kind openrouter (a selftest derives it from the kinds).
  • agy's print-mode slash commands answer without an agent turn and spend nothing (/usage read 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> and agy -p /effort --model <slug> (the efforts a model offers). The facts above were measured that way.
  • An API voice's timeout bounds 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 more timeout. A read cut this way keeps its text, graded partial, and is never retried automatically (SUPERSEDED in part in 1.119.2: on a free channel 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 timeout stops its whole process tree (opencode, mimo, Claude Code): taskkill /T on Windows, the process group elsewhere. Until 1.113.9 only the program's launcher was stopped (on Windows the opencode.CMD shim), 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), then ocdeepseekv41flashfree / ocdeepseekv41flashgo where 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 copies NVIDIA_NIM_API_KEY into 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 an opencode channel costs, in words. The plan, the preflight, the call log and the --ask header 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-settings is unchanged.
  • An API channel that opens pages itself stops opening them at its timeout. call_oai_reviewer opens no new page once the channel's timeout is spent and answers from what it has; the record marks it time_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. The timeout does 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 grok process 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 says LINGERED. An HTTP 402 from xAI reads GROK SUBSCRIPTION LIMIT, and each run records the CLI's version (cli_version).
  • Muse Code calls pass --contributor-mode from 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, or orspark12cont on OpenRouter). A channel meant to be off in the kit is distribution: both (or local) with enabled: false: package.py switches a distribution: kit channel that is off back on.

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 wholesale

The 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.json and <skill>/channels.json have identical write permissions. Anything that can write the first can write the second. Refusing model in 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.json was fingerprinted only by doctor.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.


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.


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.


  1. Spark first for anything a competent researcher with a search engine would settle.
  2. agy when that is inconclusive, for large-context reading, and as the fast sanity check.
  3. 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.


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.


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.


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.