opencode-goal-plugin 0.4.7 → 0.6.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 +31 -3
- package/CONTRIBUTING.md +8 -7
- package/README.md +98 -13
- package/SECURITY.md +29 -1
- package/demo/README.md +86 -0
- package/demo/opencode.json +11 -0
- package/demo/package.json +10 -0
- package/demo/src/math.js +3 -0
- package/demo/test/math.test.js +11 -0
- package/docs/providers.md +84 -0
- package/index.d.ts +344 -0
- package/package.json +15 -5
- package/scripts/behavior-benchmark.mjs +272 -0
- package/scripts/packed-host-contract.mjs +160 -0
- package/scripts/verify.mjs +125 -0
- package/src/completion-claim.js +127 -0
- package/src/goal-plugin.js +604 -137
- package/src/goal-tool-result.js +18 -0
- package/src/native-agent-config.js +69 -0
- package/src/opencode-session-api.js +100 -0
- package/src/persistence-lease.js +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.0 — 2026-07-10
|
|
6
|
+
|
|
7
|
+
> Adds canonical structured goal tools, native goal and read-only verifier agents, fail-closed completion verification, safer OpenCode SDK compatibility, bounded persistence, and stronger lifecycle isolation. Existing command workflows and legacy tool aliases remain supported.
|
|
8
|
+
|
|
9
|
+
- Add canonical typed goal tools alongside backward-compatible aliases and structured completion claims.
|
|
10
|
+
- Register collision-safe goal and verifier agents; completion auditing now requires an owned, distinct verifier identity with edit and shell permissions denied.
|
|
11
|
+
- Fail closed on verifier errors/timeouts and abort timed-out child sessions when the host supports cancellation.
|
|
12
|
+
- Isolate runtime state per workspace, suppress stale asynchronous continuations, handle abort/dispose races, and coalesce duplicate idle events.
|
|
13
|
+
- Add an exclusive persistence lease so two OpenCode processes cannot silently overwrite one workspace state file.
|
|
14
|
+
- Bound goal-definition inputs and rotate the sensitive lifecycle ledger using configurable size/retention ceilings.
|
|
15
|
+
- Add normalized usage diagnostics, compact continuation prompts, a 100-point behavior benchmark, and packed-tarball host-contract verification.
|
|
16
|
+
- Include provider documentation and the reproducible demo in the npm package; declare all public hooks.
|
|
17
|
+
|
|
18
|
+
## 0.5.0 — 2026-07-08
|
|
19
|
+
|
|
20
|
+
- Replace the single-line "Compatibility snapshot" in the README with an OpenCode version compatibility table, manually verified via `tmux` + the OpenCode TUI against the persisted state file for each provider/model combination.
|
|
21
|
+
- Add `docs/providers.md`, a provider/model compatibility guide covering evidence-gated marker-compliance behavior for `opencode-go/qwen3.7-plus`, `opencode-go/glm-5.2`, and `deepseek/deepseek-chat` (manually verified via the OpenCode TUI against real provider credentials on OpenCode 1.17.15), plus a step-by-step guide for testing new models.
|
|
22
|
+
- Add a reproducible `demo/` directory: a minimal Node project with a deliberately buggy `add()` function, a test that catches it, and an `opencode.json` wired to the local plugin source. Verified end-to-end via the OpenCode TUI.
|
|
23
|
+
- Scope `npm test`/`npm run test:coverage` to `test/*.test.js` explicitly, since Node's test runner otherwise recursively discovers `demo/test/*.test.js` too, which would fail the root suite whenever the demo's deliberate bug is (correctly) unfixed.
|
|
24
|
+
- **Fix project-local state persistence to actually use the active session's directory.** `GoalPlugin` previously ignored the `directory` field OpenCode passes in its `PluginInput`, so the default `.opencode/goals/state.json` path resolved against the Node process's own `process.cwd()` instead. This works fine for a one-shot CLI invocation, but silently breaks when OpenCode runs as a persistent server/daemon serving multiple projects: `process.cwd()` stays wherever the server booted, not the active session's project. Confirmed live via the OpenCode TUI — a goal set in a project directory never persisted to disk at all. `GoalPlugin` now reads `directory` from its `PluginInput` and uses it as the default `cwd` for state-path resolution (an explicit `cwd` plugin option, mainly for tests, still takes precedence).
|
|
25
|
+
- Add Node 24 to the CI matrix, a weekly scheduled CI run (Mondays 08:00 UTC) to catch upstream drift, a `test:coverage` step, and npm/CI/tests/license badges to the README.
|
|
26
|
+
- Add GitHub issue templates for bug reports (OpenCode version, provider/model, Node version, relevant plugin options, repro steps) and feature requests (problem solved, scope fit against the current multi-goal/audit feature set).
|
|
27
|
+
- Add an Examples section to the README with copy-pasteable `/goal` commands: common workflows, success criteria/constraints/budget shorthand, and an ordered (sisyphus) sequence.
|
|
28
|
+
- Add a Comparison section to the README benchmarking `/goal` support, auto-continue, per-goal overrides, no-progress/no-tool-call detection, safety limits, history, persistence, multi-goal/sisyphus sequences, evidence-gated completion, the optional completion auditor, budget wrap-up, and license against Claude Code and Codex.
|
|
29
|
+
- Add `npm run verify` / `npx opencode-goal-plugin` installation verification command (`scripts/verify.mjs`). Checks Node >= 18, the plugin module shape, that all 4 hooks (`command.execute.before`, `event`, `experimental.chat.system.transform`, `experimental.compaction.autocontinue`) register, and that `/goal status`/`/goal set` work — entirely via mock clients, with zero model calls.
|
|
30
|
+
- Add TypeScript declarations (`index.d.ts`) covering the full current `GoalPluginOptions` surface — budgets, persistence/ledger paths, `commandName`/`registerCommand`/`registerTools`, and the completion-audit options (`completionAudit`, `auditor`, `auditorOptions`, `auditMessages`, `auditMessenger`) — plus the plugin's hook map and default export. `package.json`'s `types` field points at it.
|
|
31
|
+
- Warn when `/goal <condition>` replaces the focused goal instead of silently discarding it. The response now leads with `⚠️ Replacing active goal: "<old condition>"` and points at `/goal add <condition>` as the non-destructive alternative that backgrounds the current goal instead.
|
|
32
|
+
|
|
5
33
|
## 0.4.7 — 2026-06-29
|
|
6
34
|
|
|
7
35
|
### Bug fixes (low-severity cleanups)
|
|
@@ -87,7 +115,7 @@
|
|
|
87
115
|
|
|
88
116
|
## 0.3.0 — 2026-06-14
|
|
89
117
|
|
|
90
|
-
> A large feature release. Stronger completion integrity (evidence gate, optional auditor, visible audit messages), durable lifecycle ledger with state reconstruction, multiple goals per session with focus and ordered sisyphus sequences, richer goal schema, more auto-continue guardrails, project-local state with migration, a deterministic compaction summary, and
|
|
118
|
+
> A large feature release. Stronger completion integrity (evidence gate, optional auditor, visible audit messages), durable lifecycle ledger with state reconstruction, multiple goals per session with focus and ordered sisyphus sequences, richer goal schema, more auto-continue guardrails, project-local state with migration, a deterministic compaction summary, and release-workflow groundwork. All changes are additive and backward-compatible; older state files load unchanged.
|
|
91
119
|
|
|
92
120
|
### Completion integrity & audit
|
|
93
121
|
|
|
@@ -120,7 +148,7 @@
|
|
|
120
148
|
|
|
121
149
|
- **Default goal state to a project-local path, with an env override and migration fallbacks.** State resolves as `stateFilePath` option → `OPENCODE_GOAL_STATE_PATH` env var → project-local `<cwd>/.opencode/goals/state.json` (previously `~/.opencode-goal-plugin/state.json`). When the default path is empty, the plugin migrates forward on first load from the legacy home path and the XDG path, then writes project-local. Explicit option/env paths are literal with no fallback; a present-but-corrupt primary is preserved. New `resolveStateFilePath` / `xdgStateFilePath` / `legacyStateFilePaths` helpers. Home-based fallback paths resolve from an injectable `env.HOME` (falling back to `os.homedir()`), making path resolution deterministic across platforms — `os.homedir()` ignores `$HOME` on macOS. Implements megalist items 6.1 and 6.2.
|
|
122
150
|
- _**Correction (2026-06-21):** an earlier version of this entry claimed agent-facing goal tools shipped in 0.3.0. They did not — the work was on an unmerged branch (`wr/agent-tools`) and was never included in the 0.3.0 release. The feature now actually ships; see the **Unreleased** section above. Megalist items 7.1 and 7.2._
|
|
123
|
-
- **
|
|
151
|
+
- **Release automation note.** Development included a proposed npm Trusted Publishing workflow, but `.github/workflows/publish.yml` was not part of the final release history and is not present in the current repository. Releases therefore remain manual unless a separately reviewed publishing workflow is added. No package is published solely by the CI workflow documented in this repository.
|
|
124
152
|
|
|
125
153
|
## 0.2.0 — 2026-06-14
|
|
126
154
|
|
|
@@ -235,4 +263,4 @@
|
|
|
235
263
|
|
|
236
264
|
## 0.1.0
|
|
237
265
|
|
|
238
|
-
- Initial experimental marker-based `/goal` plugin.
|
|
266
|
+
- Initial experimental marker-based `/goal` plugin.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -16,8 +16,10 @@ Run the local checks before submitting changes:
|
|
|
16
16
|
npm test
|
|
17
17
|
npm run test:coverage
|
|
18
18
|
npm run smoke
|
|
19
|
+
npm run smoke:packed-host
|
|
20
|
+
npm run benchmark:behavior
|
|
21
|
+
npm run verify
|
|
19
22
|
npm run check
|
|
20
|
-
npm run smoke
|
|
21
23
|
npm run pack:check
|
|
22
24
|
```
|
|
23
25
|
|
|
@@ -28,11 +30,11 @@ For behavior changes, add or update tests in `test/goal-plugin.test.js`.
|
|
|
28
30
|
This plugin depends on OpenCode plugin hooks, including experimental hooks. When changing hook usage, command behavior, or system-prompt transforms:
|
|
29
31
|
|
|
30
32
|
1. Check the current OpenCode plugin and command documentation.
|
|
31
|
-
2. Run `npm run smoke` to verify the
|
|
33
|
+
2. Run `npm run smoke` and `npm run smoke:packed-host` to verify the source and installed-tarball host contracts.
|
|
32
34
|
3. Test against a real OpenCode install when possible.
|
|
33
35
|
4. Update the README compatibility snapshot if the tested surface changes.
|
|
34
36
|
|
|
35
|
-
`npm run smoke` verifies the package export path and `/goal` command hook without invoking a model.
|
|
37
|
+
`npm run smoke` verifies the package export path and `/goal` command hook without invoking a model. `npm run smoke:packed-host` installs the packed artifact in an isolated directory and checks the public hook/tool contract. `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.
|
|
36
38
|
|
|
37
39
|
## Release checklist
|
|
38
40
|
|
|
@@ -42,15 +44,14 @@ Before publishing or tagging a release:
|
|
|
42
44
|
- run `npm test`
|
|
43
45
|
- run `npm run test:coverage`
|
|
44
46
|
- run `npm run smoke`
|
|
47
|
+
- run `npm run smoke:packed-host`
|
|
48
|
+
- run `npm run benchmark:behavior`
|
|
49
|
+
- run `npm run verify`
|
|
45
50
|
- run `npm run check`
|
|
46
51
|
- run `npm run pack:check`
|
|
47
52
|
- perform at least one manual OpenCode smoke test if hook behavior changed
|
|
48
53
|
- refresh compatibility notes if the tested OpenCode surface changed
|
|
49
54
|
|
|
50
|
-
`npm run smoke` verifies the published package entrypoint and `/goal` command hook without
|
|
51
|
-
invoking a model. It does not replace a manual OpenCode smoke test after hook or command
|
|
52
|
-
behavior changes.
|
|
53
|
-
|
|
54
55
|
## Pull Requests
|
|
55
56
|
|
|
56
57
|
Keep pull requests focused. Include:
|
package/README.md
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
# opencode-goal-plugin
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-goal-plugin)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-goal-plugin)
|
|
5
|
+
[](https://github.com/willytop8/OpenCode-goal-plugin/actions/workflows/ci.yml)
|
|
6
|
+
[](test/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
3
9
|
An experimental session-scoped `/goal` command for [OpenCode](https://opencode.ai/).
|
|
4
10
|
|
|
5
11
|
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.
|
|
6
12
|
|
|
7
13
|
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.
|
|
8
14
|
|
|
15
|
+
## What it provides
|
|
16
|
+
|
|
17
|
+
- Session-scoped goals that remain visible across turns and compaction.
|
|
18
|
+
- Guarded auto-continuation with turn, duration, token, no-progress, and no-tool-call limits.
|
|
19
|
+
- Project-local restart recovery backed by persisted state and a bounded lifecycle ledger.
|
|
20
|
+
- Evidence-gated completion with an optional independent, fail-closed verifier.
|
|
21
|
+
- Canonical agent tools, collision-safe goal/verifier agents, multiple goals, and ordered goal sequences.
|
|
22
|
+
|
|
23
|
+
This project is independently implemented for OpenCode. Product names used elsewhere identify their respective owners; no feature-parity or endorsement claim is implied.
|
|
24
|
+
|
|
9
25
|
## Compatibility snapshot
|
|
10
26
|
|
|
11
27
|
| Surface | Status |
|
|
12
28
|
|---|---|
|
|
13
|
-
| Node.js | Declared support: `>=18`; CI covers Node 18, 20, and
|
|
29
|
+
| Node.js | Declared support: `>=18`; CI covers Node 18, 20, 22, and 24 |
|
|
14
30
|
| Package entrypoint | `npm run smoke` verifies the package export path plus `/goal` command-hook behavior from a local install without invoking a model |
|
|
15
|
-
| 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 |
|
|
16
31
|
| Provider/backend quirks | Strict-template backends require the goal block to merge into the primary `system` message; covered by regression tests |
|
|
17
32
|
|
|
33
|
+
### OpenCode version compatibility
|
|
34
|
+
|
|
35
|
+
Manually tested via the OpenCode TUI (`tmux` + real provider credentials, no mocks), verified against the plugin's own persisted state rather than terminal display alone:
|
|
36
|
+
|
|
37
|
+
| OpenCode Version | Provider Tested | `/goal status` | Auto-continue | Evidence-gated completion | Hook Output Display |
|
|
38
|
+
|---|---|---|---|---|---|
|
|
39
|
+
| 1.17.15 | opencode-go (`qwen3.7-plus`) | ✅ | ✅ | ✅ Self-corrected after one rejection (bare `[goal:complete]` with no evidence), then completed cleanly | ⚠️ Not displayed |
|
|
40
|
+
| 1.17.15 | opencode-go (`glm-5.2`) | ✅ | ✅ | ✅ Clean `[goal:evidence]` + `[goal:complete]` on the first attempt | ⚠️ Not displayed |
|
|
41
|
+
| 1.17.15 | deepseek (`deepseek-chat`) | ✅ | ✅ | ✅ Clean `[goal:evidence]` + `[goal:complete]` on the first attempt; also verified end-to-end via the [demo](demo/) — autonomously fixed a real bug and reported evidence-backed completion | ⚠️ Not displayed |
|
|
42
|
+
|
|
43
|
+
`/goal status` and auto-continue are graded on **state correctness** (verified directly against the plugin's persisted state file: correct limits parsed, correct turn/stop accounting, correct completion detection) — not on what's rendered in the terminal, since that's tracked separately as Hook Output Display.
|
|
44
|
+
|
|
45
|
+
**Note:** Hook output display depends on OpenCode version — on 1.17.15, `command.execute.before`'s `output.parts` text is not rendered in the TUI for any provider tested; the raw command argument is instead routed to the model as a normal chat turn (see [Limitations](#limitations)). State mutations always work regardless of display: goal creation, flag parsing, auto-continue, limit enforcement, and evidence-gated completion detection were all verified correct via the persisted state file in every combination above. Re-test against your own OpenCode build before relying on unattended runs, and see [`docs/providers.md`](docs/providers.md) for the full per-model marker-compliance notes.
|
|
46
|
+
|
|
18
47
|
## Install
|
|
19
48
|
|
|
20
49
|
```sh
|
|
@@ -100,6 +129,18 @@ Clear the active goal:
|
|
|
100
129
|
|
|
101
130
|
`/goal stop`, `/goal off`, `/goal reset`, `/goal none`, and `/goal cancel` are aliases for `/goal clear`.
|
|
102
131
|
|
|
132
|
+
### Session forks and child sessions
|
|
133
|
+
|
|
134
|
+
Goals are scoped to the session where they were created. A child session or a
|
|
135
|
+
fork does not automatically inherit its parent's active goal; set a goal in the
|
|
136
|
+
new session when you want it to continue independently.
|
|
137
|
+
|
|
138
|
+
This isolation is intentional. OpenCode currently includes `parentID` for
|
|
139
|
+
ordinary child sessions but does not expose the source session in the
|
|
140
|
+
`session.created` event for forks. Inferring ancestry from a mutable title such
|
|
141
|
+
as `(fork #1)` could attach a goal to the wrong session. Automatic inheritance
|
|
142
|
+
can be added once the host exposes an explicit fork relationship.
|
|
143
|
+
|
|
103
144
|
### Multiple goals
|
|
104
145
|
|
|
105
146
|
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.
|
|
@@ -122,14 +163,35 @@ A session can hold more than one goal. `/goal <condition>` replaces the focused
|
|
|
122
163
|
|
|
123
164
|
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.
|
|
124
165
|
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
Copy-pasteable goals for common workflows:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
/goal "fix the failing tests" --max-turns 10
|
|
172
|
+
/goal "refactor auth to use new API" --max-minutes 30
|
|
173
|
+
/goal "audit for security issues" --max-turns 3
|
|
174
|
+
/goal "migrate class components to functional" --max-minutes 60 --max-tokens 400000
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
With success criteria, constraints, and a token budget shorthand:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
/goal "ship the release" --success "tests pass and changelog updated" --constraints "do not touch the public API" --budget 150k
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
An ordered sequence, run as a strict pipeline:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
/goal sisyphus build the parser; write the tests; ship the release
|
|
187
|
+
```
|
|
188
|
+
|
|
125
189
|
## How it works
|
|
126
190
|
|
|
127
191
|
1. 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.
|
|
128
192
|
2. 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.
|
|
129
|
-
3. The plugin stops auto-continuing when the assistant ends a response with `[goal:complete]` or `[goal:blocked]`, or when a safety limit is reached.
|
|
130
|
-
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.
|
|
131
193
|
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.
|
|
132
|
-
4. If OpenCode compacts the session, the plugin injects
|
|
194
|
+
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.
|
|
133
195
|
5. 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 resume` to hand control back to the goal loop.
|
|
134
196
|
|
|
135
197
|
## Completion markers
|
|
@@ -257,8 +319,12 @@ Additional plugin-level options:
|
|
|
257
319
|
- `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.
|
|
258
320
|
- `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.
|
|
259
321
|
- `registerTools` — whether the plugin registers the agent-facing goal tools (default `true`). Requires the optional `@opencode-ai/plugin` peer dependency to be present; when it is absent, tool registration is skipped and the command/event hooks still work. Set to `false` to omit the programmatic tool surface entirely. See [Agent tools](#agent-tools-optional).
|
|
322
|
+
- `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.
|
|
323
|
+
- `goalAgentName` / `verifierAgentName` — customize the registered native agent names (defaults `goal` and `goal-verify`). The verifier is a hidden subagent with workspace and goal mutation tools disabled.
|
|
324
|
+
- `sdkShape` — OpenCode session-client argument shape: `legacy` (the default generated `PluginInput` client using `{ path, body, query }`) or `flat` (clients using `{ sessionID, ... }`). The plugin remembers a successful shape per operation and only falls back after an explicit argument/schema `TypeError`; provider and transport errors are never retried as shape mismatches.
|
|
260
325
|
- `persistState` — whether to persist active goals and recent goal results to disk.
|
|
261
326
|
- `stateFilePath` — where the persisted state JSON is written. Overrides the default project-local path and the `OPENCODE_GOAL_STATE_PATH` env 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), and `OPENCODE_GOAL_STATE_PATH` can override it without editing config.
|
|
327
|
+
- `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.
|
|
262
328
|
- `resultRetentionMs` — how long a completed goal summary remains available through `/goal status` after the goal leaves active memory.
|
|
263
329
|
- `maxStoredResults` — maximum number of completed-goal summaries retained in process memory before the oldest ones are evicted.
|
|
264
330
|
|
|
@@ -268,11 +334,10 @@ In addition to the `/goal` command, the plugin can expose the same workflow to t
|
|
|
268
334
|
|
|
269
335
|
Registered tools:
|
|
270
336
|
|
|
271
|
-
- `
|
|
272
|
-
- `get_goal_history`
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- `clear_goal` — clear the current goal and discard its saved status.
|
|
337
|
+
- `goal_status`, `goal_set`, `goal_pause`, `goal_resume`, `goal_block`, and `goal_complete` are the canonical narrow operations. They return compact versioned JSON envelopes so agents can branch reliably without parsing prose.
|
|
338
|
+
- `get_goal`, `get_goal_history`, `set_goal`, `update_goal`, and `clear_goal` remain compatibility aliases with their existing text responses.
|
|
339
|
+
|
|
340
|
+
`goal_set` and `set_goal` are explicitly constrained to user-requested goals. `goal_complete` accepts a structured claim: a required non-empty `summary`, plus optional criterion/evidence pairs, checks (`passed`, `failed`, or `not-run`), changed files, and known limitations. Failed checks and empty criterion evidence are rejected before archival; accepted claims are serialized deterministically for the configured completion auditor. The legacy `update_goal` tool retains its string `evidence` field for compatibility.
|
|
276
341
|
|
|
277
342
|
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 (sisyphus) sequence auto-promotes the next.
|
|
278
343
|
|
|
@@ -288,7 +353,7 @@ By default a `[goal:complete]` is accepted on the assistant's word. You can requ
|
|
|
288
353
|
- `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).
|
|
289
354
|
- `auditor: async ({ goal, sessionID, latestText }) => ({ approved, reason })` — supply your own auditor function (takes precedence over `completionAudit`).
|
|
290
355
|
|
|
291
|
-
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
|
|
356
|
+
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 and custom auditors fail closed by default. The audit is off unless one of these options is set.
|
|
292
357
|
|
|
293
358
|
Pass `auditorOptions` to tune the built-in auditor:
|
|
294
359
|
|
|
@@ -297,11 +362,12 @@ GoalPlugin({
|
|
|
297
362
|
completionAudit: true,
|
|
298
363
|
auditorOptions: {
|
|
299
364
|
timeoutMs: 60_000, // default 120 000 ms; set lower for faster CI feedback
|
|
365
|
+
failurePolicy: "reject",
|
|
300
366
|
},
|
|
301
367
|
})
|
|
302
368
|
```
|
|
303
369
|
|
|
304
|
-
`timeoutMs` caps how long the built-in child-session auditor waits for a verdict.
|
|
370
|
+
`timeoutMs` caps how long the built-in child-session auditor waits for a verdict. `failurePolicy` defaults to `reject`: an unavailable API, missing child-session ID, provider error, or timeout rejects the audit and pauses the goal for review. Set it to `approve` only as an explicit compatibility escape hatch; an actual negative or malformed verifier verdict still rejects. `auditorOptions` is ignored when a custom `auditor` function is supplied.
|
|
305
371
|
|
|
306
372
|
## Prompt safety
|
|
307
373
|
|
|
@@ -309,12 +375,28 @@ The goal text is wrapped in `<goal_objective>` tags and labeled as user-provided
|
|
|
309
375
|
|
|
310
376
|
## Limitations
|
|
311
377
|
|
|
312
|
-
|
|
378
|
+
The assistant still signals candidate outcomes with `[goal:complete]` or `[goal:blocked]`. Completion can additionally be checked by a custom `completionAudit` 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.
|
|
313
379
|
|
|
314
380
|
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.
|
|
315
381
|
|
|
316
382
|
The plugin depends on `experimental.chat.system.transform` and other OpenCode plugin hooks that may change between OpenCode versions.
|
|
317
383
|
|
|
384
|
+
Only one persistence-enabled OpenCode plugin instance may own a given `stateFilePath` at a time. A second process fails initialization with the owning PID/host instead of risking last-writer-wins state loss. Dispose the first host or configure a different path.
|
|
385
|
+
|
|
386
|
+
## Diagnostics and recovery
|
|
387
|
+
|
|
388
|
+
Start with `/goal status`, then `/goal history`. Together they show whether a goal is active, its stop reason and budget usage, and the recent lifecycle/checkpoint trail without exposing the entire on-disk ledger to the model.
|
|
389
|
+
|
|
390
|
+
If a goal does not continue:
|
|
391
|
+
|
|
392
|
+
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.
|
|
393
|
+
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.
|
|
394
|
+
3. Check OpenCode's structured logs for persistence, SDK-shape, prompt, or auditor errors.
|
|
395
|
+
4. 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.
|
|
396
|
+
5. Run `npm run verify`, `npm run smoke`, and `npm run smoke:packed-host` against the installed source when diagnosing registration or packaging problems. `npm run benchmark:behavior` exercises completion, false-completion, loop, interruption, compaction, and restart behavior without a provider call.
|
|
397
|
+
|
|
398
|
+
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.
|
|
399
|
+
|
|
318
400
|
## Local development
|
|
319
401
|
|
|
320
402
|
Point OpenCode at the source file directly for local testing:
|
|
@@ -343,6 +425,9 @@ Keep test files outside OpenCode's auto-loaded plugin directory — OpenCode wil
|
|
|
343
425
|
npm test # run the test suite
|
|
344
426
|
npm run test:coverage # run tests with coverage
|
|
345
427
|
npm run smoke # verify package export + command hook without a model call
|
|
428
|
+
npm run smoke:packed-host # install the packed tarball and exercise the host contract
|
|
429
|
+
npm run benchmark:behavior # deterministic autonomy + token-efficiency scenarios
|
|
430
|
+
npm run verify # verify the installed plugin hook surface
|
|
346
431
|
npm run check # syntax check + tests
|
|
347
432
|
npm run pack:check # verify package contents before publishing
|
|
348
433
|
```
|
package/SECURITY.md
CHANGED
|
@@ -27,4 +27,32 @@ Relevant security-sensitive areas include:
|
|
|
27
27
|
- leakage of goal text through logs or status output
|
|
28
28
|
- malformed persisted state causing stale or unexpected goal recovery
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
## Threat Model and Trust Boundaries
|
|
31
|
+
|
|
32
|
+
The plugin is an orchestration layer, not a sandbox. OpenCode, the selected model/provider, enabled tools, repository instructions, and the local operating-system account remain separate trust boundaries. The plugin does not grant a model new tool permissions, but auto-continuation gives already-authorized tools more opportunities to run. Use OpenCode's permission controls and avoid unattended goals in repositories or sessions you do not trust.
|
|
33
|
+
|
|
34
|
+
The following inputs are untrusted:
|
|
35
|
+
|
|
36
|
+
- goal objectives, criteria, constraints, blockers, and completion evidence
|
|
37
|
+
- assistant/provider output and completion markers
|
|
38
|
+
- session events and SDK responses
|
|
39
|
+
- persisted state and lifecycle-ledger content
|
|
40
|
+
- repository files and instructions encountered by the agent
|
|
41
|
+
|
|
42
|
+
The plugin bounds and escapes prompt-control-like goal text, validates public tool inputs, limits completion-claim size, coalesces duplicate idle events, and pauses recovered goals. These controls reduce accidental loops and prompt confusion; they do not turn model output into trusted data or prove that cited evidence is true. Enable the independent completion auditor for higher-assurance work and keep its default fail-closed failure policy.
|
|
43
|
+
|
|
44
|
+
## Data Stored and Exposed
|
|
45
|
+
|
|
46
|
+
Persistence may contain objectives, criteria, constraints, assistant checkpoints, lifecycle events, blockers, completion evidence, changed-file names, and commands reported by the agent. Files are created with owner-only permissions where the platform supports POSIX modes, but filesystem permissions do not provide encryption at rest. State is project-local by default and is not intentionally uploaded by this plugin; the configured model/provider still receives prompt context through OpenCode. The lifecycle ledger rotates at a finite size and retention count, but retained generations still contain sensitive task text until they are rotated out or deleted.
|
|
47
|
+
|
|
48
|
+
Status and history are session-scoped. The plugin intentionally avoids a process-wide diagnostic dump because a long-running OpenCode host may serve multiple projects or sessions. Before sharing logs or persistence files, remove secrets, proprietary text, usernames, absolute paths, and provider identifiers.
|
|
49
|
+
|
|
50
|
+
## Security Assumptions and Residual Risk
|
|
51
|
+
|
|
52
|
+
- A compromised OpenCode host, provider, dependency, or local account is outside the plugin's protection boundary.
|
|
53
|
+
- Completion auditing improves confidence but is not a cryptographic attestation and may share the same provider failure modes as the working agent.
|
|
54
|
+
- Disabling persistence avoids local state files but also removes restart recovery and ledger durability.
|
|
55
|
+
- Setting the auditor's `failurePolicy` to `approve` weakens completion integrity and should be treated as a compatibility-only escape hatch.
|
|
56
|
+
- Experimental OpenCode hook or SDK changes can alter interception, display, or continuation behavior; re-test the exact version/provider combination used for unattended work.
|
|
57
|
+
|
|
58
|
+
Goal text is wrapped in `<goal_objective>` tags and labeled as user-provided task data. The plugin neutralizes opening and closing tags that match its prompt-control vocabulary, including role-like tags, before inserting user-controlled text. This is defense in depth for a plaintext model prompt, not a privilege boundary: treat goal text as untrusted content and do not assume tag escaping can make arbitrary third-party instructions safe.
|
package/demo/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# opencode-goal-plugin demo
|
|
2
|
+
|
|
3
|
+
A minimal, reproducible demo: a failing test suite and a `/goal` that fixes
|
|
4
|
+
it autonomously, with no supervision after you set the goal.
|
|
5
|
+
|
|
6
|
+
`src/math.js` has a deliberate bug — `add(a, b)` returns `a - b` instead of
|
|
7
|
+
`a + b`. `test/math.test.js` catches it. The goal below tells the agent to
|
|
8
|
+
fix the failing tests; the plugin auto-continues the session until the
|
|
9
|
+
agent verifies the suite passes and reports evidence-backed completion.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- Node.js >= 18
|
|
14
|
+
- OpenCode installed (`opencode --version`)
|
|
15
|
+
- At least one provider configured (`opencode auth login`)
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. From the repo root, confirm the bug is real:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
cd demo
|
|
23
|
+
npm test
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
You should see 2 failing tests — `add(2, 3)` returns `-1` instead of `5`.
|
|
27
|
+
|
|
28
|
+
2. Launch OpenCode in this directory. `demo/opencode.json` already points
|
|
29
|
+
the plugin at the source file (`file:../src/goal-plugin.js`) and
|
|
30
|
+
registers the `goal` command, so no install step is needed:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
opencode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Set the goal. If this `demo/` directory sits inside a larger checkout
|
|
37
|
+
(e.g. you cloned the whole plugin repo), be specific about scope —
|
|
38
|
+
"this repo" can otherwise be read as the surrounding project rather
|
|
39
|
+
than the demo itself:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
/goal fix the failing tests in demo/test/math.test.js --max-turns 10
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. Walk away. The plugin keeps the goal in context, auto-continues after
|
|
46
|
+
each idle turn, and stops when the agent reports a substantiated
|
|
47
|
+
completion or a safety limit is reached (10 turns here).
|
|
48
|
+
|
|
49
|
+
5. Check the result:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm test
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Both tests should now pass, and `src/math.js` should return `a + b`.
|
|
56
|
+
|
|
57
|
+
## What to expect
|
|
58
|
+
|
|
59
|
+
- The agent should read `test/math.test.js`, find the failing assertions,
|
|
60
|
+
locate the bug in `src/math.js`, fix `a - b` → `a + b`, rerun `npm test`
|
|
61
|
+
to confirm, and end its response with a `[goal:evidence]` line
|
|
62
|
+
summarizing what it verified, followed by `[goal:complete]`. A
|
|
63
|
+
`[goal:complete]` with no preceding `[goal:evidence]` is rejected by the
|
|
64
|
+
plugin and re-prompted — the agent can't just claim done, it has to show
|
|
65
|
+
its work.
|
|
66
|
+
- Run `/goal status` at any point to see the current goal, elapsed
|
|
67
|
+
turns/time/tokens, and remaining budget (plus the accepted evidence once
|
|
68
|
+
complete). Run `/goal history` to see the lifecycle events and the
|
|
69
|
+
latest checkpoint. Run `/goal list` to see all live/backgrounded goals
|
|
70
|
+
in the session.
|
|
71
|
+
- **Hook output display varies by OpenCode version and provider** — the
|
|
72
|
+
plugin's own status text may or may not render directly in the TUI (see
|
|
73
|
+
the [compatibility table](../README.md#compatibility-snapshot)). Either
|
|
74
|
+
way, the goal's state (limits, turn count, completion) is tracked
|
|
75
|
+
correctly; you can always verify it directly by inspecting the
|
|
76
|
+
persisted state at `.opencode/goals/state.json` in this directory.
|
|
77
|
+
|
|
78
|
+
## Resetting the demo
|
|
79
|
+
|
|
80
|
+
To run the demo again from a clean slate:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
git checkout -- src/math.js
|
|
84
|
+
/goal clear
|
|
85
|
+
rm -rf .opencode/goals
|
|
86
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"plugin": ["file:../src/goal-plugin.js"],
|
|
4
|
+
"command": {
|
|
5
|
+
"goal": {
|
|
6
|
+
"description": "Set a session-scoped goal and auto-continue until complete.",
|
|
7
|
+
"template": "$ARGUMENTS",
|
|
8
|
+
"agent": "build"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
package/demo/src/math.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import test from "node:test"
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import { add } from "../src/math.js"
|
|
4
|
+
|
|
5
|
+
test("add returns the sum of two numbers", () => {
|
|
6
|
+
assert.equal(add(2, 3), 5)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test("add handles negative numbers", () => {
|
|
10
|
+
assert.equal(add(-1, -1), -2)
|
|
11
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Provider / model compatibility
|
|
2
|
+
|
|
3
|
+
opencode-goal-plugin works with any model OpenCode can talk to, but two things
|
|
4
|
+
vary by provider and model:
|
|
5
|
+
|
|
6
|
+
1. **Marker compliance** — whether the model reliably ends its response with
|
|
7
|
+
`[goal:complete]` (or the bracket-less form) on its own final line, with a
|
|
8
|
+
preceding `[goal:evidence]` line summarizing what it actually verified.
|
|
9
|
+
Since the completion-evidence requirement was added, a bare
|
|
10
|
+
`[goal:complete]` with no evidence line is **rejected** and the plugin
|
|
11
|
+
re-prompts with an explicit `<evidence_required>` block — this is a
|
|
12
|
+
real safety net, not just documentation: a model that initially skips the
|
|
13
|
+
evidence line gets one automatic correction cycle before hitting its
|
|
14
|
+
turn/time/token budget.
|
|
15
|
+
2. **Hook output display** — whether the plugin's own response text (e.g.
|
|
16
|
+
`No active goal. Set one with...`) is rendered in the OpenCode TUI, or
|
|
17
|
+
whether the raw command argument is instead routed to the model as a
|
|
18
|
+
normal chat turn. This is an OpenCode-host behavior, not model-specific,
|
|
19
|
+
but is included here since it's most visible while testing a new model.
|
|
20
|
+
See the [OpenCode version compatibility table](../README.md#opencode-version-compatibility)
|
|
21
|
+
in the README for the current findings.
|
|
22
|
+
|
|
23
|
+
All rows below were verified manually against the real OpenCode TUI (`tmux`
|
|
24
|
+
+ live provider credentials, no mocks) on OpenCode 1.17.15, driving the
|
|
25
|
+
plugin through `/goal status`, `/goal <condition> --max-turns N`, and
|
|
26
|
+
inspecting the plugin's persisted state file to confirm state mutations
|
|
27
|
+
(limit parsing, turn/stop accounting, evidence-gated completion detection)
|
|
28
|
+
independent of what was rendered in the terminal.
|
|
29
|
+
|
|
30
|
+
## Tested models
|
|
31
|
+
|
|
32
|
+
| Provider | Model | Marker compliance | Notes |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `opencode-go` | `qwen3.7-plus` | ✅ Self-corrects | First attempt emitted bare `[goal:complete]` with no evidence line and was correctly rejected by the plugin. On the very next turn it read the `<evidence_required>` re-prompt, added a `[goal:evidence]` line, and completed cleanly — a good demonstration of the evidence gate actually improving behavior rather than just failing closed. |
|
|
35
|
+
| `opencode-go` | `glm-5.2` | ✅ Clean | Emitted a correct `[goal:evidence] ... [goal:complete]` pair on the first attempt. (An earlier plugin version without the evidence requirement showed GLM-5.2 sometimes trailing extra text after a bare marker — the more structured `<completion_audit>` prompt this plugin version sends appears to help.) |
|
|
36
|
+
| `deepseek` | `deepseek-chat` | ✅ Clean | Emitted a correct `[goal:evidence] ... [goal:complete]` pair on the first attempt, both in a short synthetic goal and in the full [demo](../demo/) (autonomously located and fixed a real bug, then reported evidence-backed completion). Correctly parses per-goal flags (`--max-turns`, etc.) out of the condition text. |
|
|
37
|
+
|
|
38
|
+
Untested at time of writing: `deepseek-reasoner`, `mistral/*`, `openrouter/*`,
|
|
39
|
+
and any `nvidia`/`google` provider — add rows here as they're verified. See
|
|
40
|
+
[Testing a new model](#testing-a-new-model) below.
|
|
41
|
+
|
|
42
|
+
## Strict-template backends
|
|
43
|
+
|
|
44
|
+
Some backends (notably certain Qwen deployments on vLLM, and several
|
|
45
|
+
Llama.cpp/Mistral chat templates) reject a `system` role message that isn't
|
|
46
|
+
the very first message in the conversation, with an error like `"System
|
|
47
|
+
message must be at the beginning."` opencode-goal-plugin's
|
|
48
|
+
`experimental.chat.system.transform` hook merges the goal continuation block
|
|
49
|
+
into the primary system entry instead of appending a separate one, which
|
|
50
|
+
avoids this. This is covered by regression tests in
|
|
51
|
+
[`test/goal-plugin.test.js`](../test/goal-plugin.test.js) and does not
|
|
52
|
+
require any provider-specific configuration.
|
|
53
|
+
|
|
54
|
+
## Testing a new model
|
|
55
|
+
|
|
56
|
+
No LLM call is required to verify the plugin loads correctly — run
|
|
57
|
+
`npm run verify` first. To check marker compliance and hook behavior for a
|
|
58
|
+
specific provider/model:
|
|
59
|
+
|
|
60
|
+
1. Point an OpenCode config at the plugin (see
|
|
61
|
+
[Local development](../README.md#local-development) in the README) and
|
|
62
|
+
register the `goal` command.
|
|
63
|
+
2. Launch OpenCode against the model you want to test:
|
|
64
|
+
```sh
|
|
65
|
+
opencode -m <provider>/<model>
|
|
66
|
+
```
|
|
67
|
+
3. Run a short, deterministic goal that should complete in one or two turns:
|
|
68
|
+
```
|
|
69
|
+
/goal say hi and end with [goal:complete] --max-turns 2
|
|
70
|
+
```
|
|
71
|
+
4. Check whether the first attempt includes a proper `[goal:evidence]` line
|
|
72
|
+
before `[goal:complete]`. If not, confirm the plugin rejects it and
|
|
73
|
+
re-prompts with `<evidence_required>` — then check whether the model
|
|
74
|
+
self-corrects within its remaining turn budget, or exhausts it.
|
|
75
|
+
5. Run `/goal status` and check whether the plugin's text (e.g. `No active
|
|
76
|
+
goal...` or `Active goal: ...`) is rendered directly, or whether the
|
|
77
|
+
command text was instead routed to the model as a chat turn.
|
|
78
|
+
6. Cross-check state mutations directly against the persisted state file
|
|
79
|
+
(`.opencode/goals/state.json`, project-local by default) to confirm the
|
|
80
|
+
goal's `options` reflect the flags you passed and `state`/`stopReason`
|
|
81
|
+
reflect what actually happened — this is authoritative even when the TUI
|
|
82
|
+
display doesn't show the plugin's own output.
|
|
83
|
+
7. Add a row to the table above with your findings, including the OpenCode
|
|
84
|
+
version you tested against.
|