opencode-resolve 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -3,28 +3,67 @@
3
3
  [![npm version](https://img.shields.io/npm/v/opencode-resolve.svg)](https://www.npmjs.com/package/opencode-resolve)
4
4
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
5
 
6
- Lightweight OpenCode plugin for focused task resolution with minimal agents and native `plan`/`build` preservation.
6
+ Small OpenCode plugin that turns a single instruction into a finished, verified change.
7
7
 
8
- `opencode-resolve` keeps native OpenCode `plan` and `build` behavior intact. It only injects a small set of optional agents so you can keep the useful early Oh My OpenCode-style persistence without a heavy multi-agent swarm.
8
+ `opencode-resolve` ships three roles by default **resolver** (orchestrator), **coder** (implementer), and **reviewer** (read-only auditor) and runs them with auto-approved permissions so a task drives to completion without prompting at every step. It defines roles, not model providers: agents inherit your OpenCode default model unless you pin them.
9
9
 
10
- ## Features
10
+ ---
11
11
 
12
- - Preserves native OpenCode `plan` and `build` behavior.
13
- - Injects only `coder` and `reviewer` by default.
14
- - Uses GLM-style defaults for coding and GPT-style defaults for review.
15
- - Adds a Context7 MCP preset only when one is not already configured.
16
- - Validates config strictly so typos fail fast.
17
- - Keeps Telegram notifications and remote control as companion-plugin concerns.
12
+ ## Table of Contents
18
13
 
19
- ## Install From npm
14
+ - [Roles at a glance](#roles-at-a-glance)
15
+ - [Quick Start](#quick-start)
16
+ - [Drop-in setup (give to an LLM)](#drop-in-setup-give-to-an-llm)
17
+ - [Default Behavior](#default-behavior)
18
+ - [Configuration](#configuration)
19
+ - [Configuration Reference](#configuration-reference)
20
+ - [Auto Approval](#auto-approval)
21
+ - [Parallel Subagent Limit](#parallel-subagent-limit)
22
+ - [Upgrade & Migration](#upgrade--migration)
23
+ - [Model Setup](#model-setup)
24
+ - [Agent Reference](#agent-reference)
25
+ - [Optional Commands](#optional-commands)
26
+ - [Local Development](#local-development)
27
+ - [Verification](#verification)
28
+ - [Release](#release)
29
+ - [Design Rules](#design-rules)
20
30
 
21
- Install this where OpenCode resolves plugins from. For most users, a global install is the simplest option:
31
+ ---
32
+
33
+ ## Roles at a glance
34
+
35
+ | Role | Default | Can edit? | Can run shell? | Purpose |
36
+ |---|:---:|:---:|:---:|---|
37
+ | `resolver` | Yes | Yes (auto) | Yes (auto) | Primary orchestrator. Plans, dispatches `coder` (one at a time), verifies, iterates. |
38
+ | `coder` | Yes | Yes (auto) | Yes (auto) | Focused implementer. Makes the smallest correct change and verifies. |
39
+ | `reviewer` | Yes | **Never** | **Never** | Read-only auditor. Inspects only — recommends fixes for `coder` or `resolver` to apply. |
40
+
41
+ Hard rule: **the reviewer cannot modify the project by any means.** Both `edit` and `bash` are denied for the reviewer regardless of `autoApprove`. Any required fix is routed back through `coder` or `resolver`.
42
+
43
+ ---
44
+
45
+ ## Quick Start
46
+
47
+ Install from npm:
22
48
 
23
49
  ```sh
24
50
  npm install -g opencode-resolve
25
51
  ```
26
52
 
27
- Then add it to your OpenCode config:
53
+ The package `postinstall` step:
54
+
55
+ 1. Adds `opencode-resolve` to `~/.config/opencode/opencode.json` `plugin` array.
56
+ 2. Creates `~/.config/opencode/resolve.json` from [`opencode-resolve.example.json`](./opencode-resolve.example.json) when missing.
57
+
58
+ Restart OpenCode after installation.
59
+
60
+ To skip automatic registration:
61
+
62
+ ```sh
63
+ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
64
+ ```
65
+
66
+ Manual fallback: add the plugin to `~/.config/opencode/opencode.json` yourself:
28
67
 
29
68
  ```json
30
69
  {
@@ -32,150 +71,390 @@ Then add it to your OpenCode config:
32
71
  }
33
72
  ```
34
73
 
35
- The default model IDs assume your OpenCode provider configuration can access Z.AI/GLM and OpenAI/GPT models. Override `models` if your provider IDs differ.
74
+ ---
36
75
 
37
- ## Defaults
76
+ ## Drop-in setup (give to an LLM)
38
77
 
39
- Enabled by default:
78
+ Hand the block below to any reasonably capable LLM along with the instruction _"set up opencode-resolve on this machine"_ and it can finish the install end-to-end. Everything below is the entire required configuration — no other MCP servers, environment variables, or files are needed.
40
79
 
41
- - `coder`: GLM-based implementation agent for edits, tests, and iteration.
42
- - `reviewer`: GPT-based Oracle-style review agent for requirements fit, correctness, security, tests, and maintainability.
80
+ **Step 1 install the plugin:**
43
81
 
44
- Available but disabled unless configured:
82
+ ```sh
83
+ npm install -g opencode-resolve
84
+ ```
45
85
 
46
- - `architect`: GPT-based design and task decomposition.
47
- - `gpt-coder`: GPT-based difficult implementation fallback.
48
- - `debugger`: failure reproduction and root-cause analysis.
49
- - `researcher`: codebase and documentation research.
86
+ **Step 2 `~/.config/opencode/opencode.json`** (the only required keys; merge into an existing file rather than replacing if one exists):
50
87
 
51
- The plugin also adds a `context7` MCP preset when one is not already configured.
88
+ ```json
89
+ {
90
+ "$schema": "https://opencode.ai/config.json",
91
+ "model": "<your-default-model-id>",
92
+ "plugin": ["opencode-resolve"]
93
+ }
94
+ ```
52
95
 
53
- ## Develop Locally
96
+ **Step 3 — `~/.config/opencode/resolve.json`** (created automatically by `postinstall`; the file below is the canonical default):
54
97
 
55
- From this repository:
98
+ ```json
99
+ {
100
+ "enabled": ["coder", "reviewer", "resolver"],
101
+ "preserveNative": true,
102
+ "context7": true,
103
+ "commands": false,
104
+ "autoApprove": true,
105
+ "maxParallelSubagents": 1
106
+ }
107
+ ```
56
108
 
57
- ```sh
58
- npm install
59
- npm test
60
- npm run install:local
109
+ **Step 4 — restart OpenCode.**
110
+
111
+ That is the entire setup. `context7` is the only MCP server the plugin needs (auto-registered when the key above is `true`) — no other MCP entries are required for `opencode-resolve` itself. Models are inherited from your top-level OpenCode `model` unless you pin per-role models under `models` (see [Model Setup](#model-setup)).
112
+
113
+ If you want stricter behavior, flip `autoApprove` to `false` (every action prompts for approval) and/or raise `maxParallelSubagents` to let the resolver fan out independent subtasks.
114
+
115
+ ---
116
+
117
+ ## Default Behavior
118
+
119
+ | Item | Default |
120
+ |---|---|
121
+ | Enabled agents | `coder`, `reviewer`, `resolver` |
122
+ | Primary agent for new tasks | `resolver` (`mode: "all"`) |
123
+ | Agent model | Inherits top-level OpenCode `model` |
124
+ | Native `plan` / `build` | Preserved untouched |
125
+ | Context7 MCP preset | Added if absent |
126
+ | Optional commands | Disabled |
127
+ | `autoApprove` | `true` (no per-action prompts on coder/resolver) |
128
+ | Reviewer modification | Denied (cannot be auto-approved) |
129
+
130
+ ---
131
+
132
+ ## Configuration
133
+
134
+ The plugin reads the first config file it finds:
135
+
136
+ | Priority | Path |
137
+ |---:|---|
138
+ | 1 | `.opencode/resolve.json` (project) |
139
+ | 2 | `opencode-resolve.json` (project) |
140
+ | 3 | `~/.config/opencode/resolve.json` |
141
+ | 4 | `~/.config/opencode/opencode-resolve.json` |
142
+
143
+ Inline plugin options in `opencode.json` override file config.
144
+
145
+ Config precedence:
146
+
147
+ ```text
148
+ built-in defaults -> first config file found -> inline plugin options
61
149
  ```
62
150
 
63
- `install:local` builds the plugin, links it into the OpenCode global plugin directory, and creates `~/.config/opencode/resolve.json` if it does not exist.
151
+ Minimal config (matches defaults):
64
152
 
65
- Manual equivalent:
153
+ ```json
154
+ {
155
+ "enabled": ["coder", "reviewer", "resolver"],
156
+ "autoApprove": true,
157
+ "context7": true,
158
+ "commands": false
159
+ }
160
+ ```
66
161
 
67
- ```sh
68
- npm run build
69
- mkdir -p ~/.config/opencode/plugins
70
- ln -sf "$PWD/dist/index.js" ~/.config/opencode/plugins/opencode-resolve.js
162
+ Inline form inside `opencode.json`:
163
+
164
+ ```json
165
+ {
166
+ "plugin": [
167
+ [
168
+ "opencode-resolve",
169
+ {
170
+ "enabled": ["coder", "reviewer", "resolver"],
171
+ "autoApprove": true,
172
+ "context7": true,
173
+ "commands": false
174
+ }
175
+ ]
176
+ ]
177
+ }
71
178
  ```
72
179
 
73
- Local plugin files are loaded automatically by OpenCode.
180
+ Strict validation rejects unknown agent names, misspelled keys, invalid modes, invalid permission values, and wrong value types — typos fail fast.
181
+
182
+ ---
183
+
184
+ ## Configuration Reference
185
+
186
+ Every accepted top-level option:
187
+
188
+ | Key | Type | Default | Purpose |
189
+ |---|---|---|---|
190
+ | `enabled` | `string[]` | `["coder", "reviewer", "resolver"]` | Which resolve agents to inject. Per-agent `agents.<name>.enabled` overrides this. |
191
+ | `preserveNative` | `boolean` | `true` | Native `plan`/`build` are always preserved. Accepted for readability. |
192
+ | `context7` | `boolean` | `true` | When true, registers the Context7 MCP server unless already configured. |
193
+ | `commands` | `boolean` | `false` | When true, adds `resolve`, `resolve-code`, `resolve-review` commands. |
194
+ | `autoApprove` | `boolean` | `true` | Flips default `"ask"` permissions to `"allow"` on enabled agents. Never touches `"deny"` or user-set keys. |
195
+ | `maxParallelSubagents` | `positive integer` | `1` | Cap on simultaneous subagents the resolver dispatches across coder, reviewer, etc. |
196
+ | `models` | `object` | `{}` | Alias map. Keys are agent names or `glm`/`gpt`. Values are model ids or other aliases. |
197
+ | `agents` | `object` | `{}` | Per-agent overrides (see below). |
198
+ | `config` | `string` | _none_ | Custom path to a config file (relative to the project or absolute). |
199
+
200
+ Per-agent options inside `agents.<name>`:
201
+
202
+ | Key | Type | Notes |
203
+ |---|---|---|
204
+ | `enabled` | `boolean` | Force-enable or force-disable this agent regardless of top-level `enabled`. |
205
+ | `model` | `string` | Model id or alias. Resolved against the top-level `models` map. |
206
+ | `mode` | `"subagent" \| "primary" \| "all"` | OpenCode agent mode. |
207
+ | `description` | `string` | Override the default description shown to other agents. |
208
+ | `prompt` | `string` | Override the default system prompt. (For `resolver`, this also disables the templated parallel-rule prompt.) |
209
+ | `color` | `string` | UI color. |
210
+ | `maxSteps` | `positive integer` | Per-invocation step budget. |
211
+ | `tools` | `Record<string, boolean>` | Toggle individual OpenCode tools. |
212
+ | `permission` | `object` | Permission overrides — see below. |
213
+
214
+ Permission keys (each takes `"ask"`, `"allow"`, or `"deny"`):
74
215
 
75
- After publishing/installing from npm, add the package to your OpenCode config:
216
+ `edit`, `bash`, `webfetch`, `doom_loop`, `external_directory`.
217
+
218
+ `permission.bash` may also be a per-command map:
76
219
 
77
220
  ```json
78
221
  {
79
- "plugin": ["opencode-resolve"]
222
+ "permission": {
223
+ "bash": { "npm test": "allow", "rm -rf": "deny" }
224
+ }
80
225
  }
81
226
  ```
82
227
 
83
- ## Configuration
228
+ A fully-annotated reference config ships with the package as
229
+ [`opencode-resolve.reference.jsonc`](./opencode-resolve.reference.jsonc) — copy
230
+ the keys you need into your `resolve.json` (without the comments).
84
231
 
85
- The plugin reads the first config file it finds:
232
+ ---
233
+
234
+ ## Auto Approval
235
+
236
+ `autoApprove` (default `true`) flips every `"ask"` permission on the **enabled** agents to `"allow"`, so coder and resolver work continuously without per-action prompts. It never touches `"deny"` and never overrides a permission key the user explicitly set.
237
+
238
+ | Permission state | autoApprove: true | autoApprove: false |
239
+ |---|---|---|
240
+ | Default `"ask"` | becomes `"allow"` | stays `"ask"` |
241
+ | Default `"deny"` | stays `"deny"` | stays `"deny"` |
242
+ | User explicit `"ask"` | stays `"ask"` | stays `"ask"` |
243
+ | User explicit `"allow"` | stays `"allow"` | stays `"allow"` |
244
+ | User explicit `"deny"` | stays `"deny"` | stays `"deny"` |
245
+
246
+ Reviewer permissions for `edit` and `bash` are `"deny"` in the defaults, so `autoApprove` cannot grant the reviewer modification rights.
247
+
248
+ Turn it off when you want the conservative ask-every-time behavior:
86
249
 
87
- - `.opencode/resolve.json`
88
- - `opencode-resolve.json`
89
- - `~/.config/opencode/resolve.json`
90
- - `~/.config/opencode/opencode-resolve.json`
250
+ ```json
251
+ {
252
+ "autoApprove": false
253
+ }
254
+ ```
255
+
256
+ Trust note: `autoApprove: true` assumes you trust the workspace and the model you have configured. Use a sandbox or VM for untrusted code, and keep `autoApprove: false` if you want to inspect every action.
257
+
258
+ ---
91
259
 
92
- Example:
260
+ ## Parallel Subagent Limit
261
+
262
+ `maxParallelSubagents` (default `1`) caps how many subagents the **resolver** may dispatch in parallel — across `coder`, `reviewer`, or any other subagent. The default of `1` enforces strictly serial dispatch: resolver waits for each subagent to finish before launching the next.
263
+
264
+ Raise it when you want resolver to fan out independent subtasks:
93
265
 
94
266
  ```json
95
267
  {
96
- "enabled": ["coder", "reviewer"],
268
+ "maxParallelSubagents": 3
269
+ }
270
+ ```
271
+
272
+ The limit is woven into the resolver's prompt at config-load time, so changing it does not require any code changes — restart OpenCode to pick up the new limit. If you provide a custom `agents.resolver.prompt`, the templated rule is skipped and your prompt wins.
273
+
274
+ ---
275
+
276
+ ## Upgrade & Migration
277
+
278
+ When you upgrade to a newer version of `opencode-resolve`, the `postinstall` script runs an **additive migration** on your existing `~/.config/opencode/resolve.json`:
279
+
280
+ - Adds new top-level keys (e.g. `autoApprove`, `maxParallelSubagents`) with their defaults if they are absent.
281
+ - **Never** modifies keys you have already set.
282
+ - **Never** rewrites your `enabled` list, `models` map, or `agents` overrides.
283
+ - If `enabled` is set and does not include `"resolver"`, prints a one-line tip suggesting you add it. Your file is left untouched.
284
+
285
+ Skip the migration entirely with:
286
+
287
+ ```sh
288
+ OPENCODE_RESOLVE_SKIP_POSTINSTALL=1 npm install -g opencode-resolve
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Model Setup
294
+
295
+ `opencode-resolve` does not ship a provider-specific role default.
296
+
297
+ Model resolution order for each resolve agent:
298
+
299
+ 1. `agents.<name>.model`
300
+ 2. `models.<name>` alias mapping
301
+ 3. top-level OpenCode `model`
302
+ 4. OpenCode's own fallback when no model is configured
303
+
304
+ Use the default config when all resolve roles should follow your current OpenCode model.
305
+
306
+ Pin models only when you intentionally want fixed role behavior, such as fast coding on one model and deeper review on another.
307
+
308
+ Role-specific aliases:
309
+
310
+ ```json
311
+ {
312
+ "enabled": ["coder", "reviewer", "resolver"],
97
313
  "models": {
98
- "glm": "zai-coding-plan/glm-5",
99
- "gpt": "openai/gpt-5",
314
+ "glm": "zai-coding-plan/glm-5.1",
315
+ "gpt": "openai/gpt-5.5",
100
316
  "coder": "glm",
317
+ "resolver": "gpt",
101
318
  "reviewer": "gpt"
102
- },
103
- "preserveNative": true,
104
- "context7": true,
105
- "commands": false,
319
+ }
320
+ }
321
+ ```
322
+
323
+ Pin one role directly:
324
+
325
+ ```json
326
+ {
106
327
  "agents": {
107
- "architect": {
108
- "enabled": false
109
- },
110
- "gpt-coder": {
111
- "enabled": false,
112
- "model": "gpt"
328
+ "reviewer": {
329
+ "model": "openai/gpt-5.5"
113
330
  }
114
331
  }
115
332
  }
116
333
  ```
117
334
 
118
- You can also pass options directly from `opencode.json`:
335
+ Native OpenCode agents such as `plan` and `build` are configured through the top-level OpenCode `agent`, not through `opencode-resolve`.
336
+
337
+ Mixed setup example:
119
338
 
120
339
  ```json
121
340
  {
341
+ "model": "zai-coding-plan/glm-5.1",
342
+ "agent": {
343
+ "plan": {
344
+ "model": "openai/gpt-5.5"
345
+ }
346
+ },
122
347
  "plugin": [
123
348
  [
124
349
  "opencode-resolve",
125
350
  {
126
- "enabled": ["coder", "reviewer", "debugger"],
127
- "config": "~/.config/opencode/resolve.json"
351
+ "enabled": ["coder", "reviewer", "resolver"],
352
+ "models": {
353
+ "glm": "zai-coding-plan/glm-5.1",
354
+ "gpt": "openai/gpt-5.5",
355
+ "coder": "glm",
356
+ "resolver": "gpt",
357
+ "reviewer": "gpt"
358
+ }
128
359
  }
129
360
  ]
130
361
  ]
131
362
  }
132
363
  ```
133
364
 
134
- Options in `opencode.json` override file config.
365
+ In this setup, `plan`, `resolver`, and `reviewer` use `openai/gpt-5.5`; native `build` and resolve `coder` use `zai-coding-plan/glm-5.1`.
366
+
367
+ ---
368
+
369
+ ## Agent Reference
370
+
371
+ | Agent | Default | Mode | Edit | Bash | WebFetch | Purpose |
372
+ |---|:---:|---|---|---|---|---|
373
+ | `resolver` | Yes | `all` | ask → allow | ask → allow | ask → allow | Primary orchestrator. Plans, dispatches `coder` (one at a time), verifies, iterates to completion. |
374
+ | `coder` | Yes | `subagent` | ask → allow | ask → allow | ask → allow | Focused implementer. Smallest correct change. |
375
+ | `reviewer` | Yes | `subagent` | **deny** | **deny** | ask → allow | Read-only auditor. Cannot modify by any means. |
376
+ | `architect` | No | `subagent` | deny | ask → allow | ask → allow | Design and task decomposition. |
377
+ | `gpt-coder` | No | `subagent` | ask → allow | ask → allow | ask → allow | Stronger-reasoning implementation fallback. |
378
+ | `debugger` | No | `subagent` | ask → allow | ask → allow | ask → allow | Reproduction and root-cause analysis. |
379
+ | `researcher` | No | `subagent` | deny | ask → allow | ask → allow | Codebase and documentation research. |
380
+
381
+ `ask → allow` means the default is `"ask"` and `autoApprove` (default on) flips it to `"allow"`. Set `autoApprove: false` to keep them as `"ask"`.
382
+
383
+ Supported modes:
384
+
385
+ | Mode | Meaning |
386
+ |---|---|
387
+ | `subagent` | Available only as a subagent |
388
+ | `primary` | Available as a primary agent |
389
+ | `all` | Available as both primary and subagent |
390
+
391
+ Supported permission values: `ask`, `allow`, `deny`.
392
+
393
+ Supported model alias keys: `glm`, `gpt`, and every supported agent name. Aliases only resolve when defined in `models`.
394
+
395
+ `preserveNative` is accepted for readability, but native `plan` and `build` are always preserved. The plugin never rewrites built-in OpenCode agents.
396
+
397
+ ### Resolver orchestration rules
135
398
 
136
- Config precedence is: built-in defaults, then the first config file found, then inline plugin options from `opencode.json`.
399
+ The resolver's prompt enforces the following behavior:
137
400
 
138
- The config is validated on load. Unknown agent names, misspelled keys, invalid modes, invalid permission values, and wrong value types fail fast with a clear error instead of silently changing behavior.
401
+ - Plan the smallest correct change before dispatching.
402
+ - Dispatch **only one `coder` subagent at a time**. Never call coders in parallel.
403
+ - After each coder run, verify (tests, type checks, targeted checks) when practical.
404
+ - Optionally consult `reviewer` for an independent read-only audit on risky changes; route any required fixes back through `coder`.
405
+ - Iterate until the task is resolved or clearly blocked, then return a concise summary.
139
406
 
140
- Supported agents:
407
+ ---
141
408
 
142
- - `coder`
143
- - `reviewer`
144
- - `architect`
145
- - `gpt-coder`
146
- - `debugger`
147
- - `researcher`
409
+ ## Optional Commands
148
410
 
149
- Supported model aliases:
411
+ Set `commands: true` to add helper subtask commands:
150
412
 
151
- - `glm`
152
- - `gpt`
153
- - every supported agent name
413
+ | Command | Description |
414
+ |---|---|
415
+ | `resolve` | Run the `resolver` agent end-to-end on the current task |
416
+ | `resolve-code` | Run the `coder` agent for focused implementation |
417
+ | `resolve-review` | Run the `reviewer` agent for a read-only audit |
154
418
 
155
- Supported agent modes are `subagent`, `primary`, and `all`. Supported permission values are `ask`, `allow`, and `deny`.
419
+ ---
156
420
 
157
- `preserveNative` is accepted for readability, but native `plan` and `build` are always preserved. The plugin never iterates or rewrites those built-in agents.
421
+ ## Local Development
158
422
 
159
- `enabled` selects the default active agents. A per-agent `agents.<name>.enabled` value overrides that list.
423
+ From this repository:
160
424
 
161
- Set `context7` to `false` to disable the Context7 MCP preset.
425
+ ```sh
426
+ npm install
427
+ npm test
428
+ npm run install:local
429
+ ```
162
430
 
163
- Set `commands` to `true` to add optional subtask commands:
431
+ `install:local` builds the plugin, links it into the OpenCode global plugin directory, and creates `~/.config/opencode/resolve.json` if it does not exist.
164
432
 
165
- - `resolve-code`: run the `coder` agent for focused implementation.
166
- - `resolve-review`: run the `reviewer` agent for requirement/risk review.
433
+ Manual local install:
434
+
435
+ ```sh
436
+ npm run build
437
+ mkdir -p ~/.config/opencode/plugins
438
+ ln -sf "$PWD/dist/index.js" ~/.config/opencode/plugins/opencode-resolve.js
439
+ ```
440
+
441
+ Local plugin files are loaded automatically by OpenCode.
442
+
443
+ ---
167
444
 
168
445
  ## Verification
169
446
 
447
+ Run the normal checks:
448
+
170
449
  ```sh
171
450
  npm run typecheck
172
451
  npm test
173
452
  npm run build
174
453
  ```
175
454
 
176
- The test suite executes the built plugin and verifies default agent injection, model aliases, file config, plugin option overrides, optional commands, Context7 preservation, and native `plan`/`build` preservation.
455
+ The test suite executes the built plugin and verifies default agent injection, `autoApprove` behavior, model aliases, file config, plugin option overrides, optional commands, Context7 preservation, and native `plan`/`build` preservation.
177
456
 
178
- Before publishing, run:
457
+ Before publishing:
179
458
 
180
459
  ```sh
181
460
  npm run typecheck
@@ -186,13 +465,17 @@ npm publish --dry-run
186
465
 
187
466
  `npm pack` and `npm publish` run `npm test` first through the `prepack` script.
188
467
 
468
+ ---
469
+
189
470
  ## Release
190
471
 
191
472
  Releases are published by GitHub Actions when a version tag is pushed.
192
473
 
193
474
  Required repository secret:
194
475
 
195
- - `NPM_TOKEN`: npm automation token with publish access.
476
+ | Secret | Description |
477
+ |---|---|
478
+ | `NPM_TOKEN` | npm automation token with publish access |
196
479
 
197
480
  Tag release flow:
198
481
 
@@ -205,32 +488,12 @@ You can also run the `Publish to npm` workflow manually from GitHub Actions and
205
488
 
206
489
  The release workflow runs `npm ci`, `npm run typecheck`, `npm test`, and `npm publish --access public --provenance`.
207
490
 
491
+ ---
492
+
208
493
  ## Design Rules
209
494
 
210
495
  - Do not overwrite native `plan` or `build` agents.
211
- - Keep default subagent count small.
212
- - Search and inspect before editing.
213
- - Make the smallest correct change.
214
- - Verify when practical.
215
- - Use reviewer for final requirement/risk checks instead of running many agents by default.
216
-
217
- ## Companion Plugins
218
-
219
- `opencode-resolve` intentionally stays focused on agent injection and task resolution. Use notification or remote-control plugins alongside it instead of baking those concerns into this package.
220
-
221
- Recommended companion:
222
-
223
- - [`@jshsakura/opencode-telegram-bot-plugin`](https://github.com/jshsakura/opencode-telegram-bot-plugin): Telegram notifications and permission-response support for OpenCode sessions.
224
-
225
- Install and configure the Telegram plugin separately. It needs its own bot token/chat setup; see that plugin's README for details.
226
-
227
- Example:
228
-
229
- ```json
230
- {
231
- "plugin": [
232
- "opencode-resolve",
233
- "@jshsakura/opencode-telegram-bot-plugin"
234
- ]
235
- }
236
- ```
496
+ - Keep the default agent set small and role-clear: `resolver` orchestrates, `coder` modifies, `reviewer` only reads.
497
+ - The reviewer never modifies anything — fixes always go through `coder` or `resolver`.
498
+ - The resolver dispatches at most one coder at a time.
499
+ - Search and inspect before editing. Make the smallest correct change. Verify when practical.
package/dist/index.js CHANGED
@@ -1,18 +1,9 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, isAbsolute, join, resolve } from "node:path";
4
- const DEFAULT_MODELS = {
5
- glm: "zai-coding-plan/glm-5",
6
- gpt: "openai/gpt-5",
7
- coder: "zai-coding-plan/glm-5",
8
- reviewer: "openai/gpt-5",
9
- architect: "openai/gpt-5",
10
- "gpt-coder": "openai/gpt-5",
11
- debugger: "zai-coding-plan/glm-5",
12
- researcher: "zai-coding-plan/glm-5",
13
- };
14
- const DEFAULT_ENABLED = ["coder", "reviewer"];
15
- const VALID_AGENT_NAMES = ["coder", "reviewer", "architect", "gpt-coder", "debugger", "researcher"];
4
+ const DEFAULT_MODELS = {};
5
+ const DEFAULT_ENABLED = ["coder", "reviewer", "resolver"];
6
+ const VALID_AGENT_NAMES = ["coder", "reviewer", "resolver", "architect", "gpt-coder", "debugger", "researcher"];
16
7
  const VALID_AGENT_NAME_SET = new Set(VALID_AGENT_NAMES);
17
8
  const VALID_MODEL_ALIASES = [...VALID_AGENT_NAMES, "glm", "gpt"];
18
9
  const VALID_MODEL_ALIAS_SET = new Set(VALID_MODEL_ALIASES);
@@ -25,8 +16,11 @@ const VALID_TOP_LEVEL_KEYS = new Set([
25
16
  "preserveNative",
26
17
  "context7",
27
18
  "commands",
19
+ "autoApprove",
20
+ "maxParallelSubagents",
28
21
  "config",
29
22
  ]);
23
+ const DEFAULT_MAX_PARALLEL_SUBAGENTS = 1;
30
24
  const VALID_AGENT_KEYS = new Set([
31
25
  "enabled",
32
26
  "model",
@@ -38,6 +32,26 @@ const VALID_AGENT_KEYS = new Set([
38
32
  "tools",
39
33
  "permission",
40
34
  ]);
35
+ function buildResolverPrompt(maxParallelSubagents) {
36
+ const limit = Math.max(1, Math.trunc(maxParallelSubagents));
37
+ const parallelRule = limit === 1
38
+ ? "CRITICAL: Dispatch only ONE subagent at a time. Never call multiple subagents (coder, reviewer, or any other) in parallel. Wait for the current subagent to finish and evaluate its result before dispatching the next one."
39
+ : `CRITICAL: Dispatch at most ${limit} subagents in parallel. Never exceed this limit across coder, reviewer, or any other subagent. Wait for in-flight subagents to finish and evaluate their results before launching new ones.`;
40
+ return [
41
+ "You are Resolver, the primary orchestrator agent for OpenCode Resolve.",
42
+ "Your job is to drive the user's task to a verified resolution end-to-end without unnecessary stops.",
43
+ "Workflow:",
44
+ "1. Understand the requirement. Inspect the relevant files briefly before planning.",
45
+ "2. Plan the smallest correct change.",
46
+ "3. Dispatch the coder subagent to implement the change. Pass a focused instruction with the exact files and behavior.",
47
+ `4. ${parallelRule}`,
48
+ "5. After implementation, verify when practical (run tests, type checks, or targeted checks).",
49
+ "6. If issues remain, dispatch the coder again with a focused fix, or apply a small direct edit yourself when it is clearly trivial.",
50
+ "7. Optionally consult the reviewer subagent for an independent read-only review on risky changes. The reviewer cannot modify anything; treat its output as advice and route any required fixes back through the coder. The same parallel-dispatch limit applies to the reviewer.",
51
+ "8. Repeat until the task is resolved or clearly blocked.",
52
+ "Return a concise summary of what changed, verification results, and any remaining blockers.",
53
+ ].join("\n");
54
+ }
41
55
  const DEFAULT_AGENT_CONFIG = {
42
56
  coder: {
43
57
  mode: "subagent",
@@ -61,16 +75,29 @@ const DEFAULT_AGENT_CONFIG = {
61
75
  mode: "subagent",
62
76
  color: "#8A7CFF",
63
77
  maxSteps: 8,
64
- description: "Use for Oracle-style review of requirements fit, correctness, security, tests, and maintainability risks.",
78
+ description: "Read-only Oracle-style reviewer. Inspects code for requirements fit, correctness, security, tests, and maintainability risks. Never modifies anything.",
65
79
  prompt: [
66
- "You are Reviewer, an Oracle-style review subagent for OpenCode Resolve.",
67
- "Review the work against the user's actual requirements and the repository's existing patterns.",
80
+ "You are Reviewer, a strictly read-only review subagent for OpenCode Resolve.",
81
+ "You MUST NOT modify the project by any means: no file edits, no writes, no shell commands that change state, no git commits, no package installs.",
82
+ "Use read-only tools (read, grep, glob, list, web fetch for documentation) to inspect the work against the user's requirements and the repository's existing patterns.",
68
83
  "Prioritize concrete bugs, behavioral regressions, security risks, missing tests, and maintainability issues.",
69
- "Do not rewrite code unless explicitly asked; return findings ordered by severity with file and line references when available.",
70
- "If there are no findings, say so and mention residual risks or verification gaps.",
84
+ "Return findings ordered by severity with file and line references when available. If there are no findings, say so and mention residual risks or verification gaps.",
85
+ "If a fix is needed, describe it precisely and recommend dispatching the coder or resolver agent. Never apply fixes yourself.",
71
86
  ].join("\n"),
72
87
  permission: {
73
88
  edit: "deny",
89
+ bash: "deny",
90
+ webfetch: "ask",
91
+ },
92
+ },
93
+ resolver: {
94
+ mode: "all",
95
+ color: "#FF7AC6",
96
+ maxSteps: 30,
97
+ description: "Primary orchestrator. Drives a task to completion by planning, dispatching subagents (one at a time by default), and verifying results. Iterates until the task is resolved or clearly blocked.",
98
+ prompt: buildResolverPrompt(DEFAULT_MAX_PARALLEL_SUBAGENTS),
99
+ permission: {
100
+ edit: "ask",
74
101
  bash: "ask",
75
102
  webfetch: "ask",
76
103
  },
@@ -166,6 +193,9 @@ async function loadResolveConfig(directory, opencodeConfig, options) {
166
193
  function applyResolveConfig(config, resolveConfig) {
167
194
  const enabled = new Set(resolveConfig.enabled ?? DEFAULT_ENABLED);
168
195
  const models = { ...DEFAULT_MODELS, ...resolveConfig.models };
196
+ const defaultModel = typeof config.model === "string" ? config.model : undefined;
197
+ const autoApprove = resolveConfig.autoApprove !== false;
198
+ const maxParallelSubagents = resolveConfig.maxParallelSubagents ?? DEFAULT_MAX_PARALLEL_SUBAGENTS;
169
199
  config.agent ??= {};
170
200
  for (const name of Object.keys(DEFAULT_AGENT_CONFIG)) {
171
201
  const override = resolveConfig.agents?.[name];
@@ -173,13 +203,21 @@ function applyResolveConfig(config, resolveConfig) {
173
203
  if (!isEnabled)
174
204
  continue;
175
205
  const base = DEFAULT_AGENT_CONFIG[name];
176
- const { enabled: _enabled, ...agentOverride } = override ?? {};
177
- const model = resolveModel(agentOverride.model ?? models[name], models);
178
- config.agent[name] = {
206
+ const { enabled: _enabled, model: requestedModel, permission: userPermission, ...agentOverride } = override ?? {};
207
+ const model = resolveModel(requestedModel ?? models[name] ?? defaultModel, models);
208
+ const permission = buildPermission(base.permission, userPermission, autoApprove);
209
+ const agentConfig = {
179
210
  ...base,
180
211
  ...agentOverride,
181
- model,
182
212
  };
213
+ if (name === "resolver" && agentOverride.prompt === undefined) {
214
+ agentConfig.prompt = buildResolverPrompt(maxParallelSubagents);
215
+ }
216
+ if (permission)
217
+ agentConfig.permission = permission;
218
+ if (model)
219
+ agentConfig.model = model;
220
+ config.agent[name] = agentConfig;
183
221
  }
184
222
  if (resolveConfig.context7 !== false) {
185
223
  config.mcp ??= {};
@@ -190,9 +228,15 @@ function applyResolveConfig(config, resolveConfig) {
190
228
  }
191
229
  if (resolveConfig.commands) {
192
230
  config.command ??= {};
231
+ config.command["resolve"] ??= {
232
+ template: "Drive this task to a verified resolution end-to-end. Plan, dispatch one coder at a time, verify, and iterate. $ARGUMENTS",
233
+ description: "Run the OpenCode Resolve resolver agent end-to-end",
234
+ agent: "resolver",
235
+ subtask: true,
236
+ };
193
237
  config.command["resolve-review"] ??= {
194
- template: "Review the current implementation against the user's requirements. Focus on correctness, tests, security, and maintainability.",
195
- description: "Run the OpenCode Resolve reviewer agent",
238
+ template: "Review the current implementation against the user's requirements. Focus on correctness, tests, security, and maintainability. Do not modify anything.",
239
+ description: "Run the OpenCode Resolve reviewer agent (read-only)",
196
240
  agent: "reviewer",
197
241
  subtask: true,
198
242
  };
@@ -207,11 +251,13 @@ function applyResolveConfig(config, resolveConfig) {
207
251
  function defaultResolveConfig() {
208
252
  return {
209
253
  enabled: DEFAULT_ENABLED,
210
- models: DEFAULT_MODELS,
254
+ models: {},
211
255
  agents: {},
212
256
  preserveNative: true,
213
257
  context7: true,
214
258
  commands: false,
259
+ autoApprove: true,
260
+ maxParallelSubagents: DEFAULT_MAX_PARALLEL_SUBAGENTS,
215
261
  };
216
262
  }
217
263
  function mergeResolveConfig(...configs) {
@@ -223,6 +269,8 @@ function mergeResolveConfig(...configs) {
223
269
  result.preserveNative = config.preserveNative ?? result.preserveNative;
224
270
  result.context7 = config.context7 ?? result.context7;
225
271
  result.commands = config.commands ?? result.commands;
272
+ result.autoApprove = config.autoApprove ?? result.autoApprove;
273
+ result.maxParallelSubagents = config.maxParallelSubagents ?? result.maxParallelSubagents;
226
274
  result.models = { ...result.models, ...config.models };
227
275
  result.agents = mergeAgents(result.agents, config.agents);
228
276
  }
@@ -240,6 +288,27 @@ function resolveModel(model, models) {
240
288
  return undefined;
241
289
  return models[model] ?? model;
242
290
  }
291
+ function buildPermission(basePermission, userPermission, autoApprove) {
292
+ const merged = {
293
+ ...(basePermission ?? {}),
294
+ ...(userPermission ?? {}),
295
+ };
296
+ if (Object.keys(merged).length === 0)
297
+ return undefined;
298
+ if (!autoApprove)
299
+ return merged;
300
+ const userKeys = new Set(Object.keys(userPermission ?? {}));
301
+ const result = { ...merged };
302
+ for (const key of Object.keys(result)) {
303
+ if (userKeys.has(key))
304
+ continue;
305
+ const value = result[key];
306
+ if (value === "ask") {
307
+ result[key] = "allow";
308
+ }
309
+ }
310
+ return result;
311
+ }
243
312
  function getPluginOptions(config) {
244
313
  for (const entry of config.plugin ?? []) {
245
314
  if (Array.isArray(entry) && isResolvePluginEntry(entry[0])) {
@@ -310,6 +379,15 @@ function normalizeResolveConfig(value, source) {
310
379
  result.context7 = expectBoolean(config.context7, `${source}.context7`);
311
380
  if (config.commands !== undefined)
312
381
  result.commands = expectBoolean(config.commands, `${source}.commands`);
382
+ if (config.autoApprove !== undefined)
383
+ result.autoApprove = expectBoolean(config.autoApprove, `${source}.autoApprove`);
384
+ if (config.maxParallelSubagents !== undefined) {
385
+ const limit = expectNumber(config.maxParallelSubagents, `${source}.maxParallelSubagents`);
386
+ if (!Number.isInteger(limit) || limit < 1) {
387
+ throw new Error(`${source}.maxParallelSubagents must be a positive integer`);
388
+ }
389
+ result.maxParallelSubagents = limit;
390
+ }
313
391
  if (config.config !== undefined)
314
392
  result.config = expectString(config.config, `${source}.config`);
315
393
  return result;
@@ -1,19 +1,21 @@
1
1
  {
2
- "enabled": ["coder", "reviewer"],
3
- "models": {
4
- "glm": "zai-coding-plan/glm-5",
5
- "gpt": "openai/gpt-5",
6
- "coder": "glm",
7
- "reviewer": "gpt",
8
- "architect": "gpt",
9
- "gpt-coder": "gpt",
10
- "debugger": "glm",
11
- "researcher": "glm"
12
- },
2
+ "enabled": ["coder", "reviewer", "resolver"],
13
3
  "preserveNative": true,
14
4
  "context7": true,
15
5
  "commands": false,
6
+ "autoApprove": true,
7
+ "maxParallelSubagents": 1,
8
+ "models": {},
16
9
  "agents": {
10
+ "coder": {
11
+ "enabled": true
12
+ },
13
+ "reviewer": {
14
+ "enabled": true
15
+ },
16
+ "resolver": {
17
+ "enabled": true
18
+ },
17
19
  "architect": {
18
20
  "enabled": false
19
21
  },
@@ -0,0 +1,164 @@
1
+ // opencode-resolve.reference.jsonc
2
+ //
3
+ // This file documents EVERY option opencode-resolve accepts. It is NOT loaded
4
+ // by the plugin (the plugin only reads .json). Copy what you need into
5
+ // `~/.config/opencode/resolve.json`, `.opencode/resolve.json`, or
6
+ // `opencode-resolve.json` (without the comments).
7
+ //
8
+ // Config file lookup order (first one found wins):
9
+ // 1. <project>/.opencode/resolve.json
10
+ // 2. <project>/opencode-resolve.json
11
+ // 3. ~/.config/opencode/resolve.json
12
+ // 4. ~/.config/opencode/opencode-resolve.json
13
+ //
14
+ // Inline plugin options inside opencode.json override the file config.
15
+ //
16
+ // Validation is strict: unknown keys, misspellings, invalid modes, invalid
17
+ // permission values, and wrong types throw an error at load time.
18
+
19
+ {
20
+ // -----------------------------------------------------------------------
21
+ // enabled (string[])
22
+ // Which resolve agents to inject. Default: ["coder", "reviewer", "resolver"].
23
+ // Native OpenCode agents (plan, build) are always preserved and never
24
+ // touched by this list. Per-agent `agents.<name>.enabled` overrides this.
25
+ // -----------------------------------------------------------------------
26
+ "enabled": ["coder", "reviewer", "resolver"],
27
+
28
+ // -----------------------------------------------------------------------
29
+ // preserveNative (boolean)
30
+ // Always true in practice — plan/build are never rewritten. Accepted for
31
+ // readability. Default: true.
32
+ // -----------------------------------------------------------------------
33
+ "preserveNative": true,
34
+
35
+ // -----------------------------------------------------------------------
36
+ // context7 (boolean)
37
+ // When true, registers the Context7 MCP server unless `mcp.context7` is
38
+ // already configured. Default: true.
39
+ // -----------------------------------------------------------------------
40
+ "context7": true,
41
+
42
+ // -----------------------------------------------------------------------
43
+ // commands (boolean)
44
+ // When true, adds three optional commands: `resolve`, `resolve-code`,
45
+ // `resolve-review`. Default: false.
46
+ // -----------------------------------------------------------------------
47
+ "commands": false,
48
+
49
+ // -----------------------------------------------------------------------
50
+ // autoApprove (boolean)
51
+ // When true, every default `"ask"` permission on enabled agents is flipped
52
+ // to `"allow"`. Never touches `"deny"`. Never overrides a permission key
53
+ // you set explicitly under `agents.<name>.permission`. Default: true.
54
+ //
55
+ // Set to false for the conservative ask-every-time behavior.
56
+ // -----------------------------------------------------------------------
57
+ "autoApprove": true,
58
+
59
+ // -----------------------------------------------------------------------
60
+ // maxParallelSubagents (positive integer)
61
+ // Maximum number of subagents the resolver may dispatch in parallel
62
+ // (across coder, reviewer, etc.). Default: 1 — strictly serial.
63
+ // Increase if you want resolver to fan out independent subtasks.
64
+ // -----------------------------------------------------------------------
65
+ "maxParallelSubagents": 1,
66
+
67
+ // -----------------------------------------------------------------------
68
+ // models (object)
69
+ // Alias map. Keys are either an agent name (coder, reviewer, resolver,
70
+ // architect, gpt-coder, debugger, researcher) OR one of the well-known
71
+ // aliases (`glm`, `gpt`). Values are model identifiers OR another alias.
72
+ //
73
+ // Resolution order for a given agent:
74
+ // 1. agents.<name>.model
75
+ // 2. models.<name> (resolved through the alias map)
76
+ // 3. top-level OpenCode `model`
77
+ // 4. OpenCode's own fallback
78
+ // -----------------------------------------------------------------------
79
+ "models": {
80
+ "glm": "zai-coding-plan/glm-5.1",
81
+ "gpt": "openai/gpt-5.5",
82
+ "coder": "glm",
83
+ "resolver": "gpt",
84
+ "reviewer": "gpt"
85
+ },
86
+
87
+ // -----------------------------------------------------------------------
88
+ // agents (object)
89
+ // Per-agent overrides. Each key MUST be a valid agent name. Each entry
90
+ // accepts: enabled, model, mode, description, prompt, color, maxSteps,
91
+ // tools, permission. Unknown keys throw.
92
+ //
93
+ // mode: "subagent" | "primary" | "all"
94
+ // permission.edit | bash | webfetch | doom_loop | external_directory:
95
+ // "ask" | "allow" | "deny"
96
+ // permission.bash may also be a per-command map:
97
+ // { "npm test": "allow", "rm -rf": "deny" }
98
+ // -----------------------------------------------------------------------
99
+ "agents": {
100
+ "coder": {
101
+ "enabled": true,
102
+ "model": "glm",
103
+ "mode": "subagent",
104
+ "maxSteps": 20,
105
+ "permission": {
106
+ "edit": "allow",
107
+ "bash": "allow",
108
+ "webfetch": "allow"
109
+ }
110
+ },
111
+
112
+ "reviewer": {
113
+ "enabled": true,
114
+ "model": "gpt",
115
+ "mode": "subagent",
116
+ "maxSteps": 8,
117
+ // Reviewer is read-only by design. edit and bash are denied even when
118
+ // autoApprove is true.
119
+ "permission": {
120
+ "edit": "deny",
121
+ "bash": "deny",
122
+ "webfetch": "allow"
123
+ }
124
+ },
125
+
126
+ "resolver": {
127
+ "enabled": true,
128
+ "model": "gpt",
129
+ "mode": "all",
130
+ "maxSteps": 30,
131
+ "permission": {
132
+ "edit": "allow",
133
+ "bash": "allow",
134
+ "webfetch": "allow"
135
+ }
136
+ },
137
+
138
+ // Optional agents — disabled by default. Flip enabled:true to use them.
139
+ "architect": {
140
+ "enabled": false,
141
+ "model": "gpt",
142
+ "mode": "subagent",
143
+ "maxSteps": 10
144
+ },
145
+ "gpt-coder": {
146
+ "enabled": false,
147
+ "model": "gpt",
148
+ "mode": "subagent",
149
+ "maxSteps": 20
150
+ },
151
+ "debugger": {
152
+ "enabled": false,
153
+ "model": "glm",
154
+ "mode": "subagent",
155
+ "maxSteps": 14
156
+ },
157
+ "researcher": {
158
+ "enabled": false,
159
+ "model": "glm",
160
+ "mode": "subagent",
161
+ "maxSteps": 8
162
+ }
163
+ }
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-resolve",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenCode plugin that adds a small coder/reviewer agent set while preserving native plan/build behavior.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/jshsakura/opencode-resolve#readme",
@@ -23,12 +23,14 @@
23
23
  "dist",
24
24
  "scripts",
25
25
  "opencode-resolve.example.json",
26
+ "opencode-resolve.reference.jsonc",
26
27
  "README.md",
27
28
  "LICENSE"
28
29
  ],
29
30
  "scripts": {
30
31
  "build": "tsc -p tsconfig.json",
31
32
  "install:local": "npm run build && node scripts/install-local.mjs",
33
+ "postinstall": "node scripts/postinstall.mjs",
32
34
  "prepack": "npm test",
33
35
  "test": "npm run build && node --test test/*.mjs",
34
36
  "typecheck": "tsc -p tsconfig.json --noEmit"
@@ -0,0 +1,158 @@
1
+ import { constants } from "node:fs"
2
+ import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import { homedir } from "node:os"
4
+ import { dirname, join, resolve } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+
7
+ const packageName = "opencode-resolve"
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
9
+ const configDir = process.env.OPENCODE_CONFIG_HOME || join(homedir(), ".config", "opencode")
10
+ const opencodeConfigPath = join(configDir, "opencode.json")
11
+ const resolveConfigPath = join(configDir, "resolve.json")
12
+ const exampleConfigPath = join(root, "opencode-resolve.example.json")
13
+
14
+ const ADDITIVE_DEFAULTS = {
15
+ autoApprove: true,
16
+ maxParallelSubagents: 1,
17
+ }
18
+
19
+ if (process.env.OPENCODE_RESOLVE_SKIP_POSTINSTALL === "1") {
20
+ process.exit(0)
21
+ }
22
+
23
+ try {
24
+ await registerPlugin()
25
+ } catch (error) {
26
+ console.warn(`[${packageName}] automatic OpenCode registration skipped: ${formatError(error)}`)
27
+ console.warn(`[${packageName}] add "${packageName}" to your OpenCode plugin list manually if needed.`)
28
+ }
29
+
30
+ async function registerPlugin() {
31
+ await mkdir(configDir, { recursive: true })
32
+
33
+ const config = await readOpenCodeConfig()
34
+ const changed = addPlugin(config)
35
+
36
+ if (changed) {
37
+ await writeFile(opencodeConfigPath, `${JSON.stringify(config, null, 2)}\n`)
38
+ console.log(`[${packageName}] registered in ${opencodeConfigPath}`)
39
+ } else {
40
+ console.log(`[${packageName}] already registered in ${opencodeConfigPath}`)
41
+ }
42
+
43
+ if (!(await exists(resolveConfigPath))) {
44
+ await assertReadable(exampleConfigPath)
45
+ await copyFile(exampleConfigPath, resolveConfigPath)
46
+ console.log(`[${packageName}] created ${resolveConfigPath}`)
47
+ return
48
+ }
49
+
50
+ await migrateResolveConfig()
51
+ }
52
+
53
+ async function migrateResolveConfig() {
54
+ let raw
55
+ try {
56
+ raw = await readFile(resolveConfigPath, "utf8")
57
+ } catch (error) {
58
+ console.warn(`[${packageName}] could not read ${resolveConfigPath} for migration: ${formatError(error)}`)
59
+ return
60
+ }
61
+
62
+ let parsed
63
+ try {
64
+ parsed = JSON.parse(raw)
65
+ } catch (error) {
66
+ console.warn(`[${packageName}] ${resolveConfigPath} is not valid JSON; skipping migration: ${formatError(error)}`)
67
+ return
68
+ }
69
+
70
+ if (!isObject(parsed)) {
71
+ console.warn(`[${packageName}] ${resolveConfigPath} must contain a JSON object; skipping migration`)
72
+ return
73
+ }
74
+
75
+ const updated = { ...parsed }
76
+ const added = []
77
+ for (const [key, value] of Object.entries(ADDITIVE_DEFAULTS)) {
78
+ if (updated[key] === undefined) {
79
+ updated[key] = value
80
+ added.push(`${key}=${JSON.stringify(value)}`)
81
+ }
82
+ }
83
+
84
+ if (added.length > 0) {
85
+ await writeFile(resolveConfigPath, `${JSON.stringify(updated, null, 2)}\n`)
86
+ console.log(`[${packageName}] migrated ${resolveConfigPath}: added ${added.join(", ")}`)
87
+ } else {
88
+ console.log(`[${packageName}] ${resolveConfigPath} already up to date`)
89
+ }
90
+
91
+ if (Array.isArray(updated.enabled) && !updated.enabled.includes("resolver")) {
92
+ console.log(
93
+ `[${packageName}] tip: add "resolver" to "enabled" in ${resolveConfigPath} to use the new orchestrator agent.`,
94
+ )
95
+ }
96
+ }
97
+
98
+ async function readOpenCodeConfig() {
99
+ if (!(await exists(opencodeConfigPath))) {
100
+ return {
101
+ $schema: "https://opencode.ai/config.json",
102
+ plugin: [],
103
+ }
104
+ }
105
+
106
+ const raw = await readFile(opencodeConfigPath, "utf8")
107
+ const parsed = JSON.parse(raw)
108
+ if (!isObject(parsed)) throw new Error(`${opencodeConfigPath} must contain a JSON object`)
109
+ return parsed
110
+ }
111
+
112
+ function addPlugin(config) {
113
+ config.plugin ??= []
114
+ if (!Array.isArray(config.plugin)) {
115
+ throw new Error(`${opencodeConfigPath}.plugin must be an array`)
116
+ }
117
+
118
+ if (config.plugin.some(isRegisteredPluginEntry)) return false
119
+ config.plugin.push(packageName)
120
+ return true
121
+ }
122
+
123
+ function isRegisteredPluginEntry(entry) {
124
+ if (typeof entry === "string") return isResolvePluginName(entry)
125
+ if (Array.isArray(entry) && typeof entry[0] === "string") return isResolvePluginName(entry[0])
126
+ return false
127
+ }
128
+
129
+ function isResolvePluginName(value) {
130
+ const name = value.split("/").pop() || value
131
+ return name === packageName || name.startsWith(`${packageName}@`)
132
+ }
133
+
134
+ async function assertReadable(path) {
135
+ await access(path, constants.R_OK)
136
+ }
137
+
138
+ async function exists(path) {
139
+ try {
140
+ await access(path)
141
+ return true
142
+ } catch (error) {
143
+ if (isMissingFileError(error)) return false
144
+ throw error
145
+ }
146
+ }
147
+
148
+ function isMissingFileError(error) {
149
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"
150
+ }
151
+
152
+ function isObject(value) {
153
+ return typeof value === "object" && value !== null && !Array.isArray(value)
154
+ }
155
+
156
+ function formatError(error) {
157
+ return error instanceof Error ? error.message : String(error)
158
+ }