Yet another
/goalplugin for opencode.
Cache-friendly /goal continuation for opencode. When a goal is active, idle
sessions are re-prompted with the objective as a fresh user message, not as
a system-prompt edit. Provider prompt-cache hits stay valid across continuation
turns.
npm install
npm run buildThen copy dist/plugin.js to your project's .opencode/plugin/goal.js
(auto-discovered), or point the plugin to opencode.json:
{
"plugin": ["./path/to/opencode-goal-plugin/dist/plugin.js"]
}If you would like your /goal session to save somewhere else, you can do this:
"plugin": [
["C:/path/to/plugin/opencode-goal-plugin/dist/plugin.js", { "stateDir": "C:/Users/RedTea/new/path/to/session_storage" }]
]The /goal command body is bundled into the plugin and registered via the
config hook. If another goal plugin has already registered /goal, this
plugin defers — it does not overwrite and does not throw.
/goal <objective text> set a goal
/goal budget <minutes> <objective text> set a goal with a wall-clock budget
/goal status show current goal
/goal pause pause the active goal
/goal resume resume a paused goal
/goal clear clear the goal entirely
/goal complete [evidence] manually mark complete
/goal blocked <reason> manually mark blocked
When active, the plugin re-prompts the assistant every time the session goes idle, until one of:
goal_completeis called (model confirms objective is verifiably achieved),goal_blockedis called (same unresolvable blocker persists),- the wall-clock budget runs out (auto-pauses with
blockedstatus), - or the soft turn limit (~25 continuation turns) is hit.
| Tool | Purpose |
|---|---|
goal_status |
Return the current goal state, budget, and turn count. |
goal_complete |
Mark complete. Requires a one-line evidence argument naming what was verified. |
goal_blocked |
Mark blocked. Requires a concrete reason. |
These are real tools (not system-prompt instructions), so the assistant can audit itself before declaring completion or blockage.
Each session's goal is persisted as JSON to .opencode/goal/<sessionID>.json
in the project directory. Resume-friendly.
Other plugins (e.g. opencode-goal-plugin@0.9.0 on npm) hook
experimental.chat.system.transform and inject the goal block into the system
prompt. That means every continuation turn mutates the system prompt prefix
and invalidates provider-side cache from byte 0, turning O(1) cache hits into
O(N × turns) full-context misses.
This plugin instead injects the continuation prompt as a user message via
client.session.promptAsync(...). The system prompt is never touched,
so provider-side cache prefixes stay byte-identical across turns, which increase prompt cache hits and reduce token usage.
When the assistant's session goes idle while a goal is active, the plugin
hooks the session.idle event, and:
- Reads the active goal from
.opencode/goal/<sessionId>.json. - If the goal is active and under its soft turn / wall-clock budget,
it sends a fresh user message to the session:
[continue] Goal continuation #N. Call goal_status to read the active objective…. - The message is sent via
client.session.promptAsyncwith the{ path: { id }, body: { parts: [...] } }shape — note that the flattened{ sessionID, parts }shape is rejected by the opencode server with an "Unexpected server error", so the plugin retries with the legacy shape automatically. - The continuation prompt does not contain the objective itself —
it tells the assistant to call
goal_statusto read it. This keeps continuation turns cheap and letsgoal_statusalways reflect the latest persisted state.