Working unattended

A session normally waits for a message. Four things let it take a turn when nobody sends one.

All of them carry the same distinction: a turn taken is not a goal reached. None of them can end a goal, because only /goal done does that, and it is yours to say.

A turn on the clock

/heartbeat 10m       # every ten minutes
/heartbeat 45s       # every 45 seconds
/heartbeat 2h        # every two hours
/heartbeat 30        # a bare number means minutes
/heartbeat           # when is the next one due
/heartbeat off       # stop

A heartbeat turn arrives as a message you did not write, and it says so: it names the standing goal, asks for the next step toward it rather than a status report, and states that a heartbeat firing is not progress.

The clock is only consulted between turns, so a heartbeat that comes due while a turn is running is skipped rather than stacked on top of it. The next heartbeat says how many were skipped, so the model knows more time has passed than the interval suggests.

An autonomous run

/goal make the test suite pass on the new provider
/gate cargo test
/autonomous rounds=40,seconds=900

/autonomous keeps taking turns toward the standing goal until something stops the run. It needs a standing goal to run toward and refuses without one. Four things end it:

The budget uses the same three keys as a turn budget — rounds, output_tokens, seconds — with a longer life: it spans the whole run, so the tool rounds and output tokens of every turn add up, children included, and the clock runs from the start of the run rather than the start of a turn. Every turn is told how much is left. When it runs out, the run stops and says so in the same words a turn budget uses — reaching a limit does not imply the task succeeded.

/autonomous status says what is left of the run, /autonomous off stops it.

Quality gates

/gate cargo test
/gate list
/gate clear

A gate is a command that has to pass between the turns of an autonomous run. It goes through the permission engine like any other command, so a gate the rules forbid stops the run instead of running anyway — it is not a way around the rules.

A failing gate stops the run and its output is carried into the next turn of that session, because a gate the model cannot read teaches it nothing. A passing gate is reported as exactly what it is: that check passed, and nothing else follows from it.

Waking a session from outside

revolve wake nightly
revolve wake nightly "check whether the build broke overnight"

This gives a turn to a session from outside the process. If the session runs detached, the message goes down its socket; if it does not, the session is started detached first. The message arrives wrapped the way a heartbeat is, saying that the clock started this turn.

Putting a wake into cron

revolve schedule 03:30 nightly "check the build"
revolve schedule "*/15 9-17 * * 1-5" watcher
revolve schedule @reboot morning
revolve schedule                       # what is scheduled
revolve schedule --remove nightly

The time is HH:MM for a daily run, a five-field cron expression, or a shorthand like @daily or @reboot. The state lives in your crontab as a marked pair of lines per schedule. Foreign lines are copied through untouched, and scheduling the same session twice replaces its entry instead of stacking a second one.

Cron rather than a timer inside the process, because the schedule worth having is the one that starts a session that is not running: a timer can only wake the process holding it, and it dies with the machine. Cron survives a reboot and exists on the machines this runs on, where systemd user timers do not. What lives outside the program is one line of "when"; everything about what waking means stays in revolve wake.

The task queue is in the prompt

A session reads the project's task folder — ~/.config/task-agent/projects/<repository path with the slashes turned into dashes>/tasks/ — at the start of every turn and the open tasks are part of its system prompt: the file names, the priority, tier and kind of the ones that carry a header, and nothing of the bodies. A task whose first line defers it — wait 3d counted from when the file was last written, wait 2026-09-01 until that date — is left out, which is what keeps a queue of forty files from reading as forty things to do now. With --lane the session sees only the files whose names start with its own lane.

The format is the one task-agent writes, so the two programs share one queue without either of them owning it. Nothing schedules from it: a session is told what is open and decides, which is the difference between a queue and a dispatcher.

Working the queue instead of a single objective

There is no task mode, and there is deliberately none: it composes out of three things that already exist. A session started with --lane tui sees the open tui-* files and no others, a goal aims it at them, and an autonomous run keeps it going:

revolve --lane tui
/goal Work the open tasks, one at a time, and delete each file when it is done
/autonomous rounds=40

