Goals
Goa supports autonomous goals: long-running, multi-turn tasks that the agent pursues on your behalf. This document is the goal subsystem specification: lifecycle, enforcement contracts, queue semantics, budgets, and persistence.
Concepts
- Active goal — exactly one per session. The runtime drives continuation turns until the goal ends.
- Queue — a durable FIFO of upcoming goals (
upcoming-goals.json). The head auto-promotes when the active goal completes. - Completion criterion — an optional, recorded done-condition. It gates completion (see Done-gate).
- Event log —
goal-events.jsonl, the source of truth. The TUI and session reload are projections of it.
Starting a goal
/goal # interactive: prompts for the objective
/goal:new:fix the failing checkout tests
/goal:new:implement OAuth2 login with unit tests
A goal needs a verifiable end state. If your request is vague, the model will ask for a completion criterion before creating the goal. Each goal gets a friendly alias (e.g. happy.fox) shown in status and the queue manager. The model can also create goals itself with the goal tool (create action), including several at once via the objectives array — the first starts active, the rest queue FIFO.
Lifecycle contract
| Status | Meaning | Allowed transitions |
|---|---|---|
| active | The agent is working autonomously on the goal. | → paused, blocked, complete |
| paused | Goal is parked; not pursued. | → active |
| blocked | Goal hit an external blocker; not pursued. | → active |
| complete | Terminal. The record is cleared; the queue head promotes. | — |
- Only the active goal consumes continuation turns, budget, and tokens.
completeis the only terminal status; the goal record is cleared after the completion event is emitted.cancel(/goal:cancel) discards the goal at any status without a completion event. Unlike a completion, a cancel never auto-starts the next goal: the queue head is promoted paused and waits for an explicit/goal:resume(framework transitions such aspostponeand auto-unblock still start the successor)./goal:pause:nextarms a pause-after-completion one-shot on the active goal: the goal keeps running, but when it completes the queue head is promoted paused (like a cancel) instead of auto-started — so you can review the completion evidence before the queue drains on. The flag is durable (survives a session restart), dies with the goal, and is disarmed with/goal:pause:next:off./goal:status,/goal:currentand/goal:listshow the armed state.
Control commands:
/goal:status # show current goal
/goal:pause # pause active goal
/goal:pause:next # arm pause-after-completion (successor promotes paused)
/goal:pause:next:off # disarm the pause-after-completion one-shot
/goal:resume # resume paused/blocked goal
/goal:cancel # discard current goal (next queued goal promotes paused)
/goal:cancel:current # same as /goal:cancel
/goal:cancel:all # discard current goal AND clear every queued goal
/goal:replace <objective> # ask confirmation then abandon current goal
/goal:log # show recent goal event records
/goal:verify # run the recorded verify command now
Terminal-answer contract (enforcement)
Model-initiated transitions out of active must be justified. This exists because unexplained pauses forced users to say "please continue", defeating the purpose of autonomous goals.
| Transition | reason |
expectation |
Notes |
|---|---|---|---|
paused |
required | optional | Why the goal must yield. The model is instructed to keep working whenever progress is possible and to use blocked (not paused) when waiting on input. |
blocked |
required | required | The concrete blocker, plus exactly what input/change unblocks it. Rendered in the TUI marker and the blocked reminder so the user knows what to provide. |
complete |
required when the done-gate asks (see below) | — | Carries the verification evidence; persisted as the terminal reason. |
active (resume) |
optional | — |
Missing justifications are rejected with a model-actionable error; the goal stays active.
Proactive resume: when a goal is paused or blocked, the per-turn reminder instructs the model to resume immediately — in the same turn — when the user's message asks to continue (any phrasing) or supplies the recorded unblock condition. The user never has to issue an exact command.
Done-gate (goals.done_gate)
How strictly model-initiated completion is checked before the goal may close. Configurable:
goals:
done_gate: verify # verify (default) | evidence | off
verify(default) — when a completion criterion is recorded, the model's firstcompletecall is intercepted: the goal stays active and the tool returns a verification challenge restating the criterion (without stopping the turn). The model self-audits, then callscompleteagain withreasonciting concrete evidence (commands run, outputs observed, tests passing). The second call runs machine verification (below) and closes the goal when it passes.- If the audit fails, the model simply keeps working — the goal was never closed.
- Any other transition (pause/blocked/resume) re-arms the challenge.
- Session restart mid-verification re-arms the challenge (the pending flag is in-memory only).
evidence— single-call variant:completemust carryreasonwith the validation evidence. No challenge round-trip; machine verification still applies.off— legacy behavior:completecloses immediately (no challenge, no machine verification).
The gate applies only to model-initiated completions of goals with a recorded criterion. User completions (/goal flows), runtime completions (driver, orchestrator binders), and orchestrator-managed goals bypass it.
Machine verification (verifyCommand)
A goal may record a verify command — a machine-checkable done-condition supplied at creation (goal tool create arg verifyCommand, preserved through queueing and promotion). After the model confirms completion (challenge answered, evidence given), the done-gate executes the command through the system shell ($SHELL, bash fallback) in the project directory:
- exit 0 → completion proceeds; non-zero → completion is rejected with the output tail (last 10 lines) as the failure detail.
- Execution is bounded by a configurable timeout —
goals.verify_timeout(default2m, max1h) — and the returned output is ANSI-sanitized and capped at 4 KB. The call runs unlocked, so it cannot deadlock goal state — but ESC does not interrupt it (the goal tool has no cancellable context; documented limitation). The bound is runtime-settable:/config:set goals.verify_timeout 10mapplies to the running session immediately (bugs.md 2026-09-29: the gate previously kept the startup value, so a slow suite could only be fixed by a restart). - Transparency: before the confirming
completecall runs the command, the goal widget announces it live (Running goal verification (timeout 2m0s): $ <command>), and a successful completion returns the full evidence block in the chat bubble — exact command, elapsed time, applied timeout, and output tail — so you can always see what validated the goal. goals.verify_commands: falsedisables execution globally: the gate then skips the command (judge/streak logic still runs), and/goal:verifyerrors.- Todo consistency: a gated
completeis rejected while any goal todo is notdone— finish the checklist or update it first. - Escalation: consecutive verification failures increment an in-memory streak; at
goals.max_verify_failures(default 3) the goal is auto-blocked for user review (reason: "verification failed N times consecutively").-1removes the cap (not recommended). Any transition out ofactiveor a session restart resets the streak. /goal:verifyruns the recorded command on demand and prints its output plus PASS/FAIL.
Independent judge (goals.judge)
An optional second, semantic auditor after the verify command (or alone when no command is recorded):
goals:
judge: off # off (default) | same | model:<id>
sameaudits with the active model;model:<id>uses a configured model.- The judge is read-only and sees only the objective, the recorded criterion, and the claimed evidence (capped ~8 KB). It must end with
VERDICT: PASSorVERDICT: FAIL; FAIL rejects the completion with the judge's rationale. - Fail-open: judge errors (model resolution, stream failure, unparseable verdict) never block completion — they emit
goal_judge_errortelemetry only.
Stall watchdog (goals.stall_turns)
The goal driver fingerprints progress after every continuation turn of an unmanaged goal: the hash combines the goal's todo list (ID+status) with git status --porcelain of the workspace. Outside a git repository the git half degrades to a constant, leaving a todos-only fingerprint.
- An unchanged fingerprint for
goals.stall_turns(default 5) consecutive turns injects a stall challenge ("make measurable progress, revise todos, or block with reason+expectation") and emitsgoal_stall_detected. - A changed fingerprint resets the streak and the challenge count; a new goal resets both.
- After 2 unanswered challenges the goal is auto-blocked (reason "no measurable progress", expectation "user direction on how to proceed").
stall_turns: -1(or 0) disables the watchdog. Orchestrator-managed goals are never watched (the orchestrator supervises them).- Editable at
/config→ Goals → Stall watchdog or/config:set goals.stall_turns <n>; a changed threshold applies live to unmanaged goals.
Default turn budget (goals.default_turn_budget)
When > 0 (embedded default 50), every newly created goal gets that hard turn ceiling unless a budget is set explicitly later. -1 = unlimited.
Editable at /config → Goals → Default turn budget (applies to goals created afterwards) or /config:set goals.default_turn_budget <n>.
Queued goals
/goal:next refactor the auth module # front of the queue: runs right after the active goal
/goal:next:first refactor the auth # same as /goal:next (explicit)
/goal:next:last polish the docs # end of the queue: runs after all queued goals
/goal:next # interactive: prompts for the objective
/goal:manage # open the queue manager ('e' edits the highlighted goal's description)
/goal:reorder:1B,2C,3A # reorder by letter mapping
/goal:next inserts the goal at the FRONT of the queue — it is promoted
next, right after the active goal completes (equivalent to the goal tool's
priority: "front"). Use /goal:next:last to append at the end instead.
The "Queue it for later" choice offered by /goal:new when a goal is
active always appends to the end.
Promotion contract: when the active goal completes, the queue head promotes automatically and preserves its objective, completion criterion, verify command, fresh-context flag, and friendly alias (traceability). The promoted goal also inherits the predecessor's completion evidence as a handoff note, rendered as an untrusted block in the per-turn goal reminder (rebuilt every turn, so it survives fresh-context resets). On promote failure the item is restored to the front of the queue. The model's batched create applies the call's completionCriterion to every objective, including queued ones.
Model scheduling (goal tool): beyond the FIFO default, the model schedules goals itself:
createwithpriority: "front"— insert at the front of the queue (runs next).postpone— demote the active goal to the back of the queue and start the next scheduled goal: the deprioritize primitive. The goal keeps objective, criterion, verify command, and context mode. With an empty queue the goal simply parks until promoted.promote— activate a queued goal now (goalId): the current goal is demoted to the front of the queue (resumes right after) and the chosen goal becomes active atomically.
With create (append/front), postpone, promote, reorder, and cancel the model has full todo-list-style control over what runs when. create never fails because a goal already exists: behind an active, paused, or blocked goal it appends to the queue; and when the action field is omitted it is inferred from the fields present (e.g. status → update, objective → create).
Auto-unblock (goals.auto_unblock)
When a goal blocks with justification (reason + expectation) and a queue is wired, the framework runs an "unblocking investigation": the blocked goal is re-queued at the front (keeping criterion, verify command, and a handoff note carrying the blocker + unblock condition), and an investigation goal starts in front of it. The investigation must either (a) create an execution goal implementing the solution (priority: "front") and complete citing it, or (b) block itself with reason + expectation to ask the user. Its recorded completion criterion arms the done-gate, so a prose-only "here is what to do" completion is challenged.
Two terminal guarantees keep the flow from ping-ponging (a failed investigation re-queued in front of the real goal used to re-run and re-block forever):
- Investigations are terminal. A blocked investigation never spawns a successor investigation and is never re-queued — it parks and asks the user. The kind marker (runtime-only, restored on replay) drives this.
- Spawn cap. At most 2 consecutive auto-unblock spawns without an intervening goal completion or explicit resume; after that a justified block falls back to a plain block. A failed spawn (e.g. queue insert rejected) is surfaced in the block output instead of silently implying an investigation is running.
Budgets
The model can set hard limits with goal action set_budget:
stop after 20 turns
use no more than 500k tokens
finish within 30 minutes
Supported units: turns, tokens, milliseconds, seconds, minutes, hours. When a budget is exceeded, the goal is marked blocked by the system (reason: budget reached).
Todo lists
Todos are lightweight checklist items; goals are autonomous units of work. The two are deliberately different:
| Todo | Goal | |
|---|---|---|
| What it is | A checklist item | An autonomous work unit |
| Lifecycle | pending / in_progress / done only |
active / paused / blocked / complete |
| Execution | The model works items during the current turn(s) | The runtime drives continuation turns for it |
| Budget / tokens / turns | none | tracked and enforceable |
| Completion criterion / verify command | none | optional, gated |
| Scheduling | none (ordered list) | queue, reorder, postpone, promote |
| Persistence | goal todos: in the goal event log; session todos: in-memory scratchpad | event log + durable queue |
Use goals for work that must run autonomously across turns; use todos to break the current task into visible steps.
Where todos live
todo_listtool (standalone;tools.enabled.todo_list, default on). A single tool withactionparameters —add,update,complete,remove,list,clear— available with no goal active. The session list is a conversation scratchpad.- Goal-linked todos (goal tool
add_todo/update_todo, or the sametodo_listtool while a goal is active). The linkage rules:- Blank at goal start — a new goal begins with an empty list; session items never leak into it.
- Contained by the goal — items added during the goal belong to it. They never escape: when the goal ends (complete, cancel, replace) its todos end with it, and the session list resurfaces unchanged.
remove/clearare refused while goal-linked (mark itemsdoneinstead). - Framework-visible — the list is surfaced to the model every turn in the dynamic progress reminder, the stall watchdog fingerprints status transitions as progress, and a gated
completeis rejected while any item is notdone. - Completion reminder — when a goal is achieved (completed) with still-open todos, the completion result reminds the model of the open items and suggests scheduling a follow-up goal if the work is still needed (a criterion-less completion cannot silently drop unfinished work).
Fresh context
freshContext: true runs a goal's continuation turns on a clean context (objective + system prompt only). The prior conversation is preserved in the durable transcript and is visible on reload, but is not sent to the agent for that goal; a visible boundary marker is injected at the switch. The flag survives queueing and promotion.
Default: ON. New goals start on a clean context unless opted out — per goal (freshContext: false in the goal tool, /goal:new:reuse <text> or /goal:next:reuse <text> on the command line) or globally via goals.fresh_context: false (/config → Goals → "Fresh context for new goals"). /goal:new:fresh <text> forces a clean context even when the configured default is reuse.
Persistence & audit
goal-events.jsonl— append-only event log (goal.create,goal.update,goal.clear). Status updates record actor, reason, expectation, and counters./goal:logrenders the last ~20 records (time, type, actor, status, reason, expectation).upcoming-goals.json— the durable queue (versioned, backward compatible)./goal:verify— run the active goal's recorded verify command on demand; prints output + PASS/FAIL.- On session resume, an
activegoal is demoted topaused("Paused after agent resume") so it never silently runs without the user present. goals.retentioncontrols how long terminal records are kept.- In-memory by design (reset on session restart): the pending done-gate challenge, the verification-failure streak, and the stall-watchdog streak.
Configuration reference
| Key | Default | Meaning |
|---|---|---|
goals.done_gate |
verify |
Completion gate: verify (challenge round-trip), evidence (reason required), off. |
goals.verify_commands |
true |
Execute recorded verify commands at completion. |
goals.verify_timeout |
2m |
Hard bound on a single verify-command execution; shown in the completion evidence. Max 1h; runtime-settable via /config:set goals.verify_timeout (applies live). |
goals.max_verify_failures |
3 |
Consecutive verification failures before auto-block. -1 = no cap. |
goals.stall_turns |
5 |
Stale turns before a stall challenge. -1/0 = disabled. |
goals.default_turn_budget |
50 |
Implicit hard turn ceiling for new goals. -1 = unlimited. |
goals.judge |
off |
Independent completion auditor: off, same, model:<id>. |
Telemetry: goal_challenged, goal_verify_failed, goal_judge_error, goal_auto_blocked, goal_stall_detected.
Events in the TUI
- A bordered Goal panel appears while a goal is active.
- Lifecycle transitions render as low-profile markers (blocked markers show the unblock condition:
needs: …). - Completion renders a summary card with stats.
- Goal status appears in the footer status bar: an active goal carries a ◈ goal-count sign next to the profile (one ◈ per goal up to 3, then a numeric prefix —
◈,◈◈◈,25◈), plus ⬩ markers per pending todo.
Autonomy and starting goals
In non-yolo autonomy modes, starting a goal shows a permission dialog. You can switch to auto/yolo mode or proceed in the current mode.