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 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 npm Trusted Publishing CI. All changes are additive and backward-compatible; older state files load unchanged.
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
- - **Add a `Publish` GitHub Actions workflow (`.github/workflows/publish.yml`) for npm Trusted Publishing (OIDC).** On a push to `main` it runs the full check matrix on Node 18/20/22, then publishes via OIDC with no stored `NPM_TOKEN`, using a publish-on-version-change model (only publishes when `package.json`'s version is new). The publish job requires `id-token: write` and is gated behind a `release` environment. First run still requires a human to publish an initial version and configure the npm Trusted Publisher. Implements megalist item 9.1.
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 packaged entrypoint and command hook surface.
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. It does not replace a real OpenCode smoke test after hook or command behavior changes.
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
+ [![npm version](https://img.shields.io/npm/v/opencode-goal-plugin)](https://www.npmjs.com/package/opencode-goal-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-goal-plugin)](https://www.npmjs.com/package/opencode-goal-plugin)
5
+ [![CI](https://github.com/willytop8/OpenCode-goal-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/willytop8/OpenCode-goal-plugin/actions/workflows/ci.yml)
6
+ [![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](test/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 22 |
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 the goal objective, budget usage, and latest checkpoint into the compaction context so the goal survives the compaction and the assistant keeps the thread. While a goal is active, the plugin also disables OpenCode's generic post-compaction auto-continue so it does not race the plugin's own continuation.
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
- - `get_goal` current goal status (objective, budget usage, latest checkpoint).
272
- - `get_goal_history` lifecycle history and latest checkpoint.
273
- - `set_goal` — set/replace the session goal. Its description constrains the agent to call it **only when the user explicitly asks** to set a goal. Accepts `objective` plus optional `maxTurns`, `maxTokens`, `maxDurationMs`, `successCriteria`, `constraints`, and `mode`.
274
- - `update_goal` revise the `objective` and/or set `status` to `complete` / `blocked` / `paused` / `resumed` (with `evidence` for complete or `blocker` for blocked).
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 child-session auditor fails *open* (auto-approves) if the session API is unavailable, while a custom auditor that throws is treated as a rejection (fail closed). The audit is off unless one of these options is set.
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. If the session doesn't reply within the timeout the auditor auto-approves (fail open) so the goal can still be archived. `auditorOptions` is ignored when a custom `auditor` function is supplied.
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
- This is a marker-based implementation. The assistant is responsible for outputting `[goal:complete]` or `[goal:blocked]` there is no independent evaluator verifying completion against the original goal. Claude Code's native `/goal` uses a separate evaluator model; this plugin currently approximates the workflow using OpenCode hooks and explicit completion markers. A future version could add a separate evaluator once OpenCode exposes a clean plugin API for that flow.
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
- The goal text is wrapped in `<goal_objective>` tags and the closing tag is escaped before insertion. Other structural tags used in continuation prompts (`<goal_continuation>`, `<progress_budget>`, etc.) are not escaped. Crafted goal text containing those literal strings would close the tag early in the plaintext prompt; the model still receives plaintext rather than true privileged structure, but you should still treat goal text as trusted local input rather than pasting in arbitrary third-party content.
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
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "opencode-goal-plugin-demo",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Minimal reproducible demo: a failing test and a /goal that fixes it autonomously.",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "node --test"
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ export function add(a, b) {
2
+ return a - b
3
+ }
@@ -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.