Give OpenCode a goal and it keeps working until the goal is met.
The easiest way to use it is to just say what you want — no syntax to remember:
You: "Keep working until the tests pass and don't stop on your own." OpenCode: (sets the goal itself) "Goal set: tests pass — I'll keep going and check after each step. 🎯"
opencode-autogoal adds that behavior to OpenCode: conversational tools (so the agent
sets and manages goals when you ask in plain English), a /goal command if you prefer typing, and a
background auto-loop that re-checks the goal after every turn and nudges the agent onward until the
condition holds (or a turn/time limit trips).
Designed for OpenCode Desktop (Electron) and the terminal — the engine is a server plugin (what
Desktop runs). Its only dependency is @opencode-ai/plugin (the plugin API your OpenCode already provides).
An optional terminal-only dashboard ships alongside.
Requires OpenCode ≥ 1.16 (uses the session.idle and experimental.session.compacting hooks).
opencode plugin opencode-autogoalThat single line enables the conversational tools — you can immediately say "keep going until the tests pass" and OpenCode sets the goal itself. No command needed.
If you also want the /goal slash command, add the command block (it makes /goal exist; the plugin
attempts this automatically via the config hook too, but declaring it is the reliable path):
If your project doesn't use a
buildagent, changeagentto one you have. Seeexamples/opencode.jsonc.
The plugin gives OpenCode five tools, so you manage goals in plain language — it picks the right one:
| Say something like… | Tool the agent calls |
|---|---|
| "keep going until the tests pass", "don't stop until the build is green" | set_goal |
| "what's my goal?", "how's it going?" | goal_status |
| "pause the goal for a sec" | pause_goal |
| "ok, back to the goal" | resume_goal |
| "stop the goal", "we're done with that" | clear_goal |
Tip: mention a check command and it's verified deterministically — "keep going until npm test passes."
Note: the conversational path needs a model that's good at tool-calling (e.g. Claude, GPT-class, strong DeepSeek). Smaller/free models may not invoke
set_goalfrom plain English — on those, use the/goalcommand below, which is deterministic on any model.
/goal set "<condition>" set a goal (the agent starts working immediately)
/goal set "<condition>" --command "<c>" verify completion by running a shell command (exit 0 = met)
/goal set "<condition>" stop after 10 turns
/goal show current goal + progress (bare /goal = view)
/goal pause | resume | clear control the loop (clear aliases: stop, off, reset, none, cancel)
/goal template <name> set a goal from a built-in or project template
/goal history show recent evaluation results
# v0.2.0+ live dials — change the goal mid-run
/goal turns <n> set maxTurns (1..10000)
/goal time <n> set maxTimeMinutes (1..10000)
/goal tokens <n> set maxTokens (1..10000000)
/goal condition "<new text>" edit the condition, keep id/evals/constraints
/goal steer "<hint>" append a steering note for the next nudge
/goal unsteer drop all steering notes
/goal restart clear + re-set with same condition (new id)
/goal handoff [note] serialize state to .opencode/.goal-handoff.json
/goal claim resume a handoff
Inline modifiers: stop after N turns, stop after N minutes, stop after Nk tokens,
--command "<check>" (also --turns N, --time N). Everything else is the condition.
Both paths share the same engine and state — use whichever feels natural.
The 9 dials above are also bound to the TUI keymap (with the
opencode-autogoal/tui plugin loaded) — run /goal-turns from the
command palette to get a dialog, or /goal-steer "next time try X"
to drop a hint for the next nudge without changing the goal.
/goal set "all unit tests pass" --command "npm test"
/goal set "remove every TODO comment in src/ and explain each removal" stop after 8 turns
/goal template fix-lint
- An active goal steers the conversation. Until the goal is met, the agent treats it as top priority
and gets nudged back to it each turn. Use
/goal pause(or/goal clear) before switching to unrelated work. Bare/goalshows the current status. - The
stop after N …phrasing is parsed anywhere in the condition. So/goal set "make it stop after 5 turns of retrying"would read5as a turn limit. For literal text like that, set limits with the unambiguous flags instead (--turns,--time) and keep them out of the condition prose. - Status commands cost one model turn.
/goal view|clear|pause|resume|historyare handled by the agent relaying the result, so they aren't instantaneous like a native UI button.
Two kinds of goal, two kinds of evaluation:
- Verifiable goals (
--command): after each turn the plugin runs your command in the project directory. Exit 0 = achieved. Cross-platform (uses the OS shell), deterministic, the reliable path. - Open-ended goals (no command): the plugin reads the agent's latest message read-only and looks
for an explicit completion signal — it never injects evaluation prompts or guesses. The agent declares
done with a line beginning
GOAL_COMPLETE:(andGOAL_BLOCKED:if it's stuck). These markers are line-anchored and code-fence-aware — the agent merely talking about the protocol, or showing an example inside a markdown code block, will not trip the goal. (The detector ignores lines inside```or~~~fences, and lines indented 4+ spaces — markdown's "indented code block" threshold — so the marker is only matched when the agent writes it on its own prose line.)
When a goal isn't met yet, one continue-nudge is injected to drive the next turn. Turn and time limits stop a runaway loop. On any terminal state (achieved / stopped / blocked) you get a notification both as a TUI toast (terminal) and as a status line in the conversation — the latter is what surfaces on Desktop, where TUI toasts don't render.
State lives in .opencode/.goal-state.json (atomic writes; auto-gitignored).
Built-in: fix-lint, fix-types, pass-tests. Add your own at .opencode/goals/<name>.json:
{
"description": "Ship the migration",
"condition": "the migration script runs cleanly and the app boots",
"command": "npm run migrate && npm run smoke",
"constraints": { "maxTurns": 20, "maxTimeMinutes": 30 }
}A project file with the same name overrides a built-in.
OpenCode Desktop (Electron) is driven entirely by server plugins — it does not load TUI plugins. Everything above works on Desktop via the server plugin. The interactive console below is terminal-only. It does not start an extra HTTP server and does not add runtime dependencies.
The OpenCode TUI plugin (opencode-autogoal/tui) adds a full-screen goal
dashboard plus palette commands, slash commands, dialogs, and keybindings:
alt+gopens the goal dashboard.alt+nopens a set-goal dialog.alt+sopens a steering-note dialog.alt+ppauses/resumes the goal.alt+cconfirms goal clear.Esccloses the dashboard without trapping the host palette.
It is terminal-only and off by default. Enable it in tui.json when you run OpenCode in a terminal:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-autogoal/tui"]
}A persistent goal sidebar ships as opencode-autogoal/sidebar. It shows
the live goal state in every session, without the user having to navigate to a full-screen
dashboard. The sidebar slots (sidebar_title / sidebar_content / sidebar_footer) carry:
- status icon + condition (sanitized against newlines and zero-width chars)
- progress bar + turn/time/tokens counters (thousands separators)
- last evaluation reason
- steering note count + handoff indicator (when present)
- last 3 evaluations strip with ✓ met, ! blocked, · in-progress tags
- last condition-edit timestamp ("3m ago", "2h ago", etc.)
The sidebar reuses the validated I/O from tui-logic.ts and the dial primitives from
goal-state.ts — it cannot drift from the dashboard, and a hostile state file cannot
break its layout (single source of sanitizer truth: sanitizeForPrompt in goal-state.ts).
It is terminal-only and off by default. Enable it alongside the
dashboard in your tui.json; the sidebar footer advertises the dashboard
hotkeys plus slash-command fallbacks:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-autogoal/tui", "opencode-autogoal/sidebar"]
}They have no effect on Desktop. A Desktop/web-console goal panel would require OpenCode app-side integration; this package does not ship a separate web server as a substitute for that.
npm install
npm run build # tsc → dist/ (the server plugin; zero runtime deps)
npm test # build, then run the regression suite (node --test)
npm run typecheck # type-check src/ (server + tui)src/goal-state.ts— types, arg parsing, atomic state I/O, transitions, marker detection, status formatting,parseShellWords(POSIX-style command word splitting for debug diagnostics).src/command.ts— the/goaldispatcher (pure, deterministic).src/server.ts— the plugin wiring: command hook + the auto-loop.src/tui.tsx— optional terminal dashboard (shipped as source). Hosts the keymap layer (12+ commands including the v0.2.0 dials), the file-watcher-drivenuseGoalStatehook, and the dialog openers.src/tui-logic.ts— TUI pure logic: validated reads, progress-bar math, file-watcher predicate. Unit-tested separately from the JSX layer.src/tui-dials-logic.ts— v0.2.0+ dial submit handlers: parse + validate + call the goal-state primitive + return a result. 53 tests intest/tui-dials-logic.test.mjs. The pure sibling to the dialog logic intui.tsx.src/sidebar.tsx— optional terminal sidebar (shipped as source, opt-in viaopencode-autogoal/sidebar). Registers against the host'ssidebar_title/sidebar_content/sidebar_footerslot map.src/sidebar-logic.ts— sidebar pure view-model builder: title/content/footer strings. ReusessanitizeForPromptfrom goal-state.ts so the sidebar display surface and the auto-loop's prompt-injection guard stay in lockstep. Unit-tested separately from the JSX layer.src/templates.ts— built-in goal templates.
The package ships compiled JS (dist/) so it loads regardless of whether the host runtime is Node or Bun.
npm test covers goal-state.ts and the /goal dispatcher (parsing, state, transitions, markers,
every command path) against the built output. What unit tests can't exercise is the live OpenCode
integration: the command.execute.before/config hooks, the session.idle loop, and how injected
messages render in the Desktop UI. Smoke-test the packaged tarball before publishing:
npm pack # builds dist/ and makes opencode-autogoal-x.y.z.tgz
cd /path/to/a/throwaway/project
npm i /path/to/opencode-autogoal-x.y.z.tgz # exercises the COMPILED output + real install path
# add the plugin (+ optional command block) to opencode.json, then in OpenCode:
# CONVERSATIONAL (primary path):
# "set a goal to write a haiku, then keep going until it's done" → agent calls set_goal
# "what's my goal?" → goal_status · "stop the goal" → clear_goal
# SLASH COMMAND (if you added the command block):
# /goal → shows "no active goal"
# /goal set "say hi once you've written a haiku" (no --command: tests the GOAL_COMPLETE path)
# /goal pause, /goal resume, /goal clear
# /goal set "<thing>" --command "<cmd that exits 0>" (tests deterministic completion)If /goal does nothing, the command-registration/command.execute.before path needs adjustment for
your OpenCode version — open an issue with your version (opencode --version).
The package ships a standalone opencode-autogoal binary that drives the
goal loop from any shell or external script — no OpenCode required. This
is the read/write surface for external automation: CI bots, cron
jobs, shell aliases, other agents, and any tool that can exec a
subprocess can drive the goal loop.
opencode-autogoal set "make all tests pass" # start a goal
opencode-autogoal status # view the current state
opencode-autogoal steer "focus on the flaky suite" # add a hint for the next nudge
opencode-autogoal turns 100 # edit a constraint mid-run
opencode-autogoal pause # pause the auto-loop
opencode-autogoal handoff "for tomorrow" # write a handoff file
opencode-autogoal --dir /path/to/project status # operate on a different workspace
opencode-autogoal help # full command listThe CLI is a thin wrapper around the same dispatchGoalCommand function
that the /goal slash-command uses. It reads and writes the same state
file at <dir>/.opencode/.goal-state.json. Multiple surfaces
(OpenCode TUI, OpenCode sidebar, this CLI) coordinate via the on-disk
state — there's exactly one source of truth, and it's atomic.
Any tool that can read JSON can observe the goal's state without invoking the CLI:
cat .opencode/.goal-state.json | jq '.condition, .status, .constraints'The state file is .opencode/.goal-state.json (relative to the workspace
directory). It's gitignored — runtime data, not source.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | User error (bad args, invalid value, missing required arg) |
| 2 | Precondition not met (no goal, terminal state, handoff already pending) |
| 3 | Write failed (I/O error, permission denied, disk full) |
| 4 | Corrupt state file detected and quarantined as .goal-state.json.corrupt.<ts> (v0.4.2) |
status exits 2 when there is no current goal (precondition not
met), not 0. Scripts that want to distinguish "no goal yet" from
"goal is set and idle" should branch on the exit code, not the
output text.
Scripts can branch on exit code without parsing the human-readable output.
opencode-autogoal set "ship v2" --command "make deploy"The value after --command is one shell-quoted argument. The
shell groups multi-word commands into a single argv element before
the CLI sees them. The CLI re-quotes that value (using double
quotes) for the dispatcher's parser.
If the value contains a literal double-quote character, the CLI refuses with exit 1 — this is a hard constraint; the dispatcher's parser uses double-quote delimiters and the CLI cannot safely escape an inner double quote without ambiguity.
Empty value --command "" is treated as "no command" — the
flag is stripped entirely. The goal is stored with
command: null. This is the read-back-symmetric way to express
"verification disabled" from a script.
Duplicate --command errors with exit 1. The dispatcher
doesn't silently take the first or the last; the user must
resolve the contradiction in their script.
Shell semantics. The verification command is run via
exec() (the auto-loop's evaluateDeterministic in
src/server.ts), which means it gets full shell semantics:
pipes (|), redirects (>), logical operators (&&, ||),
and backtick expansion all work. The shell is whatever the
OpenCode host spawns — typically /bin/sh on POSIX, cmd.exe
on Windows. Scripts that pass multi-step commands
("make test && make deploy") are a deliberate use of this
feature, not a footgun — but if you need to pass a value that's
guaranteed NOT to be shell-evaluated, you can't use this CLI.
opencode-autogoal doctorPrints a check table and exits 0 (healthy) / 1 (any FAIL). Useful in CI or as a first stop when something feels off:
| Check | What it means |
|---|---|
| goal state | valid (or absent) — FAIL means corrupt (file is quarantined on read) |
| chain file | valid (or absent) — same semantics |
| handoff file | valid (or absent) — same semantics |
| quarantined artifacts | WARN if any .corrupt.<ts> files are present (lists newest 3) |
| node version | >= 20 |
| package version | always OK; prints its own version |
doctor is intentionally NOT a goal action — it never routes through
the dispatcher, so it can never accidentally mutate state.
opencode-autogoal --json doctor # machine-readable, one lineopencode-autogoal tui # full-screen interactive cockpit
opencode-autogoal # bare, in a terminal, launches the same TUIA full-screen, keyboard-driven control center — the interactive sibling of
watch. The v0.7.0 release turns this into a three-pane shell (header ·
goal pane · live session pane) with drill-down history, 7 new actions, and
a categorized, paged, searchable help overlay. Every management action is a
single keypress:
| Key | Action | Key | Action |
|---|---|---|---|
p |
pause / resume | n |
set a new goal |
s |
add a steering note | H |
write a handoff |
e |
edit the condition | C |
claim a handoff |
t/m/k |
max turns / minutes / tokens | ? |
help overlay |
R |
restart (confirms) | q / Esc |
quit |
c |
clear (confirms) | ↑/↓ |
scroll history |
Tab |
enter drill-down (steering / eval history) | Enter |
open detail / select |
A |
view goal archive | T |
view templates |
D |
inline doctor check | L / O |
open .opencode/ in the file manager |
g |
copy full goal state JSON to clipboard | Ctrl+L |
redraw |
Destructive actions ask for confirmation; text actions (steer, edit, new
goal) drop into an inline editor. The drill-down mode (Tab to enter) lets
you navigate the steering list or the evaluation history with ↑/↓, open
the full-reason detail view with Enter, copy the current item to the
clipboard with c (OSC 52), and edit the selected steering note in place
with e. The help overlay (?) is paged (n/p) and searchable (just
type) — three sections (Goal / Session / Nav) cover all 18 key bindings.
It's built on pure ANSI + raw-mode stdin — zero new dependencies, runs
in any terminal, and restores the terminal cleanly on q / ctrl-c / crash.
It needs a real TTY; in a non-TTY (CI/pipe) it refuses with a hint to use
watch or --json status instead.
It works on Windows out of the box — there are no native bits, just ANSI and raw-mode stdin. For the nicest result:
npm i -g opencode-autogoal # Node >= 20
opencode-autogoal tui # or just: opencode-autogoal- Prefer Windows Terminal (with PowerShell or
cmdinside it) over the legacy console host — modern Windows Terminal renders the█░progress bar, box glyphs, and the 🎯 status icon crisply. A programming font like Cascadia Code / Cascadia Mono keeps everything aligned. - It needs a real terminal. Piping or redirecting (
opencode-autogoal tui | cat) or a non-interactive CI step makes it refuse cleanly — that's intended; usewatchor--json statusthere instead. Ctrl-Corqexits and restores the terminal; setNO_COLOR=1for monochrome.
opencode-autogoal watch # 2s refresh, TTY-rendered
opencode-autogoal watch --interval 5000 # custom refresh (250-60000 ms)Renders a live, plain-ANSI dashboard of the current goal. Works in any terminal — tmux pane, second terminal, SSH session — with zero host dependencies. No plugins, no TUI libraries.
In a non-TTY (CI/pipe) it prints one frame and exits 0 — safe to embed in scripts. SIGINT (ctrl-c) cleans up and exits 0.
Every terminal outcome (achieved / cleared / replaced) is
appended to .opencode/goal-archive.jsonl (gitignored). The file is
capped at 1 MB; when exceeded it's atomically trimmed to the newest
200 lines.
opencode-autogoal archive # list the 10 most recent outcomes
opencode-autogoal stats # totals: total, achieved, cleared, replaced, avg turns to achieve
opencode-autogoal --json stats # machine-readable, one lineArchiving is best-effort — a full disk or permission failure will not break a goal transition.
The CLI is the foundation for making this tool reusable beyond OpenCode:
- From a CI pipeline:
opencode-autogoal set "ship v2" --command "make deploy"runs the goal loop on every push. - From a cron job: a watchdog cron can
opencode-autogoal statusand alert on regressions. - From another AI agent: any agent that can exec a subprocess can drive the goal loop. The agent reads
.goal-state.jsonto observe, invokes the CLI to act. - From a shell alias:
alias goal='opencode-autogoal'gives instant goal-loop access from any terminal.
The CLI surface is intentionally a strict subset of the OpenCode-internal surface (no subagent wiring, no TUI/dialog flows, no hot-reload semantics) — just the durable primitives the state file already supports.
Huge props to @mirsella and opencode-goal — they shipped the original "Codex-style long-running goals for OpenCode," and it's a clean, lightweight take that proves the idea. If you want something simpler and model-judgment-based, go give it a star. 🌟
opencode-autogoal heads in a different direction: deterministic --command verification,
turn/time limits, templates, and a conversational tool interface so you set goals by just
talking. Different trade-offs, same good idea. Thanks for blazing the trail. 🤝
MIT © VerbalChainsaw
{ "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-autogoal"], "command": { "goal": { "description": "Set, view, pause, resume, or clear a task goal.", "agent": "build", "template": "Process the /goal command with arguments: $ARGUMENTS" } } }