An experimental session-scoped /goal command for OpenCode.
Set a goal and the plugin keeps it in context, auto-continues the session whenever the assistant goes idle, and stops when the goal is marked complete, a blocker is reported, or a safety limit is reached.
Compatibility: this plugin relies on experimental OpenCode hooks. Re-test against the exact OpenCode build and provider/backend stack you plan to use for unattended work.
| Surface | Status |
|---|---|
| Node.js | Declared support: >=18; CI covers Node 18, 20, and 22 |
| Package entrypoint | npm run smoke verifies the package export path plus /goal command-hook behavior from a local install without invoking a model |
| OpenCode host | Manually smoke-tested against OpenCode 1.15.10 using the opencode-go provider (qwen3.7-plus) on this repo's local hardening branch; re-test your own version/provider stack before relying on unattended runs |
| Provider/backend quirks | Strict-template backends require the goal block to merge into the primary system message; covered by regression tests |
npm install opencode-goal-pluginAdd the plugin and command to your OpenCode config:
{
"plugin": ["opencode-goal-plugin"],
"command": {
"goal": {
"description": "Set a session-scoped goal and auto-continue until complete.",
"template": "$ARGUMENTS",
"agent": "build"
}
}
}Set a goal:
/goal fix the failing tests and verify the suite passes
Override limits for a single goal:
/goal fix the failing tests --max-turns 20 --max-minutes 30 --max-tokens 400000
Add success criteria, constraints / non-goals, and a mode:
/goal ship the release --success "tests pass and changelog updated" --constraints "do not touch the public API" --mode ordered
--success (alias --success-criteria) and --constraints (alias --non-goals) take quoted text and are injected alongside the objective so the assistant keeps them in view. --mode is normal (default) or ordered (alias sisyphus); ordered asks the assistant to work through the objective as a strict sequence. Multi-word values must be quoted.
Flags accept either --flag value or --flag=value. If a flag is unknown, missing a value, given a non-positive integer, or (for --mode) an unrecognized mode, the plugin rejects the command with a helpful error instead of silently folding the bad flag into the goal text.
Check status:
/goal status
View lifecycle history and the latest checkpoint:
/goal history
Resume a paused or stopped goal:
/goal resume
Edit the active goal's objective without losing its budget or history:
/goal edit fix the failing tests and also update the docs
/goal edit <new objective> revises the goal in place: the turn, token, and time budget plus the lifecycle history are preserved, and any pause/blocked state is cleared so the revised goal can continue. A goal that already hit a hard limit will re-pause on the next idle — run /goal resume for a fresh budget window.
Pause without clearing the active goal:
/goal pause
Clear the active goal:
/goal clear
/goal stop, /goal off, /goal reset, /goal none, and /goal cancel are aliases for /goal clear.
A session can hold more than one goal. /goal <condition> replaces the focused goal, while /goal add <condition> keeps the current goal (backgrounding it) and focuses a new one. Only the focused goal is auto-continued; backgrounded goals are paused until you focus them.
/goal add write the migration guide
/goal list
/goal focus 1
/goal list shows the numbered live goals (which is focused, which are backgrounded) and a per-session archive of completed/cleared goals so they stay readable. /goal focus <number> switches the active goal, backgrounding the previous one. Focus is tracked per session and survives a restart.
/goal sisyphus sets up a strict execution sequence: separate the objectives with ; or newlines, and the plugin runs them one at a time, auto-focusing the next as soon as the current one completes.
/goal sisyphus build the parser; write the tests; ship the release
The first goal is focused and the rest are queued. /goal list marks the session as ordered. Auto-promotion stops when the sequence is exhausted; /goal clear ends the sequence.
- When you set a goal, the plugin stores it in session memory and injects it into the system prompt so the assistant keeps it in view on every turn.
- Each time the session goes idle, the plugin sends a continuation prompt containing the goal, the remaining budget, and a completion audit asking the assistant to verify the current state before declaring done.
- The plugin stops auto-continuing when the assistant ends a response with
[goal:complete]or[goal:blocked], or when a safety limit is reached. - If OpenCode compacts the session, the plugin injects a deterministic summary into the compaction context so the goal survives the compaction and the assistant keeps the thread. The summary — objective, status, budget usage, recent checkpoints, and recent lifecycle events — is reconstructed from the plugin's persisted goal record rather than from chat memory, so it is stable and reproducible. While a goal is active, the plugin also disables OpenCode's generic post-compaction auto-continue so it does not race the plugin's own continuation.
- The plugin stops auto-continuing when the assistant ends a response with a substantiated
[goal:complete]or[goal:blocked], or when a safety limit is reached. A[goal:complete]is only honored when it is preceded by a[goal:evidence]line; a[goal:blocked]is only honored when a concrete blocker is stated. Unsubstantiated claims are rejected and the plugin re-prompts for the missing evidence or blocker. - If OpenCode compacts the session, the plugin injects the goal objective, budget usage, and latest checkpoint into the compaction context so the goal survives the compaction and the assistant keeps the thread. While a goal is active, the plugin also disables OpenCode's generic post-compaction auto-continue so it does not race the plugin's own continuation.
- If you send a message of your own while the goal is running, the plugin treats it as the latest instruction and pauses auto-continue so it does not talk over you. The plugin's own continuation prompts are ignored for this check (they are not "your" messages). Run
/goal resumeto hand control back to the goal loop.
The plugin stops when it sees one of these at the end of an assistant response:
[goal:evidence] ran npm test (83 passing), verified the build output
[goal:complete]
The deploy step needs a production API token I don't have.
[goal:blocked]
[goal:complete] — goal is satisfied. It is only honored when the line immediately before it (or an earlier line) begins with [goal:evidence] and contains a non-empty summary of what was verified (commands run and their results, files checked). A [goal:complete] with no [goal:evidence] line is rejected, not recorded, and the plugin re-prompts for evidence. The accepted evidence is shown in /goal status after completion.
[goal:blocked] — the assistant needs input from you. The line immediately before the marker must explain the specific blocker; /goal status shows it while the goal remains in memory. A [goal:blocked] with no concrete blocker is rejected and the plugin keeps working.
Markers must appear on their own final line. The bracketed form is canonical, but the plugin also accepts bare goal:complete, goal:blocked, and goal:evidence lines because some models omit brackets. Natural-language phrases like "goal complete" are intentionally ignored.
| Limit | Default |
|---|---|
| Auto-continue turns | 10 |
| Max duration | 15 minutes |
| Context tokens | 200,000 |
| Min delay between continues | 1.5 seconds |
| No-progress pause | < 50 output tokens on a stalled turn (after a 2-turn grace window) |
| Budget wrap-up threshold | 80% of context token budget |
| Auto-continue failure pause | 3 consecutive prompt failures |
Effective turn count. Each LLM turn on a real task typically takes 30–90 seconds. At that latency, raising --max-minutes is usually more useful than raising --max-turns. At 45 s/turn, the default 15-minute window gives roughly 15–20 turns of headroom before the turn limit becomes the binding brake.
Token budget. The plugin tracks the session's context window size (input + output + reasoning tokens on the latest message). This matches the token count that OpenCode displays, so the numbers should be consistent. When the context window reaches the --max-tokens limit, the plugin sends a wrap-up prompt and stops. In high-context sessions (large codebases, long conversation history), the context can grow quickly — treat the budget as a safety brake.
No-progress heuristic. A low-output turn does not pause immediately anymore. The plugin pauses only after noProgressTurnsBeforePause consecutive stalled low-output turns — repeated turns with very little output and no meaningful change in the latest assistant checkpoint.
No-tool-call heuristic. Complementing the no-progress check, the plugin also watches for continuation turns that produce no tool calls at all (a "talk only" turn). Repeated talk-only turns usually mean the assistant is chatting to itself rather than doing work, so after noToolCallTurnsBeforePause consecutive tool-free continuation turns the plugin pauses. A turn that uses any tool (or delegates a subtask) resets the counter.
Wrap-up vs. hard stop. When a limit is reached, the plugin sends one final prompt asking the assistant to summarize what is done, what remains, and the next concrete step — rather than stopping silently. Use /goal resume to continue after any stop, including limit stops and no-progress pauses.
Goal state is persisted by default to a project-local path, .opencode/goals/state.json relative to the working directory, so goals follow the project rather than your home directory. It is only a local workflow checkpoint and is not synchronized across machines or OpenCode instances. You may want to add .opencode/goals/ to your .gitignore.
The state-file location is resolved with this precedence:
- the
stateFilePathplugin option, if set; - the
OPENCODE_GOAL_STATE_PATHenvironment variable, if set; - the project-local default
<cwd>/.opencode/goals/state.json.
When the default path has no state yet, the plugin migrates forward from older locations on first load: the legacy ~/.opencode-goal-plugin/state.json and the XDG path ${XDG_STATE_HOME:-~/.local/state}/opencode-goal-plugin/state.json. An explicit stateFilePath or OPENCODE_GOAL_STATE_PATH is used literally with no migration fallback.
The state directory is created with owner-only permissions, and the JSON state file is written as 0600 because it may contain goal text, assistant checkpoints, and workflow history.
Alongside the state file the plugin keeps an append-only lifecycle ledger (<stateFile>.ledger.jsonl, also 0600). Every lifecycle event — set, edit, auto-continue, pause, resume, blocked, completed, limit — is appended as one JSON line. Because the in-memory history is capped, the ledger is the durable record: if the main state file is missing or corrupted, the plugin reconstructs still-active (non-completed) goals from the ledger on startup and reloads them in the paused recovery state. Terminal events (complete/blocked) are written to the ledger before the main state write, so a goal's terminal outcome survives even if that write fails (fail-closed); such a failure is logged at error level.
Recovered active goals are loaded in a paused state with a recovery note, so unattended auto-continue does not resume blindly after a restart. Set "persistState": false to keep purely in-memory behavior (this also disables the ledger).
/goal resume continues the same objective with a fresh local budget window. This lets you continue after pause, blocker, no-progress pause, rate-limit failures, or a limit stop without retyping the objective.
Override any limit for a single goal:
| Flag | Controls |
|---|---|
--max-turns <n> |
Auto-continue turn limit |
--max-minutes <n> |
Duration limit in minutes |
--max-duration-ms <n> |
Duration limit in milliseconds |
--max-tokens <n> |
Context token limit |
--budget <n> |
Context token limit shorthand; accepts a k/m suffix (e.g. 100k, 1.5m) |
--cooldown-ms <n> |
Minimum delay between continues |
--no-progress-threshold <n> |
Output token floor before pausing |
--no-progress-turns <n> |
Consecutive stalled low-output turns before pausing |
--success <text> |
Success criteria that define when the goal is satisfied (quote multi-word text) |
--constraints <text> |
Constraints / non-goals to respect (alias --non-goals) |
--mode <normal|ordered> |
Execution mode; ordered (alias sisyphus) asks for a strict sequence |
--no-tool-turns <n> |
Consecutive tool-free continuation turns before pausing |
Examples:
/goal fix tests --max-turns 20 --max-tokens 400000
/goal fix tests --max-turns=20 --max-tokens=400000
/goal fix tests --no-progress-threshold 50 --no-progress-turns 2
/goal fix tests --budget 100kPass options when registering the plugin to change the defaults for all goals. To combine with the goal command, merge this plugin entry into the config shown above.
{
"plugin": [
[
"opencode-goal-plugin",
{
"maxTurns": 10,
"maxDurationMs": 900000,
"maxTokens": 200000,
"minDelayMs": 1500,
"maxRecentMessages": 50,
"noProgressTokenThreshold": 50,
"noProgressTurnsBeforePause": 2,
"noToolCallTurnsBeforePause": 2,
"budgetWrapupRatio": 0.8,
"maxPromptFailures": 3,
"persistState": true,
"stateFilePath": ".opencode/goals/state.json",
"resultRetentionMs": 604800000,
"maxStoredResults": 200
}
]
]
}Additional plugin-level options:
maxRecentMessages— how many recent session messages to scan when looking for the latest assistant turn before auto-continuing. Higher values make long, tool-heavy sessions less likely to lose the most recent assistant response.noProgressTurnsBeforePause— grace window for low-output stalls. The plugin pauses only after this many consecutive stalled low-output turns rather than on the first one.noToolCallTurnsBeforePause— grace window for tool-free continuation turns. The plugin pauses after this many consecutive continuation turns that produced no tool calls (anti self-chat loop). Default2.warnTurnsRemaining/warnDurationMsRemaining/warnTokensRemaining— thresholds at which the auto-continue prompt appends a "limits are near" warning (default3turns,60000ms,25000context tokens). Lower them to warn closer to the limit, or raise them to warn earlier.commandName— the slash command the plugin owns (defaultgoal). Set it to e.g.objectiveto drive the workflow with/objectiveinstead of/goal; a leading slash is tolerated. Remember to register the matching command name in your OpenCodecommandconfig. User-facing hints (/goal status,/goal resume, …) follow the configured name.registerCommand— whether the plugin installs itscommand.execute.beforehook at all (defaulttrue). Set it tofalseif you only want the auto-continue/persistence behavior driven programmatically and don't want the plugin to own a slash command.persistState— whether to persist active goals and recent goal results to disk.stateFilePath— where the persisted state JSON is written. Overrides the default project-local path and theOPENCODE_GOAL_STATE_PATHenv var. Useful if you want a fixed or ephemeral location. When unset, the default is<cwd>/.opencode/goals/state.json(see the persistence section above), andOPENCODE_GOAL_STATE_PATHcan override it without editing config.resultRetentionMs— how long a completed goal summary remains available through/goal statusafter the goal leaves active memory.maxStoredResults— maximum number of completed-goal summaries retained in process memory before the oldest ones are evicted.
When the assistant marks a goal complete or blocked, the plugin announces the audit instead of doing it silently: an audit-start message ("Auditing goal completion…") and an audit-result message ("completion accepted — goal archived" / "paused as blocked — …"). By default these are delivered through OpenCode's structured log (client.app.log, visible to the user). Provide an auditMessenger(sessionID, text) plugin option to route them elsewhere (for example into the live conversation once a suitable message API is available), or set auditMessages: false to disable them.
By default a [goal:complete] is accepted on the assistant's word. You can require an independent audit before a goal is archived:
completionAudit: true— the plugin spawns an independent OpenCode child session to verify the completion against the goal and workspace. The auditor replies with[audit:approved]or[audit:rejected](with a reason).auditor: async ({ goal, sessionID, latestText }) => ({ approved, reason })— supply your own auditor function (takes precedence overcompletionAudit).
On approval the goal is archived as achieved. On rejection the goal is not archived — it is paused with stop reason audit rejected and the reason in its status, so you can address the gap and /goal resume. The built-in child-session auditor fails open (auto-approves) if the session API is unavailable, while a custom auditor that throws is treated as a rejection (fail closed). The audit is off unless one of these options is set.
The goal text is wrapped in <goal_objective> tags and labeled as user-provided task data. The assistant is told to treat it as a task description, not as elevated instructions that can override system, developer, tool, or repository policies.
This is a marker-based implementation. The assistant is responsible for outputting [goal:complete] or [goal:blocked] — there is no independent evaluator verifying completion against the original goal. Claude Code's native /goal uses a separate evaluator model; this plugin currently approximates the workflow using OpenCode hooks and explicit completion markers. A future version could add a separate evaluator once OpenCode exposes a clean plugin API for that flow.
OpenCode's current command.execute.before hook does not fully intercept command text. The plugin can update in-memory goal state as a side effect, but the goal text may still be routed into the normal assistant conversation alongside the state update.
The plugin depends on experimental.chat.system.transform and other OpenCode plugin hooks that may change between OpenCode versions.
Point OpenCode at the source file directly for local testing:
{
"plugin": ["file:///absolute/path/to/opencode-goal-plugin/src/goal-plugin.js"]
}Keep test files outside OpenCode's auto-loaded plugin directory — OpenCode will attempt to load plugin-like files it finds there.
- Run
npm run smoketo verify the package export path and/goalcommand hook without a model call. - Install or file-load the plugin in a temporary OpenCode config.
- Add a
goalcommand with"template": "$ARGUMENTS". - Run
/goal status— should report no active goal. - Run
/goal inspect this repo and stop immediately with [goal:blocked] if you need user input. - Verify
/goal status,/goal pause,/goal resume, and/goal clearbehave as expected. - If you changed hook payload handling or command behavior, repeat the smoke test against the exact OpenCode version and provider/backend combination you care about.
npm test # run the test suite
npm run test:coverage # run tests with coverage
npm run smoke # verify package export + command hook without a model call
npm run check # syntax check + tests
npm run pack:check # verify package contents before publishingMIT