Goals and stop conditions
A goal is the session’s definition of done. It’s the mechanism behind closed-loop autonomy: an agent should verify its work before it comes back to you.
The four goals that ship
Section titled “The four goals that ship”From config/goals.json:
| ID | What it demands |
|---|---|
codebase-question | Research and answer inline. Do not create files, PRs, or branches. Stop in needs_input if a human asked; report back to the parent and archive if a session did. |
open-reviewed-green-pr | Open the PR through the open-pr skill, block until CI is green, run an independent fresh-eyes review, address all its feedback, re-check CI, write a ## Verification section with checked boxes and proof, then apply the ready to merge label. Then stop, and archive when the PR merges. The default for most roots. |
open-reviewed-green-pr-with-version-bump | Same, plus a mandatory version bump when server source changed. |
e2e-verified-green-pr | Same, plus: state the critical path up front, spin up a real dev server, drive it with browser automation, record video and screenshots, embed them in the PR. |
All three PR goals name the open-pr skill as the canonical way to commit, push, open, and
finalize the PR — agents are told not to hand-roll their own commit/push/PR sequence when that
skill is available. The skill’s terminal act is applying the ready to merge label, and the
goal text makes that label part of “done.”
The label is deliberately disambiguated in the goal text, because its name collides with the
“do not merge your own PR” instruction. Applying ready to merge does not merge the
PR and does not claim a human has reviewed it — it is the agent’s own claim that self-review,
fresh-eyes review, and green CI are complete. It is fully compatible with leaving the PR
unmerged; “do not merge” is not a reason to skip the label.
The three PR goals then end by telling the agent to stop in needs_input, and to archive when
the PR merges. The session that opened a PR is the session holding the work’s context, so it is
the one a human comes back to while the PR’s disposition is unsettled.
Nothing has to watch for the merge. GitHubPullRequestPollerJob sweeps unarchived sessions with
recorded PR URLs, and on the open → merged transition it delivers AutomatedPrompts.pr_merged_message
to the session. That message is the archive signal, and it makes the queue self-draining:
Both outcomes are correct by construction, and neither needs a human to tidy up. A held PR keeps its session in the queue, which is the point — and when that human merges it, the same signal releases the session. A merged PR drains its own session out. A PR closed without merging ends the work, and the session archives on that too.
The stop is conditional, and the condition is the merge message rather than a person’s attention.
A goal that makes a human the only thing able to release a session is what leaves sessions in
needs_input for weeks after their PR has landed.
Three cases stop the message arriving at all — an unrecorded PR URL, a merge the poller never saw
open, and a swallowed delivery — and a session that hits one waits forever. The goal text tells the
agent to check get_session for a recorded URL before settling in. See Limitations.
codebase-question stops for a different reason, and only when a human invoked the session
directly. A research session a parent spawned reports its answer back to the parent and archives.
How a goal is applied
Section titled “How a goal is applied”That is the entire mechanism. AgentSessionJob#build_prompt_with_goal resolves the goal id to
its description (or passes an unknown string through verbatim as free text) and appends it to
the prompt.
A blank base prompt short-circuits the whole thing — a guard against spawning an agent whose entire prompt is a bare goal string.
Where a goal comes from
Section titled “Where a goal comes from”Precedence, in order:
- Explicit
goalparam at session creation (UI form orPOST /api/v1/sessions). - The agent root’s
default_goalfromroots.json. - Nothing — the column is nullable, and the goal suffix is simply not appended.
It can be changed after the fact: PATCH /api/v1/sessions/:id accepts goal, and a follow-up
prompt can carry a new one — through POST /api/v1/sessions/:id/follow_up, the MCP action_session
follow_up action, or the enqueued-message editor, the one web surface with a goal field on a
message. A follow-up goal is applied whether the prompt is sent straight through, queued behind a
running turn, or interrupted in; a blank one preserves the goal the session already has rather than
clearing it (see
the REST API reference).
The column is validated on length only (GOAL_MAX_LENGTH). Any string is a legal goal.
Tracked in #88.
The heartbeat
Section titled “The heartbeat”A session can have heartbeat_enabled with an interval (30–86,400 seconds). HeartbeatSweepJob
runs every 30 seconds and, for each due needs_input session with a heartbeat, injects an
automated nudge prompt and resumes it — the “keep working toward your goal” loop.
It deliberately skips sessions that are blocked on an elicitation or that have pending enqueued messages, because resuming those would spawn a second process against the same clone.
The nudge goes out through Session#deliver_follow_up!. Five entry points share it: the web
follow-up form, triggers, the GitHub comment and merge-conflict pollers, and this sweep. The method
clears the stale per-turn metadata, transitions the session to running, stamps the prompt where the
recovery paths look for it, enqueues AgentSessionJob, and records running_job_id so the session
is never “running with no job.”
Two direct-delivery paths deliberately stay outside it: the REST API’s follow_up (which never
stamped a pending prompt, and would change behaviour if it started) and EnqueuedMessageProcessorService
(which delivers a message it has already claimed from a queue, under different locking).
The heartbeat is the one caller that passes stamp_pending_prompt: false. A user’s message is worth
replaying after a SIGTERM retry; a drumbeat is not — replaying one would deliver a beat for a moment
that has already passed.
needs_input vs archived
Section titled “needs_input vs archived”This trips people up. Mechanically, a session reaching needs_input just means the agent
finished a turn, and a session reaching archived means someone (or something) explicitly
archived it:
- You archived it in the UI.
- The agent called
action_sessionwitharchivethrough Zimmer’s self-session MCP server. - A health monitor archived it.
Intentionally, they mean more than that. archived is what a completed session looks like:
the agent is told, in OrchestratorSystemPromptBuilder and in the goal text, to archive itself
as its last act. needs_input is a deliberate signal that a human is required, and there are
exactly four sanctioned reasons to send it:
- The agent lacked the authorization scope or tools to finish, with no parent session to report back to.
- The session opened a PR whose merge disposition is unsettled. It archives when the PR merges; a PR the merge gate holds keeps its session here for the human who must review and merge it.
- A human invoked the session to explore something or answer a question — it is the user’s to close.
- Rare: an ambiguity both too dangerous and too irreversible to guess at.
On top of those, one rule bounds the whole queue: exactly one session per human-initiated goal stays unarchived. Usually that is the router, while it is still orchestrating the sessions below it; if the router archived itself and handed the work to a child, it is that child. One request should leave one session in the queue, not a trail.
Anything else — including “the user will want to read this” — goes in the final message, or in
Slack #updates if it is a read-only FYI and the session has a Slack server, and the session
archives. Anything the agent noticed but could not fix goes in a GitHub issue, which is the
other half of why a session can archive at all: an issue is a work item, and a parked session is
not. The prompt also names four things that look like reasons to park and are not: waiting on a
machine (CI, an outage, a rate limit, a peer session: none of those is a human, and an unmerged PR
is the one carve-out), a blocker another session is already fixing, a reason that went stale while
the agent worked (the PR merged; the question is moot), and finishing with nothing to show (a sweep
that found nothing and a gate that aborted both ran to completion).
The second of those is the one agents miss, because it looks like a handoff rather than a wait: red
CI on main from a failure unrelated to the agent’s own diff, an upstream fix in flight. The prompt
tells the agent to look for the session already working the blocker, set all three
wake_me_up_when_session_changes_state events on it — session_archived, session_needs_input and
session_failed, since a clean finish self-archives without passing through needs_input — plus a
wake_me_up_later deadline as a backstop, and then resume its own work once the blocker clears.
Escalating is right only when nobody is on the blocker, or after about three hours.
Whether agents comply is, again, a matter of the model obeying English.