cyberine
Design notes

Fleet Config — Design Draft

Status: SHIPPED (2026-09-22) — see src/core/fleetConfig.ts (loadFleetConfig, resolveRoutingForKind) and src/commands/fleet.ts (cmdFleet, the live fleet command in src/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 across workerProfiles.ts + router.ts + modelCatalog.ts.

1. Decisions locked (from interview)

#DecisionChosenConsequence for this design
1File location/formatSeparate fleet.json, nested JSONNew loader, NOT bolted onto the flat dotted-key config.json.
2SemanticsReplace (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).
3Smart routingMap 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.
4CLI surfacefleet 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 by cyberine 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 + enabled in fleet.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

LayerPathRole
Project<repo>/.cyberine/fleet.jsonIf present, fully replaces the global file (consistent with replace semantics). Commit it to pin a project's fleet.
Global~/.cyberine/fleet.jsonDefault for any repo without a project file.
Built-inWORKER_PROFILES + CATALOGUsed 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-in

Update 2026-09-22: --model and --premium were never implemented as cyberine run flags — the shipped overrides are --fleet, --mode, --cheap, --high-risk (see the run usage string in src/core/commandRegistry.ts). Premium workers are kept out of the no-arg fleet via default: false in their fleet-config entry and are selected explicitly via --fleet, not a dedicated --premium flag.

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:

FieldRequiredMeaning
kindyeskey-loop or cli-agent. Must match the bound profile.
workerIdyesExisting 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.
enabledyesMaster on/off. Off = never selected; key/login left intact.
defaultyesMember of the no-arg default fleet. Lets you keep premium CLIs enabled-but-not-default.
modelEnvKeynoEnv var used to inject the chosen model into the worker subprocess. Defaults to the profile's modelEnvKey; only set to override.
modelsyesalias -> { id, for[], maxTokens? }. id is the real model string; for lists task-kinds this model may serve; maxTokens optionally caps this model.

Model entry:

FieldRequiredMeaning
idyesReal provider model id (e.g. deepseek-v4-flash).
fornoTask-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).
maxTokensnoPer-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.ts for the loader/router integration and src/commands/fleet.ts for cmdFleet.

  1. Loader — add loadFleetConfig(cwd) (mirrors configJson.ts): read project then global fleet.json, validate against schema, return a typed FleetConfig. Absent file -> null -> everything falls back to today's behavior.
  2. Fleet assembly / availableBuildWorkerIds() — when a FleetConfig exists, filter WORKER_PROFILES to entries whose workerId is enabled (and default for the no-arg fleet) AND available. This is the replace gate.
  3. Router (router.ts:651 routeTask) — after kind classification, if routing[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.
  4. Model injection — reuse the existing Layer-2 mechanism (launchBg.ts:181-192 variantEnvOverrides): resolve routing/models to a { <modelEnvKey>: <id> } map injected into the child env. No new injection path needed.
  5. Catalog stays the facts source — fleet.json references model ids; modelCatalog.ts (cache from models.dev) still owns contextWindow/liveness, modelPricing.ts still owns cost. fleet doctor cross-checks fleet.json ids 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>

CommandDoes
fleet list / fleet statusThe 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 doctorValidate 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 editOpen 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 doctor prints a DRIFT section: "built-in providers not in your fleet.json: loop-mimo-build, loop-minimax-build ... run fleet init --merge to add them parked (enabled:false)."
  • fleet init --merge (a softer init): append any built-in providers missing from the file as enabled:false, default:false entries, 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 untouched

10. Open questions before implementation

  1. Project vs global merge: project fleet.json fully replaces global (current draft) vs deep-merge over global. Replace is simpler + consistent; merge is friendlier for "global baseline + per-project tweak". Pick one.
  2. 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?
  3. opencode sub-model routing: opencode has its own internal model selection (CYBERINE_OPENCODE_*_MODEL). Treat each opencode model as a separate provider entry, or one opencode entry with a model map? (Current draft: one entry, model map.)
  4. fleet init from what exactly: from WORKER_PROFILES defaults only, or also pre-fill routing from the current variants logic so a fresh file reproduces today's behavior 1:1? (Recommend: yes, reproduce 1:1.)
  5. Relationship to config.json build.fleet: fleet.json supersedes the build.fleet allowlist. Do we deprecate build.fleet, or keep it as a runtime-override shorthand? (Recommend: keep as env/runtime override only; fleet.json is the persistent source.)

11. Decisions (resolved 2026-06-25, auto-picked under vibe-coding A-Z autonomy)

QDecisionRationale
1 Project vs globalFull-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 valueBoth — 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-modelOne opencode entry with a models map.Matches the uniform provider shape; opencode's CYBERINE_OPENCODE_*_MODEL injects per chosen alias.
4 fleet init routingPre-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.fleetKeep 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.