Fleet Config — Design Draft
Status: SHIPPED (2026-09-22) — see
src/core/fleetConfig.ts(loadFleetConfig,resolveRoutingForKind) andsrc/commands/fleet.ts(cmdFleet, the livefleetcommand insrc/core/commandRegistry.ts). Author decisions captured from interview 2026-06-25. Goal: a single user-editable file that declares WHICH providers/models are active and WHICH model handles WHICH kind of task — externalizing the "smart model routing" that today is hardcoded acrossworkerProfiles.ts+router.ts+modelCatalog.ts.
1. Decisions locked (from interview)
| # | Decision | Chosen | Consequence for this design |
|---|---|---|---|
| 1 | File location/format | Separate fleet.json, nested JSON | New loader, NOT bolted onto the flat dotted-key config.json. |
| 2 | Semantics | Replace (source of truth) | Provider not in fleet.json = not used, even with a valid key. Requires a fleet init scaffolder + fleet doctor drift-check (see sections 6, 7). |
| 3 | Smart routing | Map by 3 task-kind (qa / code-small / code-large) | Reuses the exact vocab the router already classifies into (router.ts:692-705). No new taxonomy. |
| 4 | CLI surface | fleet list/status, fleet enable/disable, fleet set-model, fleet doctor/edit (+ fleet init, implied by replace) | A new cyberine fleet subcommand group. |
2. Core principle: policy file, NOT a secrets file
fleet.json declares policy (who is on, which model for which task). It never holds API keys or secrets. Keys stay exactly where they are today:
~/.cyberine/.env(written bycyberine auth login) and<repo>/.env- provider sections in
config.json(deepseek.apiKey, etc.)
This keeps the secret/policy split clean and lets fleet.json be committed per-project (project-level .cyberine/fleet.json) without leaking keys.
Two independent axes, deliberately decoupled:
- Available = key/login present (unchanged —
config.ts:988-1052). A provider can be available but disabled. - Active = listed +
enabledinfleet.json. This is the new layer.
active = listed-in-fleet.json AND enabled AND available. A provider that is enabled in fleet.json but missing its key is reported by fleet doctor/fleet list as enabled but NO KEY — it is skipped at runtime with a warning, never a hard error.
3. File locations + precedence
| Layer | Path | Role |
|---|---|---|
| Project | <repo>/.cyberine/fleet.json | If present, fully replaces the global file (consistent with replace semantics). Commit it to pin a project's fleet. |
| Global | ~/.cyberine/fleet.json | Default for any repo without a project file. |
| Built-in | WORKER_PROFILES + CATALOG | Used only when NO fleet.json exists anywhere (back-compat: behaves exactly like today). fleet init materializes this into a file. |
Runtime override still wins for a single run (does not mutate the file):
CLI flag (--fleet, --model, --premium) > env (CYBERINE_BUILD_FLEET, DEEPSEEK_MODEL, ...) > fleet.json > built-inUpdate 2026-09-22:
--modeland--premiumwere never implemented ascyberine runflags — the shipped overrides are--fleet,--mode,--cheap,--high-risk(see therunusage string insrc/core/commandRegistry.ts). Premium workers are kept out of the no-arg fleet viadefault: falsein their fleet-config entry and are selected explicitly via--fleet, not a dedicated--premiumflag.
So the standing behavior comes from fleet.json; a one-off experiment is a flag/env, never a file edit. This is exactly the "no arg -> use default config; pass arg -> override" model you asked for.
4. Schema (detailed)
// ~/.cyberine/fleet.json
{
"version": 1,
// ---- WHO is active. REPLACE semantics: only providers listed here exist. ----
"providers": {
"deepseek": {
"kind": "key-loop", // key-loop | cli-agent (must match WORKER_PROFILES.kind)
"workerId": "loop-deepseek-build", // the WORKER_PROFILES id this entry binds to
"enabled": true, // off = excluded from every fleet, key untouched
"default": true, // member of the no-arg default fleet
"modelEnvKey": "DEEPSEEK_MODEL", // how a chosen model is injected (from profile; overridable)
"models": {
// friendly alias -> real model id + which task-kinds it is allowed to serve
"v4-pro": { "id": "deepseek-v4-pro", "for": ["code-small", "code-large"] },
"v4-flash": { "id": "deepseek-v4-flash", "for": ["qa"], "maxTokens": 8000 }
}
},
"kimi": {
"kind": "key-loop",
"workerId": "loop-kimi-build",
"enabled": true,
"default": true,
"modelEnvKey": "MOONSHOT_MODEL",
"models": {
"k2.6": { "id": "kimi-k2.6", "for": ["code-small"] },
"thinking": { "id": "kimi-k2-thinking", "for": ["code-large"] },
"turbo": { "id": "kimi-k2-turbo-preview","for": ["qa"] }
}
},
"openrouter": {
"kind": "key-loop",
"workerId": "loop-openrouter-build",
"enabled": false, // available (has key) but parked
"default": false,
"modelEnvKey": "OPENROUTER_MODEL",
"models": { "ds-pro": { "id": "deepseek/deepseek-v4-pro", "for": [] } }
},
"claude": {
"kind": "cli-agent", // model controlled via CYBERINE_CLAUDE_MODEL, login-gated
"workerId": "claude",
"enabled": true,
"default": false, // premium: opt-in via --premium / explicit route only
"modelEnvKey": "CYBERINE_CLAUDE_MODEL",
"models": {
"sonnet": { "id": "claude-sonnet-4-6", "for": [] },
"opus": { "id": "claude-opus-5", "for": ["code-large"] }
}
},
"opencode": {
"kind": "cli-agent",
"workerId": "opencode-deepseek",
"enabled": true,
"default": false,
"modelEnvKey": "CYBERINE_OPENCODE_DEEPSEEK_MODEL",
"models": { "ds-pro": { "id": "deepseek/deepseek-v4-pro", "for": [] } }
}
},
// ---- WHICH model handles WHICH task-kind. The "smart" part, now explicit. ----
// value = "provider:modelAlias" (string = one pick) | ["a:x","b:y"] (array = fan out + judge)
"routing": {
"qa": "deepseek:v4-flash",
"code-small": "deepseek:v4-pro",
"code-large": "kimi:thinking"
},
// ---- optional global knobs (override config.ts loop.* defaults) ----
"defaults": {
"maxToolRounds": 40,
"maxTokens": 8000
}
}Field reference
Provider entry:
| Field | Required | Meaning |
|---|---|---|
kind | yes | key-loop or cli-agent. Must match the bound profile. |
workerId | yes | Existing WORKER_PROFILES id this entry drives. The bridge to all built-in metadata (tier, costClass, strengths, sandbox policy) — fleet.json does NOT re-declare those. |
enabled | yes | Master on/off. Off = never selected; key/login left intact. |
default | yes | Member of the no-arg default fleet. Lets you keep premium CLIs enabled-but-not-default. |
modelEnvKey | no | Env var used to inject the chosen model into the worker subprocess. Defaults to the profile's modelEnvKey; only set to override. |
models | yes | alias -> { id, for[], maxTokens? }. id is the real model string; for lists task-kinds this model may serve; maxTokens optionally caps this model. |
Model entry:
| Field | Required | Meaning |
|---|---|---|
id | yes | Real provider model id (e.g. deepseek-v4-flash). |
for | no | Task-kinds this model is allowed to handle. Informational + used as a validity check against routing (doctor flags routing that points at a model whose for excludes that kind). |
maxTokens | no | Per-model output cap; overrides defaults.maxTokens and loop.maxTokens. |
Routing value resolution: "deepseek:v4-flash" -> provider deepseek -> model alias v4-flash -> id deepseek-v4-flash -> inject via DEEPSEEK_MODEL into loop-deepseek-build. An array ["deepseek:v4-pro","kimi:thinking"] means fan out both for that kind and let the judge pick (the compare/judge path); a string means a single deterministic worker.
5. How it plugs into existing code (integration points, no code yet)
Update 2026-09-22: this section is now implemented, not "no code yet" — see
src/core/fleetConfig.tsfor the loader/router integration andsrc/commands/fleet.tsforcmdFleet.
- Loader — add
loadFleetConfig(cwd)(mirrorsconfigJson.ts): read project then globalfleet.json, validate against schema, return a typedFleetConfig. Absent file ->null-> everything falls back to today's behavior. - Fleet assembly /
availableBuildWorkerIds()— when aFleetConfigexists, filterWORKER_PROFILESto entries whoseworkerIdisenabled(anddefaultfor the no-arg fleet) AND available. This is the replace gate. - Router (
router.ts:651 routeTask) — after kind classification, ifrouting[kind]is set, short-circuit to the mapped worker(s)+model instead of the heuristic fleet split. If a kind is unmapped, run the existing algorithm but restricted to the enabled set. - Model injection — reuse the existing Layer-2 mechanism (
launchBg.ts:181-192variantEnvOverrides): resolverouting/modelsto a{ <modelEnvKey>: <id> }map injected into the child env. No new injection path needed. - Catalog stays the facts source —
fleet.jsonreferences model ids;modelCatalog.ts(cache from models.dev) still owns contextWindow/liveness,modelPricing.tsstill owns cost.fleet doctorcross-checksfleet.jsonids against the catalog.
Net: fleet.json is a thin policy/selection layer over unchanged metadata + injection plumbing. No duplication of profile data — it binds by workerId.
6. CLI surface — cyberine fleet <subcommand>
| Command | Does |
|---|---|
fleet list / fleet status | The 3-state table you asked for (section 8). |
fleet init [--force] | Scaffold a complete fleet.json from current WORKER_PROFILES + catalog + detected keys. The bootstrap for replace semantics — without it you'd hand-author the whole file. --force overwrites. |
fleet enable <provider> / fleet disable <provider> | Flip enabled in fleet.json. Key untouched. |
fleet set-default <provider> <on|off> | Flip default membership. |
fleet set-model <provider> <alias> [--for <kind>] | Bind/relabel a model; with --for, also write the routing[kind] entry. e.g. cyberine fleet set-model deepseek v4-flash --for qa. |
fleet route <task> | Dry-run: print which worker:model a real task string would resolve to (wraps existing cyberine route). |
fleet doctor | Validate fleet.json: enabled-but-no-key, model id not in catalog (dead model), routing pointing at a disabled provider or a model whose for excludes that kind, and drift (built-in providers/models absent from the file — the replace-mode safety net). Exit non-zero on hard errors, warn on soft. |
fleet edit | Open fleet.json in $EDITOR, re-validate on save. |
7. Replace-mode safety net (the cost of the chosen semantics)
Replace means: cyberine ships a new provider next month -> your fleet.json won't know about it -> it silently won't be available. Two guards make this livable:
fleet doctorprints a DRIFT section: "built-in providers not in your fleet.json:loop-mimo-build,loop-minimax-build... runfleet init --mergeto add them parked (enabled:false)."fleet init --merge(a softer init): append any built-in providers missing from the file asenabled:false, default:falseentries, so they show up parked and you opt them in deliberately. Never touches entries you already have.
This preserves "source of truth" (nothing activates without you listing it) while removing the "I didn't even know it existed" failure mode.
8. fleet list mock output
$ cyberine fleet list
PROVIDER KIND KEY ENABLED DEFAULT MODELS ROUTES
deepseek key-loop yes on yes v4-pro, v4-flash code-small, qa
kimi key-loop yes on yes k2.6, thinking, turbo code-large
openrouter key-loop yes off no ds-pro -
claude cli-agent login on no sonnet, opus -
opencode cli-agent - on no ds-pro -
routing: qa -> deepseek:v4-flash code-small -> deepseek:v4-pro code-large -> kimi:thinking
2 warnings: run `cyberine fleet doctor`KEY column: yes (key present), login (CLI logged in), - (CLI on PATH, no login needed), NO KEY (enabled but missing — red).
9. Worked end-to-end
cyberine fleet init # scaffold full fleet.json from current setup
cyberine fleet disable openrouter # park it (key stays)
cyberine fleet set-model deepseek v4-flash --for qa
cyberine fleet set-default claude off # keep claude enabled but out of no-arg fleet
cyberine fleet doctor # validate + drift check
cyberine fleet route "explain how the router works" # -> qa -> deepseek:v4-flash
cyberine run "refactor the auth module" # code-large -> kimi:thinking (from fleet.json)
CYBERINE_BUILD_FLEET="claude" cyberine run "..." # one-off override, file untouched10. Open questions before implementation
- Project vs global merge: project
fleet.jsonfully replaces global (current draft) vs deep-merge over global. Replace is simpler + consistent; merge is friendlier for "global baseline + per-project tweak". Pick one. - Routing string vs array default: examples use single-pick strings (deterministic, build-path friendly). Do you want fan-out arrays for the compare/judge path too, or keep routing single-pick and let the compare path use all enabled+default?
- opencode sub-model routing: opencode has its own internal model selection (
CYBERINE_OPENCODE_*_MODEL). Treat each opencode model as a separate provider entry, or oneopencodeentry with a model map? (Current draft: one entry, model map.) fleet initfrom what exactly: fromWORKER_PROFILESdefaults only, or also pre-fillroutingfrom the currentvariantslogic so a fresh file reproduces today's behavior 1:1? (Recommend: yes, reproduce 1:1.)- Relationship to
config.jsonbuild.fleet:fleet.jsonsupersedes thebuild.fleetallowlist. Do we deprecatebuild.fleet, or keep it as a runtime-override shorthand? (Recommend: keep as env/runtime override only;fleet.jsonis the persistent source.)
11. Decisions (resolved 2026-06-25, auto-picked under vibe-coding A-Z autonomy)
| Q | Decision | Rationale |
|---|---|---|
| 1 Project vs global | Full-replace — project fleet.json, if present, fully replaces global; no deep-merge. | Consistent with the replace semantics already chosen; predictable, no surprising inheritance. |
| 2 Routing value | Both — string = single deterministic pick; array = fan-out + judge for that kind. Examples default to single. | Most feature-complete; schema already supports it; build path uses single, compare/judge path can use array. |
| 3 opencode sub-model | One opencode entry with a models map. | Matches the uniform provider shape; opencode's CYBERINE_OPENCODE_*_MODEL injects per chosen alias. |
4 fleet init routing | Pre-fill routing 1:1 from current variants logic so a fresh file reproduces today's behavior exactly. | Zero behavior change on first init; user edits from a known-good baseline. |
5 build.fleet | Keep as env/runtime override shorthand only; fleet.json is the persistent source. No deprecation. | Preserves one-off CYBERINE_BUILD_FLEET=... ergonomics; runtime precedence already places it above the file. |
LSP Diagnostic Feedback Loop into Adapters — T436 Design Note
Status: shipped (T436) — see src/core/config.ts:946-966,1516-1536 (diagFeedback, diagFeedbackInterval, diagSeverity, diagMaxErrors) and src/adapters/LoopAgentAdapter.ts:1255 (runDiagnostics,...
TUI Config Interaction Standard — T418 Tier-1 Design Doc
Status: design artifact only — no code built. Tiers 2-3 (shared primitives + migration + settings screen) are TTY+decision-gated and not included here. Source cross-checked: src/tui/,...