Releases: szarkans/multi
Releases · szarkans/multi
Release list
1.12.0 — the judge lists everything before it judges
- The judge lists everything before it judges. The review report opens with
## 📋 Everything raised (N): every finding from every reviewer, numbered, with its author, itsfile:lineand its claim in the reviewer's words, and the buckets below cite those numbers. Two findings merge only when they name the same mechanism; the same line with a different mechanism stays two, and a dropped finding keeps its own text and a reason that cites the line contradicting it. Measured on 8 real bugs (commit that introduced the bug, headless, same reviewers): the old judge delivered 3 of 8 while its own reviewers had found 5 — Codex's real finding at one line was folded into a sub-agent's speculation at a neighbouring line and dropped with it, and a bug three reviewers agreed on was merged into a broader neighbour and lost its mechanism. With the inventory the report delivers what the reviewers found, 6 of 8, at the same cost per review; the one finding still lost is a compound sentence from Codex whose second half is the bug, and the rule now says a two-mechanism sentence is two lines — that clarification is checked on the judge step alone, not end-to-end. Swapping the judge model did not fix it: Opus on the same inputs lost the same finding two times out of three, by the same merge. - Control arm against the built-in
/code-review high, same bugs, same judge model: its report found 3 of 8. Scoreboard and protocol inevals/RESULTS.md; the harness isevals/run-arms.shandevals/grade-arms.sh(blind grading: reports flattened tofile:line | claim, three votes, majority). The comparison is one run per arm on eight bugs — a one-bug difference is noise; the reviewers' 5-vs-3 and the report's 6-vs-3 are the gaps that clear it. evals/run.shandrun-arms.shaccept a seventhseveritycolumn in the case file.
1.11.1 — a pool passed over is named
- A pool passed over is named.
claude-headlessbackends probe each model in the list with one token and run on the first that answers; the answer's trailer now says which ones were skipped and what they said —[multi] openrouter pools skipped before it: qwen/qwen3.8-flash (RATE LIMITED (HTTP 429))— andsetup.sh statusprints the same afterwill use. The review used to run on the second model with nothing saying the first was busy, which read as "the config order is wrong" (measured 2026-09-07: qwen first in the list, GLM ran).ALL POOLS BUSYcarries the per-pool codes too.
1.11.0 — a run never starts over a live one; wait.sh; snapshot --paths; worktree-safe headers
- A run no longer starts on top of one that is still going.
ask.shclears every answer file before launching, and aclaude/codexfrom an earlier run on the same--out-prefixthen finished into a deleted inode: its runner found an empty path and marked itNO OUTPUTwith exit 0 while the transcript held a full review. Measured 2026-09-07: four branches reviewed in one session, all on$RUN/review, and three sets of GLM and OpenRouter answers went that way — that is what "OpenRouter and GLM don't work" was.ask.shnow takes a lock (bash's ownnoclobberopen — notmkdir: the uutils coreutils that Ubuntu 25.10+ ships answer 0 to both of two racingmkdirs, measured 17 of 30 races on tmpfs), reads the previous run's roster, and refuses the prefix while any of its markers belongs to a live process, saying what to do instead; a marker whose pid is gone is a leftover, not a block. Markers are written whole and renamed into place, so a reader never sees a half-written one as "nobody here". <answer>.runningsays who and since when:<pid of the backend's runner> <start epoch> <timeout>, so anlsbeside an empty answer shows three minutes in from thirty. The pid is the backend's own subshell, notask.sh's: it outlives a SIGKILLedask.shand keeps writing, and liveness has to mean that process. It used to be an empty file (#27).<prefix>.runis the roster of a run: one line per participant, written before anything launches, then<backend> <seconds>as each one ends — so a reader that arrives mid-launch sees the whole run, and the durations say which backend was the slow one.scripts/wait.sh --prefix <prefix> [--max N]reads it, blocks until every backend has ended, and prints one line per backend —codex 5m12s ok,glm 23m04s ok,openrouter 40m00s FAILED: <the .dead text>, orstill running (timeout 2400s)when--maxran out first (exit 1: call again; a Bash tool call is capped at ten minutes). A marker nobody owns beside an answer means the runner was killed and the answer is partial, and it says so. Status is read from the roster and markers only, never from answer text and never from a glob (review-*would also match areview-2beside it). The review and check-if-done skills wait through it, say outright that an empty answer beside a live.runningis a reviewer still writing, and say thatask.shruns as a background task — GLM answered after 23 minutes with the one finding nobody else had, and had been written off at nine (#27).- A runner that ends with nothing written and no marker is marked
NO OUTPUTin its own subshell, the moment it ends; the parent used to do it only after every backend was done, so a reader in between saw a finished backend with no answer and no reason. snapshot.sh --paths "<paths>": every path named must be in the final copy, or the snapshot fails (exit 2, no path on stdout, so the skill's "snapshot failed — not reviewing" guard fires) and says why —docs/item-map is not in the copy: ignored via .git/info/exclude:40, fromgit check-ignore -v(the source and line only; the pattern is text from the reviewed repo). The copy takesgit ls-files --exclude-standard, which honours.git/info/exclude, so adocs/line there silently dropped the folder under review and Codex, OpenRouter and three sub-agents all agreed there was nothing to review. The check runs after the config purge, against the tree the reviewers get; a harness rule file (CLAUDE.md,AGENTS.md,.mcp.json, …) is stripped from the copy on purpose and is noted, not failed — its change is inreview.diff; a path the diff deletes or renames is not missing either; a glob is not checked and says so; a path that does not exist at all fails the same way. The review skill passes--pathswhenever the target is paths (#29).- The skill headers work in a git worktree session. Claude Code gates every shell command there statically and refuses
sh -c,bash <file>,${VAR:-default},forloops and$PWDarguments (measured 2026-09-07 withclaude -w); the probe header was ash -cloop, so/multi:code-reviewdied on line one withShell substitution failed, reading as a broken plugin. It is now a||chain of plain paths —"$CLAUDE_PLUGIN_ROOT/scripts/probe.sh" || "$HOME/.claude/skills/multi/scripts/probe.sh" || ./.claude/skills/multi/scripts/probe.sh— which the gate lets through;probe.shprintsscripts-dir:itself. Every skill names the failure and the one-command workaround for the case the gate changes again (#26). setup.sh statustimes the one-token check of the model that answered and prints it (OK — will use x (1s to answer one token right now)); five seconds or more addsSLOW — expect a review here to take tens of minutes; a bad choice for the default profile. One token in seconds is eighty review turns in minutes; nothing measured that before a run. It is one sample at one moment, and says so (#28).- The
isn't described by this version's model catalog … auto-compact keeps this session within 200k tokensparagraph is now named in theTIMEOUT/NO OUTPUTmarker for what it is: Claude Code's context-window notice for a model name it does not know, printed on every run on a non-Anthropic endpoint, not the cause. It was the only thing in the stderr log of a run that died silently, and read as the reason (#28). config.example.toml: OpenRouter's free/flash pools are out of the everyday profile (normal = ["codex", "glm"]) — measured 2026-09-06, 25–40 minutes for one review onqwen3.8-flashvia OpenRouter while the same model on a direct key took 5. They belong in a profile picked on purpose, and thefreeprofile pinsopenrouter:z-ai/glm-5.2:freerather than the bare backend, whose chain tries paid models first (#28). Not done from #28: a timeout derived from a measured speed — the pools' speed changes by the hour, and a measured 5-minute pool killing a 20-minute review would hide answers the current fixed budget keeps.- Known limit, left as is: liveness is
kill -0on the pid in the marker. A pid recycled by an unrelated long-lived process after a SIGKILL would hold the prefix until that process exits; the fix is--out-prefixsomething else. No portable way to tell a recycled pid from the real one was worth its size.
1.10.0 — one config file
- One config file:
~/.claude/multi/config.toml. Backends, their models, endpoints, per-backend timeouts and named profiles all live there, with comments;providers.envkeeps only keys. Read byscripts/config.py(stdlibtomllib, with a vendoredtomlifor python < 3.11). No file means the built-in default — codex, opencode, openrouter — andsetup.sh initwrites it out to edit.- Any number of Anthropic-compatible endpoints, each its own backend:
[backends.zcode]withtype = "claude-headless", its ownbase_url,modelsand key variable, next toopenrouterinstead of replacing it. Before, one endpoint could be pointed away from OpenRouter and that was all. - Profiles:
[profiles] normal = ["openrouter:x-ai/grok-4.5", "zcode", "codex"], picked withask.sh --backend normal; no--backendrunsdefault_profile.code-review,check-if-doneandaskstop hard-coding a backend list, so the config actually decides who reviews — a knob the skills bypass changes nothing.adhdkeeps its fixed two external frames on purpose: its frames are distinct engines, not a reviewer roster. backend:modelpins exactly that model with no fallback. Asking for a paid model and silently getting a free one is worse than a marker saying it failed.- Per-backend
timeout;ask.sh --timeout Nis a floor (every backend gets at least N, the review skill passes 2400), never a cut — the same rule codex already had with its 600s. - A broken config stops every run before anything launches, naming the file and the key: unknown type, unknown key, a profile naming a backend that does not exist or sharing a backend's name, a
claude-headlesswithoutmodelsorbase_url, a non-httpsendpoint (plainhttponly when the parsed host is the loopback), a model name with whitespace, a key variable or backend name that is not a plain identifier, more than one model oncodexorgemini— those two do not walk a chain yet, and the config says so instead of ignoring the rest.
- Any number of Anthropic-compatible endpoints, each its own backend:
- Removed, not aliased: the
modelsfile,MULTI_OPENROUTER_MODEL,MULTI_OPENROUTER_MODELS,MULTI_OPENROUTER_FALLBACKS,MULTI_OPENROUTER_BASE_URL,MULTI_OPENCODE_MODEL,MULTI_GEMINI_MODEL,MULTI_BACKEND_TIMEOUT,MULTI_CODEX_TIMEOUT,MULTI_OPENCODE_STALL, and the--or-model/--gemini-modelflags (name:modeldoes it, and a by-type flag picks the wrong backend once two share a type). Reading the old variables "just in case" would be a third config. Whileproviders.envstill sets any of them nothing runs: the oldMULTI_OPENROUTER_BASE_URLchose where the key goes, and a config that ignored it would send a z.ai key to openrouter.ai.setup.sh initstill works in that state, the probe saysmodels-config: LEGACYwhile the old file exists, andsetup.sh set MULTI_*explains where the value went.setup.sh setaccepts exactly the key variables the config's backends read.- Upgrading: move
MULTI_OPENROUTER_*values into[backends.openrouter](base_url,models) and themodelsfile's list into[backends.opencode]; timeouts becometimeout = Nper backend. Keys need no change.
- Upgrading: move
config.example.tomlandproviders.example.envin the repo show every backend type, every field, a second endpoint with its own key, and several profiles; a test keeps the example valid.python3is now required to run anything: bash cannot read TOML. It was already needed to read OpenCode answers.
1.9.0 — silent OpenCode dies in 180s, a killed ask.sh leaves markers
- An OpenCode model that is out of quota no longer eats the whole review budget. Out of quota,
opencode runwrites no events at all and just sits there; the fallback chain then gave every next model a fresh full timeout — measured 44 minutes of a 71-minute review on one silent model, and a 6-model chain could take 4 hours. A healthy run writes its first JSON event within seconds, so a model that has written no event forMULTI_OPENCODE_STALLseconds (default 180; a stderr warning does not count) is now killed, with its whole process tree, and the chain moves on; its marker saysSILENT, notTIMEOUT(#16). - A killed
ask.shleaves markers. Terminating it from outside (Ctrl-C,pkill, a caller's own timeout) used to leave a 0-byte transcript and neither an answer nor a.dead, so a judge reading*.deadsaw a reviewer that neither answered nor failed. TERM/INT/HUP now stop the children and write<backend>: KILLED — …for every backend still running (#23). - A CLI that cannot write under
$HOMEsays so. Run from a sandboxed shell where$HOMEis read-only, both Codex and OpenCode die on startup and were reported asNO OUTPUT, the same text a model that answered nothing gets. The marker now adds that the CLI could not write under HOME and suggests the sandbox. Only the CLI's own stderr lines can trigger it, never text the model read from the reviewed repo. An OpenCode that died at startup without a single JSON event used to be counted as a live "raw capture" answer — that is how today'sRAW CAPTURE ONLY — model=sonnet exit=1passed as alive — and is now a dead backend with a reason (#21, #22). check-if-donereviews with OpenRouter and Gemini too. It calledask.shwithout--backend, so it silently got Codex + OpenCode only whilecode-reviewandaskused every configured backend; and its--model <from probe>wording sent the agent copying the Claude sub-agent model into OpenCode. It now passes the same explicit backend list as the other skills, and says which probe line to copy (#17, #22).- Claude reviewer sub-agents cite lines from the file, not from
review.diff. The agents were pointed at the diff and asked forFILE:LINEwith nothing saying the diff's numbering is not the file's; one cited line 290 of a 16-line file, and the judge had to renumber by hand, which also broke corroboration against Codex and OpenRouter (#18). - The ponytail section of the review report is labelled for what it is: the judge's own read under a different ruleset, not a fourth independent reviewer. Its placement between independent sections implied a fourth model family (#19).
- The rule-file skip note is honest about a gitignored
CLAUDE.md. Untracked ignored files are deliberately counted as touched by the change (a.gitignoreedit is how a hostile rule file hides), but the note called such a file "modified by the reviewed change" and sent readers hunting for a diff that does not exist. It now says the file is untracked and gitignored; it is still skipped (#20). - Cleanup:
--fallbackdocumented as OpenCode-only, a duplicatedlocalin the OpenRouter runner dropped, and theMULTI_RUN_KEEP_DAYS=0test now checks that an old run survives instead of comparing two literals (#24).
1.8.1 — the timeout diagnosis was dead on macOS
- The honest timeout diagnosis now works on macOS too. 1.8.0 located the child's transcript with
find -print -quit, and-quitis a GNU extension that stock macOSfinddoes not have — there it failed silently, the transcript came back empty, and every timed-out OpenRouter reviewer was blamed on a rejected key again, which is the exact misdiagnosis 1.8.0 was written to end. It is a plain glob now, with no external command in the path at all. - The usage example at the top of
ask.shstill said--timeout 900after the default moved to 2400.
1.8.0 — reviewers survive their own timeout, and a dead one says why
- A slow reviewer is no longer killed and thrown away: the review budget went from 900s to 2400s. Measured on a real review — the OpenRouter reviewer worked for 36 model turns over 890s and was killed 5 seconds before writing its report, discarding 1.05M paid input tokens and leaving a 0-byte file.
claude -pprints nothing until it finishes, so any kill costs the whole run. - A failed OpenRouter reviewer now says what actually happened instead of guessing. It used to claim "the key was probably rejected" every time; now it counts the model turns the child really made and names its transcript, and no branch states a cause as settled — zero turns fits a rejected key, a pool that went 429, or a transcript format this code stopped recognising, and the message says so.
- OpenCode reviews code again: it runs as a plugin-owned read-only agent instead of
--agent plan, which had been silently refusing its own grep calls and returning an "I'll review…" stub. It also closes the hole where a reviewed repo's own opencode config could re-enable write and bash (#12). - OpenRouter and Gemini count as real reviewers, so a setup with either satisfies the multi-model gate. The probe stops printing OK for a key it never checked — a dead Gemini key used to show up green.
- Every backend now runs in the directory
--reponames. OpenRouter and Gemini used to run wherever the caller stood, which meant reviewing a worktree copy could report an empty diff as clean. - The isolated copy handed to reviewers is stripped of the reviewed repo's
CLAUDE.md,AGENTS.md,GEMINI.md,.claude/and.gemini/, so a hostile repository cannot load its own hooks or instructions into a reviewer. They stay visible inside the diff, where they are inert text a reviewer should see. - Model pinning moved to
~/.claude/multi/models, next to everything else this plugin owns. The old~/.config/multi/modelsis still read for one release and setup offers the one-line move. - A custom endpoint (9router, z.ai, self-hosted) must be
https://— plainhttp://is refused except on localhost — and setting one says out loud that your API key will be sent there. The endpoint decides where the key goes, so it is as sensitive as the key. - The probe reports an available paid OpenCode channel even when you already pin your own models. It only did so for users without a config, which excluded exactly the people the offer was written for.
- Reviewers are told the diff is where their reading starts, not where it ends — open the changed files, their tests and their callers before judging.
/multi:setuprewritten, with per-backend reference pages it loads only when needed.- Russian and Chinese READMEs match the rewritten English one.
1.7.0 — reviewers run on an isolated copy of the tree
- Reviewers run on an isolated copy of your work tree, and the Claude reviewer sub-agents lost their shell entirely — a hostile-config opencode or a stray
git checkout -- .can no longer wipe uncommitted edits, it hits the copy, not your work (#14, real fix; 1.5.0 was prompt-only). - The copy strips the reviewed repo's
.opencode//opencode.json, so a hostile plan config can't re-enable opencode's write+bash (#12). - Secrets stay home: an uncommitted
.env, key, or.tfstate(tracked or not) is withheld from the copy and the diff, so it never reaches the cloud reviewers. - OpenCode reviews the code again: the diff travels as a
review.difffile it can read under--agent plan, instead of the "I'll review…" stub — until now only Codex actually reviewed. .git, ignored trees and files over 2 MiB (MULTI_SNAPSHOT_MAX_FILE_BYTES) stay out of the copy, so a large repo copies source, not gigabytes.- Boundary: the copy stops cwd-relative damage (the real #14); it is not an OS sandbox — a reviewer reaching the original by absolute path is out of scope.
1.6.0 — OpenCode free-model chain + model config
- OpenCode fallback is now a chain through every free model, not one spare, and
it advances on a timeout too — not only on an empty answer. A dead free model
no longer sinks the whole review. ~/.config/multi/models— pin your own OpenCode models by hand (opencode: <primary> <fallbacks…>), no agent needed. Overrides auto-detection; absent
file keeps the free default./multi:setupnow warns loudly when you're on free models and offers to
research current models, prices and usage for you, then writes your pick.- Codex gets its own timeout (
MULTI_CODEX_TIMEOUT, default 600s): high-effort
runs stop dying at the shared 300s; review runs keep their longer budget. - OpenCode
errorevents are no longer swallowed — a failed run says why it was
empty instead of "it ran but said nothing". - Gemini stderr now lands in
.dead.logon failure instead of vanishing.
1.5.0 — reviewers anchor to the right tree
--repo: reviewers read the right worktree instead of an empty diff when the
target isn't the session checkout (#13).- Review sub-agents are read-only — they can't revert your uncommitted work (#14).
- Merge fallback no longer leaks the commit SHA into the changed-files list (#9c).
test-injection.shpasses with no global git identity, so CI stops going red
on a clean tree (#9a).MULTI_RUN_IDkeeps concurrent non-Claude-Code runs out of one shared dir (#9e-4).- Marketplace owner name fixed (
szarkan→szarkans).