The three setups worth naming are the same three pieces arranged differently. A coordinator per repository is a session with --role coordinator and no lane, which sees every open task and hands them out with StartAgent. A worker is a session with a lane, which sees only its own. Whether an agent may take another agent's work is therefore not a mode but the presence or absence of --lane, and that is the whole of it.

What none of this does is decide for you. A run stops when its budget is spent, when a gate fails, when you send a message, or when the model proposes the goal is met — and a queue that has run empty is not the same thing as work that is finished.

An agent that only coordinates

Once an agent can start agents, the useful shape for a long-running session is one that does no work itself: it holds the picture, hands each job to an agent of its own, and stays free to answer you. Nothing new is needed for it — the pieces are the role document, StartAgent and the messages back.

Write the role once, as a memory file under subagents/ beside the memory directories:

~/.config/task-agent/subagents/coordinator.md

You coordinate and do not implement. When a request needs work in files,
start an agent for it with StartAgent and give it the whole task; when it
answers, decide what happens next. Keep your own turns short, because the
user is waiting in this window.

Then run a session as that role, and let it hand out the work:

revolve detach coordinator --role coordinator
revolve attach coordinator                 # talk to it whenever you like

What it starts with StartAgent outlives its own turn, keeps a conversation and a token bill of its own, and answers by SendAgentMessage. A message names the directory it came from, so a coordinator with agents in several repositories can tell which one answered. Sending one comes back with a receipt: delivered when that agent was idle and takes the message up now, queued when it is in a turn and reads the message when that turn ends — which is how a coordinator tells an agent working on its request from one that has not seen it yet. Giving a started agent a role of its own is what makes it a personality rather than an agent that was told something once, and an every puts it on the clock.

The roles that ship with it

A fresh install is not empty. Five role documents are written into subagents/ beside the memory directories on the first start, so that turning a session into a reviewer or a coordinator takes a flag rather than a briefing:

revolve --role reviewer          # this session reviews and writes nothing
revolve detach coordinator --role coordinator

They are marked builtin true in their own header, which is how /role tells a shipped role from one you wrote. Editing or deleting one is yours to do: a later start seeds only the names that are missing, and a file you deleted on purpose stays deleted.

A role that keeps files in front of it

A role is a paragraph, not a briefing, and that is the gap this closes. The role can say “read core/src/capability/tools.rs”, but what the model then reads sits in the conversation and is summarized away at the next compaction — after which the agent still has the instruction and no longer has the file. A role document may therefore open with a header, in the same key value format the task files and goal.cfg use:

files core/src/capability/tools.rs core/src/capability/permissions.rs docs/permissions.html
lane tools

You own the tool layer. Keep the permission whitelist and the documented
tool list in step with each other, and say so when a change widens what
the agent may reach.

The header ends at the first blank line, and the body under it is the role text as before, so a document without a header is a body and every role written until now keeps working unchanged.

files names paths relative to the working directory. At the start of every turn the modification times are compared, and a file that is new or has changed since is read into the role block itself rather than into the conversation. So it is there after every compaction without anyone remembering to ask, and the cached prompt prefix breaks only when a file actually changed. A path that leaves the working directory is refused the way Files refuses everywhere, a file that disappears is named as gone rather than silently dropped, and a file over role-file-bytes is cut with the block saying so. agent_info lists the role's files with their sizes and their load state.

lane sets what --lane sets, which is what lets a role know its own address instead of the command line repeating it. A --lane actually typed wins over the header, because the session the user aimed at a lane is the one he meant. A key nobody reads is a startup note naming it rather than an error, so the format has room to grow.

Two things this deliberately does not do. A coordinator does not get a bigger budget for the agents it starts: each one is its own process with its own bill, which is the point, and it means no single number tells you what five agents cost. And a started agent inherits the environment — the config, and therefore the skills and memory that config resolves — but none of the authority: not --allow-all, not the worktree, not the lane. A peer outlives the session that was granted something, and nobody is watching when it starts.

What survives a restart

Heartbeats, gates and an autonomous run live in the process holding the session, so a restarted session comes back without them. The standing goal comes back with it, because it is stored beside the memory files.