Working unattended
A session normally waits for a message. Five 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
/gate cargo test
/goal make the test suite pass on the new provider
Setting the goal is what starts the run: /goal <text> takes the default run budget and says so in the same report, and a session resumed while its own goal still stands in goals/<session>.cfg picks the run up before you type anything — a goal belongs to the session that set it, so no other session of the project starts a run on it. A goal that is paused, or one the model has already proposed met, waits instead. /autonomous rounds=40,seconds=900 is still how a run is started by hand with a budget of your own, and /autonomous with no run going resumes one.
/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 run budget is spent
- A quality gate fails
- You send a message
- The model proposes through
GoalProgressthat the goal is met
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. Stopping the run leaves the goal standing, and /goal done or /goal clear ends the run with it.
Working the queue
/queue # start the drive with the default run budget
/queue rounds=40,seconds=1800
/queue status
/queue off
revolve detach nightly --queue # or start a session already driving
/queue is the second drive, and it is aimed at the project task queue below rather than at a goal. It takes the open, due tasks one at a time in the order the queue already has — this session's lane first, then priority, then name — and works each one to the end before it picks the next. The turn it builds names the task, says to read it with the Task tool, and says that a task counts as done when it is completed in the queue, which deletes its file. That deletion is what tells every other session sharing the folder that the work is finished.
A task marked needs user is never picked: it is a question in the inbox rather than work for an agent, and answering it puts it back in the queue with the answer in it.
What runs between two turns is a fixed order, and it is one function rather than a rule spread across the loop:
- A message that arrived — from you or from another agent — is taken first, always
- Otherwise, if a run toward the standing goal is going, it continues
- Otherwise, if the queue drive is on, it continues
- Otherwise the session waits for input
Steps two and three never both apply: a session drives toward one thing or the other, so /queue while a run is going and /goal or /autonomous while the drive is going say so and change nothing, naming the command that ends what is running. The drive is otherwise the run's sibling — the same run budget, the same quality gates, and the same rule that a message of yours hands the session back.
Where it differs is that it can finish on its own. A goal ends only with your /goal done, because nobody but you can say an objective is met; an empty queue is a fact, so the drive stops by itself and says which of three things it found:
- Nothing is left in the queue at all
- Every task left is held by a condition of its own — a
wait, anafter, anuntilor anon— so none is due right now, and they come back by themselves - Every task left is marked
needs user, so it belongs in the inbox rather than to an agent
None of the three claims the work is right. An empty queue says the files are gone, which is what completing a task does, and nothing about what was left behind.
--queue is the flag that starts a session already driving, so a detached or scheduled session can work the queue with nobody at the keyboard. It travels with every other session flag when you run revolve detach, and deliberately not to a peer an agent starts with start_agent: a peer is started for what it was asked to do, not for this session's queue.
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, in
m, h, d, w, mo of thirty days or
y of three hundred and sixty-five, and wait 2026-09-01 or
wait 2026-09-01 14:30 until a moment in local time — is left out, which is what keeps a queue of forty
files from reading as forty things to do now. A task whose header carries
after write-the-parser, write-the-tests is left out for as long as a file of either
name is still in the folder, which is how an order between two tasks is stated where the prose of a
task file cannot enforce one. Finishing a task deletes its file, so a name nothing answers to is
finished by definition and a misspelled one lets the task run early.
A header carrying until cargo test or until git diff --quiet is held back
while that command fails and comes back the moment it succeeds, which is the checkable version of
what a wait can only estimate. The command runs at most once every five minutes and the
exit status is remembered in the task folder, so building the queue never runs a program between the
user pressing Enter and the model being asked; it is authorized by the same permission rules every
other command is, and one no rule allows leaves the task visible with that reason rather than hidden
behind a check that can never run. A command that fails, times out or cannot be started reads as
"not yet".
A header carrying on tui/src or
on core/src/state/tasks.rs, core/src/state/inbox.rs is the one deferral that is not
about time: the task is left out until the session reads or writes one of those paths, and then it
is listed in that session, because a note about a module is worth reading in the session that opens
that module and worthless in the twenty that do not. A directory matches everything under it, a file
matches only itself, and a path reached once keeps the task listed for the rest of the session
rather than letting it hide again the moment the work moves on. Every condition a header states has
to be met, so an on beside a wait waits for both.
revolve tasks names what each task waits for, and two tasks waiting on each other stay
visible with the cycle spelled out. 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.
A lane that works its own queue
A session started with --lane tui sees the open tui-* files and no
others, and --queue puts the drive above on them:
revolve --lane tui --queue
That is the whole setup: the lane is which tasks it may take, the drive is what keeps it taking them, and the drive stops by itself once the lane has nothing open and due left.
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:
coordinator— holds the picture, hands every job to an agent of its own, and edits no code itself.reviewer— reads a change and answers whether the diff does what it says and nothing else, writing nothing.docs— keeps the documentation true to the code and touches no source file.tests— writes and repairs tests, runs the suite, and names any source file it had to touch to make something testable.lane— a template for one worker of a repository several agents share; you fill in itsfilesandlaneheader per project.
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 under the session's name in the project folder.