Autopilot — concepts
⚠️ Autonomous operation is inherently risky. When an autopilot plan is running, a manager agent drives a fleet of worker agents without human intervention. Review the Autopilot guide — in particular the risk warning and pause controls — before running a plan.
Autopilot is warden’s long-running autonomous mode: you author a goal in a
plan and warden plan run <id> --mode autopilot, and warden spins up a manager agent that drives a
fleet of worker agents, lands their branches into an integration branch, heals
itself when it gets stuck, and escalates to progressively cheaper backends if
rate-limited — all without waiting on a human.
Agent topology
Section titled “Agent topology”A run is a small fleet with clearly separated jobs:
- Manager (role
autopilot) — one long-lived headless agent per run. It decomposes the goal, spawns and steers workers, routes approvals, and lands finished work. (Historically called “the brain”; the ledger keyautopilot.brainkeeps that name for back-compat.) - Worker (role
worker) — the manager spawns one per task by default. A worker owns its task end-to-end — implement → self-review → open a PR on the integration branch → drive CI green → merge — and reports status back to the manager. For a large task the manager may instead spawn a pipeline ofimplementer/reviewer/auto-mergeragents. - Resolver (role
brain) — an on-demand agent the manager spawns to unblock a stuck worker or make an ad-hoc design/architecture decision without human interaction, then report the call back. Short-lived; not every run needs one. - Overwatch — a daemon-internal backstop (not an agent) that nudges a live-but-quiet manager to tend workers that fall idle or wait on input. See Overwatch below.
Core concepts
Section titled “Core concepts”Plans are canonical ScrivaDB records. Create one with warden plan create (not
warden autopilot init, which no longer exists) and start it with
warden plan run <id> --mode autopilot. Repository YAML under plans/ is an
optional inert export — not required to run.
A plan has a goal, optional constraints (injected into every manager and
worker spawn), and a task DAG. The manager decomposes the goal if you leave
the task list empty.
version: 1goal: "Ship the notifications feature"constraints: - "all changes behind a feature flag"tasks: # optional — the manager authors these if absent - id: api prompt: "Implement the notifications REST API per docs/specs/notify.md" - id: ui prompt: "Implement the notifications UI components" after: [api]| Field | Required | Notes |
|---|---|---|
version | yes | Must be 1 |
goal | yes | What the run is trying to accomplish |
constraints | no | Injected verbatim into every manager and worker spawn |
tasks[].id | no | Unique within the plan; manager authors tasks if omitted |
tasks[].prompt | no | Worker’s task description |
tasks[].after | no | Dependency edge ids |
A plan’s definition is editable only while pending. A running plan cannot be patched (409).
The daemon’s execution of one plan: one manager + its workers + its ledger. The run is bound to the canonical Plan and continues as the same logical run when restarted (the ledger persists landed tasks across manager restarts).
Multiple named runs per repository. Independent plans (e.g. plans/release.yaml
and plans/notifications.yaml) can be active concurrently — each gets its own
per-plan integration branch and stable slot ids so they cannot land into each
other’s trees.
There is no per-repo enable switch. Starting a plan with
warden plan run <id> --mode autopilot is what starts a run, and
warden plan pause|resume|stop controls it. warden plan show <id> --watch is
the live status view (executor state, backoff, integration branch, per-task
worker/PR). The plan/manager/merge template stays global in the autopilot
config block. warden autopilot status [--json] lists every run with its
integration_branch, manager_slot_id, and slot_scope (it absorbed the old
autopilot run list columns).
Removed commands. autopilot enable|on|register|start|disable|off|pause|resume|stop|unregister|list|run
and the MCP tools set_autopilot, register_autopilot_run, control_autopilot_run,
retarget_autopilot_run, rename_autopilot_run and list_autopilot_runs no longer
exist. Use plan run|pause|resume|stop; typing an old command prints its replacement.
Plan-scoped hierarchy
Section titled “Plan-scoped hierarchy”Each run renders as a stable tree root (the plan display name) on TUI, web, REST, MCP, and CLI:
<plan-name>├── <scope>-autopilot # manager slot (role autopilot)├── plan # checklist from plan YAML + ledger└── workers # grouped by ledger state └── <task-id><scope> is derived from the plan name (sanitized; disambiguated when two runs
would collide). Session records carry explicit back-refs — autopilot_run_id,
autopilot_slot (autopilot | worker; guardian only on legacy pre-drop sessions), and autopilot_task_id
(workers) — as the display contract. The run:<run_id> tags remain the
authorization channel. Workers are not parented to the manager via parent_id.
The daemon’s supervisor is not a session and does not appear in the tree; wd autopilot status reports it as the run’s supervisor field (kind daemon, never addressable).
Manager
Section titled “Manager”A single long-lived headless agent (role autopilot) occupying the stable
<scope>-autopilot slot. The Controller spawns it on the cheapest available
backend (see Cost-tier backend selection below). Guardian heal-ladder rotation
(stages 2–3) is an in-place hot-swap into this slot — the id survives
rotation and daemon restarts; context is handed off via
.warden/handoff-<slot-id>.md. The manager:
- Reads the canonical Plan and decomposes the goal into tasks (if absent from the plan)
- Spawns worker agents via warden’s MCP tools — the daemon tags each spawn with
autopilotandrun:<run_id>automatically, inherited from the calling agent’s identity, so the fleet roster never depends on the manager remembering to pass tags (this extends to pipelines the manager creates) - Spawns a resolver (role
brain) on demand when a worker needs an ad-hoc design/architecture call it can’t make itself - Watches for landed tasks and re-plans when the task set changes
- Routes approval prompts to itself (not to the operator) while autopilot is active
- Heartbeats so the guardian can detect if it stalls
The manager is a regular warden agent — you can tail its output, inspect it with
warden status, and the guardian heals it when it stalls. (The ledger records
its agent id under the key autopilot.brain, which keeps that historical name
for back-compat.)
Workers
Section titled “Workers”Agents (and pipelines) the manager spawns to do the actual coding work. By
default a task gets one worker-role agent that owns it end-to-end — implement →
self-review → open a PR on the integration branch → drive CI green → report done — and
reports status back to the manager; a large task may instead get a pipeline of
implementer/reviewer/auto-merger agents. Every worker is tagged autopilot
run:<run_id>. Workers operate exactly like normal warden agents: they have their own isolated worktrees, run the project’s checks, commit viawarden commit, push, and open a PR. When a worker finishes, it ends withwd job done; the daemon owns landing. It waits for the worker PR’s gate, uses the bounded CI fix loop for a red gate or conflict, and lands a green worker PR into the integration branch.warden autopilot landis the idempotent manual fallback, not the normal worker path.
Manual agents are invisible to autopilot’s destructive paths — autopilot
never terminates or modifies an agent that doesn’t carry the run:<run_id> tag.
Resolver
Section titled “Resolver”A short-lived brain-role agent the manager spawns on demand to unblock a stuck
worker — an unanswerable prompt, an ambiguous requirement, or an ad-hoc
design/architecture decision that must be made for the work to continue. The
resolver gathers just enough context, makes a decisive call without any human
interaction, reports it back to the manager (and the stuck worker), and
finishes. Not every run needs one.
Ledger
Section titled “Ledger”The daemon’s durable record of run state: which tasks are pending / in-progress / landed, which branch each landed task produced, and how many heal attempts have been made. Written authoritatively by the daemon (landings) and by the manager (task state transitions). Persisted across manager restarts and daemon restarts — re-enabling autopilot after a disable continues from where the ledger left off.
Canonical storage is a JSON array at autopilot.<run_id>.tasks (dot-namespaced
ctx keys). Each row’s state is one of pending, assigned, in_progress,
pr_open, gated, landed — validated on write. Optional overlay keys
autopilot.<run_id>.tasks.<id>.state and .branch exist for TUI segmentation;
the array is the source of truth. Landings live at
autopilot.<run_id>.landings. Plan-file statuses (pending / active / done
/ failed) are a separate checklist, not ledger states.
Ledger task state machine (intended transitions; illegal-transition checks are not enforced on write):
pending → assigned → in_progress → pr_open → gated gated → landed (gate passed; authoritative, daemon-written) gated → fixing → gated (gate red / conflict; manager heals or respawns worker)any → replanned (manager revises decomposition; audit-logged)fixing and replanned are manager-side notes / future stored values, not in
the canonical write set today.
Integration branch (per-plan)
Section titled “Integration branch (per-plan)”Each run resolves one integration branch at register/preflight, persists it on
the run record, and surfaces it in status APIs and the manager digest. Workers
open PRs against that branch — never main directly.
Default for new runs: autopilot/<sanitized-plan-name>. Concurrent runs in
one repo each get a distinct branch.
Config (autopilot.merge.target_branch):
| Template | Behavior |
|---|---|
empty or autopilot/integration (legacy default) | Derive autopilot/<plan> per run |
contains {{plan}} | Expand to sanitized plan name |
| other | Custom global override |
Runs already on autopilot/integration are grandfathered — warden never
re-derives a stored branch.
CI (gate: auto): workflows must list autopilot/** under
on.pull_request.branches to cover every per-plan branch. Listing only
autopilot/integration does not cover autopilot/<plan>; when no workflow
matches, gate: auto downgrades to local and preflight emits a warning.
The integration branch is never merged into main automatically. Once all
tasks are complete, the daemon opens one final integration → default-branch PR;
the operator reviews and merges that PR.
Boundary invariant: workers never commit to main directly. The integration
branch is a staging area for owner review.
Guardian
Section titled “Guardian”The daemon’s heal loop that keeps the manager alive. It runs continuously while autopilot is active and fires when the manager’s heartbeat goes stale (wedged with pending work). The heal ladder:
| Stage | What the guardian does |
|---|---|
| 1 — nudge | Send a steering message to the manager |
| 2 — restart | Hot-swap the manager in-place on the same backend (stable slot id) |
| 3 — rotate | Hot-swap onto the next backend down the cost tier (slot id unchanged) |
| 4 — backoff | All backends exhausted or rate-limited: wait (capped-exponential backoff), notify, then retry from stage 1 |
Progress watchdog. The heartbeat ladder only catches a dead manager. A
second check catches a live but going nowhere one: when an active run has had no
progress (ledger task change, landing, plan task status change, worker spawn) for
autopilot.guardian.progress_watchdog_window (default 2h) and no run-tagged
agent is working, the guardian walks the same ladder (watchdog-specific nudge →
restart → rotate), sharing the heartbeat grace so only one rung climbs per grace
window. Overwatch nudges and heartbeats are not progress. After the ladder the run
parks as needs-attention (no_progress) and the operator is told to use
wd plan restart. Disable with progress_watchdog_enabled: false.
Missing manager session (no heartbeat wait). Each guardian tick also checks that the manager’s session still exists. If it was deleted or archived, was terminated (terminate_agent, or a done/errored/orphaned record), or its tmux session is gone (tmux death, reboot or daemon-down loss; a tmux-gone observation must persist 20s so an out-of-band hot-swap is not mistaken for a death), the guardian does not wait for the heartbeat timeout and does not nudge a ghost: it clears the stale manager record, writes an autopilot.manager_missing audit event, and goes straight to the respawn step in the same slot; once the successor is up it writes autopilot.manager_respawned naming the cause (missing, terminal, tmux_gone). The successor’s digest tells it to reconcile the ledger, open PRs and live workers before acting. wd agent terminate <manager> is therefore not a stop — use wd plan pause/stop, which the guardian honours. While the slot is empty warden autopilot status reports the run as healing with no manager id (never an id that resolves to nothing); if the respawn fails it becomes degraded with the usual backoff. Only a definitive “not found” counts — a store error or daemon restart is treated as unknown and ignored — and a manager that exists but is quiet, busy, rate-limited or mid hot-swap still follows the normal ladder.
Guardian rotation calls Lifecycle.HotSwap directly — not the backend
quota-recovery coordinator. Cold start adopts a live <scope>-autopilot session
when present.
The guardian never parks permanently — it always eventually retries. Backoff
state is visible in warden autopilot status (fields: backoff, tier,
last_heartbeat, context_level).
A planned rotation (context critical or cadence interval reached) uses the same hot-swap seam: the manager saves a summary to the ledger, the guardian hot-swaps it onto a fresh context, and work continues — the slot id does not change.
Overwatch
Section titled “Overwatch”The guardian keeps the manager alive; the overwatch keeps the manager
doing its job on the workers. A manager can be perfectly alive yet quietly stop
tending its fleet, so warden adds a mechanical backstop that never relies on
persona discipline. The overwatch is daemon-internal — not an agent, not a
scheduled agent — and derives each run’s worker roster purely from the
run:<run_id> tag (nothing is persisted; a restart re-adopts the fleet for free).
It nudges a live manager on either of two triggers, both skipped while the manager is busy (a busy manager will see its workers on its own):
| Trigger | When it fires |
|---|---|
| Event-driven | One or more workers fall idle or start waiting on input, debounced to at most one nudge every ~5 minutes |
| Periodic | A heartbeat check-in roughly once an hour, so an idle manager keeps reconciling and pulling the next task |
Each nudge names the needy workers and asks the manager to answer/steer anything
waiting_for_input and clean up finished/idle workers before pulling the next
task. The cadences are fixed, generous constants — the overwatch is a backstop,
not a pacer (see warden’s frictionless-safeguards philosophy). It complements
the guardian, which watches manager liveness. With Fast-Brain triage enabled,
overwatch can resolve a recognized prompt, resume a rate-limited worker, or
redeliver its brief; otherwise it includes a targeted finding in the manager’s
nudge. It never restarts, terminates, or removes a worker.
CI, landing, and usage-limit recovery
Section titled “CI, landing, and usage-limit recovery”Worker PRs target the integration branch. The daemon watches their gate: a green PR is auto-landed, while a red gate or conflict enters a bounded fix loop. It collects failing-job evidence, may rerun a failure classified as flaky or infrastructure, and otherwise sends the evidence to the owning worker for a fix. If the bounds are exhausted, the task remains visible for resolver or operator triage rather than being silently landed.
Usage-limit and rate-limit signals are recovery inputs, not successful work. The
guardian and overwatch triage can resume an eligible rate-limited session; when a
manager cannot be resumed, the guardian’s cost-tier ladder can hot-swap it or
back off and retry. Status reports the backoff kind and error, so an operator can
distinguish a temporary backend_unavailable from a definition or repeated spawn
failure.
Cost-tier backend selection
Section titled “Cost-tier backend selection”The manager spawns on the cheapest available backend for the run. The cost ladder, from cheapest to most expensive:
| Tier | Backends | Notes |
|---|---|---|
| Free | antigravity | Google-hosted free tier; first choice when available |
| Subscription | claude, codex | Your existing plan; no per-token cost on top |
| Gated pay-per-use | any backend with API billing | Requires explicit opt-in in config |
The Controller also exposes SelectWorkerBackend(runID) — the manager can use it
to pick the cheapest available backend for each worker spawn, though the manager
may also select backends itself based on the task.
The ladder and the pay-per-use gate are derived from the backend
registry — only installed, enabled
backends are eligible, bucketed by the tier you set with warden backend tier. The gate is the store’s allow_paid_autopilot setting.
Known limitation: rotate (stage 3 of the guardian heal ladder) requires more than one free-tier backend to exercise meaningfully. If only one backend is available in the free tier, the guardian falls back directly to backoff after restart fails.
Ownership guard
Section titled “Ownership guard”Every autopilot-created agent (manager, workers, resolvers) carries an autopilot
tag and a run:<run_id> tag. The daemon enforces ownership: operations that
would destructively modify an autopilot-owned agent (terminate, remove-worktree,
hard-delete) from a different agent (one without the matching run:<run_id> tag)
are rejected with a 403 not_owned error. This prevents a manual agent or operator
mistake from silently clobbering an in-flight autopilot worker.
The operator (human, CLI, or MCP acting on behalf of a human) can always terminate or modify autopilot agents directly.
Approval routing
Section titled “Approval routing”While autopilot is active, the daemon routes approval prompts from worker agents to the manager’s inbox rather than the operator’s attention queue. The manager uses its auto-approve policy to answer routine tool-permission prompts without stalling workers on human input.
The operator’s approval queue is unaffected by autopilot — prompts from manual agents still surface normally.
Run state machine
Section titled “Run state machine”plan run ──▶ starting ──manager healthy──▶ active │ │ │ spawn fails │ manager wedged / rate-limited ▼ ▼ degraded ◀──────────────── healing │ guardian backoff loop (never parks) └──heal succeeds──▶ activeactive ──plan pause──▶ paused ──plan resume──▶ activeactive ──done_when verified──▶ finalizing ──final PR green──▶ awaiting_merge ──PR merged──▶ complete (finalizing / awaiting_merge are reported states over internal `active`; once awaiting_merge the manager and run agents are torn down and the daemon polls the PR; a closed PR parks the run as `final_pr_closed`; on merge the integration branch is deleted)plan pause is effective at any state. It stops all new spawns and landings
immediately; in-flight workers keep running to completion. The ledger is retained
so plan resume continues from where the run left off. (The deprecated
autopilot disable does the same for every active run in the repo.)
While a run is active, two daemon-internal supervisors share the guardian’s
ticker: the guardian heals manager liveness, and the overwatch nudges a
live-but-quiet manager to tend idle/waiting workers.
Boot recovery after a daemon restart
Section titled “Boot recovery after a daemon restart”On restart, live runs are re-preflighted. Structural plan failures (missing
file, bad YAML, empty goal, broken dependency graph) leave a legacy file-only run degraded
but keep a plan watcher running — restore or fix the file and the next tick
promotes the run and respawns the manager. Content-only failures (invalid
task status, done without landed_pr) do not strand the run: the daemon
normalizes those statuses to pending, spawns the manager, and surfaces the
coercions on warden autopilot status as preflight_warnings. Fix the plan
file to clear the warnings. Enable / start / resume stay strict — only boot and
the degraded watcher apply this leniency.
Plan-bound runs (plan id set) recover their goal, constraints and tasks from
ScrivaDB on boot and whenever the manager is spawned or rotated. A missing or
unparseable YAML export never fails preflight and never marks them degraded;
only legacy file-only runs rely on the file and its watcher.
Failure kinds and the needs-attention park
Section titled “Failure kinds and the needs-attention park”A failed manager spawn is recorded on the status backoff object as a kind plus
last_error: backend_unavailable, no_backend_selectable (the only case
reported as “all backends rate-limited”), definition_error, or spawn_error.
Transient kinds back off (capped-exponential) forever. A definition_error
parks the run after one attempt; identical spawn_error text parks it after
autopilot.guardian.max_identical_failures consecutive failures (default 5,
hot-reloadable). Parking sends one operator notification and writes one audit
event per episode — later ticks do not escalate — and the tree shows the run as
waiting (ordinary degraded/backoff still shows as an error). Clear the park by
changing the plan in ScrivaDB, running wd plan resume (or pause then
resume), or restarting the daemon; each resets the counter and retries the
heal ladder from the top.
Completion phase and the final PR
Section titled “Completion phase and the final PR”When the manager has verified done_when, it calls autopilot_complete. The
daemon—not the manager—then opens and gates one final PR from the integration
branch to the default branch. A green final PR moves the reported run state to
awaiting_merge; the canonical Plan remains in_progress until the PR is
observed merged.
No run agent stays alive just to wait. The daemon polls the recorded final PR at
autopilot.completion.merge_poll_interval (default 2m, minimum 30s) and does
not merge, approve, or close it. A merged PR completes the Plan and removes the
integration branch only when its tip is the merged tip. A closed-unmerged PR parks
the run as final_pr_closed; reopening it, wd plan resume, or wd plan stop are
the operator paths forward. The final PR, its gate, and the integration-branch fate
are retained in the Plan execution summary—not written into an optional YAML export.
If the final PR head changes or becomes conflicting, dirty, or protection-blocked,
the daemon leaves awaiting_merge, brings the integration branch current when
needed, and gates it again. A red gate uses the bounded final-fix loop; it does not
create a second final PR. See the
plan-finish-flow spec.
The final PR carries a conventional-commit title derived from the landed commits
(type by priority feat, fix, perf, revert, refactor, docs, test,
build, ci, chore; shared scope; ! if breaking). Squash-merge it and
wd release reads that title — and any * type(scope): … bullets in the squash
body — to recommend a bump; editing the title before merging changes the
recommendation.
Config hot-reload
Section titled “Config hot-reload”The autopilot config block hot-reloads with no daemon restart — editing
~/.warden/config.yaml re-applies the plan/manager/merge template, backend cost
ladder, and guardian heal thresholds on the next tick and re-runs the per-repo
reconcile. Adding a
plans[] entry starts it; removing one tears down its run. Only the guardian tick
interval is read once at loop start and needs a restart; a syntactically bad
edit keeps the last-good config and alerts the owner.