opencode-goal-plugin 0.4.7 → 0.5.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 +15 -0
- package/README.md +62 -2
- package/index.d.ts +299 -0
- package/package.json +9 -3
- package/scripts/verify.mjs +123 -0
- package/src/goal-plugin.js +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.0 — 2026-07-08
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- **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).
|
|
12
|
+
- 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.
|
|
13
|
+
- 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).
|
|
14
|
+
- Add an Examples section to the README with copy-pasteable `/goal` commands: common workflows, success criteria/constraints/budget shorthand, and an ordered (sisyphus) sequence.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
5
20
|
## 0.4.7 — 2026-06-29
|
|
6
21
|
|
|
7
22
|
### Bug fixes (low-severity cleanups)
|
package/README.md
CHANGED
|
@@ -1,20 +1,57 @@
|
|
|
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
|
+
## Comparison
|
|
16
|
+
|
|
17
|
+
| Feature | Claude Code | Codex | opencode-goal-plugin |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `/goal` command | ✅ Native | ✅ Native | ✅ Plugin |
|
|
20
|
+
| Auto-continue | ✅ | ✅ | ✅ |
|
|
21
|
+
| Per-goal flag overrides | ❌ | ❌ | ✅ |
|
|
22
|
+
| No-progress / no-tool-call detection | ❌ | ❌ | ✅ Both |
|
|
23
|
+
| Configurable safety limits | Limited | Limited | ✅ All tunable |
|
|
24
|
+
| Goal history | ✅ | ❌ | ✅ `/goal history` |
|
|
25
|
+
| Goal persistence | ❌ | ❌ | ✅ Survives restart, ledger-backed |
|
|
26
|
+
| Multiple concurrent goals | ❌ | ❌ | ✅ `/goal add` / `/goal focus` |
|
|
27
|
+
| Ordered goal sequences | ❌ | ❌ | ✅ `/goal sisyphus` |
|
|
28
|
+
| Evidence-gated completion | ❌ | ❌ | ✅ `[goal:evidence]` required |
|
|
29
|
+
| Independent completion audit | ✅ | ❌ | ✅ Optional child-session auditor |
|
|
30
|
+
| Budget wrap-up prompts | ❌ | ❌ | ✅ 80% threshold |
|
|
31
|
+
| Open source | ❌ | ❌ | ✅ MIT |
|
|
32
|
+
|
|
9
33
|
## Compatibility snapshot
|
|
10
34
|
|
|
11
35
|
| Surface | Status |
|
|
12
36
|
|---|---|
|
|
13
|
-
| Node.js | Declared support: `>=18`; CI covers Node 18, 20, and
|
|
37
|
+
| Node.js | Declared support: `>=18`; CI covers Node 18, 20, 22, and 24 |
|
|
14
38
|
| 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
39
|
| Provider/backend quirks | Strict-template backends require the goal block to merge into the primary `system` message; covered by regression tests |
|
|
17
40
|
|
|
41
|
+
### OpenCode version compatibility
|
|
42
|
+
|
|
43
|
+
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:
|
|
44
|
+
|
|
45
|
+
| OpenCode Version | Provider Tested | `/goal status` | Auto-continue | Evidence-gated completion | Hook Output Display |
|
|
46
|
+
|---|---|---|---|---|---|
|
|
47
|
+
| 1.17.15 | opencode-go (`qwen3.7-plus`) | ✅ | ✅ | ✅ Self-corrected after one rejection (bare `[goal:complete]` with no evidence), then completed cleanly | ⚠️ Not displayed |
|
|
48
|
+
| 1.17.15 | opencode-go (`glm-5.2`) | ✅ | ✅ | ✅ Clean `[goal:evidence]` + `[goal:complete]` on the first attempt | ⚠️ Not displayed |
|
|
49
|
+
| 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 |
|
|
50
|
+
|
|
51
|
+
`/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.
|
|
52
|
+
|
|
53
|
+
**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.
|
|
54
|
+
|
|
18
55
|
## Install
|
|
19
56
|
|
|
20
57
|
```sh
|
|
@@ -122,6 +159,29 @@ A session can hold more than one goal. `/goal <condition>` replaces the focused
|
|
|
122
159
|
|
|
123
160
|
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
161
|
|
|
162
|
+
## Examples
|
|
163
|
+
|
|
164
|
+
Copy-pasteable goals for common workflows:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
/goal "fix the failing tests" --max-turns 10
|
|
168
|
+
/goal "refactor auth to use new API" --max-minutes 30
|
|
169
|
+
/goal "audit for security issues" --max-turns 3
|
|
170
|
+
/goal "migrate class components to functional" --max-minutes 60 --max-tokens 400000
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With success criteria, constraints, and a token budget shorthand:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
/goal "ship the release" --success "tests pass and changelog updated" --constraints "do not touch the public API" --budget 150k
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
An ordered sequence, run as a strict pipeline:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
/goal sisyphus build the parser; write the tests; ship the release
|
|
183
|
+
```
|
|
184
|
+
|
|
125
185
|
## How it works
|
|
126
186
|
|
|
127
187
|
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.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for opencode-goal-plugin.
|
|
3
|
+
*
|
|
4
|
+
* These describe the plugin-level configuration object accepted in
|
|
5
|
+
* `opencode.json` under `plugin: [["opencode-goal-plugin", { ... }]]`,
|
|
6
|
+
* and the shape of the module's exports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Verdict returned by a completion auditor (built-in or custom). See
|
|
11
|
+
* {@link GoalPluginOptions.auditor} and {@link GoalPluginOptions.completionAudit}.
|
|
12
|
+
*/
|
|
13
|
+
export interface CompletionAuditVerdict {
|
|
14
|
+
/** `true` to archive the goal as achieved; `false` to reject the completion. */
|
|
15
|
+
approved: boolean
|
|
16
|
+
/** Human-readable reason, surfaced in the goal's status when rejected. */
|
|
17
|
+
reason?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Arguments passed to a custom {@link GoalPluginOptions.auditor} function. */
|
|
21
|
+
export interface CompletionAuditContext {
|
|
22
|
+
/** The goal being audited (objective, budget usage, checkpoints, etc.). */
|
|
23
|
+
goal: unknown
|
|
24
|
+
/** The OpenCode session ID the goal belongs to. */
|
|
25
|
+
sessionID: string
|
|
26
|
+
/** The assistant's latest response text, containing the `[goal:evidence]`/`[goal:complete]` claim. */
|
|
27
|
+
latestText: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options for the built-in child-session completion auditor (`completionAudit: true`). */
|
|
31
|
+
export interface CompletionAuditorOptions {
|
|
32
|
+
/**
|
|
33
|
+
* How long, in milliseconds, the built-in auditor waits for a verdict from
|
|
34
|
+
* its child OpenCode session before failing open (auto-approving).
|
|
35
|
+
* @default 120000
|
|
36
|
+
*/
|
|
37
|
+
timeoutMs?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Configuration options for opencode-goal-plugin. All fields are optional;
|
|
42
|
+
* unset fields fall back to the plugin's built-in defaults. These act as
|
|
43
|
+
* the default limits for every goal set in a session, and most of the
|
|
44
|
+
* budget/behavior fields can be overridden per-goal via `/goal` command
|
|
45
|
+
* flags (e.g. `--max-turns`, `--success`, `--mode`).
|
|
46
|
+
*/
|
|
47
|
+
export interface GoalPluginOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Maximum number of auto-continue turns sent toward a goal before it is
|
|
50
|
+
* stopped for exceeding limits. Overridable per-goal with `--max-turns`.
|
|
51
|
+
* @default 10
|
|
52
|
+
*/
|
|
53
|
+
maxTurns?: number
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Maximum wall-clock duration, in milliseconds, a goal may run before it
|
|
57
|
+
* is stopped for exceeding limits. Overridable per-goal with
|
|
58
|
+
* `--max-duration-ms` or `--max-minutes`.
|
|
59
|
+
* @default 900000
|
|
60
|
+
*/
|
|
61
|
+
maxDurationMs?: number
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Maximum context token budget a goal may consume before it is stopped
|
|
65
|
+
* for exceeding limits. Overridable per-goal with `--max-tokens` or the
|
|
66
|
+
* `--budget` shorthand (accepts a `k`/`m` suffix, e.g. `100k`, `1.5m`).
|
|
67
|
+
* @default 200000
|
|
68
|
+
*/
|
|
69
|
+
maxTokens?: number
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Minimum delay, in milliseconds, enforced between consecutive
|
|
73
|
+
* auto-continue prompts. Overridable per-goal with `--cooldown-ms`.
|
|
74
|
+
* @default 1500
|
|
75
|
+
*/
|
|
76
|
+
minDelayMs?: number
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How many recent session messages to scan when looking for the latest
|
|
80
|
+
* assistant turn before auto-continuing. Higher values make long,
|
|
81
|
+
* tool-heavy sessions less likely to lose the most recent assistant
|
|
82
|
+
* response.
|
|
83
|
+
* @default 50
|
|
84
|
+
*/
|
|
85
|
+
maxRecentMessages?: number
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Output token floor below which a turn is considered "low-output" for
|
|
89
|
+
* no-progress detection. Overridable per-goal with
|
|
90
|
+
* `--no-progress-threshold`.
|
|
91
|
+
* @default 50
|
|
92
|
+
*/
|
|
93
|
+
noProgressTokenThreshold?: number
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Grace window for low-output stalls: the goal is paused only after this
|
|
97
|
+
* many consecutive stalled low-output turns, rather than on the first
|
|
98
|
+
* one. Overridable per-goal with `--no-progress-turns`.
|
|
99
|
+
* @default 2
|
|
100
|
+
*/
|
|
101
|
+
noProgressTurnsBeforePause?: number
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Grace window for tool-free continuation turns (a "talk only" turn that
|
|
105
|
+
* calls no tool). Complements the no-progress check by catching
|
|
106
|
+
* self-chat loops that still produce output. Overridable per-goal with
|
|
107
|
+
* `--no-tool-turns`.
|
|
108
|
+
* @default 2
|
|
109
|
+
*/
|
|
110
|
+
noToolCallTurnsBeforePause?: number
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fraction (between 0 and 1, exclusive) of any budget (turns, duration,
|
|
114
|
+
* or tokens) at which the plugin sends a one-time "wrap up" prompt
|
|
115
|
+
* nudging the model to finish before the hard limit is hit.
|
|
116
|
+
* @default 0.8
|
|
117
|
+
*/
|
|
118
|
+
budgetWrapupRatio?: number
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Number of remaining auto-continue turns at which a limit-approaching
|
|
122
|
+
* warning is included in status output.
|
|
123
|
+
* @default 3
|
|
124
|
+
*/
|
|
125
|
+
warnTurnsRemaining?: number
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remaining duration, in milliseconds, at which a limit-approaching
|
|
129
|
+
* warning is included in status output.
|
|
130
|
+
* @default 60000
|
|
131
|
+
*/
|
|
132
|
+
warnDurationMsRemaining?: number
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Remaining context tokens at which a limit-approaching warning is
|
|
136
|
+
* included in status output.
|
|
137
|
+
* @default 25000
|
|
138
|
+
*/
|
|
139
|
+
warnTokensRemaining?: number
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Maximum number of consecutive prompt failures (e.g. transport errors
|
|
143
|
+
* sending the auto-continue prompt, or repeated missing-evidence /
|
|
144
|
+
* missing-blocker format violations) tolerated before the goal is
|
|
145
|
+
* stopped.
|
|
146
|
+
* @default 3
|
|
147
|
+
*/
|
|
148
|
+
maxPromptFailures?: number
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Whether to persist active/backgrounded goals and recent goal results
|
|
152
|
+
* to disk so they survive a restart. Recovered active goals are loaded
|
|
153
|
+
* in a paused state. Set to `false` for purely in-memory behavior (this
|
|
154
|
+
* also disables the lifecycle ledger).
|
|
155
|
+
* @default true
|
|
156
|
+
*/
|
|
157
|
+
persistState?: boolean
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Filesystem path where persisted goal state is written when
|
|
161
|
+
* `persistState` is enabled. Overrides both the project-local default
|
|
162
|
+
* and the `OPENCODE_GOAL_STATE_PATH` environment variable.
|
|
163
|
+
* @default "<cwd>/.opencode/goals/state.json"
|
|
164
|
+
*/
|
|
165
|
+
stateFilePath?: string
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Filesystem path for the append-only lifecycle ledger
|
|
169
|
+
* (`<event> per line`, used to reconstruct active goals if the main
|
|
170
|
+
* state file is missing or corrupted).
|
|
171
|
+
* @default "<stateFilePath>.ledger.jsonl"
|
|
172
|
+
*/
|
|
173
|
+
ledgerFilePath?: string
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* How long, in milliseconds, a completed goal's summary remains
|
|
177
|
+
* available through `/goal status` after the goal leaves active memory.
|
|
178
|
+
* @default 604800000
|
|
179
|
+
*/
|
|
180
|
+
resultRetentionMs?: number
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Maximum number of completed-goal summaries retained in process memory
|
|
184
|
+
* before the oldest ones are evicted.
|
|
185
|
+
* @default 200
|
|
186
|
+
*/
|
|
187
|
+
maxStoredResults?: number
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The slash command the plugin owns. Set to e.g. `"objective"` to drive
|
|
191
|
+
* the workflow with `/objective` instead of `/goal`; a leading slash is
|
|
192
|
+
* tolerated and stripped. Remember to register the matching command
|
|
193
|
+
* name in your OpenCode `command` config.
|
|
194
|
+
* @default "goal"
|
|
195
|
+
*/
|
|
196
|
+
commandName?: string
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether the plugin installs its `command.execute.before` hook at all.
|
|
200
|
+
* Set to `false` if you only want the auto-continue/persistence
|
|
201
|
+
* behavior driven programmatically (e.g. via {@link registerTools})
|
|
202
|
+
* and don't want the plugin to own a slash command.
|
|
203
|
+
* @default true
|
|
204
|
+
*/
|
|
205
|
+
registerCommand?: boolean
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Whether the plugin registers the agent-facing goal tools
|
|
209
|
+
* (`get_goal`, `get_goal_history`, `set_goal`, `update_goal`,
|
|
210
|
+
* `clear_goal`). Requires the optional `@opencode-ai/plugin` peer
|
|
211
|
+
* dependency; when it is absent, tool registration is silently skipped
|
|
212
|
+
* and the command/event hooks still work.
|
|
213
|
+
* @default true
|
|
214
|
+
*/
|
|
215
|
+
registerTools?: boolean
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Enables the built-in child-session completion auditor: before a
|
|
219
|
+
* `[goal:complete]` is archived, the plugin spawns an independent
|
|
220
|
+
* OpenCode session to verify the completion against the goal and
|
|
221
|
+
* workspace. Ignored if {@link auditor} is also set (the custom
|
|
222
|
+
* auditor takes precedence). Tune the built-in auditor with
|
|
223
|
+
* {@link auditorOptions}.
|
|
224
|
+
* @default false
|
|
225
|
+
*/
|
|
226
|
+
completionAudit?: boolean
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Supply a custom completion auditor instead of the built-in
|
|
230
|
+
* child-session one. Takes precedence over `completionAudit: true`.
|
|
231
|
+
* A verdict of `{ approved: false }` pauses the goal (stop reason
|
|
232
|
+
* `"audit rejected"`) instead of archiving it. A thrown error is
|
|
233
|
+
* treated as a rejection (fail closed).
|
|
234
|
+
*/
|
|
235
|
+
auditor?: (context: CompletionAuditContext) => Promise<CompletionAuditVerdict>
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Tuning options for the built-in child-session auditor. Ignored when
|
|
239
|
+
* a custom {@link auditor} is supplied.
|
|
240
|
+
*/
|
|
241
|
+
auditorOptions?: CompletionAuditorOptions
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Whether the plugin announces completion/blocked audits (an
|
|
245
|
+
* audit-start and an audit-result message) instead of running silently.
|
|
246
|
+
* @default true
|
|
247
|
+
*/
|
|
248
|
+
auditMessages?: boolean
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Custom sink for audit announcements. Defaults to routing through
|
|
252
|
+
* OpenCode's structured log (`client.app.log`). Provide this to route
|
|
253
|
+
* audit messages elsewhere, e.g. into the live conversation.
|
|
254
|
+
*/
|
|
255
|
+
auditMessenger?: (sessionID: string, text: string) => Promise<void>
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* OpenCode plugin hook map returned by the plugin's `server` factory.
|
|
260
|
+
* Matches OpenCode's plugin hook contract; kept loose (`unknown`
|
|
261
|
+
* input/output) since hook payload shapes are defined by OpenCode itself,
|
|
262
|
+
* not by this package.
|
|
263
|
+
*/
|
|
264
|
+
export interface GoalPluginHooks {
|
|
265
|
+
/** Omitted entirely when {@link GoalPluginOptions.registerCommand} is `false`. */
|
|
266
|
+
"command.execute.before"?: (input: unknown, output: unknown) => Promise<void>
|
|
267
|
+
event: (input: unknown) => Promise<void>
|
|
268
|
+
"experimental.chat.system.transform": (input: unknown, output: unknown) => Promise<void>
|
|
269
|
+
"experimental.compaction.autocontinue": (input: unknown, output: unknown) => Promise<void>
|
|
270
|
+
/**
|
|
271
|
+
* Agent-facing tool definitions, present only when
|
|
272
|
+
* {@link GoalPluginOptions.registerTools} is enabled (default) and the
|
|
273
|
+
* optional `@opencode-ai/plugin` peer dependency is installed.
|
|
274
|
+
*/
|
|
275
|
+
tool?: Record<string, unknown>
|
|
276
|
+
[hook: string]: unknown
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The plugin's `server` factory. OpenCode calls this with a client bound
|
|
281
|
+
* to the running session and the resolved plugin options from
|
|
282
|
+
* `opencode.json`.
|
|
283
|
+
*/
|
|
284
|
+
export function GoalPlugin(
|
|
285
|
+
context: { client: unknown },
|
|
286
|
+
options?: GoalPluginOptions,
|
|
287
|
+
): Promise<GoalPluginHooks>
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Default export consumed by OpenCode's plugin loader:
|
|
291
|
+
* `{ "opencode-goal-plugin": { ... } }` in `opencode.json` resolves `id`
|
|
292
|
+
* and calls `server` to obtain the plugin's hooks.
|
|
293
|
+
*/
|
|
294
|
+
declare const goalPlugin: {
|
|
295
|
+
id: "opencode-goal-plugin"
|
|
296
|
+
server: typeof GoalPlugin
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default goalPlugin
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Session-scoped /goal workflow for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/goal-plugin.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": "./src/goal-plugin.js",
|
|
9
10
|
"./server": "./src/goal-plugin.js"
|
|
10
11
|
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"opencode-goal-plugin": "./scripts/verify.mjs"
|
|
14
|
+
},
|
|
11
15
|
"files": [
|
|
12
16
|
"src",
|
|
13
17
|
"scripts",
|
|
14
18
|
"examples",
|
|
19
|
+
"index.d.ts",
|
|
15
20
|
"README.md",
|
|
16
21
|
"CHANGELOG.md",
|
|
17
22
|
"CONTRIBUTING.md",
|
|
@@ -20,9 +25,10 @@
|
|
|
20
25
|
".nvmrc"
|
|
21
26
|
],
|
|
22
27
|
"scripts": {
|
|
23
|
-
"test": "node --test",
|
|
24
|
-
"test:coverage": "node --test --experimental-test-coverage",
|
|
28
|
+
"test": "node --test test/*.test.js",
|
|
29
|
+
"test:coverage": "node --test --experimental-test-coverage test/*.test.js",
|
|
25
30
|
"smoke": "node scripts/smoke-command-hook.mjs",
|
|
31
|
+
"verify": "node scripts/verify.mjs",
|
|
26
32
|
"check": "node -c src/goal-plugin.js && npm test",
|
|
27
33
|
"pack:check": "npm pack --dry-run"
|
|
28
34
|
},
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Installation verification for opencode-goal-plugin.
|
|
3
|
+
// Checks the plugin can be loaded and wired up correctly without ever
|
|
4
|
+
// invoking a model — every check below uses the same mock-client approach
|
|
5
|
+
// as scripts/smoke-command-hook.mjs.
|
|
6
|
+
|
|
7
|
+
import assert from "node:assert/strict"
|
|
8
|
+
|
|
9
|
+
const REQUIRED_HOOKS = [
|
|
10
|
+
"command.execute.before",
|
|
11
|
+
"event",
|
|
12
|
+
"experimental.chat.system.transform",
|
|
13
|
+
"experimental.compaction.autocontinue",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const results = []
|
|
17
|
+
|
|
18
|
+
function check(name, fn) {
|
|
19
|
+
return Promise.resolve()
|
|
20
|
+
.then(fn)
|
|
21
|
+
.then(() => {
|
|
22
|
+
results.push({ name, ok: true })
|
|
23
|
+
console.log(` ✅ ${name}`)
|
|
24
|
+
})
|
|
25
|
+
.catch((error) => {
|
|
26
|
+
results.push({ name, ok: false, error })
|
|
27
|
+
console.log(` ❌ ${name}`)
|
|
28
|
+
console.log(` ${error.message}`)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("opencode-goal-plugin installation verification\n")
|
|
33
|
+
|
|
34
|
+
await check("Node.js >= 18", () => {
|
|
35
|
+
const major = Number(process.versions.node.split(".")[0])
|
|
36
|
+
assert.ok(major >= 18, `Node ${process.versions.node} is below the required >=18`)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
let pluginModule
|
|
40
|
+
let GoalPlugin
|
|
41
|
+
|
|
42
|
+
await check("plugin module resolves and exposes expected shape", async () => {
|
|
43
|
+
pluginModule = await import("opencode-goal-plugin")
|
|
44
|
+
GoalPlugin = pluginModule.GoalPlugin
|
|
45
|
+
assert.equal(pluginModule.default.id, "opencode-goal-plugin")
|
|
46
|
+
assert.equal(typeof pluginModule.default.server, "function")
|
|
47
|
+
assert.equal(typeof GoalPlugin, "function")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const sessionID = `verify-${process.pid}`
|
|
51
|
+
const promptCalls = []
|
|
52
|
+
const logCalls = []
|
|
53
|
+
|
|
54
|
+
const client = {
|
|
55
|
+
app: {
|
|
56
|
+
log: async (input) => {
|
|
57
|
+
logCalls.push(input)
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
session: {
|
|
61
|
+
messages: async () => ({ data: [] }),
|
|
62
|
+
promptAsync: async (input) => {
|
|
63
|
+
promptCalls.push(input)
|
|
64
|
+
return {}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let hooks
|
|
70
|
+
|
|
71
|
+
await check("plugin initializes and registers all 4 required hooks", async () => {
|
|
72
|
+
// registerTools defaults to true but silently no-ops without the optional
|
|
73
|
+
// @opencode-ai/plugin peer dependency, so it is not asserted here — the
|
|
74
|
+
// 4 hooks below are always present regardless of that peer dependency.
|
|
75
|
+
hooks = await GoalPlugin({ client }, { minDelayMs: 1, persistState: false })
|
|
76
|
+
for (const hookName of REQUIRED_HOOKS) {
|
|
77
|
+
assert.equal(
|
|
78
|
+
typeof hooks[hookName],
|
|
79
|
+
"function",
|
|
80
|
+
`missing or non-function hook: ${hookName}`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
async function runGoalCommand(args) {
|
|
86
|
+
const output = { parts: [] }
|
|
87
|
+
await hooks["command.execute.before"](
|
|
88
|
+
{ command: "goal", sessionID, arguments: args },
|
|
89
|
+
output,
|
|
90
|
+
)
|
|
91
|
+
assert.equal(output.parts.length, 1)
|
|
92
|
+
assert.equal(output.parts[0].type, "text")
|
|
93
|
+
return output.parts[0].text
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await check("/goal status works", async () => {
|
|
97
|
+
const text = await runGoalCommand("status")
|
|
98
|
+
assert.match(text, /No active goal/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await check("/goal set works", async () => {
|
|
102
|
+
const text = await runGoalCommand("verify the installation --max-turns 1")
|
|
103
|
+
assert.match(text, /New active goal: verify the installation/)
|
|
104
|
+
const statusText = await runGoalCommand("status")
|
|
105
|
+
assert.match(statusText, /Active goal: verify the installation/)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await check("no model calls were made during verification", () => {
|
|
109
|
+
assert.equal(promptCalls.length, 0, "expected zero promptAsync calls")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Clean up the goal created above so this script has no side effects.
|
|
113
|
+
await runGoalCommand("clear")
|
|
114
|
+
|
|
115
|
+
console.log()
|
|
116
|
+
|
|
117
|
+
const failed = results.filter((r) => !r.ok)
|
|
118
|
+
if (failed.length > 0) {
|
|
119
|
+
console.log(`${failed.length}/${results.length} checks failed.`)
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`All ${results.length} checks passed. opencode-goal-plugin is installed correctly.`)
|
package/src/goal-plugin.js
CHANGED
|
@@ -2109,11 +2109,19 @@ function createChildSessionAuditor(client, { agent = "build", timeoutMs = 120_00
|
|
|
2109
2109
|
}
|
|
2110
2110
|
}
|
|
2111
2111
|
|
|
2112
|
-
export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
2112
|
+
export const GoalPlugin = async ({ client, directory } = {}, pluginOptions = {}) => {
|
|
2113
2113
|
const defaultGoalOptions = normalizeOptions(pluginOptions)
|
|
2114
|
+
// OpenCode's PluginInput carries the active session's project directory
|
|
2115
|
+
// separately from the Node process's own process.cwd(), which — when
|
|
2116
|
+
// OpenCode runs as a persistent server/daemon serving multiple
|
|
2117
|
+
// projects/sessions — does NOT track the session's directory. Falling back
|
|
2118
|
+
// to process.cwd() here would silently resolve the project-local state
|
|
2119
|
+
// path against wherever the server happened to boot, not the project the
|
|
2120
|
+
// user is actually working in. An explicit `cwd` plugin option (mainly for
|
|
2121
|
+
// tests) still takes precedence.
|
|
2114
2122
|
const persistenceOptions = normalizePersistenceOptions(pluginOptions, {
|
|
2115
2123
|
env: pluginOptions.env,
|
|
2116
|
-
cwd: pluginOptions.cwd,
|
|
2124
|
+
cwd: pluginOptions.cwd || directory,
|
|
2117
2125
|
})
|
|
2118
2126
|
const { commandName, registerCommand } = normalizeCommandOptions(pluginOptions)
|
|
2119
2127
|
// Serialize all persist() calls through a promise chain so concurrent callers
|
|
@@ -2531,6 +2539,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
2531
2539
|
// goal and add another. Clear any ordered-sequence flag so the new
|
|
2532
2540
|
// standalone goal does not trigger sisyphus auto-promotion of old sequence
|
|
2533
2541
|
// goals that may still be in the registry (matches the agent setGoal path).
|
|
2542
|
+
const replacedGoal = goalStates.get(sessionID)
|
|
2534
2543
|
sessionOrdered.delete(sessionID)
|
|
2535
2544
|
cleanupGoal(sessionID)
|
|
2536
2545
|
lastGoalResults.delete(sessionID)
|
|
@@ -2540,6 +2549,13 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
2540
2549
|
output.parts = [
|
|
2541
2550
|
makeTextPart(
|
|
2542
2551
|
[
|
|
2552
|
+
...(replacedGoal
|
|
2553
|
+
? [
|
|
2554
|
+
`⚠️ Replacing active goal: "${replacedGoal.condition}"`,
|
|
2555
|
+
`Use \`/${commandName} add <condition>\` instead to keep it running in the background.`,
|
|
2556
|
+
"",
|
|
2557
|
+
]
|
|
2558
|
+
: []),
|
|
2543
2559
|
`New active goal: ${goal.condition}`,
|
|
2544
2560
|
goal.successCriteria ? `Success criteria: ${goal.successCriteria}` : null,
|
|
2545
2561
|
goal.constraints ? `Constraints / non-goals: ${goal.constraints}` : null,
|