cockpit fleet
Generated reference for cockpit fleet, including its syntax, options, results, and constraints from the canonical cockpit CLI source.
This reference is generated from the same canonical source distributed as cockpit CLI help. See Cockpit CLI for concepts and safe operating guidance.
Usage: cockpit fleet <command> [options] [--directory <dir>]
A Fleet is a YAML file declaring a dependency graph of agent tasks, gates, and optional bounded loop groups. A Run is one execution of that Fleet. The runtime dispatches every node whose dependencies are satisfied, up to max_parallel at a time; each agent node becomes a normal Cockpit task, so one Fleet can mix claude, codex, grok, and the rest node by node. A Run is durable: pause, resume, and retry never redo unrelated nodes that already completed.
| Scope | Location | Selector |
|---|---|---|
| Project | <repo>/.cockpit/fleets/<name>.yaml |
<name>, with --directory <repo> |
| Global | ~/.agi-tools/data/cockpit/fleets/<name>.yaml |
<name> |
A file path also works anywhere a <name> is accepted.
Commands
cockpit fleet validate <name|file> --directory /path/to/repo
cockpit fleet validate <name|file> --directory /path/to/repo --arg run_e2e=false # Preview the Run with these args
cockpit fleet list --directory /path/to/repo # Project and global Fleets
cockpit fleet list --project # Project Fleets only
cockpit fleet list --global # Global Fleets only
cockpit fleet list --runs # Include Runs
cockpit fleet show <name> # Resolved definition
cockpit fleet show <runId> # Run snapshot with every node
cockpit fleet run <name|file> --directory /path/to/repo --title "Regression check for v2.3" --arg target="search indexing"
cockpit fleet status <runId>
cockpit fleet wait <runId>
cockpit fleet wait <runId> --since 12 --timeout 110
cockpit fleet pause <runId>
cockpit fleet resume <runId> [--set '*.account=work']
cockpit fleet stop <runId>
cockpit fleet retry <runId> --node implement-a [--set '*.account=work'] [--unset implement-a.effort]
cockpit fleet retry <runId> --node "review-loop#6.e2e" # Resume the failed iteration, keeping its finished body nodes
cockpit fleet retry <runId> --node review-loop --grant-iterations 2
cockpit fleet rerun <runId> # Start over as a new Run
cockpit fleet rename <runId> --title "Regression check for v2.3"
cockpit fleet remove <runId> # Delete a terminal Run and its history
cockpit fleet logs <runId>
cockpit fleet logs <runId> --node implement-a
cockpit fleet output --json '{"issues": 0}' # Inside a contracted node: submit its structured output
printf '%s' "${json}" | cockpit fleet output --stdin
| Command | Purpose |
|---|---|
| validate | Schema, unknown fields, dependency cycles, template references, capability rules. With --arg, additionally previews the Run those args would create: enabled is evaluated, the contracted graph is checked, and the response lists each excluded node with its reason |
| list | Fleets by scope, optionally with their Runs |
| show | A Fleet's resolved definition, or a Run's full node snapshot |
| run | Resolve, create the workspace, and start dispatching. Returns the runId |
| status | Run status plus a per-node summary, cumulative loop iteration budgets, and each loop's current-iteration progress |
| wait | Block until the Run needs attention, or (with --since) until the next event after that seq |
| output | Submit the calling node's structured output. Runs inside a node task that declares output_contract; validates against the schema and answers synchronously |
| pause / resume / stop | Suspend dispatch, continue, or cancel remaining work |
| retry | Re-dispatch one failed, interrupted, canceled, or rejected node and everything downstream of it. A loop-body node resumes its own iteration instead of starting a new one |
| rerun | Explicit start-over. Creates a new runId that keeps the original title and args; no node state is inherited |
| rename | Change the Run's title. Works while running, paused, waiting, completed, failed, or stopped. Empty titles are rejected. The new title is persisted, kept across resume and retry, shown in the task list and Fleet panel, returned by status and list --runs, and recorded as a title-changed event. Node task names are left unchanged. Returns the updated Run summary |
| remove | Permanently delete a completed, failed, or stopped Run and its saved event history |
| logs | The Run event log, or one node's task reports |
run options:
| Option | Description | Default |
|---|---|---|
| --title "..." | Human-readable purpose of this Run, shown as the Run's name in the task list and Fleet panel. Always set one that says what the Run is for, not which Fleet it uses | The Fleet name |
| --arg key=value | Value for a declared args entry. Repeatable |
args.<key>.default |
| --set nodeId.field=value | Override one runtime field on one node. Repeatable | - |
| --set loopId.nodeId.field=value | Override one runtime field on a loop-body node. Repeatable | - |
| --set '*.field=value' | Override that field on every agent node | - |
| --directory | Repository for a project Fleet, and the base of its workspace | - |
| --max-parallel | Override max_parallel for this Run |
The Fleet's value |
resume and retry also accept repeatable --set options, and repeatable --unset <nodeId>.<field> options. The new values are merged into the Run's saved overrides before replacement tasks are dispatched, and --unset deletes a saved override instead of merging one. --set <nodeId>.<field>= with an empty value does the same thing. A cleared field falls back to the normal resolution order, and capability checking sees the effective value after the clear, so --unset impl.effort is how a node that was moved to an effort-capable agent moves back to one without reasoning effort. Clearing a field that has no saved override is not an error, and it leaves every other saved override untouched. Account-capable agents default to account: auto, which switches a task to another signed-in account and continues it before Fleet sees an unrecovered usage limit. Use a fixed profile override such as --set '*.account=work' when account affinity is required. An unrecovered usage limit interrupts the node without consuming output-contract corrective turns. resume rearms those interrupted nodes and immediately rechecks queued message nodes; other failed nodes still require retry.
An agent node can opt into a required boolean output_contract property with success_output. When that property is false, the node fails instead of completing; its blocker string becomes the node error. A directly dependent command gate still runs so it can verify an external outcome. If that gate later passes, Fleet changes the saved success property to true, clears the blocker, and reconciles the failed node as completed. This lets a manual GitHub merge recover a Run with cockpit fleet retry <runId> --node merge-check without changing the meaning of ordinary output fields named merged.
Merge gates must check GitHub on every evaluation instead of trusting the merge agent's saved output. In full-dev-flow-v2, read the PR URL from FLEET_NEEDS_SELECT_OUTPUT; in review-loop, use FLEET_NEEDS_PREPARE_OUTPUT:
merge:
success_output: merged
output_contract:
type: object
required: [merged, summary]
properties:
merged: { type: boolean }
blocker: { type: string }
summary: { type: string }
merge-check:
title: Merge Check
type: gate
gate: command
needs: [merge]
run: |
pr_url=$(node -e 'const output = JSON.parse(process.env.FLEET_NEEDS_SELECT_OUTPUT); process.stdout.write(output.pr_url)')
gh pr view "$pr_url" --json state,mergedAt --jq '.state == "MERGED" and .mergedAt != null' | grep -qx true
The merge agent should fetch and rebase onto the current base branch before merging, resolve conflicts in the PR worktree, rerun the configured tests, and push with --force-with-lease. If it cannot resolve or merge safely, it must submit merged: false with the exact blocker. On retry it should first query gh pr view <PR URL> --json state,mergedAt; a PR that was already merged manually is a successful result. full-dev-flow-v2 and review-loop are user-data Fleets rather than bundled application assets, so update their YAML files in the global Fleet directory manually with this snippet; the application does not overwrite user Fleet definitions on startup.
Loop retries keep the Run's cumulative iteration counter. max_iterations is the Run-wide budget, so a retry after iteration 3 continues at iteration 4 instead of resetting to 1. An exhausted loop retry is rejected unless --grant-iterations N explicitly adds a positive number of iterations. Each grant is saved in the Run by increasing that loop's total budget. N must be an integer between 1 and 100, and a grant that would push the loop's total budget (base max_iterations plus every grant) above 100 — the same limit as max_iterations — is rejected.
Retrying the loop group discards the whole failed iteration, including the body nodes that had already succeeded in it. To keep that work, retry a body node of the current iteration instead: --node "<loopId>#<n>.<nodeId>". That resumes iteration n in place — every body node of that iteration that is not completed, passed, or excluded is re-armed, the successful ones keep their reports and outputs, and the cumulative counter stays at n. Only the loop's current iteration can be resumed; a body node from an earlier iteration is rejected. --grant-iterations N is accepted here too and raises the loop's budget without consuming an iteration, which is what a last-iteration failure needs so until can still start iteration n+1. A foreach body node is resumed the same way instead of being expanded again: its finished items keep their reports, only the unfinished ones are dispatched, and --node "<loopId>#<n>.<nodeId>#<k>" resumes the iteration from one item.
fleet status returns a loopIterations entry for each loop with cumulativeIterations, baseMaxIterations, grantedIterations, totalMaxIterations, and a compact display such as iter 7/5+5. While a loop has body instances, that entry also carries currentIteration with the iteration number, its nodeCount, the completedNodes and incompleteNodes definition ids, and retryableNodes — the exact --node values that resume the iteration. completedNodes holds every body node that has settled, counting one that never ran because it was skipped or excluded, so only a genuinely unfinished iteration extends the display, as in iter 6/6 (3/5 body nodes done).
--set uses the YAML field names: agent, model, effort, account, browser_identity, service_tier, workspace, approval. Resolution order is --set <nodeId>.<field> > --set '*.<field>' > node > defaults; use --set <loopId>.<nodeId>.<field> for a loop-body node. Invalid targets, fields, agents, workspaces, models governed by the normal task policy, efforts, service tiers, and approval modes are rejected before a Run starts. Fleet agent nodes use the visual task runtime when applying model, effort, and service-tier selections, even when Settings normally default that agent to terminal mode. model is optional and falls back to the normal visual task default from Settings. Cursor visual accepts a Cursor CLI model id such as composer-2.5, claude-sonnet-5-thinking-high, or auto, an ACP-advertised model name, or a full bracketed ACP model id. run fails and lists every unresolved required field if any agent node still lacks agent, or lacks effort on an agent that supports it.
run returns the runId immediately. Do not add --wait to run. Wait in a second step with cockpit fleet wait <runId> so a killed watcher can resume from a seq cursor.
cockpit fleet wait <runId>
cockpit fleet wait <runId> --timeout 600
cockpit fleet wait <runId> --since 12 --timeout 110
| Option | Description | Default |
|---|---|---|
| --timeout | Seconds to wait before returning {"timeout": true} |
No deadline without --since; 110 with --since |
| --since | Return only events with seq greater than this value |
- |
Every Run keeps a persistent event log. seq increases per Run. Default wait is level-triggered: it long-polls until the Run needs attention, then returns the event summary, latestSeq, current run status, and needsAttention: true. Attention means the Run is terminal (completed / failed / stopped), paused, a human gate is waiting-approval, or a node is interrupted. Pass --since <last seq you processed> for the same sequential loop as task wait: it returns as soon as one or more later events exist. A {"timeout": true} result is normal — re-run wait (with --since if you are looping) to keep watching.
wait does not push into the caller task. If the watcher process dies, resume with --since <latestSeq> so events are neither skipped nor duplicated.
Fleet file
version: 1
name: implement-feature
description: Investigate, implement in parallel, integrate, and verify
args:
target:
required: true
description: What to build
defaults:
agent: claude
model: claude-opus-5
effort: high
max_parallel: 4
nodes:
investigate:
title: Investigate
prompt: |
Investigate {{args.target}} and report an implementation plan.
implement:
needs: [investigate]
workspace: isolated
prompt: |
Implement the plan.
Plan: {{needs.investigate.report}}
tests:
type: gate
gate: command
needs: [implement]
run: pnpm test
| Root key | Required | Description |
|---|---|---|
| version | yes | Schema version. Only 1 |
| name | yes | Fleet name, [A-Za-z0-9][A-Za-z0-9_-]* |
| description | no | One line shown in list and in the Fleet panel |
| args | no | Declared run arguments, keyed by name |
| defaults | no | Runtime fields inherited by every agent node |
| max_parallel | no | Concurrently active nodes, 1–32. Default 4 |
| nodes | yes | Mapping of node id to node definition. Ids match [A-Za-z0-9][A-Za-z0-9_-]* |
Unknown keys are rejected everywhere, at the root, in args, in defaults, and in a node.
args.<key> takes required (boolean), description, and default. A required argument with no --arg fails the Run before any node starts.
Runtime fields, valid in defaults and on any agent node:
| Field | Description | Requires |
|---|---|---|
| agent | claude, codex, antigravity, cursor, qoder, grok, terminal, cockpit |
- |
| model | Model id for the node's task | Any agent except terminal |
| effort | Reasoning effort | claude, codex, cursor, grok, qoder, cockpit |
| account | auto (default), default, or an account profile name/id from cockpit accounts |
claude, codex, grok, antigravity, cursor, qoder |
| browser_identity | Browser Identity name or id | - |
| service_tier | standard or fast |
codex |
| workspace | shared (default) or isolated |
isolated needs a repository to branch from: the Run's, or the node's directory |
| approval | supervised, accept-edits, or full-access |
Any agent except terminal |
Capability checking is origin-aware. A field the node asks for itself — written on the node, or set with --set <nodeId>.<field>=<value> — is a validation error when the resolved agent does not support it. A field that only arrives by broad inheritance — from defaults, or from --set '*.<field>=<value>' — is dropped for the nodes whose agent does not support it, so one defaults block can cover a mixed-agent graph. A Run records a dropped field as null for that node. A Run requires effort when the resolved agent and model advertise reasoning effort. A Grok, Qoder, or Cursor model with no advertised efforts does not require it, and an inherited value is dropped. agent: qoder with no model now requires effort, matching Claude and Grok. Existing Qoder Fleets that omitted both should add an explicit model or effort. Cursor efforts are per model (minimal–max, matching the thought level in the Cursor model picker); a Cursor model id that already encodes a level (claude-sonnet-5-thinking-high, gpt-5.5[reasoning=high]) does not require a separate effort, and an explicit effort overrides the encoded level. A Cursor node with no model, an unknown model, or a model with no thought level does not require effort. An unknown Cursor model accepts any effort level at validation. When the session starts, a model with no thought-level config keeps its default if the requested level cannot be applied; a live model that advertises thought levels rejects a level it does not list, and the session fails to start.
Node-only fields:
| Field | Required | Description |
|---|---|---|
| prompt | agent and message nodes | The initial task instruction, or the message to send. Templates allowed |
| needs | no | Dependency node ids. A node with no needs is an entry node |
| type | no | agent (default), gate, loop, or message |
| task | message nodes | Existing Cockpit task id. Templates allowed |
| session | loop-body agent nodes | fresh (default) or continue. Continued nodes reuse their task across iterations |
| followup_prompt | continued loop-body agent nodes | Instruction sent on iteration 2 and later. Falls back to prompt when omitted |
| gate | gate nodes | command or human |
| run | command gates | Shell command. Exit 0 passes |
| retries | no | Auto-retries for a command gate, 0–3. Default 0. A retry happens only when the failed run names 1–2 failing tests, and a test that fails twice in a row fails the gate |
| ask | human gates | Question text for the Cockpit Ask. Approval passes |
| when | no | Condition. False marks the node skipped |
| enabled | no | Template boolean, {{args.*}} only. Evaluated once when the Run is created; false marks the node excluded |
| foreach | no | List expression expanded into one node instance per entry |
| directory | no | Working directory for this node, bypassing the Run workspace. With workspace: isolated it must be a repository root, and the node's worktree is cut from that repository |
| title | no | Display name. Defaults to the node id |
| output_contract | agent nodes except agent: terminal, and message nodes |
JSON Schema for the node's structured output. When present, the node's task must submit that output with cockpit fleet output before it reports |
| max_iterations | loop nodes | Maximum body iterations, 1–100 |
| on_exhausted | loop nodes | Agent node to run when the loop reaches max_iterations without passing until |
| until.run | loop nodes | Shell command evaluated after each successful body iteration |
| nodes | loop nodes | Nested body DAG. Nested loops are not supported |
A gate node must not carry prompt, foreach, or any runtime field — gates are evaluated by the runtime, not by an agent. retries is a command-gate field: it is a validation error on a human gate, an agent node, a message node, and a loop node.
Template variables
| Variable | Value |
|---|---|
{{args.<key>}} |
A declared run argument |
{{needs.<id>.report}} |
The upstream node's final report text |
{{needs.<id>.files}} |
Its changed files, one per line |
{{needs.<id>.branch}} |
Its branch name, for workspace: isolated nodes in project and global Fleets alike |
{{needs.<id>.report_lines}} |
Its report as non-empty trimmed lines |
{{needs.<id>.output}} |
Its validated JSON output, encoded as JSON |
{{needs.<id>.output.<field>}} |
A required field from its validated JSON output. Nested fields and array indexes use more dot-separated segments |
{{needs.<id>.task_id}} |
The id of the Cockpit task that ran it |
{{item}} / {{index}} |
The current entry and its 0-based position, inside a foreach node |
Templates work in prompt, followup_prompt, task, ask, run, title, directory, foreach, and enabled. {{needs.<id>...}} resolves only if <id> is a transitive upstream dependency of the node; referencing a sibling or a downstream node is a validation error. Free-text and file references for an excluded node resolve to an empty value; a nested output field is unavailable because the node did not run. {{item}} and {{index}} require foreach on the same node. title is resolved before the graph runs, so it accepts only {{args.*}}, {{item}}, and {{index}} — a {{needs.*}} reference there is a validation error. enabled is resolved at Run creation and accepts only {{args.*}}.
{{needs.<id>.task_id}} is the Cockpit task id of the node's task, which is what a message node's task needs to continue the agent that already did the upstream work. It requires an agent or message node without foreach: a gate or loop node runs no task, and a foreach node has one task per instance, so neither resolves to a single id and both are validation errors. For a session: continue node it is the task the session continues in, and inside a loop body it re-resolves each iteration, so a message node pointing at a node outside the loop keeps reaching the same task. A skipped or excluded node resolves to an empty value like its other free-text references, and an empty task fails the message node with message target task is empty. Guard a message node that reads the task_id of a node which can be skipped — most notably an on_exhausted adjudicator, which is bypass-skipped whenever until passes normally — with when: needs.<adjudicator>.passed, the same condition an adjudication-only output consumer uses.
implement:
prompt: Implement the change and report what you did.
follow-up:
type: message
needs: [implement]
task: "{{needs.implement.task_id}}"
prompt: Address the review notes on the branch you just pushed.
Message existing tasks
Use a message node when the Fleet should continue a Cockpit task it did not create:
notify-master:
type: message
task: "{{args.master_task_id}}"
prompt: "Implementation finished: {{needs.implement.report}}"
Fleet waits until the target can accept a prompt, revives a completed or errored task when possible, sends the message through the normal task queue, and completes the node from the target's matching report. Busy targets and active usage limits remain queued. Once a recorded reset time has elapsed, Fleet treats the stale usage-limit flag as recoverable and attempts delivery; if no usable reset hint exists, it uses a six-hour bounded fallback from the original restriction observation. fleet resume forces that eligibility recheck immediately. The target task's account: auto recovery is attempted before this wait, but a message node cannot override the account of the external task. A missing or non-resumable target fails the message node. The Fleet observes but does not own the target: completing, stopping, or removing the Run never completes or stops that existing task. Message nodes can declare needs, when, enabled, and output_contract, but cannot select an agent, account, workspace, or directory.
Structured output contracts
Use output_contract when a downstream prompt, command gate, or loop condition needs a value rather than prose:
consolidate:
prompt: Consolidate the review findings.
output_contract:
type: object
required: [issues, accepted, dismissed]
properties:
issues: { type: integer, minimum: 0 }
accepted: { type: array, items: { type: string } }
dismissed: { type: array, items: { type: string } }
clean:
type: gate
gate: command
needs: [consolidate]
run: test "{{needs.consolidate.output.issues}}" -eq 0
The contract is JSON Schema 2020-12 and is checked by cockpit fleet validate. Fleet appends the schema and the submission instruction to the node prompt, so existing Fleet files need no change. The node's task hands the value to the runtime directly:
cockpit fleet output --json '{"issues": 0, "accepted": [], "dismissed": ["flaky-test"]}'
printf '%s' "${json}" | cockpit fleet output --stdin
Cockpit resolves the calling node from the task itself, validates the JSON against that node's output_contract while the command runs, and answers ok or the schema errors. A rejected submission can be corrected and submitted again in the same turn, and the last submission before the node completes is the one Fleet keeps. Structured output never travels through the report message, so narration in a report can no longer break a contract. Submitting from a task that is not running a contracted Fleet node, or after its node finished, is an error.
If a contracted node reports without a valid submission, Fleet asks the same task to submit and report again, for up to two corrective turns; a third report without a submission fails the node and blocks its dependents. A usage-limit or plan-restriction response interrupts the node immediately instead, because that task cannot satisfy a corrective prompt. Recover it with resume or retry --set after selecting an available account, agent, or model.
{{needs.<id>.report}} is the node's report text as written, while the submitted value is saved as the node's output. String fields render as plain text; numbers and booleans render as their JSON spelling; arrays and objects render as compact JSON. Dot-path segments match [A-Za-z0-9_-]+, with numeric segments indexing arrays. An output template must target a node that declares output_contract, or Fleet validation fails. It must also name a path that contract proves exists: object properties must be listed in required, and array indexes need a sufficient minItems. This applies to every template, prompt included, so a field the upstream node may omit is a validation error rather than an unresolved template reference failure part-way through a Run. A schema the check cannot decide — anyOf, allOf, $ref, const or enum values that carry the whole path (entries the rest of the schema rejects are ignored), a union type, no type, a permissive additionalProperties, or a required property with no properties entry — is accepted as before. A foreach node's output is an array with one entry per instance, so index it first: {{needs.<id>.output.0.<field>}}. For a value a node may genuinely have nothing to put in, keep the field required and let its schema allow an empty value, or read the whole object with {{needs.<id>.output}}. Command-gate run and loop until.run templates narrow this further to numbers, integers, and booleans, to keep agent-authored text out of a shell command. Command-gate and loop directory templates accept required string output fields because the resolved value is the process working directory, not shell text. A loop directory may read body-node output, the same way until.run can. Relative values resolve against the Run workspace; a missing directory fails the gate with command gate directory does not exist. Agent-node and human-gate directory templates take a required output field of any type. output_contract is rejected on agent: terminal because terminal tasks cannot follow the corrective prompt.
An exact array output reference in foreach, such as foreach: "{{needs.scan.output.items}}", expands one instance per validated array element without comma splitting. A contracted foreach group exposes an array containing each successful instance's validated output.
Free-text reports remain the default when output_contract is absent. Existing report, report_lines, and report environment-variable workflows continue unchanged.
Conditions and foreach
when accepts exactly one grammar: needs.<id>.passed, needs.<id>.approved (human gates only), !, &&, ||, and parentheses. There is no general expression evaluator.
notify:
needs: [review]
when: "needs.review.approved && !needs.smoke.passed"
prompt: Report what still needs attention.
A node whose when is false becomes skipped. A node is also skipped when any dependency did not succeed, so skipping propagates down the graph without failing the Run.
foreach expands one node into one instance per list entry at dispatch time.
document:
needs: [investigate]
foreach: "{{needs.investigate.report_lines}}"
prompt: Document module {{item}} (item {{index}}).
files and report_lines split on newlines; any other value splits on commas. Blank entries are dropped, and an empty list marks the group skipped. The group node stays running until every instance finishes, then completes — or fails if any instance failed — and exposes the merged reports and files of its successful instances to downstream nodes.
Optional nodes: enabled and excluded
enabled turns a node off for one Run without editing the file — for example, skipping an expensive e2e node on a lightweight run:
e2e:
needs: [fix]
enabled: "{{args.run_e2e}}"
prompt: Run the full e2e verification.
consolidate:
needs: [review, e2e]
prompt: Consolidate the findings. E2E: {{needs.e2e.report}}
enabled takes only {{args.*}} and literal booleans, and after template resolution must be true or false — anything else fails Run creation. It is evaluated once, when the Run is created; there is no runtime branching. A false node becomes excluded, which is deliberately distinct from skipped:
skipped |
excluded |
|
|---|---|---|
| Cause | An upstream node did not succeed, or when was false |
enabled evaluated to false at Run creation |
| Downstream effect | Propagates — dependents are skipped too | Does not propagate — dependents run |
Dependents of an excluded node inherit its needs (edge contraction), so execution order is preserved: with consolidate: needs [review, e2e] and e2e: needs [fix], excluding e2e makes consolidate wait on review and fix. Template references to an excluded node resolve to empty — {{needs.<id>.report}} and {{needs.<id>.files}} become empty strings and {{needs.<id>.report_lines}} an empty list, so a foreach over an excluded node's lines becomes the usual empty-list skipped group. A when term or structured .output template referencing an excluded node is a Run-creation error.
enabled works on agent, gate, and loop nodes, including loop-body nodes — an excluded body node stays excluded in every iteration and until.run still runs after the remaining body nodes. Excluding every node, or every body node of a loop, is a Run-creation error. Excluded nodes stay excluded across resume, retry, and app restart; only rerun re-evaluates enabled. Preview the effect of a set of args before running with cockpit fleet validate <name> --arg <key>=<value>.
Bounded loops
Use type: loop for a repeat-until-clean step without manually unrolling copies of the same nodes.
repair:
type: loop
needs: [investigate]
max_iterations: 3
on_exhausted: final-adjudication
until:
run: pnpm test
nodes:
fix:
prompt: Fix the failures in {{needs.investigate.report}}.
review:
needs: [fix]
session: continue
prompt: Review the first iteration. Changes: {{needs.fix.report}}
followup_prompt: Re-review after the next fix. Changes: {{needs.fix.report}}
final-adjudication:
needs: [repair]
effort: high
prompt: |
Judge every remaining finding. Dismiss findings below the review bar with reasons.
If a real defect remains, perform one direct fix-and-reverify pass. Never ask a human.
output_contract:
type: object
required: [verdict, accepted, dismissed]
properties:
verdict: { type: string, enum: [resolved, failed] }
accepted: { type: array, items: { type: string } }
dismissed: { type: array, items: { type: string } }
summarize:
needs: [final-adjudication]
prompt: Summarize the final result.
The body is a nested DAG of ordinary agent and gate nodes. Body needs refer to sibling body node ids; a body id cannot duplicate an enclosing node id, and body templates may also read the loop group's upstream nodes. After the whole body succeeds, until.run executes in the Run workspace with the same template rules and may read any body node output. Exit 0 completes the loop. A non-zero exit starts the next iteration. Without on_exhausted, reaching max_iterations fails the loop as before. With on_exhausted, Fleet holds the loop at its existing iteration count and runs the named agent node instead. The target must directly depend on the loop, cannot be a gate or foreach node, and cannot additionally depend on a descendant of the loop. If adjudication completes, the loop completes and downstream nodes proceed; if it fails, the loop fails. A body failure still fails immediately. Nested loops are rejected.
Loop-body agent nodes start a fresh task each iteration unless they set session: continue. A continued node creates its task in iteration 1, then sends followup_prompt to that same task in later iterations and uses its matching report as that iteration's output. If followup_prompt is omitted, Fleet reuses prompt. The task keeps its conversation and can still auto-switch accounts when usage limits require it. Fleet persists queued prompt delivery across app restarts. If the saved task is gone, cannot be resumed, or no longer matches the node's resolved agent, model, effort, account, browser identity, service tier, workspace, or approval selectors after an override, the current iteration falls back to a fresh task with the initial prompt. Changing one of those selectors with fleet resume --set therefore breaks the continuation chain for the next iteration instead of sending followup_prompt to the old runtime. If restart catches a delivery in the ambiguous sending state, duplicate suppression marks the node interrupted and pauses the Run; inspect the target, then use cockpit fleet resume <runId> to issue a new delivery. Retrying the loop deliberately starts a new continuation chain at the next cumulative iteration. session: continue cannot be combined with foreach.
When until passes normally, the on_exhausted node is marked skipped and behaves as a transparent dependency, so summarize in the example still runs. On exhaustion, it waits for the adjudicator. The adjudicator is an ordinary agent node: output_contract, templates, runtime selectors, and downstream references work normally. A downstream node that reads adjudication-only output should use when: needs.<adjudicator>.passed, because no structured verdict exists on the normal-pass path.
For full-dev-flow and review-loop Fleets, make this a high-effort final judge: apply the same triage bar as consolidation, record accepted and dismissed findings with reasons, and allow at most one direct fix plus revalidation rather than another review cycle. It must decide autonomously and never call cockpit ask. Fleet also appends these no-human and no-extra-iteration constraints to the adjudication task. Retrying the exhausted loop without an explicit iteration grant is rejected; retry the adjudication node itself if its task fails.
until.run and command-gate run inherit the user's full PATH, so tools installed by package managers or version managers, such as pnpm, can be invoked by name. Cockpit resolves the login-shell PATH on macOS and Linux. On Windows, the current process PATH stays first and Cockpit appends missing machine/user PATH entries and known CLI locations.
For decisions based on an agent's judgment, prefer a typed scalar from output_contract over grepping a prose report:
repair:
type: loop
max_iterations: 3
until:
run: test "{{needs.consolidate.output.issues}}" -eq 0
nodes:
consolidate:
prompt: Consolidate this pass.
output_contract:
type: object
required: [issues]
properties:
issues: { type: integer, minimum: 0 }
When a legacy free-text report must feed a command, prefer its environment variable instead of splicing {{needs.<id>.report}} into the command text: report content is arbitrary and can break shell parsing. Every until.run and command-gate run receives FLEET_NEEDS_<ID>_REPORT for each body node and upstream node that produced a report, where <ID> is the node id uppercased with non-alphanumeric characters replaced by _ (review-codex becomes FLEET_NEEDS_REVIEW_CODEX_REPORT). Values keep the report tail up to 100000 characters.
When an upstream or body node submitted a validated output_contract value, the same commands also receive FLEET_NEEDS_<ID>_OUTPUT containing that value as compact JSON. Read it with a JSON parser; do not splice string fields into the command text. The JSON is omitted rather than truncated when it would exceed 100000 characters, so a present OUTPUT variable is always parseable. Use a required string field in directory when the command should run in a path from structured output:
tests:
type: gate
gate: command
needs: [select]
directory: "{{needs.select.output.worktree}}"
run: pnpm test
The Run snapshot keeps loop body instances until that loop is retried. Restart and resume preserve the current iteration and its saved body instances, including a partially finished one. A body failure cancels the rest of that iteration, so retrying the loop group discards work that had already succeeded in it; retry --node "<loopId>#<n>.<nodeId>" resumes iteration n instead and keeps it. Retrying a failed loop group discards all of its old body instances but preserves the cumulative counter, then starts the next iteration without rerunning unrelated completed upstream nodes.
Gates
tests:
type: gate
gate: command
needs: [integrate]
run: pnpm test
approval:
type: gate
gate: human
needs: [tests]
ask: Tests are green. Approve the merge?
A command gate runs in the Run workspace and passes on exit code 0. Stopping its Run cancels the command and its descendant processes. Command gates are serialized by the git common dir of the command's working directory, as a defensive mutex against host-global collisions. A non-git working directory, including Cockpit's temporary workspace, is keyed by that path so directory-less global gates serialize across Runs. Agent-node commands are not serialized. The 30-minute timeout starts when the command process starts, not while the gate waits for the lock. Pause drops a queued gate and leaves an already-running one alone. A failed command gate records exit <code> plus the last 50 lines of captured output on the gate-result event for cockpit fleet logs; the desktop timeline shows only that first line. A human gate opens a Cockpit Ask and passes when the user approves; rejection marks it rejected and everything downstream skipped. Dismissing the Ask pauses the Run with the gate interrupted, so resume can ask again. Both count against max_parallel while active, and one human gate per Run is presented at a time. If an agent node itself uses cockpit ask, that node remains running until the Ask is answered and the resumed task reaches a later turn end.
Workspaces
A project Fleet Run creates one shared git worktree on branch fleet/<name>-<runId>, and every workspace: shared node (the default) works there. A workspace: isolated node gets its own worktree on fleet/<name>-<runId>-<nodeId>, which is what makes fan-out safe when several nodes edit the same files. The Run fails before creating any task if its shared worktree cannot be created.
The runtime never merges branches. Give the Fleet an integrate node that reads {{needs.<id>.branch}} and resolves conflicts itself.
workspace follows the node's own directory when it has one. An agent node with directory and workspace: isolated gets a worktree on fleet/<name>-<runId>-<nodeId> cut from the repository at that path, and {{needs.<id>.branch}} resolves to it; with workspace: shared the node works in that checkout as it stands. An isolated directory must be a repository root: a path that is not a repository, or that is a subdirectory of one, fails the node before its task is created rather than running it unisolated or silently at the root. Worktree creation is part of starting the node's task: if git refuses it, no task is started and the node fails with that error.
A global Fleet has no repository of its own, so isolation there always comes from directory (templates allowed, so directory: "{{args.repo_dir}}" works). workspace: isolated without directory has nothing to branch from and validate rejects it. A command gate without directory runs in Cockpit's temporary workspace.
Run and node states
| Node status | Meaning |
|---|---|
| pending | Waiting on dependencies |
| ready | Dependencies satisfied, waiting for a max_parallel slot |
| dispatched / running | The task was created and is working |
| completed | The node's task reported success |
| failed | The task failed, or a command gate exited non-zero |
| interrupted | The task was directly messaged or canceled, removed, or did not survive an app restart |
| canceled | Stopped by cockpit fleet stop |
| skipped | when was false, or a dependency did not succeed |
| excluded | enabled evaluated to false at Run creation. Does not propagate — dependents still run |
| evaluating / waiting-approval | A command gate is running / a human gate is waiting for the user |
| passed / rejected | Gate result |
Run status is running, paused, completed, failed, or stopped.
Resume, retry, rerun
stop terminates active node tasks and command gates before marking them interrupted; resume then reloads the Run, keeps every completed and passed node, and re-dispatches interrupted and canceled nodes as new tasks in the same workspace — the agent sees the work already on disk and continues from there. Nodes that were running when Cockpit restarted are re-attached if their execution runtime is still alive and marked interrupted otherwise; a Run whose only unfinished nodes are interrupted becomes paused on its own.
Sending a direct message to an active node task or canceling its turn also marks the node interrupted, detaches that task from the graph, and pauses the Run. Partial progress is never accepted as successful node output, and downstream nodes remain pending until the Run is resumed. Resume stops the detached task before starting its replacement in the same workspace.
retry --node <id> re-runs one node and resets its downstream nodes, leaving the rest of the graph intact. Retrying a loop group continues at its next cumulative iteration and throws away every saved body instance, including the ones that succeeded in the failed iteration; retry a body node of the current iteration (--node "<loopId>#<n>.<nodeId>") to resume that iteration and keep them. Once its Run-wide budget is exhausted, retry requires --grant-iterations N (1–100, with the granted total budget capped at 100). rerun is the only full start-over: it creates a new runId and repeats everything with fresh iteration budgets.
Cockpit retains the 200 most recent terminal Runs. Use cockpit fleet remove <runId> when a saved Run and its event history should be deleted immediately; a running or paused Run must be stopped first.
Show the Run to the human
cockpit side-panel fleet # Fleet and Run list
cockpit side-panel fleet <runId> # Live graph of a Run
cockpit side-panel fleet <name> # A Fleet's definition graph
cockpit side-panel fleet <runId> --node <id> # A single node's detail view
Open the panel when you start a Run so the user can watch the graph instead of reading status output.
For authoring guidance — designing the DAG, choosing workspaces, mixing agents, and worked examples — read the fleet skill.