cyberine
UsageRun recipes

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 whatever fleet.json enables, 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 fresh git worktree --detach off HEAD as its cwd, implements there, and its git diff is captured. DiffJudge scores each diff by applies cleanly + npm run typecheck passes; when 2+ diffs both pass, an LlmJudge tie-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 in npm run tui.

  • Binary files survive the diff (k112c). Every worktree diff capture (the implement step above, SwarmMergeJudge's per-chunk and merged diffs, swarmMergeHelpers's shared capture used by LaneMergeJudge, and a swarm lane's cumulative diff) runs git diff --cached --binary --full-index, not plain git diff --cached. A worker that adds or edits a PNG, font or other binary file gets a GIT binary patch hunk instead of an unapplyable Binary files ... differ summary, so git 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 by npm run selftest:binary-diff-capture.

  • Known limitation (typecheck oracle): DiffJudge detects the oracle from the candidate worktree, not from the caller. A repo with no package.json, or one whose package.json has no typecheck script, skips gate 2 and scores a cleanly-applied diff 2/2 with the detail no typecheck oracle in this repo (...); the implementer prompt drops its npm run typecheck clause 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 before cyberine apply. Guarded by npm run selftest:diffjudge-oracle. On the autonomy loop's sync-verify path a skipped gate 2 is never reported as done, and a repo can declare its own oracle with verify.command; see "Apply-only verification is never done" (keeb9).

  • Verdict rationale shapes: DiffJudge's rationale distinguishes "nobody tried" from "everybody tried and failed" -- reading only winner === null cannot tell them apart. No candidate carries a diff — nothing to apply or verify. means no result carried a raw.diff at all (nothing to grade). N candidates carried a diff, 0 applied: <agentId>: <detail>; <agentId>: <detail> means at least one candidate had a diff but git apply rejected 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 an applyOutcomes array ({ agentId, hadDiff, applied, detail? } per submitted result, an extra property alongside the documented Verdict fields) with the full per-agent picture, including agents that never carried a diff at all -- read it from run.json's steps[].verdict.applyOutcomes when 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 apply refuses it with cannot apply binary patch to '<path>' without full index line) and is reported by name, not worked around -- DiffJudge never 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 captured git diff); every capture site Cyberine controls now emits --binary --full-index (see "Binary files survive the diff" above). Guarded by npm run selftest:diffjudge-apply-reason.

  • Read-only steps never write to the live repo (k3pjg). A step without isolate: true (the design and review steps here, the review step of swarm-task, the mine step of auto-evolve, a cyberine run --bg compare 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: a command-code worker created PROBE-COMMANDCODE-CLI.md in the live repo during design. Now each worker that CAN write gets its own git worktree --detach off HEAD as 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 every loop-*-build worker. Its changes are never captured or applied; when there are any, the run records a progress event 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-*-direct on 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 with r in the TUI (a restart-worker control 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 --bg row) 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 sees HEAD.
    • 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 progress event says so.
    • A worktree left behind by a killed engine process is not reaped; remove it with git worktree remove --force <dir> or git worktree prune, the same as for isolate steps.
    • Not covered: an architectEditor step (its editor writes the live tree by design; use isolate: true when it must not), imperative script workflows (WorkflowScriptRuntime dispatches in the script's cwd), and the foreground orchestrate() compare path (cyberine run --image, or a build fleet outside git), which runs no workflow steps.
    • A custom Agent that is neither kind: 'cli' nor sets info.mayWriteWorkingTree is treated as read-only.

Security: the per-worker worktree isolates the workers from each other and keeps main clean — 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 via HOST_SANDBOX_MODE:

BackendMode valuePlatformMechanism
macOS SeatbeltseatbeltmacOSsandbox-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-only
gVisorgvisorLinuxdocker run --runtime=runsc — Go-based userspace kernel; strongest isolation
bubblewrapbubblewrapLinuxbwrap namespace container; only /usr,/bin,/sbin,/lib*,/etc,/opt,/var + ~/.gitconfig bound read-only (NOT the whole rootfs — a full --ro-bind / / let a worker read ~/.ssh/~/.aws/sibling repos' .env by absolute path even though writes were confined), plus the worktree/tmp/CLI-config-dir writables
Dockerdockercross-platformdocker run --read-only --cap-drop ALL; worktree bind-mounted
Podmanpodmancross-platformrootless podman run; same flags as Docker backend
WindowswindowsWindowsLow-Integrity job object via cyberine-sandbox helper

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 /tmp and the CLI's own config dir like ~/.claude), so a CLI worker cannot mutate the host repo or $HOME. Control it with HOST_SANDBOX_MODE and HOST_SANDBOX_NETWORK (allow by 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. Set HOST_SANDBOX_NETWORK=deny for strict isolation on network-free tasks; buildDefaultAdapters() surfaces a note when a sandbox is active and you have opted into deny). 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 from cyberine run --bg / chat /run (launchBg.ts) and the MCP server (mcp.ts), not only the in-process TUI/autonomy-loop paths. Set CYBERINE_TASK_TRUSTED=false to 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 to main.

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-watchdog

Known 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-evidence

Known 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-cause

Known 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.ts

NEVER 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 stderr

Known limitations and deferred work. Three boundaries around that usage figure:

  • It is one blended number, not a split. codex exec's human output prints only blended_total = (input_tokens - cached_input_tokens) + output_tokens; there is no input/output breakdown to read (that exists only in --json mode's turn.completed.usage, which the separate makeCodexAdapter factory reads). The whole blended total is therefore reported as outputTokens — deliberately the conservative direction, since estimateCostUsd prices output above input, so a cost derived from a Codex worker over-estimates rather than under-estimates. inputTokens and costUsd stay absent rather than being invented.
  • It is the cumulative session total. Harmless today because the adapter cold-spawns a fresh codex exec per dispatch (no resume/--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's recordRealizedSpend() call is gated on adapter.info.kind === 'loop', so no CLI worker's cost enters CYBERINE_SPEND_CAP_USD's cumulative tally — correct for the default codex worker, 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) or OPENAI_API_KEY.
  • claude — macOS keychain item Claude Code-credentials (or ~/.claude/.credentials.json), or ANTHROPIC_API_KEY.
  • gemini — GEMINI_API_KEY, or ~/.gemini/oauth_creds.json with a refresh_token.
  • openhands — OPENHANDS_API_KEY env, or ~/.openhands/agent_settings.json with a non-empty llm.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 diff

What happens, in order:

  1. Trigger. The review text is parsed for VERDICT:, RISK: and the CONCERNS: bullets (markdown bold is tolerated, - none counts as no concern). risk, or an improvised block / reject, with one or more concerns triggers the pass at any RISK level. approve, or a verdict with no concerns, does not.
  2. Fix pass. A synthetic step review-fix is added to the run. It gets a fresh worktree off HEAD with 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 against HEAD and judged by the same chain as the implement step (DiffJudge, or BehaviorJudge when a verifyCmd is set), then by the TrustGate when it is on.
  3. Adoption. The fixed diff replaces steps[implement].winnerOutput, so cyberine apply, cyberine land, digest and 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 as unchanged and skips the re-review.
  4. Re-review. An adopted fix gets one more synthetic step, review-2, with the same prompt and reviewers as review, run on the fixed diff. Whatever it says is recorded and shown, and nothing loops again.

Recorded in run.json:

  • steps[review].winnerOutput is the first review and steps[review-2].winnerOutput the second.
  • steps[review].reviewFix holds outcome (running / adopted / unchanged / not-adopted / skipped), detail, the parsed verdict / risk / concerns that triggered it, implementer, fixStepId, reviewStepId when a re-review ran, preFixDiff (the original implement diff) when adopted, and fixTrustGateVerdict when a rejected fix had been gated.
  • events.jsonl carries a progress line prefixed review-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.ts

Reading the block:

  • The three keys are design, implement and review — 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 the providers map: a seat-bound Claude entry is claude-<seat> (claude-kt), never bare claude; a standing key-loop worker is loop-deepseek-direct; an opt-in build worker is loop-deepseek-build. cyberine fleet doctor names 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.design simply 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.json wholly replaces the global one, so a project file that omits routing.steps does NOT inherit the global block (only seats and routing.seats are carried across layers).
  • Both pinned steps and the subscription note land in the run's notes, so run.json records 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-task only. The swarm-task and research/deep-research workflows carry their own step fleets and do not read routing.steps at all; pinning a step here changes nothing about them. Same for the single-step cyberine run compare 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_FLEET still decides what gets BUILT. routing.steps selects 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 a fleet doctor warning, 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 async hands each task to a background implement-task run, 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.steps pins decide the sync dispatch" under the whole-board command.
  • Ids are not checked against live availability. fleet doctor validates a pinned id against what fleet.json declares and enables; whether that worker's CLI is logged in or its key is funded is cyberine 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.ts

Reading the block:

  • model may be an alias from that worker's own providers.<name>.models map (flash above resolves to whatever id the entry gives it) or a raw model id (claude-fable-5-1). The alias is resolved once, at load, so fleet 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 workers claude (and any claude-<seat>), codex and command-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 against cyberine models' catalog and a model outside it is refused by name. Where it does not — command-code routes to many providers, codex resolves 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 under providers.<name>.models and the pin resolves against your entry instead.
  • Observe it in run.json: the step's workerRuns[].model shows the pinned model for the pinned step and the worker's normal model elsewhere, and an events.jsonl progress line reads step 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[].model records the model the CLI actually SERVED, not the one requested (k2a1y): for a claude-shaped worker this is read off the final result event's modelUsage map (the resolved id with the most output tokens, so an internal auxiliary call like a small claude-haiku-4-5-20251001 routing turn never masquerades as the answer), falling back to the system/init event's model when the result carried no modelUsage. When the served model differs from the requested one (the pin above, or the worker's normal model with no pin), a second progress line 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, codex and command-code are wired; muse, qwen-code, crush, goose, openhands, gemini, aider and the rest resolve their model at adapter construction (and muse validates 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. --resume carries 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 passing warmSession.
  • Unlike an unknown worker ID, an invalid model pin is a STRUCTURAL error. An unresolvable id is a fleet doctor warning 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 doctor prints the exact message and fix.
  • The step-wide WorkflowStep.model is unchanged. It stays a loop-only hint that CLI workers ignore; a per-worker pin wins over it for the workers it names.
  • implement-task only, same as the rest of routing.steps: swarm-task, the research workflows and the single-step cyberine run compare path do not read this block.
  • No per-step model for a judge. routing.judges picks 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 again

With 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 bench scores 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 selectAdapters degrades 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 run cyberine fleet disable <provider> yourself.
  • The billing class is informational. It appears in the note and in run.json judging.backends[].billingClass; it never adds or drops a candidate, and a claude worker with no login resolves as credits from 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 reads CYBERINE_CLAUDE_MODEL or the claude profile's model, so a fleet.json model pinned on the seat entry ranks the design candidate but not the judge.
  • swarm-task and execStrategy are 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 rowClassdesignimplementreview
model: haiku/sonnet and effort: S/Mcheapcheap bandcheap bandcheap band
model: opus/fableexpensivethinking bandcheap bandthinking band
effort: Lexpensivethinking bandcheap bandthinking band
area: or title naming security, concurrency or a migrationexpensivethinking bandcheap bandthinking band
no model: tier (or -)expensivethinking bandcheap bandthinking 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.ts

Extend 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:

  1. 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's taskProfile.steps[].source reads routing.steps for such a step, with the band the profile WOULD have picked still recorded next to it.
  2. CYBERINE_TASK_COST_CLASS — the operator (or a CLI flag / wrapper that exports it) pinning the class for this dispatch.
  3. The board row's own model: + effort:.
  4. 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. Since kk34x the 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 through selectAdapters' degrade warning — see "The routed fleet carries every band the cost class needs" under cyberine route. An explicit --fleet pin is still dispatched verbatim, so a pinned fleet that misses a band still degrades.
  • implement-task only. swarm-task, the research def and execStrategy carry their own step fleets and read neither routing.profiles nor the class, exactly as they already ignore routing.steps.
  • The autonomy loop's sync-verify path carries the class only under pins. Without routing.steps pins, cyberine autopilot on --dispatch sync (and cyberine loop --verify) dispatches in-process through the beam-search path, which has no design/review step to profile. With pins (kenmv) it runs implement-task in-process and classifies the board row exactly as the background dispatch does.
  • Escalating tags are matched, not curated. security, concurrency and migration are matched as whole words against the row's area: and title (ESCALATING_TAG_RE in src/core/taskFleetProfile.ts). A task whose risk is real but unnamed stays in whatever class its tier gives it — raise its model: 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:

  1. fleet.json operator answer — providers.<name>.billing.
  2. 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, so subscription. A bare claude with a claude.ai login is subscription even when ANTHROPIC_API_KEY is set, because the key is scrubbed from the spawn; with no login the key bills, so credits. ANTHROPIC_AUTH_TOKEN (gateway bearer) or CLAUDE_CODE_USE_BEDROCK / _VERTEX / _FOUNDRY mean credits.
    • codex: a ChatGPT login (auth_mode: chatgpt in $CODEX_HOME/auth.json) is subscription, OPENAI_API_KEY included (it is scrubbed); an API key with no ChatGPT login, or a top-level model_provider other than openai in config.toml, is credits.
    • muse: META_API_KEY set means credits, because it overrides the account login (kd6g0: a 402 billing_not_configured is the unfunded key, not the subscription); the account login alone is subscription.
    • Command Code (loop-commandcode-build, loop-commandcode-free-build, command-code): a zero-credit model id (:free / -free, the whole free lane) is free. A paid model is not decided by the key, see below.
    • Key-loop workers: the provider key (DEEPSEEK_API_KEY, XIAOMI_API_KEY, ...) means credits; the GitHub Copilot login means subscription; local Ollama means free.
  3. Provider billing endpoint — Command Code only, GET /alpha/billing/credits for the key the worker would use.
  4. WORKER_PROFILES costModel default — subscription, metered (credits), free-local / free (free). Not trusted for claude, codex or muse when no credential is found, because those CLIs can bill either way.
  5. Unknown — treated as credits, named in cyberine 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 drive

A 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 key

The 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"
    }
  }
}
  • billing is optional and must be subscription, credits or free; any other value is a structural error that invalidates the file, same as a bad kind.
  • It beats every detected fact, so declare it only when detection is wrong or silent: fleet doctor warns 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 same fleet.json it 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() in src/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 list refreshes 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-build and the other CLIs fall back to their profile costModel; declare billing if 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 credits counts against CYBERINE_SPEND_CAP_USD and the daily USD cap, and subscription / free calls 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_provider in config.toml is read, not one selected through a [profiles.*] block.
  • fleet enable asks once per invocation. A skipped answer stores nothing, so the next fleet enable on 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 own

Two model-id traps if you repoint an opencode worker's model:

  • ids are CASE-SENSITIVE. minimax/MiniMax-M2.5 works; minimax/minimax-m2.5 fails with an opaque error (this cost one false "MiniMax is dead" reading). Only the provider half is ever safe to lowercase.
  • deepseek/deepseek-chat and deepseek/deepseek-reasoner were 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. Use deepseek-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 workerCooldown records it and cyberine doctor reports the recorded reason.
  • The six opencode/*-free models 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 to opencode.json). Both keep the worker registered — breaking a working custom route is worse than an over-broad check.
  • opencode-deepseek / opencode-kimi still map to '' in the credit ledger, so their spend is invisible to cyberine credits and 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-limit usage window was removed, so cyberine billing no 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's costModel to subscription in 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 the cmd CLI), never on the daily money cap. A model id ending in :free or -free records costUsd: 0, which matches the account's credit balance (measured unchanged across a free-model run). Paid models record a notional costUsd from 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, or MODEL_NOT_IN_PLAN) is classified as auth-failure: the worker cools down instead of retrying, and cyberine doctor shows 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-build
Command 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 used at or over cap), a paid-model Command Code worker is dropped from the candidate set until that window's resetAt, with a skipped (never reached the router) note naming the window and the reset. An explicit --fleet loop-commandcode-build skips 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_LIMITED refusal parks the worker until the reset the refusal names (rateLimit.reset, epoch seconds, times 1000), falling back to the resets at <ISO> text in the message, then to the response's Retry-After header, and then to the standard 30-minute rate-limit cooldown when none is present (a reset in the body always wins over Retry-After). The window comes from rateLimit.window or the message text. A refusal scoped to one model (a rateLimit.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 cooldown lists the park and cyberine cooldown clear loop-commandcode-build lifts 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 when CMD_API_KEY is unset. CYBERINE_QUOTA_COOLDOWN=0 turns 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' with freeLaneKinds: ['qa', 'code-small']. For a qa task 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. For code-small it races next to the preferred pool, or alone before any metered API when no preferred-pool worker is available. For code-large it is held back and used only when nothing else is available. A fleet.json routing.<kind> entry still wins over all of this.
  • Closed-window fallthrough: when loop-commandcode-build is parked (a closed window, or an earlier refusal) and the free lane resolves, an explicit --fleet loop-commandcode-build / CYBERINE_BUILD_FLEET builds loop-commandcode-free-build in its place with a falling 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=0 turns the substitution off.
  • Force it: name loop-commandcode-free-build in --fleet or CYBERINE_BUILD_FLEET, enable it in fleet.json (cyberine fleet init --merge adds it disabled), or point fleet.json routing.qa at it. CMD_FREE_MODEL sets 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 paid deepseek/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 left monthlyCredits and 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 upstream temporarily unavailable 429s 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 and CYBERINE_CLI_TIMEOUT_MS as the wall-clock limit. The prompt rides in the same argv entry as the flag because -p takes 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 with No query provided (measured 2026-09-17 on the real binary; --print=- say ok answered 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 --yolo is passed (a dont-ask allowlist does not unlock it, measured 2026-09-17), and --yolo has no path confinement, so the host sandbox is the only confinement. On macOS the Seatbelt profile lets the worker read its own ~/.commandcode and write only the state the CLI needs there (projects/, file-history/, sessions/, updates.json, update.lock, telemetry-install-id), never auth.json, providers.json, mcp.json, mods/, settings.json or a per-project projects/<slug>/mcp.json. Every other worker is denied reading ~/.commandcode.
  • Output: the last NDJSON line with type: "result" is parsed. usage.inputTokens / outputTokens map to the run's tokens and cacheReadTokens to cacheHitTokens / cacheHitRatio; sessionId and stopReason are optional. A non-success subtype, or an exit 0 with no result line, is an error rather than an empty success.
  • Exit codes: 3 (auth) records an auth-failure cooldown and is never retried; 8 (max turns) and 9 (no final text) return partial with 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, with CMD_API_KEY set, 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 as command-code::<model>, so a run on another CYBERINE_COMMANDCODE_MODEL still spawns; 4 (permission denied: a retired free tier such as meituan/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) and 6 (connection) cool down under the usual kinds.
  • Cost model: subscription, the same credit windows as loop-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. With CMD_API_KEY set 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 / -free model is exempt from the window gate and records costUsd: 0. cyberine cooldown clear command-code lifts its own park, and cyberine 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.ts

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