opencode-goal-plugin 0.6.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +24 -4
- package/index.d.ts +36 -0
- package/package.json +1 -1
- package/scripts/verify.mjs +9 -1
- package/src/goal-plugin.js +1117 -72
- package/src/opencode-session-api.js +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.8.0 — 2026-08-06
|
|
6
|
+
|
|
7
|
+
Both new options in this release were contributed by
|
|
8
|
+
[@harryzhou2000](https://github.com/harryzhou2000) in
|
|
9
|
+
[#53](https://github.com/willytop8/OpenCode-goal-plugin/pull/53).
|
|
10
|
+
|
|
11
|
+
- Add `noInterruptOnUserMessage` plugin option. When `true`, a new human message steers an active goal — the loop keeps running and the message is included in the next continuation — instead of pausing it with `stopReason: "user intervention"`. The pause-on-intervention default is unchanged.
|
|
12
|
+
- Add `noContinueWhileChildrenActive` plugin option. When `true`, auto-continue is deferred while the session has active child sessions (subagents, background tasks), so the goal loop does not prompt the orchestrator over work a child is already doing; the goal stays running and continues on a later idle once the children finish. A child counts as active only while the host reports a non-idle status for it. Each deferral episode records a `deferred` history event and a status line so `/goal status` distinguishes "waiting on a subagent" from a hung loop. A deferred goal is re-driven by the child's own idle event, since a parent that is already idle emits no event of its own while a child runs. Hosts that cannot report children/status, and sessions with more concurrent children than the plugin can track, fail open and log once per plugin instance.
|
|
13
|
+
|
|
14
|
+
## 0.7.0 — 2026-08-02
|
|
15
|
+
|
|
16
|
+
- Make `/goal status` add explicit `State:` and `Completion audit:` lines without changing its existing `Active goal:` header; make `/goal list` report `active`, `paused`, or `blocked` and preserve the reason for stopped focused goals. Completion audit reporting distinguishes the evidence gate, built-in independent verifier, and custom completion auditor.
|
|
17
|
+
- Add bounded, transition-only lifecycle notices through OpenCode's structured log and TUI toast, with independent `lifecycleMessages` and `lifecycleMessenger` controls. Delivery is advisory, does not create model turns, and does not announce routine idle/checkpoint activity. Completion/block uses the audit-result message when `auditMessages` is enabled and one lifecycle fallback only when it is disabled.
|
|
18
|
+
- Harden lifecycle persistence around the new feedback path: failed completion writes cannot resurrect an older goal over newer session state, blocked ledger events repair a lagging snapshot with their concrete reason, and clear operations disclose when neither snapshot nor ledger recorded the deletion durably.
|
|
19
|
+
|
|
3
20
|
## 0.6.8 — 2026-08-02
|
|
4
21
|
|
|
5
22
|
- Serialize fresh-namespace migration-marker publication across concurrent processes so Windows does not reject competing first-start renames with `EPERM`.
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Compatibility: this plugin relies on experimental OpenCode hooks. Re-test agains
|
|
|
18
18
|
- Guarded auto-continuation with turn, duration, token, no-progress, and no-tool-call limits.
|
|
19
19
|
- Project-local restart recovery backed by persisted state and a bounded lifecycle ledger.
|
|
20
20
|
- Evidence-gated completion with an optional independent, fail-closed verifier.
|
|
21
|
+
- Explicit `active`, `paused`, and `blocked` status plus transition-only lifecycle notices.
|
|
21
22
|
- Canonical agent tools, collision-safe goal/verifier agents, multiple goals, and ordered goal sequences.
|
|
22
23
|
|
|
23
24
|
This project is independently implemented for OpenCode. Product names used elsewhere identify their respective owners; no feature-parity or endorsement claim is implied.
|
|
@@ -38,7 +39,7 @@ surface and versioning expectations.
|
|
|
38
39
|
|
|
39
40
|
Tested against real OpenCode 1.17.15 processes with live provider credentials and no mocked plugin hooks. State, ledger entries, and workspace files were checked independently of terminal or model prose:
|
|
40
41
|
|
|
41
|
-
| OpenCode Version | Provider Tested | `/goal status` | Auto-continue | Evidence-gated completion | v0.6.6
|
|
42
|
+
| OpenCode Version | Provider Tested | `/goal status` | Auto-continue | Evidence-gated completion | Historical custom-command presentation (v0.6.6) |
|
|
42
43
|
|---|---|---|---|---|---|
|
|
43
44
|
| 1.17.15 | opencode (`deepseek-v4-flash-free`) | ✅ Canonical tool | ✅ Checkpoint + idle continuation | ✅ Structured `goal_complete` claim | ⚠️ Command text routed to model; mutation guard verified |
|
|
44
45
|
| 1.17.15 | opencode-go (`qwen3.7-plus`) | ✅ | ✅ | ✅ Self-corrected after one rejection (bare `[goal:complete]` with no evidence), then completed cleanly | ⚠️ Not displayed |
|
|
@@ -49,6 +50,8 @@ Tested against real OpenCode 1.17.15 processes with live provider credentials an
|
|
|
49
50
|
|
|
50
51
|
**Note:** The table records the v0.6.6 live-provider matrix. In that release, OpenCode 1.17.15 retained the original command-parts array, so assigning a new `output.parts` array did not replace the raw command argument sent to the model. The current implementation mutates that retained array in place, making the plugin-generated command result the prompt for the turn. OpenCode custom commands still run through the model rather than rendering hook output directly, so the visible response may summarize or paraphrase the result (see [Limitations](#limitations)). Re-test against the exact OpenCode build and provider/backend stack you rely on for unattended work, and see [`docs/providers.md`](docs/providers.md) for the full historical model matrix.
|
|
51
52
|
|
|
53
|
+
Separately, the lifecycle-feedback implementation included in v0.7.0 passed a real OpenCode 1.18.11 host canary covering create, status, pause, resume, edit, and default lifecycle logging with a deterministic localhost provider. That canary validates host integration, not another live-provider compatibility row.
|
|
54
|
+
|
|
52
55
|
## Install
|
|
53
56
|
|
|
54
57
|
```sh
|
|
@@ -100,6 +103,8 @@ Check status:
|
|
|
100
103
|
/goal status
|
|
101
104
|
```
|
|
102
105
|
|
|
106
|
+
`/goal status` keeps its existing `Active goal:` heading and adds an explicit `State:` line: `active` while the goal can continue, `blocked` when the assistant recorded a concrete blocker, and `paused` for other retained stops such as user intervention, a safety limit, or an audit rejection. A `Completion audit:` line distinguishes the always-on evidence gate from an optional built-in independent verifier or custom completion auditor.
|
|
107
|
+
|
|
103
108
|
View lifecycle history and the latest checkpoint:
|
|
104
109
|
|
|
105
110
|
```
|
|
@@ -156,7 +161,7 @@ A session can hold more than one goal. `/goal <condition>` replaces the focused
|
|
|
156
161
|
/goal focus 1
|
|
157
162
|
```
|
|
158
163
|
|
|
159
|
-
`/goal list` shows numbered live goals (focused and backgrounded) plus achieved goals retained in the per-session archive. `/goal clear` intentionally removes live goals and saved status from these views; its terminal ledger entries remain available for crash-safe recovery decisions. `/goal focus <number>` switches the active goal, backgrounding the previous one. Focus is tracked per session and survives a restart.
|
|
164
|
+
`/goal list` shows numbered live goals (focused and backgrounded) plus achieved goals retained in the per-session archive. Each live entry includes its explicit `active`, `paused`, or `blocked` state; a stopped focused goal keeps its bounded stop or blocker reason visible. `/goal clear` intentionally removes live goals and saved status from these views; its terminal ledger entries remain available for crash-safe recovery decisions. `/goal focus <number>` switches the active goal, backgrounding the previous one. Focus is tracked per session and survives a restart.
|
|
160
165
|
|
|
161
166
|
#### Ordered sequences
|
|
162
167
|
|
|
@@ -324,6 +329,8 @@ Additional plugin-level options:
|
|
|
324
329
|
- `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.
|
|
325
330
|
- `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.
|
|
326
331
|
- `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). Default `2`; set the plugin option to `0` for legitimate tool-free writing/research workflows.
|
|
332
|
+
- `noInterruptOnUserMessage` — when `true`, a new human message no longer pauses an active goal ("user intervention"); the goal loop keeps running and the message steers the next continuation. Because typing a message no longer stops the loop, `/goal pause` and `/goal stop` become the way to halt it. Default `false`, which pauses for `/goal resume` as before.
|
|
333
|
+
- `noContinueWhileChildrenActive` — when `true`, auto-continue is deferred while the session has active child sessions (subagents, background tasks): the goal stays running but does not prompt the orchestrator until the children finish. A child counts as active only while the host reports a non-idle status for it, and each deferral is reported in `/goal status` and the lifecycle history so a waiting goal is never mistaken for a hung one. Default `false`. Enabling it adds a `children` and a `status` call to each idle the goal loop evaluates. The gate fails open — continuation proceeds — for hosts that cannot report children/status, for sessions with more concurrent children than the plugin can track, and for children that run goals of their own. Note that the gate relies on the child's own idle event to resume, so a host that never emits one leaves the goal waiting; `/goal status` reports the deferral in that case.
|
|
327
334
|
- `warnTurnsRemaining` / `warnDurationMsRemaining` / `warnTokensRemaining` — thresholds at which the auto-continue prompt appends a "limits are near" warning (default `3` turns, `60000` ms, `25000` context tokens). Lower them to warn closer to the limit, or raise them to warn earlier.
|
|
328
335
|
- `commandName` — the slash command the plugin owns (default `goal`). Set it to e.g. `objective` to drive the workflow with `/objective` instead of `/goal`; a leading slash is tolerated. Remember to register the matching command name in your OpenCode `command` config. User-facing hints (`/goal status`, `/goal resume`, …) follow the configured name.
|
|
329
336
|
- `registerCommand` — whether the plugin installs its `command.execute.before` hook at all (default `true`). Set it to `false` if you only want the auto-continue/persistence behavior driven programmatically and don't want the plugin to own a slash command.
|
|
@@ -336,6 +343,8 @@ Additional plugin-level options:
|
|
|
336
343
|
- `ledgerMaxBytes` / `ledgerRetentionFiles` — bound the lifecycle ledger to 2 MiB per generation and three rotated generations by default. Set retention to `0` to discard the active ledger when it reaches the size ceiling.
|
|
337
344
|
- `resultRetentionMs` — how long a completed goal summary remains available through `/goal status` after the goal leaves active memory.
|
|
338
345
|
- `maxStoredResults` — maximum number of completed-goal summaries retained in process memory before the oldest ones are evicted.
|
|
346
|
+
- `lifecycleMessages` — announce applied goal-state transitions (default `true`). Set to `false` to disable lifecycle notices without disabling audit messages or persistence.
|
|
347
|
+
- `lifecycleMessenger(sessionID, text)` — route lifecycle notices to a custom sink instead of the default structured-log/TUI-toast path.
|
|
339
348
|
|
|
340
349
|
## Agent tools
|
|
341
350
|
|
|
@@ -352,12 +361,23 @@ These operate on the same per-session multi-goal state as the command path: a to
|
|
|
352
361
|
|
|
353
362
|
> Integration note: the tool execute-context shape (`ctx.sessionID`) and Zod argument definitions follow the OpenCode plugin docs. The tool **logic** is unit-tested independently, but live registration should still be confirmed against the exact OpenCode host used in production (see the smoke-test checklist).
|
|
354
363
|
|
|
364
|
+
## Lifecycle messages
|
|
365
|
+
|
|
366
|
+
The plugin announces meaningful, applied state transitions such as goal creation, focus changes, pause/resume, recovery, ordered-goal promotion, and clearing. It does not emit a notice for every idle event, checkpoint, or continuation attempt. Messages are bounded and avoid dumping the full objective, evidence, or filesystem paths.
|
|
367
|
+
|
|
368
|
+
By default, lifecycle notices go to OpenCode's structured log and to a TUI toast when that host capability is available. Provide a `lifecycleMessenger(sessionID, text)` plugin option to route them elsewhere, or set `lifecycleMessages: false` to disable them. Delivery is advisory: notices do not start an assistant turn or make any extra model call, and a log, toast, or custom-messenger failure does not undo the recorded state transition.
|
|
369
|
+
|
|
370
|
+
Lifecycle notices and audit messages are separate controls. Lifecycle notices describe applied goal state; audit messages describe completion/block validation. When `auditMessages` is `true`, its audit-result message is the sole completion/block announcement. When `auditMessages` is `false` and `lifecycleMessages` is `true`, the lifecycle channel emits one terminal fallback instead. Other transitions follow `lifecycleMessages`; disabling one control does not disable the other.
|
|
371
|
+
|
|
355
372
|
## Audit messages
|
|
356
373
|
|
|
357
|
-
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 written to OpenCode's structured log and shown as a TUI toast when that client capability is available. Provide an `auditMessenger(sessionID, text)` plugin option to route them elsewhere, or set `auditMessages: false` to disable them.
|
|
374
|
+
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 written to OpenCode's structured log and shown as a TUI toast when that client capability is available. Provide an `auditMessenger(sessionID, text)` plugin option to route them elsewhere, or set `auditMessages: false` to disable them. The audit-result message owns the terminal completion/block announcement while `auditMessages` is enabled, so the lifecycle channel does not duplicate it.
|
|
375
|
+
|
|
376
|
+
Audit messages are visibility only; enabling them does not turn on the independent completion auditor. The evidence gate always applies. Independent verification is enabled only with `completionAudit: true` or a custom `auditor`.
|
|
377
|
+
|
|
358
378
|
## Completion auditor (optional)
|
|
359
379
|
|
|
360
|
-
|
|
380
|
+
Every `[goal:complete]` claim must first pass the local evidence gate described above. By default, that evidence gate is the only verifier. You can additionally require an independent audit before a goal is archived:
|
|
361
381
|
|
|
362
382
|
- `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).
|
|
363
383
|
- `auditor: async ({ goal, sessionID, latestText }) => ({ approved, reason })` — supply your own auditor function (takes precedence over `completionAudit`).
|
package/index.d.ts
CHANGED
|
@@ -174,6 +174,25 @@ export interface GoalPluginOptions {
|
|
|
174
174
|
*/
|
|
175
175
|
noToolCallTurnsBeforePause?: number
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* When `true`, a new human message does not pause an active goal: the goal
|
|
179
|
+
* loop keeps running and the message steers the next continuation instead of
|
|
180
|
+
* stopping with `stopReason: "user intervention"`. Plugin-owned command and
|
|
181
|
+
* continuation messages are never treated as interventions either way.
|
|
182
|
+
* @default false
|
|
183
|
+
*/
|
|
184
|
+
noInterruptOnUserMessage?: boolean
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* When `true`, auto-continue is deferred while the session has active child
|
|
188
|
+
* sessions (subagents or background tasks), so the goal loop does not prompt
|
|
189
|
+
* the orchestrator over work a child is already doing. The goal stays
|
|
190
|
+
* running and the next idle event continues once the children are done.
|
|
191
|
+
* Hosts that cannot report children/status fail open (continuation proceeds).
|
|
192
|
+
* @default false
|
|
193
|
+
*/
|
|
194
|
+
noContinueWhileChildrenActive?: boolean
|
|
195
|
+
|
|
177
196
|
/**
|
|
178
197
|
* Fraction (between 0 and 1, exclusive) of any budget (turns, duration,
|
|
179
198
|
* or tokens) at which the plugin sends a one-time "wrap up" prompt
|
|
@@ -324,6 +343,23 @@ export interface GoalPluginOptions {
|
|
|
324
343
|
*/
|
|
325
344
|
auditorOptions?: CompletionAuditorOptions
|
|
326
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Whether the plugin announces applied goal-state transitions such as
|
|
348
|
+
* creation, pause/resume, recovery, promotion, and clearing. Routine
|
|
349
|
+
* idle/checkpoint activity is not announced. Completion/block uses the
|
|
350
|
+
* audit-result channel when enabled and this lifecycle channel only as its
|
|
351
|
+
* disabled fallback.
|
|
352
|
+
* @default true
|
|
353
|
+
*/
|
|
354
|
+
lifecycleMessages?: boolean
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Custom sink for bounded lifecycle notices. Defaults to routing through
|
|
358
|
+
* OpenCode's structured log (`client.app.log`) and TUI toast when those host
|
|
359
|
+
* APIs are available. Delivery is advisory and does not make model calls.
|
|
360
|
+
*/
|
|
361
|
+
lifecycleMessenger?: (sessionID: string, text: string) => Promise<void> | void
|
|
362
|
+
|
|
327
363
|
/**
|
|
328
364
|
* Whether the plugin announces completion/blocked audits (an
|
|
329
365
|
* audit-start and an audit-result message) instead of running silently.
|
package/package.json
CHANGED
package/scripts/verify.mjs
CHANGED
|
@@ -146,7 +146,8 @@ await check("/goal set works", async () => {
|
|
|
146
146
|
assert.match(text, /New active goal: verify the installation/)
|
|
147
147
|
const statusText = await runGoalCommand("status")
|
|
148
148
|
assert.match(statusText, /Active goal: verify the installation/)
|
|
149
|
-
assert.
|
|
149
|
+
assert.match(statusText, /State: active/)
|
|
150
|
+
assert.match(statusText, /Completion audit: evidence gate only \(independent verifier off\)/)
|
|
150
151
|
})
|
|
151
152
|
|
|
152
153
|
await check("no model calls were made during verification", () => {
|
|
@@ -156,6 +157,13 @@ await check("no model calls were made during verification", () => {
|
|
|
156
157
|
// Clean up the goal created above so this script has no side effects.
|
|
157
158
|
await runGoalCommand("clear")
|
|
158
159
|
|
|
160
|
+
await check("lifecycle transitions are visible without leaking objective text", () => {
|
|
161
|
+
assert.deepEqual(logCalls.map((entry) => entry.body.extra.kind), ["goal-lifecycle", "goal-lifecycle"])
|
|
162
|
+
assert.match(logCalls[0].body.message, /Goal (?:active|started)/i)
|
|
163
|
+
assert.match(logCalls[1].body.message, /Goal cleared/i)
|
|
164
|
+
assert.ok(logCalls.every((entry) => !entry.body.message.includes("verify the installation")))
|
|
165
|
+
})
|
|
166
|
+
|
|
159
167
|
console.log()
|
|
160
168
|
|
|
161
169
|
const failed = results.filter((r) => !r.ok)
|