Operating modes
drover runs the same lifecycle five ways, from a session you steer turn by turn to a queue that implements, reviews and merges overnight while you sleep. They differ in who steers, what gets recorded, and who does the waiting, and the waiting is where most of the cost goes. This page covers what each mode is for and how they fit together. Lifecycle gives the order of the commands, and the Playbook covers sizing an item and choosing a reviewer.
At a glance
Each mode in the following table is a command you type. Operator sessions are not a command: they're the short interactive sessions you open yourself to make the decisions drover leaves to you.
| Mode | Who steers | Who waits | Choose it for |
|---|---|---|---|
| Interactive run | you, every turn | you | the first item in a project, an item you doubt |
| Headless run | nobody, once started | you, at the terminal | one item at a time, reviewed and audited by hand |
| Drive | nobody | drover's own loop | a queue, bounded by a clock |
| Detached drive | nobody | drover's own loop | the steady state: queue it, get notified, walk away |
| Supervised drive | an agent, per event | drover watch, which blocks |
handling routine parks without you |
| Operator sessions | you | nobody | writing items, accepting, handling a park |
Interactive run
The following command opens the runner's own interface on the item's branch, with the item's prompt as the first message:
drover run B1
You read every turn and can stop it at the third, which makes this the right mode for the first item in a new project and for any item you're not sure is sized right. A badly sized item shows itself early, and you find out while it's cheap.
What it doesn't leave behind is the reason to stop using it: no log, so no self-report for the reviewer, no [~] implemented state, and no record of turns or cost. Interactive or headless has the full comparison.
Headless run
The following commands run one item unattended, then hand its review to a second session:
drover run B1 --headless
drover review B1 --runner claude --headless
drover audit B1 verified --code-by agent "traced each requirement to the diff"
The event stream goes to a log, so drover report B1 shows the run's own account, its turn count and its cost, and the reviewer gets that account to check against the diff. You still decide when each step starts, which is the point: it's the mode for learning what your merge gate and review doctrine catch before you let anything merge without you.
A headless run is not detached. It inherits your terminal, and closing the terminal kills it.
Drive
The following command takes the ready queue and walks every item through implement, review, merge gate, pull request and merge, until the clock runs out:
drover drive --for 8h
Running drive is deciding that green-and-reviewed code may merge without you reading it first. Whether to drive covers what stands between a session and your trunk, and the case for --no-merge on the first night.
Two properties make drive cheaper than the same work done by hand:
- Every stage is a fresh session. An implement session ends when its item is done, and the review starts from the diff and the contract, not from the implementer's conversation. No session's context grows past one item.
- The waiting is Go, not a model. Waiting for CI, for a merge, for the next slot or for a sibling to clear the trunk happens in drover's own loop, and costs nothing.
--jobs N runs up to N items at once, each in its own worktree. That finishes a queue sooner and spends tokens N times as fast. Cost covers what that means across several projects.
Detached drive
The following commands start a drive in its own session, return once it holds the lease, and tell you when it needs a person:
drover drive --detach --for 8h
drover watch --until needs_operator,exit
--detach is drive with the terminal taken out of the picture: stdin on /dev/null, output in a log beside the drive's event file, and nothing tying it to the shell you started it from. drover drive --enqueue <id>... adds work to the running drive, drover attach follows it, and drover drive --stop drains it. drover attach --session shows what a running item is doing, each tool call and message of its session beneath the item's stage line, and --item <id> narrows it to one item.
To be told rather than to watch, give [notify] a destination. An ntfy destination POSTs to an ntfy topic your phone subscribes to; each event it routes arrives with the project and item in the title, the priority you chose for that event, and a body saying what happened or what to do. Put it in the per-user config and one entry serves every project:
[notify.destinations.phone]
type = "ntfy"
url = "https://ntfy.example.com/drover"
token_file = "~/.config/drover/ntfy-token"
events = { started = "low", merged = "low", parked = "default", needs_operator = "high", needs_supervisor = "default", exit = "default", exit_undone = "high" }
That one table tells you when a drive starts and what it queued, each merge and park, every stop that needs you, and how the drive ended. The exit names what was left undone and why, such as E72 skipped: blocked by F14 (parked gate_refused), and an exit that left anything parked, skipped or held goes out at exit_undone's priority, so a drive that exits 0 with work behind it still rings.
For anything ntfy does not reach, a command destination runs an argv with each event it routes as one JSON line on stdin, from the project root, with 30 seconds to finish. The line carries the event, the priority it was routed at and the message an ntfy destination would send. The following entry raises a desktop notification for each stop and merge:
[notify.destinations.desk]
type = "command"
argv = ["notify-send", "drover"]
events = { needs_operator = "high", merged = "default" }
Destinations of different names both apply, each to the events it names, and a slow one delays neither the drive nor the other. drover notify --test sends a test event through each and prints what it answered, so a wrong token turns up before a drive stops for you; drover notify --test --event exit_undone shows what a drive that left work behind sends, at the priority each destination routes it.
Two [notify] keys keep a long drive from waking you for nothing. quiet_hours holds every message routed below high until the window ends and then sends what it held as one message per destination; needs_operator and needs_supervisor are never held, and a drive that exits inside the window folds what it held into its exit, sent at min so ntfy delivers it without sound unless you routed exit at high or above. coalesce folds events of one kind that arrive close together into one message listing their ids, such as filed: B91 B91 B92:
[notify]
quiet_hours = "22:30-07:00"
coalesce = "2m"
A stop you miss still idles the queue until you look. repeat on a destination sends an unanswered needs_operator again on that interval (at least 5m), up to repeat_max times (default 6), titled (reminder N of M), and stops once the item is accepted, merged, rerun or shelved, or the drive resumes from the pause that raised it. Each destination that reminded you then gets one answered: <action> <id> at low. Reminders run only while the drive does: once it exits, its exit message is the last you hear from it.
[notify.destinations.phone]
repeat = "15m"
repeat_max = 4
A config written for an earlier release with [supervise] notify or [supervise.ntfy] is refused at load; scripts/migrate-notify.sh <file> rewrites either into a destination. See [notify] for every key.
This is the mode to settle into. The queue runs, the waiting costs nothing, and the only thing that reaches you is an event a person has to handle.
Running past the weekly allotment
A Claude account runs out twice, in sequence: first the weekly allotment, then any extra-usage credit you bought to work past it. A drive that runs into either without a plan loses the session in flight and stops. Two levels per window tell drive what to do before that.
drover can see the percent of each window left: Claude Code reports the five-hour and seven-day windows on every request, in steps of 1%, and drover keeps the last reading of each session on its usage row. It cannot see a remaining credit balance: once the week reads 100%, the reading says only that sessions are drawing on purchased credit and, at the very end, out_of_credits. The only dollar figure drover holds is each session's list-price cost_usd, which is not what the account was charged.
Choose the levels in the per-user config, since the limit is the account's and every project shares it:
[usage.weekly]
notify_remaining = 20
pause_remaining = 5
- The notify level is how much warning you want. At 20% of the week left,
drivesends oneusage_warningthroughnotifyandntfyand keeps going. Pick it at roughly a day's worth of your usual use, so the notice arrives with time to decide. - The fail-safe level is what you keep back for yourself. At 5% left,
usage_admissionholds the drive: sessions already running finish their stage, since that work is what the margin is for, and nothing new starts. The drive pauses asdrive --pausewould and emitsneeds_operator. Pick it at what an operator session or two of your own needs, so a drive never spends the last of the week on your behalf. - A hold inside the bound lifts itself. Under
--for 48h, a weekly reset 30 hours away is inside the bound, and the drive resumes at the reset with no one typing anything. Under an unbounded drive it waits fordrover drive --resume, and a deadline that arrives first ends the drive with exit 0 and the waiting items reportedheld. - A refusal holds too. A session the service turns away for quota sends its item back to the head of the queue at the stage that was refused, and the drive holds as above, instead of parking the item
runner_fault. - One reading can cross several holds, and each
--resumereleases one. A week at 0% left with sessions on purchased credit crosses both the weeklypause_remainingand[usage.overage] mode = "pause". Resuming past the weekly percent is spending its last points, not consenting to credit, so the firstdrover drive --resumereleases the weekly hold and the drive pauses again on overage; the second releases that. The pause line names every hold standing behind the one taken,needs_operator'srunsays what each resume releases,drover drive --resumeprints the hold it released and the ones still standing, anddrover driveslists them besidepaused.
[usage.overage] decides what happens once the week is spent and credit is in play. The default, mode = "pause", holds the drive the moment a reading shows sessions on purchased credit, so nothing spends it unattended. mode = "continue" runs on credit, and max_usd bounds it at a list-price sum across every registered project since the week began, with notify_usd as the warning on the way. Since the sum is list price and the balance is invisible, set max_usd comfortably below what you bought, and expect out_of_credits to hold the drive anyway if the estimate runs long. Under continue, leave [usage.weekly] pause_remaining unset if you mean to run straight onto credit, or drover drive --resume past the weekly hold when it comes.
drover status prints the reading as one footer line, and drover drive --dry-run says whether usage_admission would hold now and why. Every key is in [usage].
Supervised drive
drover supervise launches one interactive session with DROVER_ACTOR=supervisor to watch drives and take the actions this project's [supervise] policy assigns to a supervisor: resolving a branch-update conflict, relaunching an item whose session died, writing the requirements of a scaffold a review filed. The following command prints what that session will be told without launching anything:
drover supervise --dry-run
Its prompt is built to keep the session small. It waits with drover watch --until parked,needs_operator,merged,exit, which blocks until one of those events arrives, so the session wakes once per event rather than once per poll. It is told not to loop on sleep, the ledger or the lease, and it stops when the queue is empty and nothing at supervisor level is left.
Choose it when routine parks would otherwise wait for you overnight. Skip it when a detached drive and a notification cover what comes up: a session that wakes on events still pays to read its whole conversation on every wake, and a drive with notify configured pays nothing to wait.
Who handles each event
A drive meets situations it can't settle from the merge gate and the review alone: a branch-update conflict after a sibling merged, an implementing session that died, a scaffold a review filed, a pull request held by protected_paths. [supervise] decides, action by action, who handles each one. Every action takes one of four policies:
| Policy | What happens |
|---|---|
auto |
The drive does it itself, with no session involved. |
supervisor |
The drive emits needs_supervisor and leaves it for a drover supervise session. |
operator |
The drive emits needs_operator, sends it to [notify], and waits for you. |
off |
Nothing happens, and the item stays parked. |
A project with no [supervise] table runs every action at its default, and several defaults are supervisor. resolve_conflict, relaunch_after_runner_death, write_scaffold_requirements, close_changes, enqueue and cleanup_interrupted_drive all default there. With no drover supervise session running, those events park and wait, and a [notify] destination reaches you for them only when its events names needs_supervisor. In practice someone notices the parks and starts handling them in whatever chat is open, and that chat becomes a supervisor nobody configured: the long-lived session that cost tracking catches later.
Decide each action once instead. The following command walks the catalog by category and offers each action only the policies it allows. It appends a [supervise] table to .drover.toml when the file has none, and prints one to paste over the existing table when it has one:
drover init supervise # asks about every action
drover init supervise --defaults # writes every action at its default, to edit by hand
Without --defaults it needs a terminal to ask on, and inside a session drover launched it refuses whatever the flags, because those policies bound that session. A policy left at its default is written commented out, so --defaults changes nothing about how a drive behaves: it lists every action in the file, and you decide by uncommenting a line and changing its value. Some guidance for the choices:
- Raise what you'd always approve to
auto.resolve_conflictandrelaunch_after_runner_deathcan reachauto, and atautoneither needs a session. - Some actions stop short of
autoby design.merge_drive_pr,merge_protected_paths,write_scaffold_requirements,triage_findingandclose_changescap atsupervisor, andaccept,signoff,override_review_limit,auth_expiredandpush_refused_by_hookcap atoperator. Asking for more is a load error that names the ceiling. - Don't leave anything at
supervisorunless you rundrover supervise. Without one, set it tooperatorso a[notify]destination routingneeds_operatorreaches you, or toautowhere the ceiling allows.
drover config prints every action's effective policy, marked default where the file sets nothing. Read it after any drover upgrade. A project whose config predates [supervise] has no table, so the defaults apply without the file saying so. drover config --diff lists each action no file sets at its default and names the ones at supervisor, and once the ledger records a drive drover check reports SUPERVISOR UNSET for them. Both point at drover init supervise.
Operator sessions
Some decisions drover leaves to you on purpose: writing an item's requirements, drover accept, merging what protected_paths held, reading a park and deciding what it means. You make them in an interactive session of your own, and how long that session lives decides most of what it costs.
Open one for a decision and close it when the decision is made. For example:
- A notification says
B12parked ontrunk_conflict. - Open a session, run
drover show B12, resolve the conflict, and push. drover drive --enqueue B12, then close the session.
The next park gets a new session with an empty context. One session kept open across days of drives becomes the most expensive thing on the account without doing any of the work: Long-lived sessions explains why.
With a T3 Code host paired to your phone, the event can open that session for you. A command destination whose argv is drover t3 thread --from-event opens one thread per needs_operator or needs_supervisor, in the [t3] project, titled <project> <id>: <action>. Its first message is the notification's text, the output of drover show <id> --no-tracker, and the command the event names, fenced, as the first thing to run. The thread runs under [t3] operator_permission, which only takes supervised: it was opened with nobody watching, so every tool call waits for your approval. A second stop for the same item while its thread is still open goes into that thread as a new message; a thread you stopped or deleted gets a new one. The drive records each thread as a thread_opened event that drover attach and drover watch show, and the item's later ntfy messages (a reminder, its parked, its merged) open the thread when no pull request exists yet. drover notify --test --event needs_operator sends a sample through it and prints the thread it opened, so a wrong project name or an expired grant shows up before a drive parks.
T3 Code threads
Any of the modes above can run its stages as T3 Code threads instead of a CLI drover execs, so each stage's transcript and checkpoints are readable from the T3 Code app on a phone while it runs. drover still assembles the prompt, owns the branch and worktree, reads the result and writes the ledger. The t3 runner is drover t3 run, which launches the thread over the server's MCP endpoint, follows it to the end and prints a log drover reads like any other runner's. The bridge runs on the same host as the T3 Code server, because that is where it reads usage from.
Setting the host up takes four steps:
- Add each drover project as a T3 Code project on the host:
t3 project add --title drover <checkout root>. The bridge never creates one, and a project it cannot find stops the stage with an error naming that command. The title is what[t3] projectnames, the drover project's own name by default. - Set
[t3] urlto the server's/mcpaddress, in the per-user config. - Run
drover t3 login. The sign-in page asks for a pairing code, which you mint on the host witht3 auth pairing create, and for an access level. Choose Full access for the grant drive stages run under: a Supervised thread waits on approvals, and nobody answers them during a drive.drover t3 discoverthen prints the server's version and its tools. - Run
drover check. On the T3 host it reports a missing or refused grant asBRIDGE SIGNED OUT, and a server version the bridge was not tested against asBRIDGE VERSION.
On the phone, each stage is a thread in the project, titled from the first line of its prompt, with its transcript, its tool calls and a checkpoint at the end of each run. A thread outlives its stage. When drover stops a stage early, the bridge interrupts the run it launched and leaves the thread readable as the record of what happened; it never touches a thread it did not launch. drover t3 thread --stop <id> interrupts one by hand.
Usage tracking goes through one more hop. No MCP response carries turns, tokens, cost or the rate-limit reading, so the bridge reads them from the provider event log T3 Code's Claude driver writes on the host, and prints them in the shape a direct claude log carries. usage_admission, [usage.weekly] and drover report then treat a T3 Claude thread like a direct session. That log is T3's internal format and is documented nowhere, so a T3 release can move it without the MCP server reporting an error. When it does, or for a Codex thread that writes no such log, the bridge says usage_unavailable rather than reporting zeros, drover drive --dry-run says usage_admission is blind for that runner (read off its newest completed session, so stopping a stage does not count), and a drive on it refuses to start while [usage.weekly] sets a level, unless you pass --allow-blind-usage. A thread costs more context than an exec, too: Cost has the measurement.
Recommended setup
Most projects end up with the following arrangement once the first few items have proved the merge gate:
-
Run
drover init supervise, so every action has an owner you chose (Who handles each event). -
Write and accept items in a short operator session.
-
Start
drover drive --detach --for <hours>from a terminal, not as a background task inside an agent session. -
Let a
[notify]destination reach you onneeds_operatorandexit_undone, and usedrover superviseonly if routine parks keep waiting overnight. On a T3 Code host, put a thread destination beside the ntfy one, so each stop arrives with a session already opened for it (Operator sessions):[notify.destinations.phone] type = "ntfy" url = "https://ntfy.example.com/drover" token_file = "~/.config/drover/ntfy-token" events = { needs_operator = "high", needs_supervisor = "default", parked = "default", exit_undone = "high" } [notify.destinations.thread] type = "command" argv = ["drover", "t3", "thread", "--from-event"] events = { needs_operator = "high", needs_supervisor = "default" } -
Set
[usage.weekly]notify_remainingandpause_remainingin the per-user config, so no drive spends the end of the week for you. -
Handle each event in a fresh operator session.
-
Read
drover status, or the page[[status.render]]writes, whenever you like: reading it costs nothing.
Where to go next
- Cost: where tokens go in each mode, and the patterns that multiply them.
- Playbook: sizing items, choosing a reviewer, and the case for and against driving.
- Configuration: every
[supervise]key,notifyincluded. - Automation: the event shapes
drover watchanddrover attachprint.