opencode-goal-plugin 0.6.6 → 0.6.8
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 +11 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +23 -14
- package/docs/compatibility.md +53 -3
- package/docs/providers.md +24 -18
- package/docs/releasing.md +1 -1
- package/index.d.ts +16 -7
- package/package.json +4 -10
- package/scripts/verify.mjs +47 -6
- package/src/goal-plugin.js +1009 -274
- package/src/persistence-lease.js +594 -59
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.8 — 2026-08-02
|
|
4
|
+
|
|
5
|
+
- Serialize fresh-namespace migration-marker publication across concurrent processes so Windows does not reject competing first-start renames with `EPERM`.
|
|
6
|
+
- Keep a second OpenCode process usable when it opens a session whose goal-state shard is already leased: ordinary chat and unrelated tools remain available, while goal commands and tools fail safely without reading, changing, or prompting from that session's goal state. After the owner exits, the next explicit goal command or tool retries ownership and reloads any active goal paused. Lease contention is now typed and owner metadata is sanitized; unrelated filesystem failures still fail closed.
|
|
7
|
+
- Harden the single-writer lease with immutable per-owner claims so delayed publication, simultaneous stale reclaim, and concurrent release cannot remove a replacement owner's lease. A complete compatibility guard is published atomically with no replacement, preventing old/current startup races; legacy, incomplete, tampered, or unsupported lease layouts fail closed for explicit manual recovery. Owner reads remain bounded and symlink-safe, slow passive command guards keep blocking tools until an authenticated new boundary, delayed control errors cannot pause a newer goal run, takeover retains Plan/model execution context, and advisory host logging cannot stall loading or disposal.
|
|
8
|
+
|
|
9
|
+
## 0.6.7 — 2026-08-01
|
|
10
|
+
|
|
11
|
+
- Fix `/goal` routing on OpenCode 1.17.15 and 1.18.10 by replacing the host-retained prompt parts in place, authenticating each resolved command turn (including host-expanded file attachments), framing control results with direct reporting instructions, blocking tools during those reporting turns, and excluding control responses from goal completion and progress analysis. Unreadable command attachments now become read-only error reports and pause safely without losing command provenance.
|
|
12
|
+
- Register all 11 agent-facing goal tools in a standard clean install through the package's direct Zod dependency; a separate optional `@opencode-ai/plugin` install is no longer required.
|
|
13
|
+
|
|
3
14
|
## 0.6.6 — 2026-07-29
|
|
4
15
|
|
|
5
16
|
- Shard persisted goal state and lifecycle ledgers by hashed OpenCode session ID so independent sessions can run concurrently in one project. Keep same-session single-writer leases, lazy session loading, crash recovery, and safe migration of aggregate, legacy, and XDG state.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -37,7 +37,7 @@ This plugin depends on OpenCode plugin hooks, including experimental hooks. When
|
|
|
37
37
|
3. Test against a real OpenCode install when possible.
|
|
38
38
|
4. Update the README compatibility snapshot if the tested surface changes.
|
|
39
39
|
|
|
40
|
-
`npm run smoke` verifies the package export path and `/goal` command hook without invoking a model. The packed-host and packed-tool checks install the npm artifact in isolated consumer projects and verify the public
|
|
40
|
+
`npm run smoke` verifies the package export path and `/goal` command hook without invoking a model. The packed-host and packed-tool checks install the npm artifact in isolated consumer projects and verify the public hooks and clean-install default tool contracts. `npm run type:check` compiles installed-package consumers with NodeNext and Bundler resolution. `npm run benchmark:behavior` covers deterministic autonomy and token-efficiency scenarios. None replaces a real OpenCode smoke test after hook, SDK, or command behavior changes.
|
|
41
41
|
|
|
42
42
|
## Release checklist
|
|
43
43
|
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ surface and versioning expectations.
|
|
|
38
38
|
|
|
39
39
|
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
40
|
|
|
41
|
-
| OpenCode Version | Provider Tested | `/goal status` | Auto-continue | Evidence-gated completion | Hook Output Display |
|
|
41
|
+
| OpenCode Version | Provider Tested | `/goal status` | Auto-continue | Evidence-gated completion | v0.6.6 Hook Output Display |
|
|
42
42
|
|---|---|---|---|---|---|
|
|
43
43
|
| 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
44
|
| 1.17.15 | opencode-go (`qwen3.7-plus`) | ✅ | ✅ | ✅ Self-corrected after one rejection (bare `[goal:complete]` with no evidence), then completed cleanly | ⚠️ Not displayed |
|
|
@@ -47,7 +47,7 @@ Tested against real OpenCode 1.17.15 processes with live provider credentials an
|
|
|
47
47
|
|
|
48
48
|
`/goal status` and auto-continue are graded on **state correctness** (verified directly against persisted state: correct limits, turn/stop accounting, completion state, and file effects), not on terminal rendering. The `deepseek-v4-flash-free` canary suite additionally covers pause/resume across processes, blocker/restart, hard-process recovery, real host compaction, and stale-history clear enforcement. See [`docs/providers.md`](docs/providers.md) for the complete lifecycle matrix and session evidence.
|
|
49
49
|
|
|
50
|
-
**Note:**
|
|
50
|
+
**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
51
|
|
|
52
52
|
## Install
|
|
53
53
|
|
|
@@ -193,8 +193,8 @@ An ordered sequence, run as a strict pipeline:
|
|
|
193
193
|
|
|
194
194
|
## How it works
|
|
195
195
|
|
|
196
|
-
1. When you set a goal, the plugin stores it in session
|
|
197
|
-
2. Each time the session goes idle, the plugin sends a continuation prompt containing the
|
|
196
|
+
1. When you set a goal, the plugin stores it in per-session state and replaces the custom-command turn with a plugin-generated work instruction containing that objective. On hosts that invoke `experimental.chat.system.transform`, it also reinforces the active goal in the system prompt; command correctness does not rely on that experimental hook.
|
|
197
|
+
2. Each time the session goes idle, the plugin sends a continuation prompt containing the remaining budget and completion audit while the original goal remains in conversation history. Continuations retain the agent, provider/model, and variant that initiated the goal. Before sending after a cooldown, the plugin re-checks that the session is still idle and no human message, newer assistant turn, Plan-agent switch, rejected permission, abort, or provider error has superseded the request.
|
|
198
198
|
3. 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.
|
|
199
199
|
4. 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.
|
|
200
200
|
5. If you send a message of your own while the goal is running, the plugin treats it as the latest instruction, pauses auto-continue, and asks OpenCode to abort an already accepted continuation so it does not talk over you. The plugin's own continuation prompts are ignored for this check (they are not "your" messages). A durable claim on the source assistant turn also prevents different idle event IDs from sending the same continuation twice. Run `/goal resume` to hand control back to the goal loop.
|
|
@@ -255,6 +255,10 @@ Alongside each session shard the plugin keeps an **append-only lifecycle ledger*
|
|
|
255
255
|
|
|
256
256
|
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).
|
|
257
257
|
|
|
258
|
+
Only one OpenCode process may own a given session shard at a time. If the same session is opened in a second process, that process enters **passive goal mode** instead of failing the whole session: ordinary chat and unrelated tools continue to work, but `/goal` commands and goal tools report that another process owns the workflow. Canonical goal tools return the stable envelope code `error: "session_owned_elsewhere"`. The passive process does not read, mutate, persist, or auto-continue that session's goal state. After the owner exits, retry an explicit goal command or goal tool; the process will acquire the shard and load any recovered active goal paused. To work concurrently without waiting, create a new session with `opencode --continue --fork` (or `opencode --session <id> --fork`).
|
|
259
|
+
|
|
260
|
+
Lease ownership uses immutable per-process claim files so a delayed stale-lock cleanup or duplicate release cannot delete a newer owner's lease. The plugin publishes a complete regular-file compatibility guard atomically at `<shard>/state.json.lock`, then elects the current owner from claims in the sibling `<shard>/state.json.lock.claims-v2/` directory. That no-replace publication makes startup safe against older releases: either the older lock directory wins and the current plugin stays passive, or the guard file wins and the older release cannot reclaim it. Automatic ownership handoff requires the current release. Legacy, incomplete, tampered, or unsupported lease layouts fail closed instead of being rewritten online; filesystems must support regular-file hard links and preserve the guard's future timestamp. After confirming that every process using the session is closed and upgraded, either fork or remove only the affected shard's adjacent `.lock` file or legacy directory **and** `.lock.claims-v2` directory; keep its state and ledger.
|
|
261
|
+
|
|
258
262
|
`/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.
|
|
259
263
|
|
|
260
264
|
### Per-goal flags
|
|
@@ -323,7 +327,7 @@ Additional plugin-level options:
|
|
|
323
327
|
- `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.
|
|
324
328
|
- `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.
|
|
325
329
|
- `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.
|
|
326
|
-
- `registerTools` — whether the plugin registers the agent-facing goal tools (default `true`).
|
|
330
|
+
- `registerTools` — whether the plugin registers the agent-facing goal tools (default `true`). Set to `false` to omit the programmatic tool surface entirely. See [Agent tools](#agent-tools).
|
|
327
331
|
- `registerAgents` — whether the config hook adds native `goal` and `goal-verify` agents (default `true`). Existing agents with those names are preserved unchanged; the plugin never changes your default agent.
|
|
328
332
|
- `goalAgentName` / `verifierAgentName` — customize the registered native agent names (defaults `goal` and `goal-verify`). The verifier is a hidden subagent with a default-deny tool policy; only `read`, `glob`, and `grep` are allowed.
|
|
329
333
|
- `sdkShape` — OpenCode session-client argument shape: `legacy` (the default generated `PluginInput` client using `{ path, body, query }`) or `flat` (clients using `{ sessionID, ... }`). Read-only `messages`/`get` calls may probe the alternate shape after an argument/schema `TypeError`; mutating calls are never replayed, so set this option correctly for embedded clients.
|
|
@@ -333,9 +337,9 @@ Additional plugin-level options:
|
|
|
333
337
|
- `resultRetentionMs` — how long a completed goal summary remains available through `/goal status` after the goal leaves active memory.
|
|
334
338
|
- `maxStoredResults` — maximum number of completed-goal summaries retained in process memory before the oldest ones are evicted.
|
|
335
339
|
|
|
336
|
-
## Agent tools
|
|
340
|
+
## Agent tools
|
|
337
341
|
|
|
338
|
-
In addition to the `/goal` command, the plugin
|
|
342
|
+
In addition to the `/goal` command, the plugin registers the same workflow as callable model tools by default, so the agent can inspect and manage the goal itself. A normal `opencode-goal-plugin` install includes the schema dependency needed for these definitions; no separate OpenCode helper package is required. Disable the tool surface explicitly with `registerTools: false`.
|
|
339
343
|
|
|
340
344
|
Registered tools:
|
|
341
345
|
|
|
@@ -346,7 +350,7 @@ Registered tools:
|
|
|
346
350
|
|
|
347
351
|
These operate on the same per-session multi-goal state as the command path: a tool-set goal persists, shows up in `/goal list`, and is driven by the idle auto-continue; completing a goal in an ordered sequence auto-promotes the next.
|
|
348
352
|
|
|
349
|
-
> Integration note: the tool execute-context shape (`ctx.sessionID`) and
|
|
353
|
+
> 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).
|
|
350
354
|
|
|
351
355
|
## Audit messages
|
|
352
356
|
|
|
@@ -385,11 +389,15 @@ The goal text is wrapped in `<goal_objective>` tags and labeled as user-provided
|
|
|
385
389
|
|
|
386
390
|
The assistant still signals candidate outcomes with `[goal:complete]` or `[goal:blocked]`. Completion can additionally be checked by a custom `auditor` callback or the built-in child-session auditor before the goal becomes terminal. Marker quality therefore remains model-dependent when auditing is disabled, and audit quality depends on the configured verifier model and evidence available in the session. The built-in verifier performs static inspection with `read`, `glob`, and `grep`; it cannot execute shell commands.
|
|
387
391
|
|
|
388
|
-
OpenCode
|
|
392
|
+
OpenCode custom commands are prompts, not direct plugin-rendered TUI responses. After `command.execute.before` runs, OpenCode sends its retained command-parts array through a normal model turn. The plugin mutates that array in place so the model receives the deterministic plugin-generated result instead of the raw `/goal` argument. The model still produces the visible response and may summarize or paraphrase that result.
|
|
393
|
+
|
|
394
|
+
Objective-bearing commands preserve file attachments. OpenCode may expand those files into synthetic Read/MCP text and file parts before `chat.message`; the plugin accepts that expansion only when it matches the one-shot command correlation, retained-file count, and generated message/session identity. Other mixed text is treated as a new human instruction and pauses an active loop. If OpenCode reports an attachment-read error during that expansion, the goal pauses with `attachment resolution error` while retaining the correct command provenance.
|
|
395
|
+
|
|
396
|
+
For `/goal status`, `/goal history`, `/goal list`, `/goal pause`, and `/goal clear` (including aliases), the rewritten user turn carries an escaped control-result envelope with direct instructions to report the supplied data without treating it as new work. `tool.execute.before` rejects every tool call for that reporting turn, and the parent-correlated assistant response is excluded from checkpoint, completion, blocker, and stall analysis. These protections do not depend on the model following the reporting instruction.
|
|
389
397
|
|
|
390
|
-
The plugin
|
|
398
|
+
The plugin still registers `experimental.chat.system.transform` as defense in depth for hosts that invoke it. Real OpenCode 1.17.15 and 1.18.10 do not call that hook, so the command-control protections above are deliberately self-contained. Other OpenCode plugin hooks may change between versions.
|
|
391
399
|
|
|
392
|
-
Distinct OpenCode sessions may own shards under the same `stateFilePath` concurrently. A second process using the same session shard
|
|
400
|
+
Distinct OpenCode sessions may own shards under the same `stateFilePath` concurrently. A second process using the same session shard remains usable in passive goal mode, but goal commands and tools are denied until it can acquire that shard. The passive process never falls back to an unpersisted copy of the same goal workflow, which avoids divergent state and last-writer-wins data loss. Use the owner, wait and retry an explicit goal control after it exits, or fork to a new session. A no-replace compatibility guard prevents an older release and the current release from both acquiring during startup; current immutable claims protect takeover and release among upgraded processes. Older processes cannot take over a guarded shard, so all processes participating in automatic same-session handoff must run the current release.
|
|
393
401
|
|
|
394
402
|
## Diagnostics and recovery
|
|
395
403
|
|
|
@@ -399,9 +407,10 @@ If a goal does not continue:
|
|
|
399
407
|
|
|
400
408
|
1. Check for a deliberate pause: user intervention, a hard limit, repeated tool-free/no-progress turns, prompt failures, or a rejected completion audit all stop unattended work by design.
|
|
401
409
|
2. Run `/goal resume` only after resolving the reported reason. Resume creates a fresh local budget window; it does not erase the objective or history.
|
|
402
|
-
3.
|
|
403
|
-
4.
|
|
404
|
-
5.
|
|
410
|
+
3. If a goal control reports that another process owns the session, close that owner and retry the control, or fork to a new session. If it instead reports an older, incomplete, tampered, or unsupported lease, close every process that could own the session and upgrade them first; if the report persists, remove only the affected shard's adjacent `.lock` file or legacy directory and `.lock.claims-v2` directory, or fork. Keep the state and ledger. Do not point two copies of the same session at different state paths: that creates divergent goal histories.
|
|
411
|
+
4. Check OpenCode's structured logs for persistence, SDK-shape, prompt, or auditor errors.
|
|
412
|
+
5. Confirm the configured project directory and state-path precedence described under [Safety limits](#safety-limits). A daemon started elsewhere can otherwise make a manually configured relative path surprising.
|
|
413
|
+
6. Run `npm run verify`, `npm run smoke`, and `npm run smoke:packed-host` against the installed source when diagnosing registration or packaging problems. Maintainers can run `npm run release:check` for the complete artifact and quality gate. `npm run benchmark:behavior` exercises completion, false-completion, loop, interruption, compaction, and restart behavior without a provider call.
|
|
405
414
|
|
|
406
415
|
Do not paste `state.json`, its ledger, or verbose logs into a public issue without reviewing them first: they can contain goal text, assistant checkpoints, blockers, local paths, and command evidence. Prefer the bounded status/history output and redact project-specific content. There is intentionally no broad "dump diagnostics" tool: exposing process-wide session state or persistence paths to the model would add more privacy risk than troubleshooting value.
|
|
407
416
|
|
package/docs/compatibility.md
CHANGED
|
@@ -11,17 +11,47 @@ The latest published release is the supported line. Public compatibility covers:
|
|
|
11
11
|
- the six canonical goal tools and five legacy tool aliases
|
|
12
12
|
- persisted-state recovery from versions documented in the changelog
|
|
13
13
|
- concurrent persistence for distinct OpenCode sessions in one project, with
|
|
14
|
-
single-writer protection retained per session
|
|
14
|
+
single-writer protection retained per session and passive goal behavior for
|
|
15
|
+
a same-session process that does not own the lease
|
|
15
16
|
|
|
16
17
|
The package requires Node.js 18 or newer and OpenCode 1.17.15 through the latest
|
|
17
18
|
compatible 1.x release. CI runs the complete unit suite on Node 18, 20, 22, and
|
|
18
19
|
24. Installed-package contracts compile TypeScript consumers using both NodeNext
|
|
19
|
-
and Bundler resolution and
|
|
20
|
-
|
|
20
|
+
and Bundler resolution and require a clean npm-tarball install to expose the
|
|
21
|
+
default agent-tool surface without a separately installed OpenCode helper package.
|
|
21
22
|
|
|
22
23
|
Filesystem-sensitive lifecycle tests run on Linux, macOS, and Windows. POSIX file
|
|
23
24
|
mode and symbolic-link protections are applied where the operating system supports
|
|
24
25
|
them; the plugin does not claim that Windows provides equivalent POSIX semantics.
|
|
26
|
+
The Windows job also runs the installed-package type, host, and tool contracts
|
|
27
|
+
so their portable npm launcher path is exercised in CI.
|
|
28
|
+
|
|
29
|
+
When two processes open the same OpenCode session, only the lease owner may read
|
|
30
|
+
or change that session's goal workflow. The contender keeps ordinary chat and
|
|
31
|
+
unrelated tools available, but goal controls are denied and ambient hooks do not
|
|
32
|
+
attempt a takeover. Canonical goal tools return the stable envelope code
|
|
33
|
+
`session_owned_elsewhere`; a `/goal` slash command instead produces a
|
|
34
|
+
human-readable denial through its normal model-rendered command turn. Once the
|
|
35
|
+
owner exits, an explicit goal command or tool may acquire the shard; recovered
|
|
36
|
+
active goals load paused and require an explicit resume. Forking creates a
|
|
37
|
+
distinct session shard and remains the supported way to work concurrently from
|
|
38
|
+
the same conversation.
|
|
39
|
+
|
|
40
|
+
The immutable-claim lease protocol atomically hard-links a complete regular-file
|
|
41
|
+
compatibility guard at `<shard>/state.json.lock`; active owners publish unique
|
|
42
|
+
claims in the sibling `<shard>/state.json.lock.claims-v2/` directory. Publication
|
|
43
|
+
is no-replace: an older lock directory and the current guard cannot both win the
|
|
44
|
+
same startup race. Older releases treat the future-dated guard as non-reclaimable,
|
|
45
|
+
while current releases determine ownership only from immutable claims. Automatic
|
|
46
|
+
takeover requires all participating processes to run the current release.
|
|
47
|
+
Legacy, incomplete, tampered, or unsupported lease layouts fail closed rather
|
|
48
|
+
than being rewritten online. If that condition persists, first close every
|
|
49
|
+
OpenCode process that could own the session and upgrade them; then either fork
|
|
50
|
+
the session or manually remove only the affected shard's adjacent `.lock` file
|
|
51
|
+
or legacy directory and `.lock.claims-v2` directory. Do not remove its state or
|
|
52
|
+
lifecycle ledger. The local filesystem must support regular-file hard links
|
|
53
|
+
and preserve the guard's future timestamp; the plugin does not fall back to a
|
|
54
|
+
weaker publication protocol.
|
|
25
55
|
|
|
26
56
|
## OpenCode host compatibility
|
|
27
57
|
|
|
@@ -31,6 +61,26 @@ generated-client shape, but a real-host smoke test remains required when hook or
|
|
|
31
61
|
SDK behavior changes. The current manual provider matrix is maintained in
|
|
32
62
|
[providers.md](providers.md).
|
|
33
63
|
|
|
64
|
+
OpenCode custom commands still become model turns. The plugin handles `/goal`
|
|
65
|
+
arguments in `command.execute.before` and mutates the host-retained parts array
|
|
66
|
+
in place so the turn contains the plugin-generated command result rather than
|
|
67
|
+
raw command text. This makes command routing deterministic, but does not turn the
|
|
68
|
+
hook into a direct-render API: the selected model remains responsible for the
|
|
69
|
+
visible response.
|
|
70
|
+
|
|
71
|
+
For objective-bearing commands, retained file attachments may be expanded by
|
|
72
|
+
OpenCode into synthetic Read/MCP text and file parts before `chat.message`. The
|
|
73
|
+
plugin correlates that host-resolved shape to the exact one-shot command and
|
|
74
|
+
generated message/session before treating it as plugin-owned. Each retained
|
|
75
|
+
file must yield at least one resolved companion part; a host-reported read error
|
|
76
|
+
pauses the goal without reclassifying the command as human intervention.
|
|
77
|
+
|
|
78
|
+
OpenCode 1.17.15 and 1.18.10 do not invoke
|
|
79
|
+
`experimental.chat.system.transform`. Control-command correctness therefore
|
|
80
|
+
comes from the rewritten turn's escaped reporting frame, fail-closed tool
|
|
81
|
+
blocking, and parent-correlated lifecycle suppression. The system transform
|
|
82
|
+
remains registered as additional protection for hosts that support it.
|
|
83
|
+
|
|
34
84
|
## Versioning
|
|
35
85
|
|
|
36
86
|
Semantic-versioning intent is:
|
package/docs/providers.md
CHANGED
|
@@ -12,20 +12,23 @@ vary by provider and model:
|
|
|
12
12
|
real safety net, not just documentation: a model that initially skips the
|
|
13
13
|
evidence line gets one automatic correction cycle before hitting its
|
|
14
14
|
turn/time/token budget.
|
|
15
|
-
2. **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
2. **Custom-command presentation** — OpenCode sends custom commands through a
|
|
16
|
+
normal model turn rather than directly rendering plugin-hook output. The
|
|
17
|
+
plugin now mutates OpenCode's retained command-parts array in place, so that
|
|
18
|
+
turn contains the deterministic plugin-generated result instead of raw
|
|
19
|
+
`$ARGUMENTS`. Control results include an escaped reporting frame, backed by
|
|
20
|
+
tool blocking and lifecycle suppression; the model may still summarize or
|
|
21
|
+
paraphrase the supplied data.
|
|
22
|
+
This is primarily host behavior, but it remains worth checking with each
|
|
23
|
+
provider/backend. See the [OpenCode version compatibility table](../README.md#opencode-version-compatibility)
|
|
24
|
+
for the historical v0.6.6 findings.
|
|
22
25
|
|
|
23
26
|
All rows below were verified against real OpenCode processes with live
|
|
24
|
-
provider credentials and no mocked plugin hooks on OpenCode 1.17.15
|
|
25
|
-
the plugin through `/goal status`,
|
|
26
|
-
|
|
27
|
-
(limit parsing, turn/stop accounting, evidence-gated
|
|
28
|
-
independent of what was rendered in the terminal.
|
|
27
|
+
provider credentials and no mocked plugin hooks on OpenCode 1.17.15 before the
|
|
28
|
+
in-place command-parts change. They drove the plugin through `/goal status`,
|
|
29
|
+
`/goal <condition> --max-turns N`, and inspected the persisted state file to
|
|
30
|
+
confirm state mutations (limit parsing, turn/stop accounting, evidence-gated
|
|
31
|
+
completion detection) independent of what was rendered in the terminal.
|
|
29
32
|
|
|
30
33
|
## Tested models
|
|
31
34
|
|
|
@@ -64,9 +67,11 @@ Some backends (notably certain Qwen deployments on vLLM, and several
|
|
|
64
67
|
Llama.cpp/Mistral chat templates) reject a `system` role message that isn't
|
|
65
68
|
the very first message in the conversation, with an error like `"System
|
|
66
69
|
message must be at the beginning."` opencode-goal-plugin's
|
|
67
|
-
`experimental.chat.system.transform` hook
|
|
68
|
-
into the primary system entry instead of appending a
|
|
69
|
-
avoids this.
|
|
70
|
+
`experimental.chat.system.transform` hook, on hosts that invoke it, merges the
|
|
71
|
+
goal continuation block into the primary system entry instead of appending a
|
|
72
|
+
separate one, which avoids this. OpenCode 1.17.15 and 1.18.10 do not invoke that
|
|
73
|
+
experimental hook; command control on those releases uses the self-contained
|
|
74
|
+
reporting frame described above. The merge behavior is covered by regression tests in
|
|
70
75
|
[`test/goal-plugin.test.js`](../test/goal-plugin.test.js) and does not
|
|
71
76
|
require any provider-specific configuration.
|
|
72
77
|
|
|
@@ -91,9 +96,10 @@ specific provider/model:
|
|
|
91
96
|
before `[goal:complete]`. If not, confirm the plugin rejects it and
|
|
92
97
|
re-prompts with `<evidence_required>` — then check whether the model
|
|
93
98
|
self-corrects within its remaining turn budget, or exhausts it.
|
|
94
|
-
5. Run `/goal status` and
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
5. Run `/goal status` and confirm the model turn reports the plugin-generated
|
|
100
|
+
status rather than interpreting the raw word `status` as a new request.
|
|
101
|
+
The final wording may be summarized or paraphrased because OpenCode custom
|
|
102
|
+
commands still run through the selected model.
|
|
97
103
|
6. Cross-check state mutations directly against the persisted state file
|
|
98
104
|
(`.opencode/goals/state.json`, project-local by default) to confirm the
|
|
99
105
|
goal's `options` reflect the flags you passed and `state`/`stopReason`
|
package/docs/releasing.md
CHANGED
|
@@ -14,7 +14,7 @@ artifact after all checks pass.
|
|
|
14
14
|
|
|
15
15
|
`release:check` runs the unit and coverage suites, consumer type compilation,
|
|
16
16
|
critical mutation contract, behavior benchmark, source and installed-artifact
|
|
17
|
-
smoke tests,
|
|
17
|
+
smoke tests, clean-install default tool registration, and package-content check.
|
|
18
18
|
|
|
19
19
|
## Publish
|
|
20
20
|
|
package/index.d.ts
CHANGED
|
@@ -282,9 +282,9 @@ export interface GoalPluginOptions {
|
|
|
282
282
|
* (canonical `goal_status`, `goal_set`, `goal_pause`, `goal_resume`,
|
|
283
283
|
* `goal_block`, `goal_complete`, plus legacy `get_goal`,
|
|
284
284
|
* `get_goal_history`, `set_goal`, `update_goal`, `clear_goal`).
|
|
285
|
-
* Canonical tools return versioned JSON envelopes.
|
|
286
|
-
*
|
|
287
|
-
*
|
|
285
|
+
* Canonical tools return versioned JSON envelopes. The tools are registered
|
|
286
|
+
* by default from dependencies included with this package; set this to
|
|
287
|
+
* `false` to omit the tool surface.
|
|
288
288
|
* @default true
|
|
289
289
|
*/
|
|
290
290
|
registerTools?: boolean
|
|
@@ -348,9 +348,19 @@ export interface GoalPluginOptions {
|
|
|
348
348
|
export interface GoalPluginHooks {
|
|
349
349
|
/** Registers collision-safe native goal and verifier agents. */
|
|
350
350
|
config: (config: unknown) => Promise<void>
|
|
351
|
-
/**
|
|
351
|
+
/** Remembers the initiating agent, model, and variant for later continuation turns. */
|
|
352
|
+
"chat.params": (input: unknown) => Promise<void>
|
|
353
|
+
/** Distinguishes real user input from plugin-generated command and continuation messages. */
|
|
354
|
+
"chat.message": (input: unknown, output: unknown) => Promise<void>
|
|
355
|
+
/**
|
|
356
|
+
* Handles the configured OpenCode custom command by replacing its retained
|
|
357
|
+
* prompt parts in place. OpenCode still submits those parts as a model turn
|
|
358
|
+
* rather than rendering the hook result directly; handled control results
|
|
359
|
+
* include a self-contained reporting frame. Omitted entirely when
|
|
360
|
+
* {@link GoalPluginOptions.registerCommand} is `false`.
|
|
361
|
+
*/
|
|
352
362
|
"command.execute.before"?: (input: unknown, output: unknown) => Promise<void>
|
|
353
|
-
/**
|
|
363
|
+
/** Blocks tool execution while an already-handled control-command result is being reported. */
|
|
354
364
|
"tool.execute.before": (input: unknown, output: unknown) => Promise<void>
|
|
355
365
|
event: (input: unknown) => Promise<void>
|
|
356
366
|
"experimental.chat.system.transform": (input: unknown, output: unknown) => Promise<void>
|
|
@@ -358,8 +368,7 @@ export interface GoalPluginHooks {
|
|
|
358
368
|
"experimental.session.compacting": (input: unknown, output: unknown) => Promise<void>
|
|
359
369
|
/**
|
|
360
370
|
* Agent-facing tool definitions, present only when
|
|
361
|
-
* {@link GoalPluginOptions.registerTools} is enabled (default)
|
|
362
|
-
* optional `@opencode-ai/plugin` peer dependency is installed.
|
|
371
|
+
* {@link GoalPluginOptions.registerTools} is enabled (default).
|
|
363
372
|
*/
|
|
364
373
|
tool?: Record<string, unknown>
|
|
365
374
|
/** Cancels pending continuation work and releases this plugin instance. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Durable, guarded goal workflows for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/goal-plugin.js",
|
|
@@ -55,14 +55,6 @@
|
|
|
55
55
|
"agent",
|
|
56
56
|
"goal"
|
|
57
57
|
],
|
|
58
|
-
"peerDependencies": {
|
|
59
|
-
"@opencode-ai/plugin": ">=1.17.15 <2"
|
|
60
|
-
},
|
|
61
|
-
"peerDependenciesMeta": {
|
|
62
|
-
"@opencode-ai/plugin": {
|
|
63
|
-
"optional": true
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
58
|
"license": "MIT",
|
|
67
59
|
"engines": {
|
|
68
60
|
"node": ">=18",
|
|
@@ -80,7 +72,9 @@
|
|
|
80
72
|
"name": "willytop8"
|
|
81
73
|
},
|
|
82
74
|
"devDependencies": {
|
|
83
|
-
"@opencode-ai/plugin": "1.17.18",
|
|
84
75
|
"typescript": "5.9.3"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"zod": "4.1.8"
|
|
85
79
|
}
|
|
86
80
|
}
|
package/scripts/verify.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import assert from "node:assert/strict"
|
|
|
8
8
|
|
|
9
9
|
const REQUIRED_HOOKS = [
|
|
10
10
|
"config",
|
|
11
|
+
"chat.params",
|
|
12
|
+
"chat.message",
|
|
11
13
|
"command.execute.before",
|
|
12
14
|
"tool.execute.before",
|
|
13
15
|
"event",
|
|
@@ -16,6 +18,20 @@ const REQUIRED_HOOKS = [
|
|
|
16
18
|
"experimental.session.compacting",
|
|
17
19
|
]
|
|
18
20
|
|
|
21
|
+
const EXPECTED_TOOLS = [
|
|
22
|
+
"clear_goal",
|
|
23
|
+
"get_goal",
|
|
24
|
+
"get_goal_history",
|
|
25
|
+
"goal_block",
|
|
26
|
+
"goal_complete",
|
|
27
|
+
"goal_pause",
|
|
28
|
+
"goal_resume",
|
|
29
|
+
"goal_set",
|
|
30
|
+
"goal_status",
|
|
31
|
+
"set_goal",
|
|
32
|
+
"update_goal",
|
|
33
|
+
]
|
|
34
|
+
|
|
19
35
|
const results = []
|
|
20
36
|
|
|
21
37
|
function check(name, fn) {
|
|
@@ -70,11 +86,9 @@ const client = {
|
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
let hooks
|
|
89
|
+
let commandMessageCounter = 0
|
|
73
90
|
|
|
74
91
|
await check(`plugin initializes and registers all ${REQUIRED_HOOKS.length} required hooks`, async () => {
|
|
75
|
-
// registerTools defaults to true but silently no-ops without the optional
|
|
76
|
-
// @opencode-ai/plugin peer dependency, so it is not asserted here — the
|
|
77
|
-
// These hooks are always present regardless of that peer dependency.
|
|
78
92
|
hooks = await GoalPlugin({ client }, { minDelayMs: 1, persistState: false })
|
|
79
93
|
for (const hookName of REQUIRED_HOOKS) {
|
|
80
94
|
assert.equal(
|
|
@@ -83,16 +97,42 @@ await check(`plugin initializes and registers all ${REQUIRED_HOOKS.length} requi
|
|
|
83
97
|
`missing or non-function hook: ${hookName}`,
|
|
84
98
|
)
|
|
85
99
|
}
|
|
100
|
+
assert.deepEqual(Object.keys(hooks.tool ?? {}).sort(), EXPECTED_TOOLS)
|
|
101
|
+
for (const name of EXPECTED_TOOLS) {
|
|
102
|
+
assert.equal(typeof hooks.tool[name].execute, "function", `${name} must be executable`)
|
|
103
|
+
}
|
|
86
104
|
})
|
|
87
105
|
|
|
88
106
|
async function runGoalCommand(args) {
|
|
89
|
-
|
|
107
|
+
// Match OpenCode's host contract: it retains this exact array after the hook
|
|
108
|
+
// returns, so replacing output.parts would leave the raw command untouched.
|
|
109
|
+
const hostParts = [{ type: "text", text: args }]
|
|
110
|
+
commandMessageCounter += 1
|
|
111
|
+
const messageID = `verify-command-${commandMessageCounter}`
|
|
112
|
+
const output = {
|
|
113
|
+
message: { id: messageID, role: "user", sessionID },
|
|
114
|
+
parts: hostParts,
|
|
115
|
+
}
|
|
90
116
|
await hooks["command.execute.before"](
|
|
91
117
|
{ command: "goal", sessionID, arguments: args },
|
|
92
118
|
output,
|
|
93
119
|
)
|
|
94
|
-
assert.
|
|
95
|
-
assert.equal(
|
|
120
|
+
assert.strictEqual(output.parts, hostParts)
|
|
121
|
+
assert.equal(hostParts.length, 1)
|
|
122
|
+
assert.equal(hostParts[0].type, "text")
|
|
123
|
+
assert.equal(hostParts[0].synthetic, true)
|
|
124
|
+
assert.equal(hostParts[0].metadata?.["opencode-goal-plugin"]?.kind, "command")
|
|
125
|
+
assert.match(hostParts[0].metadata?.["opencode-goal-plugin"]?.id, /^[0-9a-f-]{36}$/)
|
|
126
|
+
output.parts = hostParts.map((part, index) => ({
|
|
127
|
+
...part,
|
|
128
|
+
id: `verify-command-part-${commandMessageCounter}-${index}`,
|
|
129
|
+
messageID,
|
|
130
|
+
sessionID,
|
|
131
|
+
}))
|
|
132
|
+
await hooks["chat.message"](
|
|
133
|
+
{ sessionID, messageID, agent: "build", model: { providerID: "test", modelID: "test" } },
|
|
134
|
+
output,
|
|
135
|
+
)
|
|
96
136
|
return output.parts[0].text
|
|
97
137
|
}
|
|
98
138
|
|
|
@@ -106,6 +146,7 @@ await check("/goal set works", async () => {
|
|
|
106
146
|
assert.match(text, /New active goal: verify the installation/)
|
|
107
147
|
const statusText = await runGoalCommand("status")
|
|
108
148
|
assert.match(statusText, /Active goal: verify the installation/)
|
|
149
|
+
assert.doesNotMatch(statusText, /State: Paused/)
|
|
109
150
|
})
|
|
110
151
|
|
|
111
152
|
await check("no model calls were made during verification", () => {
|