7. Implementer fleet: do a real task (multiple providers compete, judge picks the best diff)
Beyond Q&A/design, Cyberine can run a real file-changing task as an implementer fleet: several vendor
coding agents each implement the same task in their OWN isolated git worktree, and a DiffJudge
applies each candidate diff in a throwaway worktree, type-checks it, and picks the winner. Nothing
touches main — the winning diff is reviewed before it is applied.
The workflow is src/engine/implementTaskWorkflow.ts (design -> implement). It reads the task from
CYBERINE_TASK; the writing workers are opt-in via CYBERINE_BUILD_FLEET (a comma list of worker ids):
CYBERINE_TASK="Add a sandbox-pinned grep_files tool (regex search over file contents) as
src/tools/grep.ts following the Tool interface, and register it where appropriate." \
CYBERINE_BUILD_FLEET="opencode-deepseek,opencode-kimi,claude,codex" \
npm run wf -- src/engine/implementTaskWorkflow.ts-
design step fans out to the strongest enabled workers (
DESIGN_FLEET— the top strength band of whateverfleet.jsonenables, subscription seats included; see "Strongest enabled models plan, split and judge" below) and a text-judge panel picks the best plan. It is a read-only step: a worker that can write files runs in a disposable worktree, and nothing it writes reaches the live repo (see "Read-only steps" below). -
implement step (
isolate: true) fans out to whichever cheap-class build workers are enabled (every build worker, when the fleet has no cheap-class worker at all). Each gets a freshgit worktree --detachoffHEADas its cwd, implements there, and itsgit diffis captured.DiffJudgescores each diff by applies cleanly +npm run typecheckpasses; when 2+ diffs both pass, anLlmJudgetie-break picks the best on correctness/quality (DeepSeek-direct then OpenRouter; falls back to shortest-diff when no LLM key) and the verdict carries an auditable reasoning string. A single passing diff skips the LLM call. The winning diff becomes the step output. Watch it live innpm run tui. -
Binary files survive the diff (k112c). Every worktree diff capture (the
implementstep above,SwarmMergeJudge's per-chunk and merged diffs,swarmMergeHelpers's shared capture used byLaneMergeJudge, and a swarm lane's cumulative diff) runsgit diff --cached --binary --full-index, not plaingit diff --cached. A worker that adds or edits a PNG, font or other binary file gets aGIT binary patchhunk instead of an unapplyableBinary files ... differsummary, sogit apply(the same apply oracle every judge uses,src/judges/diffApply.ts) lands it byte-identical rather than the candidate being silently pruned. Before this, any task that produced an image or other binary was dropped at judging with no error surfaced beyond the apply-failure detail below. Guarded bynpm run selftest:binary-diff-capture. -
Known limitation (typecheck oracle):
DiffJudgedetects the oracle from the candidate worktree, not from the caller. A repo with nopackage.json, or one whosepackage.jsonhas notypecheckscript, skips gate 2 and scores a cleanly-applied diff 2/2 with the detailno typecheck oracle in this repo (...); the implementer prompt drops itsnpm run typecheckclause the same way. No fallback oracle (tsc --noEmit,go build, ...) is invented for non-Node repos -- the apply gate is the only programmatic evidence there, so review the winner beforecyberine apply. Guarded bynpm run selftest:diffjudge-oracle. On the autonomy loop's sync-verify path a skipped gate 2 is never reported asdone, and a repo can declare its own oracle withverify.command; see "Apply-only verification is neverdone" (keeb9). -
Verdict rationale shapes:
DiffJudge'srationaledistinguishes "nobody tried" from "everybody tried and failed" -- reading onlywinner === nullcannot tell them apart.No candidate carries a diff — nothing to apply or verify.means no result carried araw.diffat all (nothing to grade).N candidates carried a diff, 0 applied: <agentId>: <detail>; <agentId>: <detail>means at least one candidate had a diff butgit applyrejected every one of them --<detail>is git's own stderr (first line only, capped at ~200 chars) so the digest names the offending file/hunk instead of a generic "diff does not apply" message.Ruled for <agentId>: <detail>.is the normal winning case. Every verdict also carries anapplyOutcomesarray ({ agentId, hadDiff, applied, detail? }per submitted result, an extra property alongside the documentedVerdictfields) with the full per-agent picture, including agents that never carried a diff at all -- read it fromrun.json'ssteps[].verdict.applyOutcomeswhen the one-line rationale is not enough. -
Known limitation (apply-failure detail): git's stderr is quoted verbatim, first line only -- a multi-line error (e.g. several conflicting hunks) only shows its first line in the rationale and
applyOutcomes[].detail; read the run's own worktree/log for the rest. A binary patch missing its full-index line still fails the same way (git applyrefuses it withcannot apply binary patch to '<path>' without full index line) and is reported by name, not worked around --DiffJudgenever retries with a different apply strategy. Since k112c this only bites a candidate diff that never went through Cyberine's own worktree capture (e.g. a CLI worker that hands back a hand-built patch instead of a capturedgit diff); every capture site Cyberine controls now emits--binary --full-index(see "Binary files survive the diff" above). Guarded bynpm run selftest:diffjudge-apply-reason. -
Read-only steps never write to the live repo (k3pjg). A step without
isolate: true(thedesignandreviewsteps here, thereviewstep ofswarm-task, theminestep of auto-evolve, acyberine run --bgcompare step with no build fleet) used to hand every worker the live checkout as its cwd, and only the prompt kept a write-capable worker from editing it: acommand-codeworker createdPROBE-COMMANDCODE-CLI.mdin the live repo duringdesign. Now each worker that CAN write gets its owngit worktree --detachoffHEADas its cwd, removed when the worker finishes, cancelled or errors. A worker can write when it is a CLI worker, or a loop worker holding any tool outside the read-only set (read_file,list_dir,grep_files,glob_files,todo_read,web_search,fetch_url,web_fetch,spill_retrieve,session_search,consult_oracle,lsp_query,calculator) -- so everyloop-*-buildworker. Its changes are never captured or applied; when there are any, the run records aprogressevent naming the worker and the paths, and the engine log prints the same line:grep 'discarded' .local/runs/<runId>/events.jsonl # ... "detail":"command-code wrote files during a read-only step; discarded: PROBE-COMMANDCODE-CLI.md"A read-only loop worker (
loop-*-directon the read-only toolset) still runs in the live repo. Files the prompt names that are gitignored or untracked are inlined into the prompt, exactly as for an isolate worker, and the step's text output (the plan, the review verdict) is unchanged. The same guard covers a search fan-out step, a chunk-handoff chain (one worktree for the whole chain), and a worker restarted withrin the TUI (arestart-workercontrol message) on any step, since a restart never captures a diff. -
Known Limitations and Deferred Work (read-only steps,
WorkflowEngine.runOffLiveTree):- A worker in a worktree sees
HEAD, not uncommitted edits (the implement step already did), while a read-only loop worker in the same step still sees the live tree, so two workers can plan from different trees. Commit first when the plan depends on uncommitted work. - Cost: each write-capable worker adds one serialized
git worktree add, a change scan and a removal. Measured on this repository with three write-capable workers that do no work: the design step went from 1.77 s to 2.53 s, about 250 ms per worker. - The worktree moves the cwd; it is not a host sandbox. A CLI worker running without a host
sandbox (or as a
claude --bgrow) can still write the live repo by absolute path, and an MCP tool reaches whatever its server can. A loop worker holding an MCP or plugin tool counts as write-capable, so it too runs in a worktree and seesHEAD. - Files written to gitignored paths are discarded with the worktree without being named.
- Outside a git repository, or before the first commit, there is nothing to check out: the
worker runs in the live tree and a
progressevent says so. - A worktree left behind by a killed engine process is not reaped; remove it with
git worktree remove --force <dir>orgit worktree prune, the same as for isolate steps. - Not covered: an
architectEditorstep (its editor writes the live tree by design; useisolate: truewhen it must not), imperative script workflows (WorkflowScriptRuntimedispatches in the script's cwd), and the foregroundorchestrate()compare path (cyberine run --image, or a build fleet outside git), which runs no workflow steps. - A custom
Agentthat is neitherkind: 'cli'nor setsinfo.mayWriteWorkingTreeis treated as read-only.
- A worker in a worktree sees
Security: the per-worker worktree isolates the workers from each other and keeps
mainclean — by itself it is NOT a host sandbox. Each CLI build worker runs its CLI with auto-approve/bypass flags, so absent further confinement it has full host privileges. To contain that, the CLI-worker subprocess is wrapped in a host sandbox when one is available. Six pluggable backends are supported, selected viaHOST_SANDBOX_MODE:
Backend Mode value Platform Mechanism macOS Seatbelt seatbeltmacOS sandbox-exec— kernel policy profile; reads denied for a credential list (~/.ssh,~/.aws,~/.config/gcloud,~/.config/gh,~/.gnupg,~/.git-credentials,~/.docker/config.json,~/.kube/config,~/.netrc,~/.npmrc,$CYBERINE_HOME, every known CLI's config dir, and dotenv-shaped files anywhere), writes confined to worktree +/tmp+ the worker's own CLI config dirs, minus those dirs' instruction/settings files (CLAUDE.md,AGENTS.md,settings*.json,opencode.json,config.toml,hooks/), which stay read-onlygVisor gvisorLinux docker run --runtime=runsc— Go-based userspace kernel; strongest isolationbubblewrap bubblewrapLinux bwrapnamespace container; only/usr,/bin,/sbin,/lib*,/etc,/opt,/var+~/.gitconfigbound read-only (NOT the whole rootfs — a full--ro-bind / /let a worker read~/.ssh/~/.aws/sibling repos'.envby absolute path even though writes were confined), plus the worktree/tmp/CLI-config-dir writablesDocker dockercross-platform docker run --read-only --cap-drop ALL; worktree bind-mountedPodman podmancross-platform rootless podman run; same flags as Docker backendWindows windowsWindows Low-Integrity job object via cyberine-sandboxhelper
auto(the default) picks the best available backend in the order above. Reads are scoped (see the deny-list / bind-list per backend above); writes are denied outside the worker's worktree (plus/tmpand the CLI's own config dir like~/.claude), so a CLI worker cannot mutate the host repo or$HOME. Control it withHOST_SANDBOX_MODEandHOST_SANDBOX_NETWORK(allowby default — CLI workers all need their own model API, so deny-by-default would break them; the read-scoping above is the real exfil control. SetHOST_SANDBOX_NETWORK=denyfor strict isolation on network-free tasks;buildDefaultAdapters()surfaces a note when a sandbox is active and you have opted intodeny). When no sandbox is available the CLI worker runs unsandboxed with a warning when the task is trusted (CYBERINE_TASK_TRUSTED=true, the default) — the warning now also prints to stderr fromcyberine run --bg/ chat/run(launchBg.ts) and the MCP server (mcp.ts), not only the in-process TUI/autonomy-loop paths. SetCYBERINE_TASK_TRUSTED=falseto require a sandbox: the CLI worker then refuses (HostSandboxError) instead of running with full host access when no sandbox is available. Loop-implementer workers are unaffected (already sandbox-pinned via their tool set). The winning diff is always reviewed before it is applied tomain.
Build-worker ids (CYBERINE_BUILD_FLEET) come in two families:
CLI workers (need their CLI installed): opencode-deepseek and opencode-kimi (OpenCode in
build mode on those models — opencode-kimi is DEAD as of 2026-08-25 and self-skips, see the
"OpenCode billing surfaces" recipe below), claude (Claude Code headless), codex (Codex CLI headless),
gemini (Gemini CLI headless), openhands (OpenHands CLI headless, model-agnostic — see the
"OpenHands build worker" recipe below). Each requires its CLI installed AND authorized for auto-approve,
exactly like the existing opencode worker — the
non-interactive flags (--dangerously-skip-permissions / --dangerously-bypass-approvals-and-sandbox
/ --approval-mode yolo / --always-approve) are reduced-safety modes the machine must permit.
Models are tunable via
CYBERINE_CLAUDE_MODEL (default claude-sonnet-4-6), CYBERINE_CODEX_MODEL, CYBERINE_GEMINI_MODEL (default
gemini-2.5-flash), CYBERINE_OPENCODE_KIMI_MODEL (default moonshotai/kimi-k2.7-code),
CYBERINE_OPENHANDS_MODEL (per-run override for the openhands LLM; the operator's persisted
~/.openhands/agent_settings.json is the default source of truth). The claude build
worker streams --output-format stream-json, so its tool activity shows live in the TUI and a
timeout leaves a trace (not a silent blank); pair this with CYBERINE_CLI_TIMEOUT_MS (deprecated
alias OPENCODE_TIMEOUT_MS, default 30 min).
First-activity watchdog for claude workers (kem49). A claude worker (every seat worker, claude-<seat>, included) that has emitted no model activity within CYBERINE_CLI_FIRST_ACTIVITY_MS of spawn (default 300000, 5 min) is killed and reported as CLI agent "claude" stalled: no model activity within <N>ms of spawn (first-activity watchdog, CYBERINE_CLI_FIRST_ACTIVITY_MS), followed by the last api_retry the CLI reported (attempt, HTTP status, error tag) and the head of its stderr. Model activity means an assistant, user, stream_event or result line, or a system/thinking_tokens or system/compact_boundary heartbeat; system/init, hook events, api_retry and rate_limit_event do not count, because a worker that prints only those never reached the model. A worker that did produce activity is never killed by the watchdog; it stays under CYBERINE_CLI_TIMEOUT_MS. Every result that saw activity carries raw.firstActivityAtMs (ms from spawn), and a stalled one carries raw.firstActivityStall: true plus raw.lastApiRetry. In an autopilot fleet run the blocked task's audit line and its workerRuns[].error in run.json now name the worker's own error instead of only the gate verdict.
CYBERINE_CLI_FIRST_ACTIVITY_MS=120000 cyberine run --fleet claude-ft "<task>"
CYBERINE_CLI_FIRST_ACTIVITY_MS=0 cyberine autopilot --engine fleet --dispatch sync
npm run selftest:first-activity-watchdogKnown limitations: the watchdog is wired for the claude factory only; other CLI workers keep the plain CYBERINE_CLI_TIMEOUT_MS behaviour. It is disabled when its value is not below CYBERINE_CLI_TIMEOUT_MS. A stalled worker is not retried automatically and records no cooldown (kyksr owns retry). The 2026-09-24 incident that motivated it (seat workers silent to the 90-minute timeout on claude 2.1.281) is not yet confirmed resolved on 2.1.282 by a live dispatch.
Failed-worker evidence (kmq00). In the autopilot fleet loop, a beam candidate that errored, timed out or produced an empty diff is no longer pruned without a trace. Under .local/runs/<loopId>/<taskId>/ it leaves <candidate>.stdout.log (the worker's raw stream-json, last 2 MB), <candidate>.stderr.log and <candidate>.worktree.txt (git status --porcelain plus git diff HEAD of its worktree), and the worktree itself is kept. The [beam-search] kept evidence for failed candidate ... log line names the cause and every path. At most CYBERINE_KEEP_FAILED_WORKTREES (default 3) worktrees stay per loop, listed in .local/runs/<loopId>/kept-worktrees.json; the oldest goes first. 0 keeps the logs and snapshot but removes every worktree.
CYBERINE_KEEP_FAILED_WORKTREES=5 cyberine autopilot --engine fleet --dispatch sync
npm run selftest:failed-worker-evidenceKnown limitations: kept worktrees outlive the loop until the next loop's bound or a manual git worktree remove; the bound is per loop, not global. Only CLI workers write the stdout/stderr logs; a loop (API) worker leaves the snapshot only. A winner's logs are deleted once it is picked.
Why a fork produced no diff (kr7ea). When no candidate carries a diff, the DiffJudge / BehaviorJudge / SwarmMergeJudge rationale now ends with Per candidate: <agentId>: <cause>; ..., where the cause is one of worker timed out after <N>ms, worker stalled: no model activity within <N>ms of spawn, worker exited code <X> (stderr: <head>), adapter error: <first line>, worker cancelled, diff capture failed: <reason> or worker succeeded but produced an empty diff. The same text lands in the beam-search [beam-search] DiffJudge verdict: log line, in each applyOutcomes[].detail, in the autopilot fleet loop's blocked audit line, and in that task's workerRuns[].error in run.json.
npm run selftest:no-diff-causeKnown limitations: the cause is read from the adapter's own error text, so a CLI adapter that reports a failure only in its output (no error) shows as adapter error: <first output line>.
Run recipe — a DeepSeek-backed Codex worker (T682). Codex can run against DeepSeek's native
Responses API with no Cyberine code change: the adapter passes CYBERINE_CODEX_MODEL through as
-m <model> with no allowlist, and passes no provider override, so the provider comes entirely from
whatever Codex config the process reads. Scope it with CODEX_HOME so the global ~/.codex — shared
with every other Codex client on the machine — is never touched:
export CODEX_HOME="$PWD/.local/tmp/codex-deepseek"
mkdir -p "$CODEX_HOME"
# config.toml: top-level keys FIRST, then [model_providers.deepseek] with
# base_url = "https://api.deepseek.com/", wire_api = "responses", experimental_bearer_token = "<key>"
# Full file + the models.json schema: docs/providers/codex-cli.md
chmod 600 "$CODEX_HOME/config.toml"
CODEX_HOME="$CODEX_HOME" CYBERINE_CODEX_MODEL=deepseek-v4-flash CYBERINE_BUILD_FLEET=codex \
npm run wf -- .local/dogfood/t682-codex-deepseek.workflow.tsNEVER run DeepSeek's own curl-pipe-to-bash setup script. It rewrites ~/.codex/config.toml
globally, flipping every Codex client onto API-key billing and hiding all ChatGPT-subscription
session history. CODEX_HOME achieves the same result with zero global effect — verified 2026-08-25
against codex 0.149.0 (the global config's md5 was unchanged across every probe).
Run recipe — Codex worker token usage (T694). A codex dispatch now records usage. codex exec
ends every run by printing a tokens used label and the count on the NEXT line (stderr, ANSI-dim
wrapped, digits grouped in the operator's locale), and the adapter parses it, so run.json carries
usage.outputTokens for a Codex worker instead of usage: null. Nothing to enable. To see the count
for any run:
jq '.steps[].workerResults[] | select(.agentId=="codex") | .usage' .local/runs/<runId>/run.json
npm run selftest:codex-usage # offline: pins the parse against captured codex stderrKnown limitations and deferred work. Three boundaries around that usage figure:
- It is one blended number, not a split.
codex exec's human output prints onlyblended_total = (input_tokens - cached_input_tokens) + output_tokens; there is no input/output breakdown to read (that exists only in--jsonmode'sturn.completed.usage, which the separatemakeCodexAdapterfactory reads). The whole blended total is therefore reported asoutputTokens— deliberately the conservative direction, sinceestimateCostUsdprices output above input, so a cost derived from a Codex worker over-estimates rather than under-estimates.inputTokensandcostUsdstay absent rather than being invented. - It is the cumulative session total. Harmless today because the adapter cold-spawns a fresh
codex execper dispatch (noresume/--last), so the session total is that dispatch's total. A future warm-session mode for codex would have to diff consecutive readings. - Recorded usage still does not feed the hard spend cap.
WorkflowEngine'srecordRealizedSpend()call is gated onadapter.info.kind === 'loop', so no CLI worker's cost entersCYBERINE_SPEND_CAP_USD's cumulative tally — correct for the defaultcodexworker, which is subscription-billed and has no metered cost to count, but it means a DeepSeek-backed Codex worker's metered spend is still not capped. That is what keeps the DeepSeek-backed Codex recipe a NO-GO as a standing build-fleet worker: the go/no-go turned on metered spend reaching the cap, and only the usage half of that is fixed. Making the cap distinguish a metered CLI worker from a subscription one is the remaining piece.
Also note cyberine run prints a runId but persists no run.json — only npm run wf does — and
in the T682 probe a step's agentIds: ['codex'] did not restrict the fleet to one worker.
Login gating (CLI workers only). Before a CLI worker is spawned the orchestrator runs a fast,
headless login check (src/core/cliAvailability.ts) and only registers it when the CLI is both
installed AND logged in. An installed-but-logged-out CLI is skipped with a note — you can
ignore the login if you don't want that worker. How each is detected:
opencode— two gates, not one (changed 2026-08-25): the BINARY is trusted whenever it's on PATH, but opencode is a multi-provider router, so each opencode worker is additionally gated on whether opencode can authenticate the provider behind ITS OWN pinned model. The check (checkOpencodeProviderCredential) reads the provider's fallback env var, then~/.local/share/opencode/auth.json's provider keys; it fails a worker only when that store is readable and holds nothing for a provider it recognises, and stays fail-open on an unreadable store or an unrecognised prefix (a custom gateway route). See the "OpenCode billing surfaces" recipe below.codex—~/.codex/auth.json(ChatGPT OAuth token or API key) orOPENAI_API_KEY.claude— macOS keychain itemClaude Code-credentials(or~/.claude/.credentials.json), orANTHROPIC_API_KEY.gemini—GEMINI_API_KEY, or~/.gemini/oauth_creds.jsonwith arefresh_token.openhands—OPENHANDS_API_KEYenv, or~/.openhands/agent_settings.jsonwith a non-emptyllm.model/llm.base_url(the LLM the user picked at first-run setup). No subscription-OAuth path — the API key is owned by the chosen provider (DeepSeek/Qwen/Ollama/vLLM/anything OpenAI-compatible), not by OpenHands itself.
No quota pre-check. Login is verified, but remaining usage/quota is NOT — deliberately. No CLI
exposes a quota signal that is both headless and within terms: ~/.claude.json
cachedExtraUsageDisabledReason is overage-credits only (misleading — it does not reflect your
plan's base 5h/7d quota), and the real usage endpoint requires routing the subscription OAuth token
to api.anthropic.com, which Anthropic's terms prohibit for non-native clients. So a logged-in CLI
is considered available; if its quota is actually exhausted, the worker fails at runtime and the
orchestrator degrades to the remaining workers.
Review findings get one fix pass (kd8dj)
The review step of implement-task no longer just reports risk. When its judged review returns any verdict other than approve and lists at least one concern, the implement winner gets exactly one fix pass on its own diff, the review runs once more on the result, and the fixed diff becomes the one that lands. In the 2026-09-23 fleet experiment every one of six runs ended VERDICT: risk with concrete file:line concerns that typecheck and DiffJudge had passed, and the dispatching session fixed all of them by hand; this moves that pass into the run.
Nothing new to type: any run of the workflow gets it, and digest shows both reviews.
cyberine run --bg "add a --limit flag to cyberine tasks list"
cyberine wait <runId> && cyberine digest <runId>
# skeptic: risk (medium), 2 concern(s)
# - src/commands/tasks.ts:88: --limit 0 is accepted and prints nothing
# - src/commands/tasks.ts:102: the help text does not mention --limit
# fix pass: adopted - fix pass by codex adopted; re-review approve (none), 0 concern(s) left
# skeptic after fix: approve (none)
cyberine apply <runId> # lands the fixed diffWhat happens, in order:
- Trigger. The review text is parsed for
VERDICT:,RISK:and theCONCERNS:bullets (markdown bold is tolerated,- nonecounts as no concern).risk, or an improvisedblock/reject, with one or more concerns triggers the pass at anyRISKlevel.approve, or a verdict with no concerns, does not. - Fix pass. A synthetic step
review-fixis added to the run. It gets a fresh worktree offHEADwith the implement winner's diff applied, so the worker starts from its own change, and it is dispatched to that same worker only: a strict one-worker pin, so no failover hop to another worker. The prompt carries the task and the full review text. The worker's diff is captured cumulatively againstHEADand judged by the same chain as the implement step (DiffJudge, orBehaviorJudgewhen averifyCmdis set), then by the TrustGate when it is on. - Adoption. The fixed diff replaces
steps[implement].winnerOutput, socyberine apply,cyberine land,digestand the autonomy loop all land it without a flag. It is NOT adopted, and the original diff stands, when the fix worker fails, the judge finds no winner, the TrustGate rejects it, the run was stopped, or the fixed diff fails the typecheck gate the original passed. A fix that changed nothing is recorded asunchangedand skips the re-review. - Re-review. An adopted fix gets one more synthetic step,
review-2, with the same prompt and reviewers asreview, run on the fixed diff. Whatever it says is recorded and shown, and nothing loops again.
Recorded in run.json:
steps[review].winnerOutputis the first review andsteps[review-2].winnerOutputthe second.steps[review].reviewFixholdsoutcome(running/adopted/unchanged/not-adopted/skipped),detail, the parsedverdict/risk/concernsthat triggered it,implementer,fixStepId,reviewStepIdwhen a re-review ran,preFixDiff(the original implement diff) when adopted, andfixTrustGateVerdictwhen a rejected fix had been gated.events.jsonlcarries aprogressline prefixedreview-fix:at each stage.
cyberine digest --json adds reviews (one { stepId, verdict, risk, concerns } per parsed review, first review first) and reviewFix ({ outcome, detail, implementer }, or null when no pass was considered).
Turn it off for one run or a whole shell with CYBERINE_REVIEW_FIX=off (0 and false work too). A review that would have triggered is then recorded as outcome: skipped with that reason, and the run behaves as before.
A workflow def other than implement-task opts in by putting reviewFix: { implementStep: '<isolate step id>', prompt: '<template, may use {{review}}>' } on its review step. The implement step must be a single-diff isolate step, not swarm chunks.
Verified by npm run selftest:review-fix-pass: stub workers in a throwaway repo, no real dispatch, covering an adopted fix with a re-review, an approving review that runs no pass, a failed fix that leaves the original diff and the run done, a fix that changes nothing, and CYBERINE_REVIEW_FIX=off.
Known limitations and deferred work. One pass only, by ruling: a re-review that still says risk is shown and left for the integrator; there is no second fix loop. The concerns are not checked for being real; the worker is told to leave wrong ones alone and say why, and the re-review is the only check on that. The fix worktree is a fresh checkout of the live HEAD with the diff reapplied, not the implementer's original directory, which the engine removes as soon as a worker finishes; if HEAD moved during the run and the diff no longer applies, the pass fails and the original stands. A fix step or re-review that fails is marked cancelled rather than error so it never fails a run whose implement diff was fine, and the run's N/M step(s) counts still use the def's step count. An implement winner that was a failover hop built on demand is not in the run's fleet list, so the pass is skipped for it with that reason. Only implement-task opts in: swarm-task and the compete/execStrategy review steps keep advisory reviews. The TUI shows the two synthetic steps as ordinary rows and has no dedicated fix-pass view. A crash-resume after the review step does not re-run the pass.
Pin a fleet per step — fleet.json routing.steps (kha9c)
Each implement-task step picks its own fleet: design and review run the strongest enabled
workers (DESIGN_FLEET, see "Strongest enabled models plan, split and judge" below), implement
offers the cheap-class ones. An optional routing.steps block in fleet.json overrides that per
step and is the explicit last word — a pinned step dispatches exactly the ids you name and never
consults the strength split.
Run recipe. Add the block to the fleet.json already in effect (~/.cyberine/fleet.json, or a
project .cyberine/fleet.json), next to providers:
{
"version": 1,
"providers": { "...": "unchanged" },
"routing": {
"steps": {
"design": ["claude-kt", "codex"],
"implement": ["loop-deepseek-build", "loop-commandcode-build"],
"review": ["loop-deepseek-direct"]
}
}
}cyberine fleet doctor # prints the policy + every pinned id that would not resolve
CYBERINE_TASK="Add a clamp helper to src/util.ts" \
npm run wf -- src/engine/implementTaskWorkflow.tsReading the block:
- The three keys are
design,implementandreview— any subset; any other key is an error naming the three. A step the block does not name keeps its built-in fleet. - A value is a list of adapter ids as the engine sees them (
AgentInfo.id), not provider names from theprovidersmap: a seat-bound Claude entry isclaude-<seat>(claude-kt), never bareclaude; a standing key-loop worker isloop-deepseek-direct; an opt-in build worker isloop-deepseek-build.cyberine fleet doctornames any id that does not resolve. - A pinned step dispatches exactly those workers. When none of them was built on this machine the run FAILS, naming the pinned ids and what was built — it never widens to the rest of the fleet, because a widened step would bill workers the operator did not choose. An UNPINNED step keeps the old behaviour (widen to every built adapter, with a WARN) byte for byte.
- When some pinned ids were built and others were not, the step runs the ones that were and logs
[engine] WARN: step "<id>" pins workers [...] that were not built on this machine. - There is no ban on subscription-billed workers any more (khv45, 2026-09-17): an UNPINNED design
step takes the strongest ENABLED workers whatever they bill, and pinning one in
routing.steps.designsimply names it instead of deriving it. Either way the run records a note saying which subscription the design step bills. - The block may live in either layer and follows the same full-replace rule as the rest of the
file: a project
.cyberine/fleet.jsonwholly replaces the global one, so a project file that omitsrouting.stepsdoes NOT inherit the global block (onlyseatsandrouting.seatsare carried across layers). - Both pinned steps and the subscription note land in the run's
notes, sorun.jsonrecords which fleet each step was asked to run.
Extend recipe. The step vocabulary is WORKFLOW_STEP_IDS in src/core/fleetConfig.ts; a new
workflow step becomes pinnable by adding its id there and spreading stepDispatch('<id>', <default>)
into the step in src/engine/implementTaskWorkflow.ts. The workersStrict: true that
stepDispatch sets is what turns selectAdapters' widen-on-empty into the refusal above, and it is
available to any workflow def that wants an exact fleet.
Known Limitations and Deferred Work.
implement-taskonly. Theswarm-taskand research/deep-research workflows carry their own step fleets and do not readrouting.stepsat all; pinning a step here changes nothing about them. Same for the single-stepcyberine runcompare path, which runs no workflow steps.- A model pin is the object entry form — see "Pin the model a step's worker runs" immediately below. A bare worker-id string still means "this worker, on its normal model".
CYBERINE_BUILD_FLEETstill decides what gets BUILT.routing.stepsselects among the adapters that were built for the run; an id no build layer registers cannot run, however it is pinned — it produces the WARN above, or the refusal when it was the only pinned id.- An unknown or disabled id does not invalidate
fleet.json. It is afleet doctorwarning, not a structural error, on purpose: a hard error nulls the whole config, and a null config turns the REPLACE-semantics gate OFF and admits every worker with a key — the opposite of what a pinned step is for. The refusal happens at dispatch instead. - Autopilot honours the pins on both dispatch modes (kenmv).
--dispatch asynchands each task to a backgroundimplement-taskrun, and--dispatch sync(the fleet engine's default) runs the same workflow in-process whenever a step is pinned, refusing the run by name when a pinned step resolves to no built worker. See "routing.stepspins decide the sync dispatch" under the whole-board command. - Ids are not checked against live availability.
fleet doctorvalidates a pinned id against whatfleet.jsondeclares and enables; whether that worker's CLI is logged in or its key is funded iscyberine doctor --liveness's question, answered at build time.
Pin the model a step's worker runs — routing.steps { worker, model } (k7ysm)
Choosing the worker per step is not the whole choice: the same Claude seat should plan on Fable and
need not implement on it. A routing.steps entry may therefore be an OBJECT instead of a bare id,
naming the model that worker runs for that step only. Nothing else changes — the bare-string form
above still means "this worker, on its normal model", and a file written before this existed keeps
behaving identically.
Run recipe. In the fleet.json already in effect, replace any entry with its object form:
{
"version": 1,
"providers": { "...": "unchanged" },
"routing": {
"steps": {
"design": [{ "worker": "claude-kt", "model": "claude-fable-5-1" }, "codex"],
"implement": [{ "worker": "loop-deepseek-build", "model": "flash" }, "command-code"],
"review": ["loop-deepseek-direct"]
}
}
}cyberine fleet doctor # the step-policy line now reads design=claude-kt@claude-fable-5-1+codex
CYBERINE_TASK="Add a clamp helper to src/util.ts" \
npm run wf -- src/engine/implementTaskWorkflow.tsReading the block:
modelmay be an alias from that worker's ownproviders.<name>.modelsmap (flashabove resolves to whatever id the entry gives it) or a raw model id (claude-fable-5-1). The alias is resolved once, at load, sofleet doctor, the run note and the run record all show the real id.- The pin reaches the worker as a per-dispatch value, never as an environment variable: the same worker runs on its normal model in every step that does not pin one, and nothing about the pin survives the step. Two workers in the same step can carry different pins, or none.
- Which workers accept a pin: every key-loop worker (
loop-*-build,loop-*-direct— their adapter picks its model per run), and the CLI workersclaude(and anyclaude-<seat>),codexandcommand-code, whose spawn argv is built per dispatch. Any other CLI worker resolves its model once when its adapter is built, so a pin on it is refused at load with a message naming that worker's run-wide env var instead (muse->CYBERINE_MUSE_MODEL). - Where Cyberine knows the worker's whole model namespace (a Claude seat is Anthropic ids, a
loop-deepseek-*worker is DeepSeek ids), the pin is checked againstcyberine models' catalog and a model outside it is refused by name. Where it does not —command-coderoutes to many providers,codexresolves its own — any id is accepted and the CLI's own refusal is the authority. Escape hatch for a model newer than the committed catalog: declare it underproviders.<name>.modelsand the pin resolves against your entry instead. - Observe it in
run.json: the step'sworkerRuns[].modelshows the pinned model for the pinned step and the worker's normal model elsewhere, and anevents.jsonlprogressline readsstep model pins (fleet.json routing.steps): claude-kt=claude-fable-5-1. That line lists the pins that actually DISPATCHED, so a pin on a worker this machine did not build never appears. workerRuns[].modelrecords the model the CLI actually SERVED, not the one requested (k2a1y): for aclaude-shaped worker this is read off the finalresultevent'smodelUsagemap (the resolved id with the most output tokens, so an internal auxiliary call like a smallclaude-haiku-4-5-20251001routing turn never masquerades as the answer), falling back to thesystem/initevent's model when the result carried nomodelUsage. When the served model differs from the requested one (the pin above, or the worker's normal model with no pin), a secondprogressline is appended alongside the pins line:served model differs from requested: worker claude-kt requested claude-fable-5-1, served claude-sonnet-5. Agreement is the common case and costs no extra line.
Extend recipe. A CLI worker becomes pinnable by reading Task.modelPin where its factory builds
the model flag, then adding its id to STEP_MODEL_PIN_CLI_ADAPTERS in src/core/stepModelPin.ts;
every model-scoped decision that factory makes (argv, cost estimate, rate-limit scope, the reported
model) must read the same per-dispatch value, as makeCommandCodeAdapter does. A worker's curated
catalog is bound in STEP_MODEL_PIN_CATALOGS in the same file — bind one only when the catalog
provider is that worker's ENTIRE model namespace. Any workflow def can use the mechanism directly
via WorkflowStep.workerModels (adapter id -> model id), which is what implementTaskWorkflow
fills from this block.
Known Limitations and Deferred Work.
- Most CLI workers cannot be pinned. Only
claude,codexandcommand-codeare wired;muse,qwen-code,crush,goose,openhands,gemini,aiderand the rest resolve their model at adapter construction (andmusevalidates it against its local catalog there), so they are refused at load rather than half-honoured. Their run-wide env var still works. - A warm-resumed Claude session ignores a pin.
--resumecarries no--model; the session keeps the model it started on. Warm sessions are opt-in and off in the build path, so this only bites an embedder passingwarmSession. - Unlike an unknown worker ID, an invalid model pin is a STRUCTURAL error. An unresolvable id is
a
fleet doctorwarning on purpose (see above); a pin that cannot be delivered nulls the config, which also turns the REPLACE-semantics gate off until it is fixed. The trade is deliberate — a silently-dropped pin would bill a model the operator did not choose — but it means a typo here costs more than a typo in a worker id.cyberine fleet doctorprints the exact message and fix. - The step-wide
WorkflowStep.modelis unchanged. It stays a loop-only hint that CLI workers ignore; a per-worker pin wins over it for the workers it names. implement-taskonly, same as the rest ofrouting.steps:swarm-task, the research workflows and the single-stepcyberine runcompare path do not read this block.- No per-step model for a judge.
routing.judgespicks judge BACKENDS; it takes no model pin.
Strongest enabled models plan, split and judge (khv45)
Enabling a worker in fleet.json is what tells Cyberine it may use that worker for every role — planning, task splitting, reviewing and judging, not only implementing. There is no separate consent field, no acknowledgement prompt and no billing gate: Cyberine cannot decide for you whether a call is worth your money, so it states what it is about to do and leaves the decision where it belongs. To take a worker out of every role, disable it:
cyberine fleet list # which providers are enabled, and what each one bills
cyberine fleet disable <provider> # out of design, review, judging and implementing at once
cyberine fleet enable <provider> # back in, for every role againWith the defaults (no routing.steps, no routing.judges) each role now derives its fleet from the model strength table (docs/providers/model-strength.md, see "Model strength table and fleet.json override" below):
- design / review (
implement-task) and plan (research-task) dispatch the strongest enabled workers — every candidate in the top strength band, widened one band at a time until at least two workers compete, so losing one to a dead credential is not a single point of failure. - implement dispatches the cheap-class workers (cost class 1 in the strength table). A fleet with no cheap-class worker keeps the full build fleet there, exactly as before.
- judging orders the backends by model strength, preferring a vendor no candidate worker runs, and putting an API backend ahead of a CLI-spawn judge at equal strength. An enabled Claude seat is now a default judge candidate rather than a
routing.judges-only opt-in. - A fleet whose enabled workers are all cheap-class behaves exactly as it did before: there is no strong/cheap split to make, so the whole pool plans, reviews and implements.
CYBERINE_TASK="Add a clamp helper to src/util.ts" \
npm run wf -- src/engine/implementTaskWorkflow.ts
# stderr:
# NOTE: implement-task: strength routing: the design, plan and review steps dispatch the strongest enabled workers claude-kt (claude-opus-5, strength 5, cost class 3), loop-deepseek-build (deepseek-v4-pro, strength 4, cost class 1), loop-deepseek-direct (deepseek-v4-pro, strength 4, cost class 1). Implement candidates are the cheap-class workers [loop-deepseek-build, loop-mimo-build].
# NOTE: implement-task: billing: the design, plan and review steps dispatch subscription-billed worker(s) claude-kt (auth-mechanism). Enabling a worker in fleet.json is what makes Cyberine plan, split and judge with it; run `cyberine fleet disable <provider>` to take one out of every role.
jq -r 'select(.kind=="progress") | .detail' .local/runs/<runId>/events.jsonl | grep '^note: '
jq '.judging.backends' .local/runs/<runId>/run.json # [{ id, label, model, subscriptionBilled, billingClass }]Upgrading? This changes what your existing fleet.json bills. Before 2026-09-17 a subscription-billed CLI (claude, codex, cursor-agent, loop-copilot-build) was banned from the design and review steps by an import-time guard, and a Claude seat judged only when routing.judges named it. Both are gone. If your fleet.json enables a Claude seat or codex and you did NOT want it planning or judging, cyberine fleet disable <provider> it, or pin the cheap ids explicitly with routing.steps and routing.judges — those two blocks still override everything.
Extend recipe. The split lives in src/core/strengthFleet.ts: resolveStrengthFleet() ranks every enabled entry with resolveWorkerStrength (model entry > provider entry > shipped table > tier stand-in), returns the thinking list, the cheap list and the notes, and resolves each thinking candidate's billing class through src/core/billingClass.ts for the note text only. CHEAP_COST_CLASS and MIN_THINKING_CANDIDATES are the two dials. Correct a rank you disagree with in fleet.json (providers.<name>.strength, or a per-model strength) rather than in code.
Known Limitations and Deferred Work.
- The pick is not measured. It reads the shipped strength table, not this machine's own win rate;
cyberine eval benchscores workers but nothing feeds that back into the split yet. - No liveness check. A strongest-enabled worker whose CLI is not logged in is simply never built, and
selectAdaptersdegrades the step to whatever was built, with a WARN. Nothing disables the entry for you — a worker that fails at run time with a billing, auth or quota refusal stays enabled until you runcyberine fleet disable <provider>yourself. - The billing class is informational. It appears in the note and in
run.jsonjudging.backends[].billingClass; it never adds or drops a candidate, and aclaudeworker with no login resolves ascreditsfrom a profile default rather than from a live account check. - Cost class 1 is the whole definition of "cheap". A cost-class-2 worker is neither a thinking default nor an implement default unless it lands in the top strength band. Which BAND each step draws from is now per-task (see "A task's cost class picks its step profile" below, kjmft); what is IN each band is still this split alone.
- A seat judge reports its profile model.
claudeJudgeModel()still readsCYBERINE_CLAUDE_MODELor theclaudeprofile's model, so afleet.jsonmodel pinned on the seat entry ranks the design candidate but not the judge. swarm-taskandexecStrategyare untouched. They carry their own hardcoded step fleets and do not consult the strength split.
A task's cost class picks its step profile — fleet.json routing.profiles (kjmft)
The board already records, per task, how much model a task deserves: the model: tier the planner assigned and the effort: it estimated. Until kjmft the fleet path ignored both — every board task ran the same design/implement/review split. Now those two EXISTING fields (no new board field exists, and docs/tasks-manifest.md stays normative) map onto one of exactly two classes, and the class picks which half of the strength split (see "Strongest enabled models plan, split and judge" above) each step draws from.
| Board row | Class | design | implement | review |
|---|---|---|---|---|
model: haiku/sonnet and effort: S/M | cheap | cheap band | cheap band | cheap band |
model: opus/fable | expensive | thinking band | cheap band | thinking band |
effort: L | expensive | thinking band | cheap band | thinking band |
area: or title naming security, concurrency or a migration | expensive | thinking band | cheap band | thinking band |
no model: tier (or -) | expensive | thinking band | cheap band | thinking band |
"Cheap" is a quota-and-strength statement, not a billing one. On a fleet that is mostly subscription seats plus a free lane, the cheap band is the cost-class-1 half of src/core/strengthFleet.ts — the workers that cost little quota and rank low — and the billing class each worker resolves to stays informational and gates nothing, here as everywhere else.
Escalation is deliberately asymmetric: a row must satisfy EVERY cheap condition to run cheap, and any single expensive signal is enough. A board with no tiers at all therefore behaves exactly as it did before this existed, because expensive is also the default when no class reaches the dispatch.
Run recipe. The class is resolved once, where a board task becomes a dispatch (src/launchBg.ts) — so cyberine autopilot, cyberine loop and any --bg dispatch carrying a board task id all get it. Read it back off the run:
jq '.taskProfile' .local/runs/<runId>/run.json
# {
# "costClass": "cheap",
# "reason": "board task k1abc: model tier sonnet + effort M",
# "source": "task",
# "profileSource": "default",
# "steps": [
# { "step": "design", "band": "cheap", "source": "profile", "workers": ["loop-deepseek-build", "loop-mimo-build"] },
# { "step": "implement", "band": "cheap", "source": "profile", "workers": ["loop-deepseek-build", "loop-mimo-build"] },
# { "step": "review", "band": "cheap", "source": "profile", "workers": ["loop-deepseek-build", "loop-mimo-build"] }
# ]
# }
jq -r 'select(.kind=="progress") | .detail' .local/runs/<runId>/events.jsonl | grep 'task cost class'
# note: implement-task: task cost class cheap (board task k1abc: model tier sonnet + effort M) — design=cheap band, implement=cheap band, review=cheap band.To force a class for one dispatch — including a plain npm run wf run, which carries no board task — set it yourself. An already-set value is never overwritten by the board lookup, so this is also how a wrapper script pins the class:
CYBERINE_TASK_COST_CLASS=cheap \
CYBERINE_TASK="Add a clamp helper to src/util.ts" \
npm run wf -- src/engine/implementTaskWorkflow.tsExtend recipe. routing.profiles in fleet.json redefines either profile, per step. It MERGES over the built-in table above, so naming one step moves that step and leaves the rest alone:
{
"version": 1,
"providers": { "...": {} },
"routing": {
"profiles": {
"cheap": { "review": "thinking" }, // cheap tasks still get a strong skeptic
"expensive": { "implement": "thinking" } // and expensive ones implement on the strong band too
}
}
}Only cheap and expensive are valid class keys, only design / implement / review are valid step keys, and only thinking / cheap are valid bands — anything else is a hard fleet.json error naming the exact path (routing.profiles.cheap.design), the same treatment routing.steps gets.
Precedence, tightest first:
routing.steps— a pinned step dispatches exactly those workers and never consults a profile at all (and still fails rather than widening, per its own recipe above).run.json'staskProfile.steps[].sourcereadsrouting.stepsfor such a step, with the band the profile WOULD have picked still recorded next to it.CYBERINE_TASK_COST_CLASS— the operator (or a CLI flag / wrapper that exports it) pinning the class for this dispatch.- The board row's own
model:+effort:. expensive— the standing default, identical to the pre-kjmft behaviour.
Known Limitations and Deferred Work.
- Two classes, by decision. There is no
medium. The operator's ruling is that a strong model already decided cheap-vs-expensive at planning time and Cyberine only honours it; a third class would re-introduce the judgement call this removes. - The class does not change what gets BUILT — but the router now reads it.
CYBERINE_BUILD_FLEET(an explicit--fleet/--cheap, or the router's own pick) still decides which adapters exist for the run, and the profile only chooses among them. Sincekk34xthe ROUTER's own pick is band-aware, so a routed fleet carries at least one worker of every band the class needs instead of falling throughselectAdapters' degrade warning — see "The routed fleet carries every band the cost class needs" undercyberine route. An explicit--fleetpin is still dispatched verbatim, so a pinned fleet that misses a band still degrades. implement-taskonly.swarm-task, the research def andexecStrategycarry their own step fleets and read neitherrouting.profilesnor the class, exactly as they already ignorerouting.steps.- The autonomy loop's sync-verify path carries the class only under pins. Without
routing.stepspins,cyberine autopiloton--dispatch sync(andcyberine loop --verify) dispatches in-process through the beam-search path, which has no design/review step to profile. With pins (kenmv) it runsimplement-taskin-process and classifies the board row exactly as the background dispatch does. - Escalating tags are matched, not curated.
security,concurrencyandmigrationare matched as whole words against the row'sarea:and title (ESCALATING_TAG_REinsrc/core/taskFleetProfile.ts). A task whose risk is real but unnamed stays in whatever class its tier gives it — raise itsmodel:on the board, which is the field that is meant to carry that judgement. - Free Command Code models have daily caps. The cheap band is whatever cost class 1 resolves to on your fleet; if that is a single free lane, a cheap-class run can exhaust it and degrade. Keep a paid cheap-class worker enabled alongside the free one rather than relying on the class to notice.
Billing class per worker — fleet.json providers.<name>.billing (kxsw9)
Every enabled worker resolves to one billing class — subscription, credits or free — plus the
evidence that decided it. The answer is derived from facts, never from a model: no code path asks
an LLM how a worker is billed. src/core/billingClass.ts is the single resolver; the router, the
run note, the autopilot quota preflight, cyberine fleet list and cyberine fleet doctor all read
it. It informs; it blocks nothing.
Evidence order, first match wins:
fleet.jsonoperator answer —providers.<name>.billing.- Auth mechanism in use — the credential the spawned worker would really authenticate with,
after Cyberine's own env scrubbing:
claude: a seat worker (claude-<seat>) uses only its seat OAuth token, sosubscription. A bareclaudewith a claude.ai login issubscriptioneven whenANTHROPIC_API_KEYis set, because the key is scrubbed from the spawn; with no login the key bills, socredits.ANTHROPIC_AUTH_TOKEN(gateway bearer) orCLAUDE_CODE_USE_BEDROCK/_VERTEX/_FOUNDRYmeancredits.codex: a ChatGPT login (auth_mode: chatgptin$CODEX_HOME/auth.json) issubscription,OPENAI_API_KEYincluded (it is scrubbed); an API key with no ChatGPT login, or a top-levelmodel_providerother thanopenaiinconfig.toml, iscredits.muse:META_API_KEYset meanscredits, because it overrides the account login (kd6g0: a 402billing_not_configuredis the unfunded key, not the subscription); the account login alone issubscription.- Command Code (
loop-commandcode-build,loop-commandcode-free-build,command-code): a zero-credit model id (:free/-free, the whole free lane) isfree. A paid model is not decided by the key, see below. - Key-loop workers: the provider key (
DEEPSEEK_API_KEY,XIAOMI_API_KEY, ...) meanscredits; the GitHub Copilot login meanssubscription; local Ollama meansfree.
- Provider billing endpoint — Command Code only,
GET /alpha/billing/creditsfor the key the worker would use. WORKER_PROFILEScostModeldefault —subscription,metered(credits),free-local/free(free). Not trusted forclaude,codexormusewhen no credential is found, because those CLIs can bill either way.- Unknown — treated as
credits, named incyberine fleet doctor.
Command Code decision. One account key draws one credit pool, so the key alone says nothing
about cost. While the plan's included monthly pool has credit left, a paid call draws the
subscription pool: subscription. Once that pool is used up and purchased credits remain, a paid
call draws purchased credits: credits (also credits when neither has credit left). Zero-credit
models are free either way. With no endpoint reading (offline, a 404, a reshaped body), the profile
default subscription applies and the evidence names the missing reading.
Run recipe.
cyberine fleet list # BILLING column (class + source) and a "billing evidence" block per enabled entry
cyberine fleet doctor # names every enabled worker whose billing is unknown, and any fleet.json answer the detected mechanism contradicts
cyberine route --json "fix the flaky test" # workerDetails[].billing carries the resolution; the text output adds "<id> billing:" reasons
cyberine fleet enable <provider> # on a TTY, asks once when the worker cannot be classified
cyberine autopilot --dry-run # prints a "billing" line next to "quota" for the claude session it would driveA dispatch prints the run note through the usual NOTE channel ([engine] NOTE: or NOTE:):
billing: loop-commandcode-build=subscription (endpoint), muse=subscription (auth), claude-kt=subscription (auth)
billing: muse is credits via auth-mechanism -- META_API_KEY present: it overrides the muse account login (oauth), so every run bills the keyThe first line lists every built worker; a second line per worker spells out the evidence whenever
that worker bills credits or contradicts a fleet.json answer.
Extend recipe. Declare the class for a worker Cyberine cannot classify (a plugin worker, a CLI
with no detectable credential) on its provider entry, in either fleet.json layer:
{
"version": 1,
"providers": {
"my-plugin-worker": {
"kind": "cli-agent",
"workerId": "my-plugin-worker",
"enabled": true,
"default": false,
"models": {},
"billing": "credits"
}
}
}billingis optional and must besubscription,creditsorfree; any other value is a structural error that invalidates the file, same as a badkind.- It beats every detected fact, so declare it only when detection is wrong or silent:
fleet doctorwarns when the detected mechanism disagrees with it. cyberine fleet enable <provider>asks only when the resolver lands on unknown. On a TTY it offers the three classes plus skip and writes the answer into the samefleet.jsonit enables the entry in; on a non-TTY (a pipe, CI, an agent) it stores nothing and prints a WARN naming the key to set.- A new detector belongs in
detect()insrc/core/billingClass.ts; evidence strings name mechanisms and presence only, never a key or token value.
No new environment variables.
Known Limitations and Deferred Work.
- Resolved per call, never cached as a class. The class follows whatever credential is in place
at dispatch (a logout or a newly exported key changes it on the next run). The only reused input
is the Command Code credits reading: the router and the run note read the last cached reading up
to 30 minutes old without a request, while
fleet listrefreshes it (at most one GET, 60-second cache, 2.5 s timeout). - Detectors exist for
claude,codex,muse, the Command Code workers and the key-loop workers.gemini,cursor-agent,mmx-text-buildand the other CLIs fall back to their profilecostModel; declarebillingif that default is wrong for your account. - The class does not gate dispatch or routing. It does not refuse a worker, change router
selection or skip the autopilot quota probe; the cheap/expensive split and the enabled-means-consent
note are later work (khv45). Since khz46 it DOES decide the money caps: only class
creditscounts againstCYBERINE_SPEND_CAP_USDand the daily USD cap, andsubscription/freecalls record credits instead — see "Dollars and credits are counted apart". - Seat workers are trusted to be subscription seats. A seat's token file is not opened; its presence and the seat declaration are the evidence.
- Codex profiles are not followed. Only a top-level
model_providerinconfig.tomlis read, not one selected through a[profiles.*]block. fleet enableasks once per invocation. A skipped answer stores nothing, so the nextfleet enableon the same unclassifiable worker asks again.
OpenCode billing surfaces: what died, what still works
Verified 2026-08-25. opencode has two independent billing surfaces and only ONE of them lapsed. Knowing which is which is the difference between "OpenCode is dead, stop using it" (wrong) and a correctly-scoped fleet.
GONE — opencode's own branded credit tier ("opencode-go"). The subscription lapsed and is not being renewed. opencode-go/* is absent from opencode models entirely, and opencode/big-pickle times out. No Cyberine worker routes through it.
STILL WORKING — separate auth per provider, on your own balance. opencode keeps one credential per provider in auth.json, unaffected by the branded tier lapsing. Live-probed 2026-08-25: deepseek/deepseek-v4-flash, xiaomi/mimo-v2.5, minimax/MiniMax-M2.5, minimax/MiniMax-M3 all OK; the six opencode/*-free ids are still free.
opencode-deepseek was never on the branded tier — it is an own-balance DeepSeek route, already labelled costModel: 'metered' — so it kept working straight through and needs no change.
Check what your machine is actually credentialled for:
opencode auth list # provider names only, never key values
cyberine doctor # one [PASS]/[FAIL] line per opencode worker (see below)cyberine doctor's "Build-fleet CLIs" section now reports the binary and each opencode worker separately, because "opencode: installed, logged in" says nothing about whether a given worker's pinned provider can authenticate:
Build-fleet CLIs:
[PASS] opencode: installed (binary-level; per-provider auth below)
[PASS] opencode-deepseek: deepseek/deepseek-v4-pro — provider "deepseek" credentialled (opencode auth.json holds "deepseek")
[FAIL] opencode-kimi: pinned to moonshotai/kimi-k2.7-code, but opencode has NO credential for provider "moonshotai" (fix: ...)(the real [FAIL] line carries a full (fix: ...) hint naming both opencode auth login and the MOONSHOT_API_KEY fallback; elided above for width)
A [FAIL] line sets a non-zero exit code. It is host-dependent by design: the same worker can be healthy on one machine and dead on another, so read it from cyberine doctor on the host that will run the dispatch rather than assuming.
cyberine fleet reads the same check, so its KEY column no longer disagrees with what a dispatch will actually do. Before this it keyed opencode workers off DEEPSEEK_API_KEY / MOONSHOT_API_KEY alone and so reported NO KEY for a route opencode was perfectly authed for through opencode auth login.
opencode-kimi is dead twice over and self-skips. It pins moonshotai/kimi-k2.7-code, and (1) opencode holds no moonshotai credential, (2) the Moonshot account behind MOONSHOT_API_KEY is separately suspended for insufficient balance. It stays registered rather than deleted, and buildAdapters filters it out before spawn — so it costs no fan-out wall-clock and never puts a guaranteed-failed candidate in front of the judges. Revive it by fixing either half:
opencode auth login # add a moonshotai credential, then:
export MOONSHOT_API_KEY=<funded-key> # ...or use the env fallback instead
cyberine doctor # opencode-kimi flips back to [PASS] on its ownTwo model-id traps if you repoint an opencode worker's model:
- ids are CASE-SENSITIVE.
minimax/MiniMax-M2.5works;minimax/minimax-m2.5fails with an opaque error (this cost one false "MiniMax is dead" reading). Only the provider half is ever safe to lowercase. deepseek/deepseek-chatanddeepseek/deepseek-reasonerwere removed from DeepSeek's API on 2026-07-24 but are still listed in opencode's catalog — either id looks like a valid route and errors at call time. Usedeepseek-v4-flash/deepseek-v4-pro.
Regression guard. npm run selftest:opencode-auth-gate locks the whole contract against a synthetic auth.json (no network, no spawn, same result on a fully-authed machine and a bare one): the verdict table including both fail-open cases, the case-sensitivity rule, the single prefix -> env-var map shared with spawn-time env scoping, the absence of a free-via-opencode claim, the router candidate list closing AND reopening, and the removal of the phantom go-limit window.
Known Limitations and Deferred Work
- The gate catches "no credential at all", never "credential to a suspended or out-of-balance account" — no provider exposes that headlessly. A suspended account surfaces as a runtime failure, after which
workerCooldownrecords it andcyberine doctorreports the recorded reason. - The six
opencode/*-freemodels are unproven for build work and no free-tier worker is registered. The lineup has already churned once; prove one with a recorded dispatch (a run whose diff applies and typechecks) before adding a profile. - Fail-open by design in two cases, so they are NOT protected: an unreadable/absent
auth.json, and a provider prefix Cyberine does not recognise (a custom gateway route added toopencode.json). Both keep the worker registered — breaking a working custom route is worse than an over-broad check. opencode-deepseek/opencode-kimistill map to''in the credit ledger, so their spend is invisible tocyberine creditsand never gates a dispatch. Mapping them to a real provider prefix is a spend-enforcement change, not a labelling one, and is deferred.- The retired
go-limitusage window was removed, socyberine billingno longer prints a phantom "1,000,000 remaining" row for the dead tier. If opencode's own tier is ever re-subscribed, re-add the window AND flip the profile'scostModeltosubscriptionin the same change.
Key-only loop-implementers (need ONLY an API key — zero CLI install): loop-deepseek-build
(needs DEEPSEEK_API_KEY), loop-openrouter-build (needs OPENROUTER_API_KEY),
loop-kimi-build (needs MOONSHOT_API_KEY — Moonshot Kimi, OpenAI-compatible; thinking mode is
supported, its reasoning_content is replayed across tool rounds), and loop-mimo-build
(needs XIAOMI_API_KEY — Xiaomi MiMo, OpenAI-compatible; default model mimo-v2.5-pro via
XIAOMI_MODEL). These are raw-API
LoopAgentAdapter workers wired with the sandbox-pinned implementer tool set (read_file,
list_dir, grep_files, write_file, edit_file, run_bash) and a high tool-round budget
(LOOP_MAX_TOOL_ROUNDS, default 25). They write files inside their isolated worktree exactly like a
CLI worker and DiffJudge type-checks the result — but they cannot leave that worktree (every
tool routes through resolveInSandbox), so they are safer than the CLI workers (which run with
full host privileges). If a backing key is unset the worker is skipped with a note. This is the path
for a machine that has API keys but no coding CLI installed.
The loop-worker file tools use bounded ACI-style interactions: read_file returns numbered
100-line windows (start_line scrolls and search finds line numbers within one file), while
grep_files returns matching paths only. write_file, edit_file, and multi_edit parse
JavaScript, TypeScript, and JSON before writing and block changes that introduce syntax errors.
Output-bounding hardening (k8vfg, 2026-09-17). read_file clamps any single returned line
over 2,000 chars (... [+N chars clamped], noted once per window) so one minified/JSON-blob line
cannot blow the whole window's token budget; a bad path returns up to 5 same-basename matches
found elsewhere under the cwd (Did you mean:) instead of a bare ENOENT, so a worker that guessed
the wrong directory gets a usable correction in one round trip. grep_files and glob_files now
report a skipped-count note (... [N more matching file(s) not shown; raise max_results to see them]) instead of silently stopping at max_results, matching list_dir's existing [N more]
behavior. grep_files/glob_files/list_dir recursive walks skip .next, dist, coverage,
and .turbo in addition to node_modules/.git (an explicit glob pattern targeting .local/
is still honored — only the unqualified recursive walk changed). run_bash frames a grep
exit code 1 as [exit code 1 — grep found no matches; this is expected, not an error] rather
than a bare [exit code 1], so a worker does not read "no matches" as "the command failed" and
retry the same search. See .local/research/commandcode/2026-09-17-tool-capability-audit.m3max.md
for the full 37+23 capability audit this was scored against and the measured before/after deltas.
Known limitations and deferred work: the per-line clamp has no matching aggregate byte ceiling
across a whole window (no real trace showed a window with many long lines at once, only isolated
long lines); bash_jobs still has no cursor-based incremental read or wait-for-exit mode (0 real
bash_jobs calls were found in any recorded run to justify the added state); read_file still
takes exactly one path per call (no bulk/glob-in-read_file consolidation — that is an input-shape
change deferred to avoid colliding with the concurrent tool-input-repair task); run_bash output
is not fenced as untrusted (a security hardening item, not a token-efficiency one, out of this
audit's scope).
Local Ollama implementer (keyless, free, private): loop-ollama-build runs the same sandbox-pinned
implementer loop against a local Ollama server's OpenAI-compatible endpoint
(OLLAMA_BASE_URL, default http://localhost:11434/v1). No API key — Ollama ignores the bearer
value — so availability is explicit env OPT-IN: setting OLLAMA_MODEL (or OLLAMA_BASE_URL)
registers both the read-only loop-ollama adapter and the build worker. The model must already be
pulled (ollama pull llama3.1:8b); a missing model surfaces as a clean worker error (HTTP 404). Pick a
tool-capable model (qwen3, llama3.1+, qwen2.5-coder); tiny thinking models tend to reason
instead of calling tools. Thinking traces stream via the compat shim's reasoning delta field —
already handled by the loop adapter. E.g.
OLLAMA_MODEL=llama3.1:8b CYBERINE_BUILD_FLEET="loop-deepseek-build,loop-ollama-build" npm run wf -- <def>.
See docs/providers/ollama.md for the full API reference and gotchas (context length, thinking).
Ollama Cloud implementer (hosted, key-only): loop-ollama-cloud-build runs the same sandbox-pinned
implementer loop against Ollama Cloud's hosted OpenAI-compatible endpoint (OLLAMA_CLOUD_BASE_URL,
default https://ollama.com/v1) — large open models (gpt-oss:120b-cloud) without local hardware.
Unlike the local loop-ollama-build, it requires OLLAMA_CLOUD_API_KEY (https://ollama.com/settings/keys);
setting it OPTS IN both the read-only loop-ollama-cloud compare adapter and the loop-ollama-cloud-build
worker. Override the model with OLLAMA_CLOUD_MODEL. E.g.
CYBERINE_BUILD_FLEET="loop-deepseek-build,loop-ollama-cloud-build" npm run wf -- <def>.
Command Code implementer (subscription credit pool, key-only): loop-commandcode-build runs the sandbox-pinned implementer loop against the Command Code Provider API (CMD_BASE_URL, default https://api.commandcode.ai/provider/v1), which serves Command Code's open-model catalog (DeepSeek, Kimi, GLM, MiniMax, Qwen and others) behind one key. It registers when CMD_API_KEY is set (the same key the cmd CLI stores in ~/.commandcode/auth.json) and, like every build worker, only dispatches when it is enabled in fleet.json or named explicitly. Override the model with CMD_MODEL (default deepseek/deepseek-v4-flash); the live catalog is curl https://api.commandcode.ai/provider/v1/models -H "Authorization: Bearer $CMD_API_KEY". Read docs/providers/command-code.md before changing any of this.
cyberine run --fleet loop-commandcode-build "<task>"
CMD_MODEL='inclusionai/ling-3.0-flash-sante:free' cyberine run --fleet loop-commandcode-build "<task>"- The profile is
costModel: 'subscription': spend draws on the plan's rolling 5-hour and weekly credit windows (shared with thecmdCLI), never on the daily money cap. A model id ending in:freeor-freerecordscostUsd: 0, which matches the account's credit balance (measured unchanged across a free-model run). Paid models record a notionalcostUsdfrom the provider fallback rate, not the credits actually drawn. - An Anthropic id in
CMD_MODEL(claude-*) skips the worker at registration with a note, because Command Code serves those only on/provider/v1/messages(and not at all below the Pro plan) while this worker speaks/chat/completions. - A model the endpoint refuses (
400 ... is not supported on this endpoint,must be called via /provider/v1/messages, orMODEL_NOT_IN_PLAN) is classified asauth-failure: the worker cools down instead of retrying, andcyberine doctorshows why.
Check the credit headroom before a fan-out. cyberine route is the dry run: when CMD_API_KEY is set it makes one GET to /alpha/billing/credits on the CMD_BASE_URL host and prints a Command Code headroom block with the remaining monthly, purchased and free credits, both windows as used / cap with their reset times, and each Command Code worker's verdict. --json carries the same data as commandCodeHeadroom (null when the key is unset).
cyberine route "<task>"
cyberine route --json "<task>" | jq .commandCodeHeadroom
cyberine cooldown
cyberine cooldown clear loop-commandcode-buildCommand Code headroom (GET https://api.commandcode.ai/alpha/billing/credits, read 2026-09-16T19:32:13Z):
credits 69.9037 monthly remaining, 0 purchased, 0 free
fiveHour 0.0963 / 14 credits used, resets 2026-09-16T23:31:06Z (in 3h 59m)
weekly 0.0963 / 35 credits used, resets 2026-09-23T18:31:06Z (in 6d 22h)
loop-commandcode-build (deepseek/deepseek-v4-flash, paid): eligible
command-code (deepseek/deepseek-v4-flash, paid): eligible- Window gate: when the endpoint reports a window exceeded (or
usedat or overcap), a paid-model Command Code worker is dropped from the candidate set until that window'sresetAt, with askipped (never reached the router)note naming the window and the reset. An explicit--fleet loop-commandcode-buildskips it with the same note. A free model id (:free/-free) is exempt and stays eligible, because it draws no credits. - 429 parking: a
429/RATE_LIMITEDrefusal parks the worker until the reset the refusal names (rateLimit.reset, epoch seconds, times 1000), falling back to theresets at <ISO>text in the message, then to the response'sRetry-Afterheader, and then to the standard 30-minute rate-limit cooldown when none is present (a reset in the body always wins overRetry-After). The window comes fromrateLimit.windowor the message text. A refusal scoped to one model (arateLimit.model, or the per-model texts measured on 2026-09-17:You've used all 100 free <model> requests for today,today's limit on <model>,pick another model,Upstream model provider is temporarily unavailable) cools only the model that was requested, as the cooldown key<worker>::<model>, so the worker stays eligible on its other models; only a window refusal or one the parser cannot tie to a model parks the worker id (kg7a3). The OpenAI SDK's own 429 retries are switched off for this worker, and a parked worker returns its park note without sending a request, so an Orchestrator retry or a later workflow step costs nothing. A window refusal also closes that window for every other paid Command Code worker on the same key.cyberine cooldownlists the park andcyberine cooldown clear loop-commandcode-buildlifts the worker's own park (a model cooldown is cleared by its full key,cyberine cooldown clear 'loop-commandcode-build::<model>'); the window closure recorded from the refusal lifts at its reset, or earlier once a fresh credits reading (after the 60 s cache) shows that window open again. - Unknown headroom: a 404, a non-JSON or reshaped body, or no answer within 2.5 s reads as
Command Code headroom: unknown - <reason>, and the worker stays eligible under the normal gates. The reading is cached for 60 s in$CYBERINE_HOME/commandcode-headroom.json, keyed by a hash of host and key, so a fan-out makes one call. No call is made whenCMD_API_KEYis unset.CYBERINE_QUOTA_COOLDOWN=0turns the window gate and parking off along with every other cooldown.
Known Limitations and Deferred Work: there is no compare-fleet (-direct) adapter, only the build worker. The /alpha/billing/credits route is undocumented and can vanish; the gate then degrades to headroom unknown rather than blocking. The gate parks only on a closed window: there is no minimum-headroom threshold, so a dispatch can still start with, say, 0.1 credits left and be refused mid-run (the refusal then parks it). A real 429 has never been provoked live; the parser follows the cmd CLI bundle's handler and was exercised only against a local fake server in both payload shapes. A window refusal, or a 429 the parser cannot tie to one model, is parked by worker id, so switching that worker's CMD_MODEL to a free id while it is parked needs cyberine cooldown clear loop-commandcode-build; a per-model refusal cools only that model, so switching the model is enough. Per-model detection reads rateLimit.model or the refusal text, and a reworded refusal falls back to the worker-wide park. The cmd CLI harness does stretch the same credits further than this loop (the km3ga bake-off solved 3 of 3 tasks at about 0.035 credits each where this loop solved 1 of 3), which is why the command-code CLI worker below exists; the the zero-credit models run as the free lane below (kp1nc).
Command Code free lane (zero credits, key-only): loop-commandcode-free-build is the same sandbox-pinned implementer loop bound to a model the live catalog marks free. It registers when CMD_API_KEY is set and the catalog lists at least one :free / -free id. Nothing names a model in code: the paths that prime Command Code headroom (cyberine route, the router, and orchestrate() or the workflow engine when the build fleet names a Command Code worker) also read GET /provider/v1/models (cached 30 min in $CYBERINE_HOME/commandcode-catalog.json; a failed read is retried after 60 s and the last good catalog stays in use), keep the ids isCommandCodeFreeModel accepts, and bind the first as the model with up to three more as in-run fallbacks. A free id that stops being free drops out on the next catalog read, so the lane keeps running on the rest. A free model that answers 429 or 5xx hands the run to the next free model and cools down for that worker only, so the next build skips it.
cyberine route "summarize what src/core/router.ts does"
cyberine run --fleet loop-commandcode-free-build "<task>"
CMD_FREE_MODEL='inclusionai/ling-3.0-flash-sante:free,poolside/laguna-s-2.1-free' cyberine run --fleet loop-commandcode-free-build "<task>"
cyberine route --json "<task>" | jq .commandCodeHeadroom.freeLane- How it is selected: the profile is
costModel: 'free'withfreeLaneKinds: ['qa', 'code-small']. For aqatask the router keeps its preferred (subscription) pool first and picks the free lane as the single worker only when that pool is empty, before any metered API: the bench graded code, not answers, so questions are not moved off the subscription seats on its evidence. Forcode-smallit races next to the preferred pool, or alone before any metered API when no preferred-pool worker is available. Forcode-largeit is held back and used only when nothing else is available. Afleet.jsonrouting.<kind>entry still wins over all of this. - Closed-window fallthrough: when
loop-commandcode-buildis parked (a closed window, or an earlier refusal) and the free lane resolves, an explicit--fleet loop-commandcode-build/CYBERINE_BUILD_FLEETbuildsloop-commandcode-free-buildin its place with afalling through to loop-commandcode-free-build (<models>)note, and the router's candidate set keeps the free lane while the paid worker is skipped.CMD_FREE_FALLBACK=0turns the substitution off. - Force it: name
loop-commandcode-free-buildin--fleetorCYBERINE_BUILD_FLEET, enable it infleet.json(cyberine fleet init --mergeadds it disabled), or pointfleet.jsonrouting.qaat it.CMD_FREE_MODELsets the preference order as a comma-separated list; an entry that is not a free id, or that the catalog no longer lists, is ignored with a note. - Measured (kp1nc, FLEET-BENCH v1.0.0,
.local/research/commandcode/2026-09-17-free-lane-bench.m3max.md): on 16 small single-module tasks the free models then listed solved 15/16 (meituan/LongCat-2.0:free), 16/16 (poolside/laguna-s-2.1-free) and 14/14 with two unrun (inclusionai/ling-3.0-flash-sante:free) against 16/16 for paiddeepseek/deepseek-v4-flash(0.0378 credits for the batch), and 6/6, 8/8 and 8/8 against 8/8 (0.0110 credits) when the worker had to write the file itself on an 8-task subset; every free batch leftmonthlyCreditsand the 5-hour window unchanged. Mean wall clock per task was 31.9 s, 132.9 s and 13.7 s free against 14.5 s paid. Capacity binds, not quality: 100 free requests a day per model per account (429 RATE_LIMITED, resets 00:00 UTC, seen on LongCat 2.0 after 69 of this measurement's requests plus the day's other use) and upstreamtemporarily unavailable429s on Laguna lasting over ten minutes.
Known Limitations and Deferred Work (free lane): the ranking among free models is the catalog's own order, not a measured preference (CMD_FREE_MODEL is the override). Free models are offered while capacity lasts: poolside/laguna-s-2.1-free answered 429 Upstream model provider is temporarily unavailable for runs of minutes during the measurement, and every free model is capped at 100 free requests a day per account, shared with the cmd CLI and every other session (verified on LongCat 2.0, documented for Ling 3.0 Flash), which one 16-task bench nearly spends. A per-model 429 (the daily free cap, an upstream capacity refusal) cools only that model, until the reset it names, else Retry-After, else 30 minutes, even when it is the last model in the chain: the resolver skips it on the next build, an adapter built earlier skips it in its own chain, and a run is parked without a request only when every model in its chain is cooling (kg7a3). cyberine cooldown lists it as loop-commandcode-free-build::<model>; clear it by that full key. A 429 that names no model and matches no per-model text still parks the whole free worker (cyberine cooldown clear loop-commandcode-free-build lifts it). A paid worker refused in the middle of a run stays parked for the rest of that run; only the next build falls through. The catalog carries no price field, so "free" is the id suffix isCommandCodeFreeModel checks, verified against the credit balance rather than read from the API. The lane is not in CHEAP_BUILD_FLEET (cyberine run --cheap): --cheap pins any task size, and the numbers only cover small single-module work and a daily request cap a multi-file loop task can spend alone (41 to 86 requests per task in km3ga), so --cheap still pins CHEAP_BUILD_FLEET and pointing it at Command Code is left to the operator (kvbc9). Multi-file implementation on a free model is unmeasured.
Command Code CLI worker (subscription credit pool, CLI): command-code runs Command Code's own cmd harness headlessly inside the isolated worktree, so the fleet gets the harness that solved 3 of 3 bake-off tasks (km3ga), not just its models. It needs the command-code binary on PATH (npm i -g command-code, Node 22 or newer; the package also installs cmd, cmdc and commandcode, and the worker spawns the unambiguous command-code name) and a login: ~/.commandcode/auth.json with an apiKey (command-code login), or COMMAND_CODE_API_KEY in the environment. Like every build worker it only dispatches when it is named explicitly or enabled in fleet.json. Read docs/providers/command-code.md before changing any of this.
cyberine run --fleet command-code "<task>"
CYBERINE_COMMANDCODE_MODEL='inclusionai/ling-3.0-flash-sante:free' cyberine run --fleet command-code "<task>"
CYBERINE_COMMANDCODE_MAX_TURNS=100 cyberine run --fleet command-code "<task>"
CYBERINE_BUILD_FLEET="command-code,loop-deepseek-build" npm run wf -- <def>- Invocation:
command-code --print=<prompt> --output-format json --model <CYBERINE_COMMANDCODE_MODEL> --max-turns <CYBERINE_COMMANDCODE_MAX_TURNS> --skip-onboarding --no-auto-update --no-session --no-skills -t --yolo, with the task's worktree as the working directory andCYBERINE_CLI_TIMEOUT_MSas the wall-clock limit. The prompt rides in the same argv entry as the flag because-ptakes an optional value: a prompt passed as the next entry that starts with-(a task written as a bullet list) is read as an option, and the CLI exits 1 withNo query provided(measured 2026-09-17 on the real binary;--print=- say okanswered normally). It never passes-w: Cyberine's worktree is the only isolation, and the engine's diff capture is the only proof of an edit. - Sandbox: print mode refuses every write unless
--yolois passed (adont-askallowlist does not unlock it, measured 2026-09-17), and--yolohas no path confinement, so the host sandbox is the only confinement. On macOS the Seatbelt profile lets the worker read its own~/.commandcodeand write only the state the CLI needs there (projects/,file-history/,sessions/,updates.json,update.lock,telemetry-install-id), neverauth.json,providers.json,mcp.json,mods/,settings.jsonor a per-projectprojects/<slug>/mcp.json. Every other worker is denied reading~/.commandcode. - Output: the last NDJSON line with
type: "result"is parsed.usage.inputTokens/outputTokensmap to the run's tokens andcacheReadTokenstocacheHitTokens/cacheHitRatio;sessionIdandstopReasonare optional. A non-successsubtype, or an exit 0 with no result line, is an error rather than an empty success. - Exit codes:
3(auth) records anauth-failurecooldown and is never retried;8(max turns) and9(no final text) returnpartialwith usage kept, so a diff the worker left can still be judged;5(usage window) parks the worker until the reset the CLI names (and, withCMD_API_KEYset, closes that window for every paid Command Code worker), except a per-model limit (today's limit on <model>), which cools only the worker's current model ascommand-code::<model>, so a run on anotherCYBERINE_COMMANDCODE_MODELstill spawns;4(permission denied: a retired free tier such asmeituan/LongCat-2.0:free, measured 2026-09-19, or a spend cap) is an error naming the model that records the usual cooldown;130(interrupted, also the CLI's answer to SIGTERM, with no result line) is an error with no cooldown;10(out of credits) and6(connection) cool down under the usual kinds. - Cost model:
subscription, the same credit windows asloop-commandcode-build. Every run checks the worker's own refusal park before spawning, so a parked worker returns its park note without starting the CLI. WithCMD_API_KEYset it also reads the credit windows (the account behind that key), and a closed paid window drops the worker from the candidate set; a:free/-freemodel is exempt from the window gate and recordscostUsd: 0.cyberine cooldown clear command-codelifts its own park, andcyberine cooldown clear 'command-code::<model>'a model cooldown.
Known Limitations and Deferred Work: print mode only works with --yolo, so the host sandbox is the whole confinement: HOST_SANDBOX_MODE=none (or a trusted task on a host with no sandbox) runs it with full host access. The narrow ~/.commandcode write grant and the projects/<slug>/mcp.json carve-out are Seatbelt rules. The bubblewrap, container (docker, podman, gvisor) and Windows backends cannot keep a per-project mcp.json read-only inside a writable projects/, so the worker refuses to run under them (kg7a3): it is dropped from the candidate set with a "command-code" unavailable - host sandbox "<mode>" cannot keep ... note, and a direct dispatch fails in its setup hook before any request or spawn. It therefore runs only under Seatbelt (macOS), or unsandboxed on a trusted task with no backend (HOST_SANDBOX_MODE=none), the full-host-access opt-in every CLI worker shares. A CLI installed under ~/.nvm would not be visible inside bubblewrap either, so this worker is verified on macOS only. The headless output has no cost field: a paid run records a notional costUsd from the model price table, not the credits drawn, and the real draw is only visible through cyberine route. The headroom gate reads the account behind CMD_API_KEY and assumes it is the same account command-code is logged into. cyberine doctor --liveness reports the row as funding-unknown. Document conversion inside the CLI (its ~/.commandcode/cache npm installs) is not writable in the sandbox. The worker passes --no-skills, so skills under ~/.agents/skills, ~/.commandcode/skills and the target repo's .commandcode/skills / .agents/skills are not loaded (the same isolation from operator config the Claude worker gets from --safe-mode); the CLI has no switch for the operator's MCP servers or mods. Those were MEASURED loading on 2026-09-21 (v1.58.0, canary MCP server and mod in a throwaway HOME, worker flags, free model) and are now closed for SANDBOXED runs: the generated Seatbelt profile denies READ on mcp.json and mods/ inside every known CLI config dir, after the own-config re-allow, so auth.json, sessions and settings stay readable and the run still succeeds. The positive control -- the same profile with those two rules removed -- loaded both, so the deny is what closes it. Known limitation: an UNSANDBOXED run (HOST_SANDBOX_MODE=none, or a trusted task on a host with no backend) still starts the operator's MCP servers and imports their mods into the worker's process, and no smoke guards the profile shape yet -- both are kj3g0 follow-ups. The CLI reads AGENTS.md but never CLAUDE.md (canary-verified 2026-09-19). Since kx3gg the adapter closes that gap: a CLI worker is told which instruction filenames it reads for itself, and every OTHER project instruction file discovery finds is prepended to its prompt exactly once -- so a CLAUDE.md-only repo now reaches codex and command-code as well. claude and muse inject nothing, the first because it reads both files and the second because its own AGENTS.md-then-CLAUDE.md fallback is the same first-match-wins rule discovery applies. The GLOBAL tier is never injected. The per-worker coverage table and its evidence are in docs/providers/cli-agents.md. The adapter path was verified offline (a stub binary for every exit code, and the real binary under the generated profile with the network denied); on 2026-09-19 the real binary also answered a free-model prompt under the generated profile with the network allowed, but a full live dispatch through cyberine run has not run yet.
Gateway-backed loop-implementers (OpenAI / Anthropic via the LiteLLM gateway): loop-claude-build
(routes LOOP_MODEL_A, default anthropic/claude) and loop-gpt-build (routes LOOP_MODEL_B,
default openai/gpt). Same sandbox-pinned LoopAgentAdapter implementer as the key-only family, but
the base URL/key point at the gateway (GATEWAY_BASE_URL / GATEWAY_API_KEY) so a single
OpenAI-compatible loop reaches Claude and GPT without any provider-specific code — the gateway
translates to the native APIs. They register only when the gateway is configured (any
GATEWAY_* / LOOP_MODEL_* env var set; a live local gateway at the documented defaults counts) and
need docker compose up -d running. Strong tier, cost class 3 — reserve them for hard/critical tasks
and race them against the cheap DeepSeek worker, e.g.
CYBERINE_BUILD_FLEET="loop-deepseek-build,loop-claude-build,loop-gpt-build".
Native Anthropic implementer (key-only, no gateway, no CLI): loop-claude-native-build talks
Claude's NATIVE Messages API directly via @anthropic-ai/sdk (the AnthropicAgentAdapter). It is the
zero-install, gateway-free path to Claude — needs only ANTHROPIC_API_KEY, no LiteLLM/Docker and no
claude CLI — and keeps full native features (prompt caching, structured outputs, thinking output)
that the OpenAI-compat shim drops. Same sandbox-pinned implementer toolset as the other loop-*-build
workers. Model via ANTHROPIC_MODEL (default claude-sonnet-4-6), base via ANTHROPIC_BASE_URL. Use
this to put Claude in the writing fleet on a machine that has an Anthropic key but no Docker and no
coding CLI, e.g. CYBERINE_BUILD_FLEET="loop-deepseek-build,loop-claude-native-build".
The adapter always guards against Claude Fable 5 / Mythos 5 refusals: a request declined by the
safety classifiers comes back as a normal HTTP 200 with stop_reason: "refusal" (empty or partial
content), and the adapter checks for it before reading content — so a refusal yields a clean
Result (status: 'partial', no crash) instead of ever indexing into empty content. Set
CYBERINE_ANTHROPIC_FABLE_FALLBACK=1 (only meaningful when ANTHROPIC_MODEL is claude-fable-5 /
claude-mythos-5) to have a refused request transparently re-served by claude-opus-4-8 instead.
OpenHands build worker (model-agnostic CLI, headless): openhands wraps the All-Hands AI
OpenHands CLI as a one-shot, headless, model-agnostic coding worker. OpenHands itself does not
ship a model — the operator picks the LLM at first-run setup
(uv tool install openhands then openhands to configure the LLM, e.g. DeepSeek / Qwen / Ollama
/ vLLM / anything OpenAI-compatible) and the choice is persisted to
~/.openhands/agent_settings.json. The Cyberine adapter reuses that config; no per-invocation
--model flag is required. Invocation shape (mirrors codex headless):
openhands --headless --always-approve --json -t "<prompt>". --always-approve is MANDATORY for
non-interactive operation (otherwise the first action prompt stalls the subprocess). The adapter
is wrapped in the host sandbox (Seatbelt / bubblewrap / docker) exactly like the other CLI
workers; cliConfigDirs() allow-lists ~/.openhands/ so the subprocess can read
agent_settings.json and write back any LLM tweaks. Per-run override:
CYBERINE_OPENHANDS_MODEL=<model> (injected as LLM_MODEL into the child env). Login-gated by
src/core/cliAvailability.ts::checkOpenHands — usable when OPENHANDS_API_KEY is set OR
~/.openhands/agent_settings.json has a non-empty llm.model / llm.base_url. Recipe:
# 1) install + pick the LLM at first-run (one-time)
uv tool install openhands
openhands # walks you through the LLM setup
# 2) put openhands in the build fleet alongside another writer
CYBERINE_BUILD_FLEET="loop-deepseek-build,openhands" \
npm run wf -- src/engine/implementTaskWorkflow.ts
# 3) (optional) override the LLM for a single run
CYBERINE_OPENHANDS_MODEL="deepseek/deepseek-v4-flash" \
CYBERINE_BUILD_FLEET="openhands" npm run wf -- src/engine/implementTaskWorkflow.tsLeave CYBERINE_BUILD_FLEET empty (the default) and ordinary runs never spin up writing agents — instead
the startup notes report what this machine can implement with (CLIs on PATH, the key-only loop
worker, or nothing yet) and suggest the right CYBERINE_BUILD_FLEET value.
Steering a running loop worker (between tool rounds)
control.json carries one of two actions — stop (above) or steer. A steer message injects free-text operator guidance into a running worker between tool rounds for loop workers, or at the next step...
Which verdict wins a step (k0fxw)
Every judge in a step's panel returns a verdict; the step records ONE of them as verdict. ConsensusJudge wins only when a real majority of candidates produced the same answer (agreement above 0.5,...