cyberine
UsageRun recipes

Judge backend fallback and unjudged runs

The LLM judges (llm-judge, adversarial), the research ClaimVerifier and the SynthesizerReducer use every configured backend in precedence order. Without routing.judges in fleet.json the candidates are Moonshot (only with CYBERINE_JUDGE_BACKEND=moonshot), DeepSeek-direct, OpenRouter, the Command Code Provider API (the zero-credit free lane first, then the paid loop-commandcode-build model) and every enabled claude / claude-<seat> entry — each only when its key or token is present and its worker is enabled in fleet.json — ordered by model strength, with an API backend ahead of a CLI-spawn judge at equal strength (khv45). A routing.judges list replaces that order (see "Choose the judge backends" below). When the active backend REFUSES a call (HTTP 402 out of credits, 429 rate-limit or a free model's daily cap, 401/403 auth, or a CLI judge that returns no answer) the panel switches to the next backend for the rest of the run and says so once:

NOTE: judge model: deepseek-v4-flash (tier=standard, deepseek-direct); fallback on refusal: openrouter/<model>; default precedence (no routing.judges)
NOTE: judge backend deepseek-direct refused (out-of-credits: 402 Insufficient Balance) -- switched to openrouter (<model>) for the rest of this run.

When NO backend can answer, the LLM judges abstain (winner=(none)) instead of silently ruling for the first worker. cyberine run then falls back to the first successful worker and labels it:

[verdict:positional] winner=loop-mimo-direct -- NOT judged: every judge abstained (see NOTE lines); ...
state:  done (unjudged)

-p still prints that positional answer so a pipe keeps working; the stderr line and the (unjudged) state are the signal a wrapping script should read before trusting the pick.

Known limitation: on the single-step cyberine run path, workers are ranked by their TEXT output only — a worker that edited nothing can still win when the judges prefer its summary. The "never crown an empty diff" rule needs per-worker diffs, which the single-step path does not capture yet (see SINGLE-STEP-ISOLATION in the task board); the engine isolate: true path already enforces it through DiffJudge.

For the npm run cli path, progress is emitted in the same way (stderr) in human mode. In --json mode all progress lines are suppressed so stdout carries only the JSON payload:

npm run cli -- "What is 42 times 7?"           # progress on stderr, human output on stdout
npm run cli -- --json "What is 42 times 7?"    # no progress; stdout = JSON blob only

Which adapters stream: LoopAgentAdapter and CliAgentAdapter both implement the StreamingAgent interface and produce progress events. Adapters that only implement Agent.run() are silently run non-streaming — no progress lines for those workers, falling back to the post-completion summary already shown.

What is emitted per line: tool-call events (tool: write_file(src/foo.ts)) and one-liner status events (detail field). Raw token text is NOT streamed to stderr — only structured events — to avoid flooding stderr with large code-generation output.

Stderr channel contract: progress lines go to stderr unconditionally, regardless of --print or --json flags. This matches the existing behaviour where worker completion summaries ([agent-id] status=success duration=1234ms) already go to stderr; streaming extends that pattern with live updates during the run.

Choose the judge backends: fleet.json routing.judges (kz13x)

Without the key, text tasks are judged by whatever the default precedence above finds. A fleet whose direct API providers are all disabled (out of credit) but whose Command Code entries are enabled now gets llm-judge and adversarial (and research verification and synthesis) on the Command Code Provider API instead of the consensus judge alone. routing.judges names the judge backends explicitly, in precedence order, and overrides the default strength ordering. Since khv45 it is no longer the only way a subscription worker judges: an enabled claude or claude-<seat> entry is a default candidate too.

Run recipe. Add the key to the fleet.json already in effect (~/.cyberine/fleet.json, or a project .cyberine/fleet.json), next to providers:

{
  "version": 1,
  "providers": { "...": "unchanged" },
  "routing": {
    "judges": ["loop-commandcode-free-build", "loop-commandcode-build", "claude-kt"]
  }
}
cyberine run "Summarise the trade-offs of optimistic locking"
# stderr:
#   NOTE: judge model: <free model id> (tier=trivial, commandcode-free); fallback on refusal: commandcode-free/<id> -> commandcode/<CMD_MODEL> -> claude-kt/claude-sonnet-5; chosen by fleet.json routing.judges (<path>)
#   NOTE: judging bills the claude-kt subscription (claude-sonnet-5) on every LLM-judge call once the chain reaches it - fleet.json routing.judges names it (<path>). A CLI judge spawns one process per call, far slower than an API judge.
cyberine run --bg "<task>"                 # engine path: run.json records the plan under "judging"
jq .judging .local/runs/<runId>/run.json   # { source, backends: [{ id, label, model, subscriptionBilled, billingClass }], notes }
CMD_JUDGE_MODEL='<open-model id from GET /provider/v1/models>' cyberine run "<task>"
cyberine fleet doctor                      # validates routing.judges before any run; see below

Reading the list:

  • Accepted ids: loop-deepseek-build (alias loop-deepseek-direct), loop-openrouter-build (alias loop-deepseek-openrouter), loop-kimi-build, loop-commandcode-free-build, loop-commandcode-build, and the subscription CLI judges claude-<seat> (a seat declared in the global seats, bound by an enabled entry) or bare claude (an enabled seatless claude entry).
  • loop-commandcode-free-build expands to the free lane: the first free model the cached live catalog lists plus up to three more, in CMD_FREE_MODEL order when set, skipping any model cooling down after a refusal. loop-commandcode-build judges on CMD_JUDGE_MODEL, else CMD_MODEL; an Anthropic id is refused, and a closed credit window or an earlier refusal parks it with the same note the implementer gets.
  • An id is refused with a fleet.json routing.judges: "<id>" refused - <why> NOTE and skipped when its worker is not enabled in fleet.json, its key is missing, it is unknown, it is a known worker that is not a judge backend (muse, codex, command-code, ...), or its seat is undeclared or has no token file on this machine. When nothing in the list resolves, only the consensus judge runs: the default precedence is never substituted for a list the operator wrote.
  • A file without the key takes the default candidates above, ordered by strength — including any enabled claude entry, because an enabled worker is used for every role. With no fleet.json at all every keyed API backend counts as enabled, CMD_API_KEY included, exactly as the REPLACE-semantics gate treats workers; no seat judge is derived in that case, since nothing declares one.
  • Subscription billing is decided by src/core/billingClass.ts (fleet.json billing answer > auth mechanism > billing endpoint > profile default), reached through judgeBackendBilling / judgeBackendBillsSubscription in src/judges/judgeBackends.ts — so a claude judge running on an API key rather than a login is reported as credits, which the old cost-model guess got wrong. A subscription-billed judge adds the judging bills the <id> subscription NOTE with the deciding evidence, lands in run.json judging.backends[] with subscriptionBilled: true and billingClass: "subscription", and runs as claude -p ... --tools "" (no tools at all) in a throwaway temp directory under the host sandbox, one process per judge call, with a 5-minute timeout.
  • Refusals move along the chain for the rest of the run, as above. A Command Code refusal also cools that model (or parks the paid worker until its window reopens) in the shared cooldown store, so the free-lane and paid implementers skip it too. When the whole chain is exhausted the LLM judges abstain, the consensus judge still rules, and the run does not fail.
  • Same-vendor bias: when the default order applies, the WHOLE chain (not just the Command Code pair, as before khv45) is reordered so every backend whose model family a candidate worker also runs goes last, whenever a different-vendor backend is available — applied after the strength ordering, so vendor independence wins the primary slot. A routing.judges order is never changed. Either way, a primary judge that shares a model family with a candidate adds judge <label>/<model> shares its model family with candidate worker(s) [...], and every verdict rationale ends with [judge-model: <model that was active>].
  • The key may live in either layer and follows the full-replace rule: a project .cyberine/fleet.json that omits routing.judges does NOT inherit the global list. A malformed value (not an array, empty, a duplicate, a non-string entry) is a structural fleet.json error, like a malformed routing.steps.
  • When a Command Code judge may be used, orchestrate() and the engine refresh the Command Code catalog and headroom before building the judges: the same cached GETs cyberine route makes, each bounded by a 2.5 s timeout.

Extend recipe. A new API judge backend is a worker id in API_JUDGE_WORKER_IDS plus a transport in src/judges/judgeBackends.ts, and its base URL and key in judgeEndpoint in src/orchestrator/buildJudges.ts. A new CLI judge needs a text-only invocation of that CLI (the Claude one relies on --tools "") wired next to makeClaudeCliJudgeCompletion, and a default candidate for it next to defaultSeatChoices; the billing decision is judgeBackendBilling, which delegates to src/core/billingClass.ts and needs no per-backend code.

Known Limitations and Deferred Work.

  • Claude is the only CLI judge. muse, codex and the command-code CLI are refused as judges; each needs its own verified no-tools invocation first.
  • CLI judge speed is unmeasured. A CLI judge is a process spawn per call, so an adversarial round on N candidates is 2N spawns (parallel across candidates, sequential within one). Since khv45 it CAN be a default when it is the strongest enabled backend, and no live seat-judge timing was measured — the ordering rule (API first at equal strength) is the only protection. The --tools "" invocation was checked in the generated argv, not in a live verdict.
  • Strength orders the chain, it does not pick a model. khv45 sorts the backends by the strength of the model each one would use, but the paid Command Code judge still uses one model for every tier and the free-lane order is still the catalog's order; picking a different model per tier by strength is still deferred.
  • Vendor matching is a heuristic. It compares model families parsed from ids (deepseek/deepseek-v4-flash and deepseek-v4-pro match, anthropic/claude-* and claude-* match), not a vendor registry, and a CLI worker whose provider label carries no model is never matched.
  • cyberine fleet doctor no longer waits for a run to surface a bad id. It resolves every routing.judges entry through the same planJudgeBackends resolver buildDefaultJudges uses and prints one line per bad id (unknown, disabled, keyless, unsupported, or an undeclared seat) with the identical reason text; an unknown worker id is a structural error (cyberine fleet doctor exits non-zero), the rest are warnings, and a list that resolves cleanly prints the resolved backend chain instead.
  • Free-model budgets are shared. About 100 free requests a day per model per account, shared with the free-lane implementer and the cmd CLI: an adversarial round on three candidates is at least six calls, and a research verify step makes many more.
  • Only refusals advance the chain. A 5xx or a network error on an API judge makes that one call abstain; the chain stays on the same backend.
  • Accounting and neighbours. Aux spend on a paid Command Code judge call is estimated in USD into the daily-cap ledger (free models record zero), although the account draws credits, not money. The DiffJudge self-heal and swarm integration-fix agents still use DeepSeek-direct or OpenRouter only. CYBERINE_CLAUDE_OUTPUT_SCHEMA, when set, also constrains the Claude CLI judge's answer.