| 1 | # Agent fleet |
| 2 | |
| 3 | > Baca terjemahan bahasa Indonesia: [id/FLEET.md](id/FLEET.md) |
| 4 | |
| 5 | Agent fleet is the local-first roster and member-selection layer for durable |
| 6 | multi-worker runs. It does not execute or authorize work. After fleet resolves |
| 7 | who should participate, the delegated coordinator launches a headless |
| 8 | `codewhale exec` run and the Runtime tracks it durably. See |
| 9 | [AGENT_RUNTIME.md](AGENT_RUNTIME.md) for how sub-agents, `exec`, and |
| 10 | fleet-backed workers converge on one runtime. In product language, a user may |
| 11 | still "open a sub-agent"; in architecture language, durable nested work uses a |
| 12 | fleet member identity with delegated runtime execution. |
| 13 | |
| 14 | ## Naming and compatibility boundary |
| 15 | |
| 16 | **Fleet** is the public product noun. The durable ledger, saved rosters, config |
| 17 | tables, and `--fleet` flag share that name: |
| 18 | |
| 19 | | Surface | Canonical | |
| 20 | | --- | --- | |
| 21 | | CLI | `codewhale fleet …` | |
| 22 | | Slash command | `/fleet …` | |
| 23 | |
| 24 | These shared names are load-bearing wherever changing them would break |
| 25 | existing workspaces, receipts, or scripts: |
| 26 | |
| 27 | - the durable ledger `.codewhale/fleet.jsonl` and the log directories |
| 28 | `.codewhale/fleet/` and `.codewhale/fleet-host/`; |
| 29 | - saved rosters `fleets/<name>.toml` and their `schema = "fleet"` header; |
| 30 | - the `[fleet]` config table (inline `[fleets.*]` tables were removed in 0.9.14; named fleets live in `fleets/<name>.toml` files); |
| 31 | - the `codewhale workflow run --fleet <name>` flag; |
| 32 | - wire, receipt, and control-plane operation ids such as `fleet.status`. |
| 33 | |
| 34 | The rest of this document uses fleet as the public product noun and retains |
| 35 | these literal paths, keys, flags, and ids. |
| 36 | |
| 37 | Use a fleet roster rather than anonymous short-lived `agent` fanout whenever a |
| 38 | delegated run needs stable member identities across retries, sleep/restart, |
| 39 | remote execution, receipts, or a ledgered audit trail. The initial CLI surface |
| 40 | is: |
| 41 | |
| 42 | For a guided start-to-monitor walkthrough that combines fleet task specs with |
| 43 | Workflow authoring, see [fleet + Workflow Tutorial](FLEET_WORKFLOW_TUTORIAL.md). |
| 44 | |
| 45 | ```sh |
| 46 | codewhale fleet init |
| 47 | codewhale fleet run tasks.json --max-workers 4 |
| 48 | codewhale fleet status |
| 49 | codewhale fleet inspect <worker-id> |
| 50 | codewhale fleet logs <worker-id> |
| 51 | codewhale fleet artifacts <worker-id> |
| 52 | codewhale fleet interrupt <worker-id> |
| 53 | codewhale fleet restart <worker-id> |
| 54 | codewhale fleet resume <run-id> |
| 55 | codewhale fleet stop --all |
| 56 | ``` |
| 57 | |
| 58 | `codewhale fleet resume <run-id>` is the restart-recovery verb: it replays the |
| 59 | ledger, reconciles any in-flight lease whose worker stopped heartbeating |
| 60 | (retrying within the task's budget, else failing and escalating per the alert |
| 61 | policy), and prints the post-resume status. It launches no new work and is |
| 62 | idempotent, so it is safe to run after a manager exit, laptop sleep, or runtime |
| 63 | restart. |
| 64 | |
| 65 | Coordinator state for fleet-backed runs is stored under the workspace in |
| 66 | `.codewhale/fleet.jsonl`. Worker logs and adapter logs are stored under |
| 67 | `.codewhale/fleet/` and `.codewhale/fleet-host/`. |
| 68 | |
| 69 | ## Public contract: identity, membership, and selection |
| 70 | |
| 71 | **Fleet** = the user's model inventory: who is in the roster and which member is selected. |
| 72 | |
| 73 | A public fleet identity consists only of: |
| 74 | |
| 75 | - a stable member id and an optional user-facing name; |
| 76 | - a semantic role, such as `explore`, `implement`, or `reviewer` (the legacy |
| 77 | spellings `worker`, `scout`, `builder`, `verifier`, `consultant`, and `oracle` |
| 78 | are still accepted on input and map to `general`, `explore`, `implement`, |
| 79 | `test`, and `advisor`); |
| 80 | - an exact provider/model identity, or an explicit inherited route; |
| 81 | - visible roster state or origin. |
| 82 | |
| 83 | Project or workspace trust, filesystem and network reach, secret access, |
| 84 | approval mode, sandboxing, tool authorization, and every other form of runtime |
| 85 | authority are separate delegated-coordination and Runtime policy inputs. They |
| 86 | are never fleet identity fields, and they never select or reroute a fleet |
| 87 | member. The Runtime applies and clamps those policies only after member |
| 88 | selection; if the selected member cannot run inside the effective envelope, |
| 89 | launch fails closed instead of choosing somebody else. |
| 90 | |
| 91 | Natural-language member selection is deterministic. A caller may name: |
| 92 | |
| 93 | - an exact member id, optionally as `member:<id>` or `id:<id>`; |
| 94 | - a unique user-facing member name, optionally as `name:<name>`; |
| 95 | - a unique semantic role, for example `explore` or `role:explore`; |
| 96 | - an exact pinned model id, for example `deepseek-v4-flash`, or its offline |
| 97 | display name, for example `DeepSeek V4 Flash`; or |
| 98 | - an exact `route:<provider>/<model>`. |
| 99 | |
| 100 | An unqualified exact member id wins. Every other match succeeds only when it |
| 101 | identifies one distinct roster member. Multiple matches produce an ambiguity |
| 102 | error that names the candidates and asks for `member:<id>`; Codewhale never |
| 103 | picks whichever match happened to be listed first. Users do not need to know |
| 104 | an internal role label such as `explore`: a unique member name, display model, or |
| 105 | exact model id is equally valid. Saved v2 fleets store that optional human name |
| 106 | as `display_name` (the input alias `name` is also accepted); it must be one |
| 107 | trimmed printable line of at most 80 characters. |
| 108 | |
| 109 | ### Your fleet as models |
| 110 | |
| 111 | The same fleet file answers a third question: **which models has this person |
| 112 | put in their fleet?** Every exact `provider` + `model` pin in the selected |
| 113 | fleet — the operator route, each pinned member, and each explicitly marked |
| 114 | shortlist row — is a fleet model. Executable member rows supply the roles it |
| 115 | fills; a shortlist row has no role. All remain in the same fleet file. Legacy |
| 116 | members that omit `role` still use their id as the role identity. |
| 117 | Shortlist rows carry only model choices; reasoning, instructions, and capability |
| 118 | requirements belong on executable role members and are refused on shortlist rows. |
| 119 | |
| 120 | - `/fleet models` prints the fleet: `provider/model · roles · price · context · |
| 121 | tools`, facts read from the model catalog. With no selected fleet the line |
| 122 | reads "Your fleet is the session model only". |
| 123 | - `/fleet add <provider> <model> [role…]` adds a model (one member row per |
| 124 | role, or one `shortlist = true` row for a role-less add). The provider must be one you configured |
| 125 | and, when the catalog knows the provider, must serve that exact id. |
| 126 | A role member asked to run the fleet's own operator route inherits it |
| 127 | instead of pinning — the role follows when the operator moves; a pin on |
| 128 | any other route is the deliberate opt-out. Files that already pin the |
| 129 | operator route are read as inheritance. |
| 130 | With no fleet selected, a user-global fleet named `My fleet` is created and |
| 131 | selected first. `/fleet remove <provider> <model>` drops every row that pins |
| 132 | the route; the operator route is changed with `/fleet save`, not removed. |
| 133 | - In `/model`, `⇧F` adds or removes an explicit shortlist row for that exact |
| 134 | route. It preserves saved role pins and the operator route; |
| 135 | fleet models are listed first, labelled `fleet · <roles>`, ahead of your |
| 136 | own `⇧P` pins and the provider lists. `/models` prints the fleet before the |
| 137 | provider's list. |
| 138 | |
| 139 | The operator model reads this list when it assigns sub-agents (design |
| 140 | `MODEL-ROUTING-CATALOG-20260901.md` §10, slice F2). |
| 141 | The model-facing roster resolves roles through the same route admission code |
| 142 | as a start. Explicit profiles and manual role pins remain authoritative; a |
| 143 | unique saved role pin also applies to a start naming only its role. Task model |
| 144 | choices are available for unpinned roles, constrained to the selected models |
| 145 | plus the session route. Shortlist model rows disclose their own exact route |
| 146 | independently of any role pin. |
| 147 | |
| 148 | ### Interactive and persistent status |
| 149 | |
| 150 | `/fleet status` and `codewhale fleet status` are the **same** command on two |
| 151 | surfaces. Both read the durable `.codewhale/fleet.jsonl` ledger for the |
| 152 | workspace, through one shared control-plane contract, and both report the same |
| 153 | verb id (`fleet.status`), read-vs-write authority, persistence scope, and |
| 154 | receipt. When the workspace has no ledger they say so with a typed reason |
| 155 | (`no_fleet_ledger`) instead of rendering an empty-looking "all clear" — and |
| 156 | neither creates the ledger as a side effect of reading it. |
| 157 | |
| 158 | The current interactive session's sub-agents are a **different set**, and now |
| 159 | have their own name: |
| 160 | |
| 161 | - `/fleet workers` (or `/subagents`, or `n`) shows sub-agents attached to the |
| 162 | current TUI session. It does not read the persistent ledger. |
| 163 | - `/fleet list|status|interrupt|resume` and `codewhale fleet |
| 164 | list|status|interrupt|resume` act on the durable ledger. |
| 165 | - `codewhale fleet restart <worker-id>` is CLI-only: it re-leases the task and |
| 166 | then drives the manager loop to completion. `/fleet restart` does not |
| 167 | silently do a smaller thing — it reports `surface_not_supported` and names |
| 168 | the CLI command. |
| 169 | |
| 170 | Before v0.9.2, `/fleet status` showed session sub-agents. That reading is gone; |
| 171 | `/fleet workers` replaces it. |
| 172 | |
| 173 | The contract behind this — descriptors, availability reasons, exact-identity |
| 174 | targets, receipts, typed unknowns, and bounds — is documented in |
| 175 | [`docs/COMMAND_CONTROL_PLANE.md`](COMMAND_CONTROL_PLANE.md). |
| 176 | |
| 177 | ## Authoring agent profiles (`/fleet setup`) |
| 178 | |
| 179 | `/fleet setup` (also `/fleet setup edit` / `new`) opens an in-TUI wizard for |
| 180 | authoring a reusable agent-team profile. Bare `/fleet` and the |
| 181 | `roster`/`roles`/`profiles`/`party` aliases open the selected fleet's member |
| 182 | roster. `/fleet saved` opens the named saved-fleet picker. `/fleet workers` opens the |
| 183 | current-session worker view; `/subagents` is a |
| 184 | compatibility shortcut for that view. For durable run history, use |
| 185 | `/fleet status` or the shell command `codewhale fleet status` described above — |
| 186 | they are the same command. |
| 187 | |
| 188 | The wizard is progressive: you make one focused choice at a time — a **role**, |
| 189 | then a **model** (`inherit`, or a concrete model from *any configured |
| 190 | provider*, not only the one the parent session is currently using), then |
| 191 | **where the profile lives**, and finally a **review** of the member identity |
| 192 | and route. When the review also previews thinking, tools, approvals, or another |
| 193 | execution control, those rows summarize separate Runtime policy; they do not |
| 194 | become fleet identity or member selectors. The header shows "Saves to: …" on |
| 195 | every step — the choice you still have to make, or the exact resolved file |
| 196 | once you have made it. Nothing is written until you activate the save control |
| 197 | on the review step. |
| 198 | |
| 199 | The **Destination** step is a focused two-option list: |
| 200 | |
| 201 | - **This project** writes `<workspace>/.codewhale/agents/<role>.toml`. It |
| 202 | applies to this project only and takes precedence over a Personal profile |
| 203 | with the same id. When project profiles are disabled for the session |
| 204 | (`--no-project-config`) or the workspace folder is unavailable, the option is |
| 205 | shown disabled with that reason; the wizard never falls back to Personal on |
| 206 | its own. |
| 207 | - **Personal** writes `$CODEWHALE_HOME/agents/<role>.toml` and is available in |
| 208 | every project on this machine, except where a project has its own profile |
| 209 | with the same id. |
| 210 | |
| 211 | For the highlighted option the step shows the exact file, whether saving would |
| 212 | create a new file or **replace an existing one**, and the precedence |
| 213 | consequence for the roster. The review step repeats those facts under |
| 214 | "Saves to" and names the final action by its effect — **Save to this |
| 215 | project**, **Save as Personal profile**, or **Replace …**. Replacing an |
| 216 | existing file asks for a second confirmation on the save control. Reopening a |
| 217 | saved member from `/fleet` starts from what is on disk: its member identity, |
| 218 | route, and save scope. Thinking (`inherit`, `off`, `low`, `medium`, `high`, |
| 219 | `max`, or `auto`) is adjusted on the review step, but remains a route |
| 220 | execution setting rather than part of the member's fleet identity. |
| 221 | |
| 222 | Profile scope controls where a role definition is reusable; it does not widen |
| 223 | the authority of a running operation and is not a project-trust setting. To |
| 224 | coordinate several nearby repositories, start Codewhale from their shared |
| 225 | parent directory so that parent is the workspace. Project/workspace trust, |
| 226 | external paths, filesystem and network reach, secrets, approvals, sandboxing, |
| 227 | and tool authorization come from delegated-coordination and Runtime policy. |
| 228 | For nested delegation, Runtime intersects the requested child posture with the |
| 229 | live parent. For standalone `codewhale fleet` execution, Runtime instead uses |
| 230 | the bounded tool-authority envelope minted from the task's explicit write |
| 231 | scope together with live config, sandbox, and platform enforcement. Neither |
| 232 | path reads authority from the profile's storage scope or identity selector. |
| 233 | |
| 234 | Picking a concrete model pins its provider explicitly: the saved profile records both |
| 235 | `model` and `provider` fields, so the route it names doesn't depend on |
| 236 | whichever provider happens to be active when the profile is later loaded. |
| 237 | Pressing **Enter** ("start") on the review step previews the exact starter |
| 238 | profile TOML inline on that same screen; nothing is written until you save it. |
| 239 | The `provider` field may be a built-in provider id such as `openrouter` or a |
| 240 | user-named OpenAI-compatible provider configured under `[providers.<name>]` |
| 241 | such as `lm-studio`; the launch path preserves that id and fails closed if the |
| 242 | provider is not configured. |
| 243 | |
| 244 | Profiles are also how the model-facing `agent` tool selects a route since the |
| 245 | v0.9.9 schema slim (#5324, #5123): the advertised surface no longer carries |
| 246 | `model` or `thinking` — a child either runs as a `profile` (whose saved route |
| 247 | and thinking tier it uses exactly) or inherits the operator's model. Removed |
| 248 | fields stay parse-accepted for saved transcripts, ACP/MCP clients and fleet |
| 249 | configs; see docs/SUBAGENTS.md for the advertised 12-field list and the |
| 250 | compat list. |
| 251 | |
| 252 | When a provider is configured, the review step also offers model-assisted |
| 253 | drafting behind an explicit preview-before-save gate: |
| 254 | |
| 255 | - Press **`m`** to have your first configured model draft the profile. The |
| 256 | draft arrives sanitized and bounded. Separately, the Runtime keeps its |
| 257 | conservative execution floor (no shell or trust escalation and approval |
| 258 | required) regardless of what the model proposes. |
| 259 | - **Drafting is not saving.** The exact rendered TOML preview renders |
| 260 | inline on the review step (not in a separate scrollable viewer), so nothing |
| 261 | is saved until you press **`g`** or **Enter** to save (or press `m` again |
| 262 | to redraft). Saving writes the profile to the project or personal scope |
| 263 | shown in the preview. |
| 264 | |
| 265 | ## Naming: Modes, Workflow, and fleet |
| 266 | |
| 267 | These names describe different layers, not competing systems. Plan and Act are |
| 268 | the everyday work modes. Operate accepts ordinary messages and keeps the |
| 269 | parent's normal tool surface under the same approval, sandbox, shell, ask-rule, |
| 270 | and repository protections as Act. It prefers background fleet workers for |
| 271 | independent, parallel, isolated, or long-running work, but does not require a |
| 272 | worker for every executable step. Workflow is an optional orchestration overlay |
| 273 | for work that needs ordering, gates, shared budgets, replay, or deterministic |
| 274 | fan-in. |
| 275 | |
| 276 | The short public vocabulary is: |
| 277 | |
| 278 | - **Fleet** is the durable roster and deterministic member-selection surface. |
| 279 | It records member ids and names, semantic roles, provider/model identities, |
| 280 | and roster state. Fleet is also the name used by storage and wire formats. |
| 281 | - **Workflow** = what order the work follows: phases, gates, budgets, replay, |
| 282 | and fan-in. |
| 283 | - **Lane** = one running Workflow instance and its live progress. |
| 284 | - **Runtime** = where, how, and with what authority selected work executes. |
| 285 | Runtime owns the local or remote process, provider route, project/workspace |
| 286 | trust, filesystem, network, secrets, approvals, sandbox, tools, and API |
| 287 | boundary. |
| 288 | |
| 289 | - **Workflow** is the repeatable plan and user-facing orchestration |
| 290 | overlay: a script/IR that decides which phases and agents run next, keeps |
| 291 | intermediate results out of the main conversation, and can be inspected or |
| 292 | rerun. A Workflow run should have a visible progress view and a clear active |
| 293 | header state instead of feeling like a hidden background task. |
| 294 | - **Fleet** is the durable roster and deterministic member-selection surface: |
| 295 | member ids and names, semantic roles, pinned or inherited provider/model |
| 296 | identities, and roster state. The delegated |
| 297 | coordinator and Runtime own launch concurrency, leases, heartbeats, logs, |
| 298 | receipts, tools, sandboxing, approvals, and authority. |
| 299 | - **High fan-out** is a behavior of a Workflow run, not a separate system: |
| 300 | when a phase needs many workers at once, Workflow dispatches them as a |
| 301 | fleet-backed run (durable workers, receipts, goal re-dispatch) rather than |
| 302 | reviving prompt-only sub-agent fanout. |
| 303 | - **Fan-in is explicit:** when the user needs one combined result, an owner |
| 304 | aggregates, verifies, and synthesizes the worker receipts. Independent tasks |
| 305 | may finish separately; dispatch is never presented as completion. |
| 306 | |
| 307 | UI guidance: keep the main transcript calm. A Workflow run should appear as a |
| 308 | compact progress card plus workbar rows (the strip under the composer, or |
| 309 | a side workbar) with phase names, worker counts, receipts, and nested |
| 310 | indentation for child workers. Use the whale mark sparingly as an active |
| 311 | header/status signal; avoid repeating emoji-heavy rows for every worker. |
| 312 | |
| 313 | ## Saved fleets and the Reasoning Router |
| 314 | |
| 315 | A selected v2 fleet freezes each selected member's id, semantic role, provider, |
| 316 | and model identity into the durable run before a Workflow starts. Save the |
| 317 | fleet as `fleets/<name>.toml` in the workspace or under `$CODEWHALE_HOME`. |
| 318 | Models cannot replace those identity or route assignments at runtime: |
| 319 | |
| 320 | ```toml |
| 321 | schema = "fleet" |
| 322 | schema_revision = 2 |
| 323 | name = "release" |
| 324 | |
| 325 | [operator] |
| 326 | provider = "deepseek" |
| 327 | model = "deepseek-v4-pro" |
| 328 | |
| 329 | [[members]] |
| 330 | id = "implementer" |
| 331 | display_name = "Release Builder" |
| 332 | role = "implement" |
| 333 | provider = "zai" |
| 334 | model = "glm-5.2" |
| 335 | |
| 336 | [[members]] |
| 337 | id = "advice" |
| 338 | role = "advisor" |
| 339 | provider = "openai" |
| 340 | model = "gpt-5.6" |
| 341 | ``` |
| 342 | |
| 343 | The workflow crate's older `schema = "exact"`, revision 1 files are migration |
| 344 | input only. Do not author them for v0.9.11; the selected roster and setup UI |
| 345 | read and write only `schema = "fleet"`, revision 2. |
| 346 | |
| 347 | Reasoning is a separate route-execution decision, not fleet identity. The |
| 348 | optional Reasoning Router is a reusable Runtime service, not a fleet member. |
| 349 | Save one profile at `routers/<name>.toml` in either search root and reference it |
| 350 | from any number of fleets: |
| 351 | |
| 352 | ```toml |
| 353 | name = "luna-low" |
| 354 | schema = "reasoning_router" |
| 355 | schema_revision = 1 |
| 356 | provider = "openai" |
| 357 | model = "gpt-5.6-luna" |
| 358 | call_reasoning = "low" |
| 359 | ``` |
| 360 | |
| 361 | At runtime it may choose only the reasoning tier for an already-frozen worker |
| 362 | route. It cannot change the member, provider, model, or semantic role. The |
| 363 | Router call itself is capped at `off` or `low`; more expensive values are |
| 364 | rejected. A manually selected worker reasoning tier makes no Router call. Route |
| 365 | and reasoning receipts name the worker model and, when used, the Router's exact |
| 366 | provider/model so the operator can see which model did which job. If the same |
| 367 | bare Router or fleet name exists in both roots, qualify it as |
| 368 | `workspace/<name>` or `codewhale_home/<name>` instead of relying on shadowing. |
| 369 | |
| 370 | Compatibility schemas may serialize `reasoning`, `permissions`, tool hints, or |
| 371 | other execution settings beside a member. Those values are not fleet identity, |
| 372 | member selectors, or active authority. A valid legacy `schema = "exact"` roster snapshot |
| 373 | retains its old `permissions` bytes only while verifying and replaying that |
| 374 | snapshot's recorded content hash; a fresh capture emits the authority-free |
| 375 | member shape. New-run validation rejects legacy roster |
| 376 | `security_policy` and worker `trust_level` fields; configure execution authority |
| 377 | through Runtime policy. The delegated coordinator resolves and durably freezes |
| 378 | the member first. Runtime then applies either the delegating parent's effective |
| 379 | ceiling or, for standalone fleet CLI work, Runtime execution configuration plus |
| 380 | live sandbox/platform enforcement. That boundary may |
| 381 | reduce or refuse the selected worker's execution surface, but it must never |
| 382 | choose a different member or route. See |
| 383 | [`docs/MODES.md`](MODES.md), [`docs/SUBAGENTS.md`](SUBAGENTS.md), and |
| 384 | [`docs/AGENT_RUNTIME.md`](AGENT_RUNTIME.md) for the enforcement contract. |
| 385 | |
| 386 | Reasoning receipts record the requested tier *and* the tier the provider was |
| 387 | actually asked for. Those differ whenever a route cannot express the requested |
| 388 | one — Codewhale's route normalizer sends `high` for a requested `low` on most |
| 389 | routes, and Z.AI's GLM routes express only thinking on/off — so the receipt |
| 390 | reports the real request rather than the label that was selected. The value a |
| 391 | call actually carries is spelled by that route's own normalizer, not by the tier |
| 392 | label: an OpenAI Codex route is asked for `xhigh`, not `max`, and cannot be |
| 393 | asked for `off` at all. |
| 394 | |
| 395 | A v0.9.11 durable fleet CLI receipt keeps the selected profile id in |
| 396 | `effective_permissions.profile_id`, the resolved semantic role in |
| 397 | `resolved_route.role`, and the effective Runtime surface in the permission, |
| 398 | shell, and tool-scope fields. An exact Workflow launch receipt records |
| 399 | `member_role` separately from an optional Runtime `posture_role`, plus the |
| 400 | fingerprint of the effective authority envelope checked at the spawn boundary. |
| 401 | A member named `auditor` can therefore retain that identity while Runtime |
| 402 | reports a `custom` posture and independently proves the narrower surface it |
| 403 | enforced. |
| 404 | |
| 405 | A Workflow start fails closed on anything decidable locally: an unresolvable |
| 406 | provider or model, a missing credential, a client that cannot be built for a |
| 407 | member's route, or an `auto` member with no usable Reasoning Router. Per-task |
| 408 | validation that the spawn boundary would refuse anyway — notably a write-capable |
| 409 | member with no declared `write_roots`/`exact_files`/`coordination_contracts` — |
| 410 | is checked before the Router is called, so an invalid task never spends a |
| 411 | routing request. If a spawn fails *after* a Router decision, the receipt is |
| 412 | still recorded: the tokens were spent, and any cross-provider disclosure already |
| 413 | happened. |
| 414 | |
| 415 | ## Manager-owned Workflow fan-in |
| 416 | |
| 417 | When parallel work must return one combined answer, prefer a manager-owned |
| 418 | Workflow over a flat `agent` fan-out. Default shape: |
| 419 | |
| 420 | 1. **Cast one manager** (operator or workflow orchestrator). |
| 421 | 2. **Fan out** child tasks through `workflow` (`task()`, `parallel()`, |
| 422 | `pipeline()`, `phase()`) or a single manager session that owns the children. |
| 423 | 3. **Wait** for child receipts or completion events. |
| 424 | 4. **Aggregate and verify** load-bearing claims before treating them as facts. |
| 425 | 5. **Synthesize** one result the operator can depend on. |
| 426 | |
| 427 | Raw `agent` fan-out fits independent work with no combined result. When |
| 428 | results must be merged, compared, or verified, route through `workflow` so |
| 429 | the manager owns fan-in — that is what the shape above is for, not a ban |
| 430 | on simpler patterns when nothing needs combining. |
| 431 | |
| 432 | ## Workflow on fleet |
| 433 | |
| 434 | The intended high-capability path is agent-authored. When the main agent |
| 435 | decides a task needs more durable coordination than turn-by-turn sub-agent |
| 436 | calls, it drafts a Workflow script/IR, presents the run plan according to the |
| 437 | active permission mode, and the runtime compiles it into typed fleet work. |
| 438 | |
| 439 | fleet remains the sub-agent roster and member-selection surface. It owns member |
| 440 | identity, membership, semantic roles, saved provider/model pins or inheritance, |
| 441 | and roster state. Workflow owns the orchestration plan: |
| 442 | branch, sequence, loop, expand, review, and reduce decisions. The delegated |
| 443 | coordinator and Runtime own slot admission, launch concurrency, the execution |
| 444 | ledger, and every authority decision. A workflow script receives no direct |
| 445 | shell, filesystem, network, provider-secret, cancellation, or TUI authority; |
| 446 | workers perform real work as `codewhale exec` processes under the effective |
| 447 | Runtime policy. |
| 448 | |
| 449 | Default Workflow-to-fleet validation is intentionally bounded: |
| 450 | |
| 451 | - 1,000 total worker agents per Workflow run; |
| 452 | - 16 live worker agents at once; larger populations queue (block) on the host's |
| 453 | per-run concurrency gate until a live slot frees, then route through fleet; |
| 454 | - Workflow IR structural nesting no deeper than 5; |
| 455 | - Runtime child delegation defaults to 3 levels and has an opt-in hard ceiling |
| 456 | of 8, independently of the Workflow document's structural depth; |
| 457 | - bounded loops only (`max_iterations` required); |
| 458 | - bounded dynamic expansion only (`max_children` plus a template required). |
| 459 | |
| 460 | These are delegated-coordination population limits, not fleet identity and not |
| 461 | a demand to launch everything at once. A 1,000-agent Workflow should still |
| 462 | drain through the configured Runtime worker pool. They are also not model-step |
| 463 | budgets: omitted or zero `max_steps` remains unbounded. An explicit positive |
| 464 | `max_steps` may cap that task, while wall-clock timeouts, cancellation, |
| 465 | provider safeguards, heartbeats, and admission controls remain independent. |
| 466 | |
| 467 | Recommended model layouts, such as a DeepSeek Pro orchestrator with Flash |
| 468 | workers in the first ring and cheaper workers farther out, are presets only. |
| 469 | Every slot can inherit the active model or carry an explicit model override. |
| 470 | Inheritance is literal: the model you select in `/model` is the **operator** |
| 471 | (the pinned first row in `/fleet roster`), and any worker whose task spec and |
| 472 | roster profile pin no model runs on that session model. Once a selected member |
| 473 | has an exact provider/model pin, the Runtime does not silently reroute that |
| 474 | identity because a policy input differs; it either runs that route inside the |
| 475 | effective envelope or fails closed. Route receipts record the requested and |
| 476 | resolved identity. |
| 477 | |
| 478 | The setup UI should render this as an expanding grid: an orchestrator plus a |
| 479 | small number of visible sub-agent slots, with Right/Enter drilling into a slot's |
| 480 | next recursive ring rather than trying to show the whole tree at once. |
| 481 | |
| 482 | ## Task Spec |
| 483 | |
| 484 | `codewhale fleet run` accepts JSON or TOML. A minimal JSON spec: |
| 485 | |
| 486 | ```json |
| 487 | { |
| 488 | "name": "local smoke", |
| 489 | "tasks": [ |
| 490 | { |
| 491 | "id": "lint", |
| 492 | "name": "Lint", |
| 493 | "instructions": "Run the lint check and report failures.", |
| 494 | "expected_artifacts": ["log"] |
| 495 | } |
| 496 | ] |
| 497 | } |
| 498 | ``` |
| 499 | |
| 500 | Workers are optional. If omitted, Codewhale creates local worker slots up to |
| 501 | `--max-workers`. |
| 502 | |
| 503 | Task specs are typed in Rust and keep verification data separate from worker |
| 504 | transcripts. Only the `worker` member/role reference participates in fleet |
| 505 | identity selection. The remaining execution fields are delegated-coordination |
| 506 | or Runtime inputs applied after the member is resolved. A task can declare: |
| 507 | |
| 508 | - `id`, `name`, `description`, `objective`, and `instructions` |
| 509 | - `worker` role, tool profile, tools, and required capabilities |
| 510 | - `workspace` root, required files, writable paths, and environment allowlist |
| 511 | - `input_files`, extra `context`, `budget`, `timeout_seconds`, and `retry_policy` |
| 512 | - `expected_artifacts`, `scorer`, `tags`, and free-form `metadata` |
| 513 | |
| 514 | None of those execution-policy fields becomes part of a fleet identity or an |
| 515 | alternate member selector. Omitted or zero `max_steps` means no model-step |
| 516 | ceiling; Codewhale must not synthesize a default step budget. Explicit positive |
| 517 | step limits, timeouts, cancellation, provider safeguards, heartbeats, and |
| 518 | admission control are enforced independently by the delegated coordinator and |
| 519 | Runtime. |
| 520 | |
| 521 | Workers write bounded artifact files under `.codewhale/fleet/` and ledger only |
| 522 | the artifact refs: kind, path, checksum, MIME type, and size. Receipts record |
| 523 | `pass`, `fail`, `partial`, `skip`, or `timeout`; failed receipts may also mark |
| 524 | the source as `transport`, `task`, or `verifier`. `codewhale fleet status` |
| 525 | surfaces those failure-source counts separately. |
| 526 | |
| 527 | Deterministic built-in scorers are `exit_code`, `file_exists`, `regex_match`, |
| 528 | and `json_path`. Specs may also declare `command`, |
| 529 | `code_whale_verifier_prompt`, or `manual`; those record a partial receipt until |
| 530 | an explicit verifier pass completes. |
| 531 | |
| 532 | ### Using Role Presets |
| 533 | |
| 534 | Tasks can reference a semantic role name to select one unique roster member. |
| 535 | Built-in role names (`smoke-runner`, `reviewer`, `builder`, `read-only`) remain |
| 536 | available for compatibility, and custom roles may be defined in |
| 537 | `[fleet.roles]`. |
| 538 | |
| 539 | ```json |
| 540 | { |
| 541 | "name": "smoke check", |
| 542 | "tasks": [ |
| 543 | { |
| 544 | "id": "lint", |
| 545 | "name": "Lint check", |
| 546 | "instructions": "Run lint and report failures.", |
| 547 | "worker": { "role": "smoke-runner" }, |
| 548 | "expected_artifacts": ["log"] |
| 549 | } |
| 550 | ] |
| 551 | } |
| 552 | ``` |
| 553 | |
| 554 | After identity resolution, compatibility role presets may provide tool, |
| 555 | timeout, or retry defaults to the delegated coordinator. Those defaults do not |
| 556 | grant authority, do not change which member was selected, and remain subject to |
| 557 | Runtime clamping. A task spec may request its execution settings explicitly: |
| 558 | |
| 559 | ```json |
| 560 | { |
| 561 | "id": "deep-review", |
| 562 | "name": "Deep review", |
| 563 | "instructions": "Review the entire crate for soundness issues.", |
| 564 | "worker": { |
| 565 | "role": "reviewer", |
| 566 | "tools": ["cargo", "rg", "git"], |
| 567 | "capabilities": ["rust"] |
| 568 | }, |
| 569 | "input_files": ["crates/**/*.rs"], |
| 570 | "budget": { "max_tokens": 32000 }, |
| 571 | "expected_artifacts": ["log", "report"], |
| 572 | "scorer": { "kind": "regex_match", "path": ".codewhale/fleet/report.md", "pattern": "finding|all clear" } |
| 573 | } |
| 574 | ``` |
| 575 | |
| 576 | ### Multi-Task Run Example |
| 577 | |
| 578 | A single fleet run can dispatch several independent tasks in parallel: |
| 579 | |
| 580 | ```json |
| 581 | { |
| 582 | "name": "CI gate", |
| 583 | "tasks": [ |
| 584 | { |
| 585 | "id": "check", |
| 586 | "name": "Compile check", |
| 587 | "instructions": "Run cargo check --workspace and report errors.", |
| 588 | "worker": { "role": "builder" }, |
| 589 | "expected_artifacts": ["log"], |
| 590 | "scorer": { "kind": "exit_code" } |
| 591 | }, |
| 592 | { |
| 593 | "id": "clippy", |
| 594 | "name": "Clippy lint", |
| 595 | "instructions": "Run cargo clippy --workspace and report warnings.", |
| 596 | "worker": { "role": "reviewer", "tools": ["cargo", "cargo-clippy"] }, |
| 597 | "expected_artifacts": ["log"], |
| 598 | "scorer": { "kind": "exit_code" } |
| 599 | }, |
| 600 | { |
| 601 | "id": "security", |
| 602 | "name": "Secret audit", |
| 603 | "instructions": "Search for plaintext secrets and report any matches.", |
| 604 | "worker": { "role": "read-only", "tools": ["rg"] }, |
| 605 | "input_files": ["crates/**/*.rs"], |
| 606 | "expected_artifacts": ["log", "report"], |
| 607 | "retry_policy": { "max_attempts": 1 } |
| 608 | } |
| 609 | ] |
| 610 | } |
| 611 | ``` |
| 612 | |
| 613 | ## Alerts |
| 614 | |
| 615 | fleet alerting is disabled by default. A caller must supply an enabled alert |
| 616 | config before anything is sent. Routes match typed fleet event classes, not log |
| 617 | strings: |
| 618 | |
| 619 | - `stale` |
| 620 | - `restart_exhausted` |
| 621 | - `needs_human` |
| 622 | - `budget_exceeded` |
| 623 | - `verifier_failed` |
| 624 | - `run_completed` |
| 625 | |
| 626 | Adapter config stores environment variable names, not secret values. Send-time |
| 627 | code resolves those names from the environment or a future secrets provider. |
| 628 | Ledger records store only audit labels such as `slack`, `webhook`, or |
| 629 | `pagerduty`; task specs persisted in the ledger redact webhook URLs and routing |
| 630 | keys. |
| 631 | |
| 632 | Example alert config shape: |
| 633 | |
| 634 | ```json |
| 635 | { |
| 636 | "enabled": true, |
| 637 | "dry_run": true, |
| 638 | "routes": [ |
| 639 | { |
| 640 | "events": ["stale", "restart_exhausted", "verifier_failed"], |
| 641 | "adapter": "ops-slack" |
| 642 | }, |
| 643 | { |
| 644 | "events": ["restart_exhausted"], |
| 645 | "adapter": "pager" |
| 646 | } |
| 647 | ], |
| 648 | "adapters": { |
| 649 | "ops-slack": { |
| 650 | "kind": "slack", |
| 651 | "webhook_env": "CODEWHALE_FLEET_SLACK_WEBHOOK", |
| 652 | "channel": "#codewhale-fleet" |
| 653 | }, |
| 654 | "pager": { |
| 655 | "kind": "pager_duty", |
| 656 | "routing_key_env": "CODEWHALE_FLEET_PAGERDUTY_ROUTING_KEY", |
| 657 | "severity": "critical" |
| 658 | } |
| 659 | } |
| 660 | } |
| 661 | ``` |
| 662 | |
| 663 | Use dry-run to inspect a redacted adapter payload without sending: |
| 664 | |
| 665 | ```sh |
| 666 | codewhale fleet alert-dry-run \ |
| 667 | --event stale \ |
| 668 | --run-id fleet-demo \ |
| 669 | --worker-id fleet-demo-local-1 \ |
| 670 | --task-id release-triage \ |
| 671 | --reason "worker heartbeat stale since 2026-06-13T02:00:00Z" \ |
| 672 | --adapter slack |
| 673 | ``` |
| 674 | |
| 675 | The payload includes the run id, worker id, task id, status, short reason, and |
| 676 | safe inspection commands such as `codewhale fleet status` and |
| 677 | `codewhale fleet inspect <worker-id>`. Endpoints, webhook secrets, and |
| 678 | PagerDuty routing keys are shown as `<redacted:env:...>`. |
| 679 | |
| 680 | ## Status Surfaces |
| 681 | |
| 682 | `codewhale fleet status` shows compact counts for queued, running, completed, |
| 683 | partial, failed, restarted, escalated, cancelled, stale, and verifier/transport |
| 684 | failure sources. `inspect` shows the worker state plus the current task |
| 685 | objective, role, host, heartbeat, latest event, artifact refs, latest error, and |
| 686 | alert state. `logs` prints bounded log artifact contents, and `artifacts` lists |
| 687 | artifact refs without embedding large payloads. |
| 688 | |
| 689 | The Runtime API exposes the same ledger-backed projection behind the existing |
| 690 | runtime auth middleware: |
| 691 | |
| 692 | ```text |
| 693 | GET /v1/fleet/runs |
| 694 | GET /v1/fleet/runs/{run_id} |
| 695 | GET /v1/fleet/runs/{run_id}/workers |
| 696 | GET /v1/fleet/workers/{worker_id} |
| 697 | POST /v1/fleet/workers/{worker_id}/interrupt |
| 698 | POST /v1/fleet/workers/{worker_id}/restart |
| 699 | POST /v1/fleet/runs/{run_id}/stop |
| 700 | ``` |
| 701 | |
| 702 | Action endpoints call the same manager controls as the CLI and record their |
| 703 | decisions in the fleet ledger. |
| 704 | |
| 705 | ## Manager-Agent Runbook |
| 706 | |
| 707 | Manager agents should treat fleet operations as typed, ledgered control-plane |
| 708 | work. Start with `codewhale fleet status`, then inspect one run or worker with |
| 709 | `codewhale fleet inspect <worker-id>`, `logs`, and `artifacts`. Use direct |
| 710 | reads of `.codewhale/fleet.jsonl`, host logs, or remote files only when the |
| 711 | typed CLI/API surface cannot provide the required evidence. |
| 712 | |
| 713 | Classify the worker before taking action: |
| 714 | |
| 715 | - `transient failure`: stale heartbeat, host timeout, interrupted transport, |
| 716 | retryable provider/network error, or an adapter status that can plausibly |
| 717 | recover without changing the task. |
| 718 | - `task failure`: the worker completed but produced an incorrect result, |
| 719 | domain failure, missing required artifact, or explicit task-level error. |
| 720 | - `verifier failure`: the worker result exists, but the scorer/verifier failed, |
| 721 | timed out, or disagrees with the receipt. |
| 722 | - `needs-human`: missing authority, secret request, destructive operation, |
| 723 | repeated restart exhaustion, ambiguous product decision, or conflicting |
| 724 | evidence that the manager cannot resolve from typed artifacts. |
| 725 | |
| 726 | Choose one typed action: |
| 727 | |
| 728 | - Restart a worker only when the failure is transient, retry budget remains, |
| 729 | the task is idempotent or retry-safe, and no permission or secret boundary is |
| 730 | involved: `codewhale fleet restart <worker-id>`. |
| 731 | - Interrupt or stop only when the current task is unsafe to continue or the |
| 732 | operator explicitly asks for cancellation: `codewhale fleet interrupt |
| 733 | <worker-id>` or `codewhale fleet stop --all`. |
| 734 | - Do not restart pure task failures by default; preserve artifacts and hand the |
| 735 | receipt to the task owner unless the task spec says retrying can produce new |
| 736 | evidence. |
| 737 | - For verifier failures, inspect scorer inputs and artifact refs first. If the |
| 738 | verifier cannot be corrected through typed fleet actions, escalate for human |
| 739 | review. |
| 740 | - For `needs-human`, draft an escalation instead of sending it unless alert |
| 741 | config explicitly authorizes sending. |
| 742 | |
| 743 | Safe Slack or PagerDuty draft: |
| 744 | |
| 745 | ```text |
| 746 | Codewhale fleet needs attention |
| 747 | Run: <run-id> |
| 748 | Worker: <worker-id> |
| 749 | Task: <task-id or unknown> |
| 750 | Classification: <transient failure | task failure | verifier failure | needs-human> |
| 751 | Reason: <one sentence, no secrets> |
| 752 | Latest typed evidence: codewhale fleet inspect <worker-id>; codewhale fleet artifacts <worker-id> |
| 753 | Safe log excerpt: <3 lines max or "see artifact <ref>"> |
| 754 | Requested decision: <restart approval | verifier review | task owner review | permission decision> |
| 755 | ``` |
| 756 | |
| 757 | Post-run summaries should include the run id, workers checked, classification, |
| 758 | typed action taken or drafted, expected ledger effect, artifact refs reviewed, |
| 759 | and next owner. Keep summaries bounded; link artifact refs instead of copying |
| 760 | full logs or transcripts. |
| 761 | |
| 762 | The bundled `fleet-manager` skill mirrors this runbook for manager agents. It |
| 763 | is a first-party system skill and should be discoverable through the normal |
| 764 | skill registry after system skills are installed or refreshed. |
| 765 | |
| 766 | ## Host Adapters |
| 767 | |
| 768 | The Runtime host-adapter boundary supports local child processes and explicit |
| 769 | SSH workers. Host choice is Runtime placement on a worker spec, not fleet member |
| 770 | identity or a member selector. It does not authenticate the host or grant |
| 771 | access. Adapters expose the same operations: start, read status, read bounded |
| 772 | logs, interrupt, restart, stop, and cleanup. |
| 773 | |
| 774 | Local workers run as child processes with stdin closed and stdout/stderr written |
| 775 | to bounded host-adapter logs. They inherit only a small safe base environment |
| 776 | such as `PATH` and explicitly allowlisted variables. |
| 777 | |
| 778 | SSH workers run through the system `ssh` client with `BatchMode=yes` and a |
| 779 | bounded connect timeout. Remote environment variables are sent with OpenSSH |
| 780 | `SendEnv`; values are not embedded in the local ssh argv or fleet logs. |
| 781 | |
| 782 | Example SSH worker spec: |
| 783 | |
| 784 | ```json |
| 785 | { |
| 786 | "id": "builder-1", |
| 787 | "name": "Builder 1", |
| 788 | "host": { |
| 789 | "kind": "ssh", |
| 790 | "host": "builder.example.com", |
| 791 | "user": "codewhale", |
| 792 | "port": 22, |
| 793 | "identity": "~/.ssh/codewhale_fleet", |
| 794 | "working_directory": "/srv/codewhale/work", |
| 795 | "env_allowlist": ["CODEWHALE_PROFILE"], |
| 796 | "codewhale_binary": "/usr/local/bin/codewhale" |
| 797 | }, |
| 798 | "capabilities": ["local", "linux", "tests"], |
| 799 | "max_concurrent_tasks": 1 |
| 800 | } |
| 801 | ``` |
| 802 | |
| 803 | Defaults are intentionally conservative: |
| 804 | |
| 805 | - no hosted control plane or cloud provisioning is enabled; |
| 806 | - SSH requires an explicit host, working directory, and Codewhale binary path; |
| 807 | - secret-like environment names such as `TOKEN`, `SECRET`, `PASSWORD`, |
| 808 | `API_KEY`, and `PRIVATE_KEY` are rejected from adapter allowlists; |
| 809 | - secrets should remain in Codewhale config providers or remote host config, |
| 810 | not in task instructions, argv, or fleet logs. |
| 811 | |
| 812 | ## Runtime policy and authority are not fleet identity |
| 813 | |
| 814 | fleet does not define a project/workspace trust level, filesystem or network |
| 815 | reach, secret access, approval mode, sandbox, tool set, or execution authority. |
| 816 | Those belong to delegated-coordination and Runtime policy. This separation is |
| 817 | load-bearing: |
| 818 | |
| 819 | - member resolution considers only member id/name, semantic role, |
| 820 | provider/model identity, and roster state; |
| 821 | - the selected identity is frozen before any authority policy is evaluated; |
| 822 | - the Runtime applies the live parent ceiling when one exists; standalone |
| 823 | fleet CLI launches instead carry an explicit bounded authority envelope, |
| 824 | and both paths remain subject to live sandbox and platform enforcement; |
| 825 | - no trust, permission, capability, secret, sandbox, approval, or tool-policy |
| 826 | value may select another member or silently change its provider/model route; |
| 827 | and |
| 828 | - receipts report requested and effective Runtime posture separately from the |
| 829 | fleet member identity. |
| 830 | |
| 831 | Older persisted configuration and protocol shapes may still contain fields such as |
| 832 | `security_policy`, `trust_level`, `permissions`, `capability_grants`, secret |
| 833 | references, host authentication, environment allowlists, or tool profiles. |
| 834 | They remain deserializable for ledger replay, but new fleet run creation rejects |
| 835 | `security_policy` and worker `trust_level` rather than pretending they grant |
| 836 | authority. Their presence in old data does not make them fleet variables or |
| 837 | grants. The active Runtime remains the final authority and fails closed when a |
| 838 | requested operation cannot be enforced. |
| 839 | |
| 840 | For current enforcement behavior, use [Modes](MODES.md), |
| 841 | [Sub-agents](SUBAGENTS.md), [Agent Runtime](AGENT_RUNTIME.md), and the |
| 842 | [Command Control Plane](COMMAND_CONTROL_PLANE.md). Keep secret values out of |
| 843 | task instructions, arguments, logs, and receipts; adapter and Runtime layers |
| 844 | must continue to redact or reject them independently of fleet selection. |
| 845 |