opencode-resolve 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +98 -53
- package/README.md +98 -53
- package/dist/agents.d.ts +26 -0
- package/dist/agents.js +355 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +398 -0
- package/dist/hooks/index.d.ts +18 -0
- package/dist/hooks/index.js +493 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -702
- package/dist/state.d.ts +33 -0
- package/dist/state.js +20 -0
- package/dist/tools/index.d.ts +252 -0
- package/dist/tools/index.js +1209 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +371 -0
- package/opencode-resolve.example.json +5 -2
- package/opencode-resolve.reference.jsonc +78 -19
- package/package.json +6 -2
- package/scripts/postinstall.mjs +311 -31
package/dist/index.js
CHANGED
|
@@ -1,707 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const UPDATE_CHECK_FILE = join(homedir(), ".cache", "opencode-resolve", "update-check.json");
|
|
10
|
-
const PLUGIN_CACHE_DIR = join(homedir(), ".cache", "opencode", "packages", "opencode-resolve@latest");
|
|
11
|
-
function readPluginVersion() {
|
|
12
|
-
try {
|
|
13
|
-
const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
|
-
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
15
|
-
return typeof pkg?.version === "string" ? pkg.version : "unknown";
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return "unknown";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
console.log(`[opencode-resolve] v${PLUGIN_VERSION} loaded`);
|
|
22
|
-
const DEFAULT_MODELS = {};
|
|
23
|
-
const DEFAULT_ENABLED = ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner"];
|
|
24
|
-
const VALID_AGENT_NAMES = [
|
|
25
|
-
"coder",
|
|
26
|
-
"reviewer",
|
|
27
|
-
"resolver",
|
|
28
|
-
"architect",
|
|
29
|
-
"gpt-coder",
|
|
30
|
-
"debugger",
|
|
31
|
-
"researcher",
|
|
32
|
-
"explorer",
|
|
33
|
-
"deep-reviewer",
|
|
34
|
-
"planner",
|
|
35
|
-
];
|
|
36
|
-
const VALID_AGENT_NAME_SET = new Set(VALID_AGENT_NAMES);
|
|
37
|
-
const VALID_MODEL_ALIASES = [
|
|
38
|
-
...VALID_AGENT_NAMES,
|
|
39
|
-
"glm",
|
|
40
|
-
"gpt",
|
|
41
|
-
"quick",
|
|
42
|
-
"deep",
|
|
43
|
-
"fast",
|
|
44
|
-
"strong",
|
|
45
|
-
"mini",
|
|
46
|
-
"codex",
|
|
47
|
-
"bronze",
|
|
48
|
-
"silver",
|
|
49
|
-
"gold",
|
|
50
|
-
];
|
|
51
|
-
const VALID_MODEL_ALIAS_SET = new Set(VALID_MODEL_ALIASES);
|
|
52
|
-
const VALID_MODES = new Set(["subagent", "primary", "all"]);
|
|
53
|
-
const VALID_PERMISSION_VALUES = new Set(["ask", "allow", "deny"]);
|
|
54
|
-
const VALID_TOP_LEVEL_KEYS = new Set([
|
|
55
|
-
"enabled",
|
|
56
|
-
"models",
|
|
57
|
-
"agents",
|
|
58
|
-
"preserveNative",
|
|
59
|
-
"context7",
|
|
60
|
-
"commands",
|
|
61
|
-
"autoApprove",
|
|
62
|
-
"maxParallelSubagents",
|
|
63
|
-
"autoUpdate",
|
|
64
|
-
"config",
|
|
65
|
-
]);
|
|
66
|
-
const DEFAULT_MAX_PARALLEL_SUBAGENTS = 2;
|
|
67
|
-
const VALID_AGENT_KEYS = new Set([
|
|
68
|
-
"enabled",
|
|
69
|
-
"model",
|
|
70
|
-
"mode",
|
|
71
|
-
"description",
|
|
72
|
-
"prompt",
|
|
73
|
-
"color",
|
|
74
|
-
"maxSteps",
|
|
75
|
-
"tools",
|
|
76
|
-
"permission",
|
|
77
|
-
]);
|
|
78
|
-
function buildResolverPrompt(maxParallelSubagents) {
|
|
79
|
-
const explicitLimit = typeof maxParallelSubagents === "number" && Number.isFinite(maxParallelSubagents)
|
|
80
|
-
? Math.max(1, Math.trunc(maxParallelSubagents))
|
|
81
|
-
: undefined;
|
|
82
|
-
const parallelRule = explicitLimit === undefined
|
|
83
|
-
? "Parallel dispatch policy: fan out coder dispatches when the work is genuinely independent (different files, no shared state). Rate limits are per-model — coder/silver-tier models (e.g. GLM coding-plan) can throttle under heavy burst, so back off if you see rate-limit errors. Explorer runs on a lighter model and fans out freely when scope discovery genuinely needs it. Reviewer, deep-reviewer, and planner are dispatched as singletons by their nature."
|
|
84
|
-
: explicitLimit === 1
|
|
85
|
-
? "CRITICAL: User has pinned the coder concurrency cap to 1. Dispatch at most ONE coder concurrently. Wait for an in-flight coder to finish before dispatching another. Explorer is unrestricted (read-only, light model). Reviewer, deep-reviewer, and planner are singletons by nature."
|
|
86
|
-
: `CRITICAL: User has pinned the coder concurrency cap to ${explicitLimit}. Dispatch at most ${explicitLimit} coders concurrently. Wait for in-flight coders to finish before dispatching more. Explorer is unrestricted (read-only, light model). Reviewer, deep-reviewer, and planner are singletons by nature.`;
|
|
87
|
-
return [
|
|
88
|
-
"You are Resolver, the context-efficient orchestrator agent for OpenCode Resolve.",
|
|
89
|
-
"Your job is to drive the user's task to a verified resolution using minimal context and the fewest LLM calls possible.",
|
|
90
|
-
"",
|
|
91
|
-
"Core path: You and Coder form the fixed-role verified resolve loop — this is the default path.",
|
|
92
|
-
"Internal specialist subagents (explorer, reviewer, deep-reviewer, planner) are available by default as subagents, but they are NOT the default path.",
|
|
93
|
-
"Dispatch them only when justified — avoid context waste.",
|
|
94
|
-
"",
|
|
95
|
-
"Checkpointed execution: for large tasks, decompose work into small verified checkpoints. For each checkpoint, iterate up to 3 attempts on the same failing checkpoint. When a checkpoint passes verification, carry forward only: decisions, changed files, verification results, and blockers — then proceed to the next checkpoint. If blocked after max 3 attempts on one checkpoint, report the exact blocker with evidence. This preserves context and handles arbitrarily long tasks.",
|
|
96
|
-
"",
|
|
97
|
-
"Workflow (default fixed-role path):",
|
|
98
|
-
"1. CLASSIFY the work as quick (trivial fix), normal (standard feature), deep (complex refactor), or risky (security/architecture/high-impact).",
|
|
99
|
-
"2. INSPECT only relevant files — avoid broad exploration. Use local tools (read, grep, glob) to gather facts, not subagents.",
|
|
100
|
-
"3. For trivial/quick work: inspect relevant files directly and apply a small edit yourself. No subagent needed.",
|
|
101
|
-
"4. PLAN the smallest correct patch. Dispatch coder with exact file paths and focused behavior instructions.",
|
|
102
|
-
`5. ${parallelRule}`,
|
|
103
|
-
"6. VERIFY with the cheapest meaningful check first (targeted test, type check, or lint). Do not run full suites unless the change is wide.",
|
|
104
|
-
"7. If issues remain, RETRY from verification logs: dispatch coder again with a focused fix. Max 3 attempts for the same failing checkpoint; then move forward or report the blocker.",
|
|
105
|
-
"8. REPORT a concise evidence summary: what changed, verification results, and any remaining blockers.",
|
|
106
|
-
"",
|
|
107
|
-
"Internal specialist subagents (available by default, but NOT the default path — use only when justified):",
|
|
108
|
-
"- explorer: fast read-only codebase scout. Prefer local read/grep/glob for narrow scope; dispatch explorer only when scope is genuinely unknown and local tools are insufficient.",
|
|
109
|
-
"- reviewer: lightweight read-only audit. Dispatch only for post-change verification gaps on non-trivial changes.",
|
|
110
|
-
"- deep-reviewer: thorough read-only review. Dispatch ONLY for risky, security-sensitive, architectural, or high-impact changes.",
|
|
111
|
-
"- planner: advanced read-only planner. Dispatch ONLY when the user explicitly asks for a plan, decomposition, or implementation strategy. Do NOT call planner for routine sub-task planning you can absorb inline.",
|
|
112
|
-
"",
|
|
113
|
-
"Note: this parallel rule is enforced via prompt only — there is no runtime cap on subagent dispatches. Honor it strictly to avoid file conflicts and wasted context.",
|
|
114
|
-
].join("\n");
|
|
115
|
-
}
|
|
116
|
-
const DEFAULT_AGENT_CONFIG = {
|
|
117
|
-
coder: {
|
|
118
|
-
mode: "subagent",
|
|
119
|
-
color: "#7CFC00",
|
|
120
|
-
maxSteps: 20,
|
|
121
|
-
description: "Use for focused implementation, file edits, test runs, and fixing issues until the task is resolved.",
|
|
122
|
-
prompt: [
|
|
123
|
-
"You are Coder, a focused implementation subagent for OpenCode Resolve.",
|
|
124
|
-
"You are one of two default agents. Together with Resolver you form a verified resolve loop.",
|
|
125
|
-
"",
|
|
126
|
-
"Context budget: read ONLY the files you need. Avoid broad exploration or discovering the entire codebase.",
|
|
127
|
-
"Preserve native OpenCode behavior and make the smallest correct change.",
|
|
128
|
-
"Before editing, inspect only the relevant files and existing patterns in those files.",
|
|
129
|
-
"Implement the smallest patch that satisfies the requirement.",
|
|
130
|
-
"Run targeted verification when practical (single test file, type check, or lint — not full suites).",
|
|
131
|
-
"Return a concise summary: changed files list + command results. No unnecessary prose.",
|
|
132
|
-
"",
|
|
133
|
-
"Scope-discovery gate: dispatch the `explorer` subagent ONLY when the scope is genuinely unclear — i.e. you need to locate 3+ unknown files, OR a named pattern's location is genuinely unknown. For known files, single-file uncertainty, or routine doubt, read directly with the local read/grep/glob tools. Never dispatch explorer for casual exploration — token efficiency is the discipline.",
|
|
134
|
-
].join("\n"),
|
|
135
|
-
permission: {
|
|
136
|
-
edit: "ask",
|
|
137
|
-
bash: "ask",
|
|
138
|
-
webfetch: "ask",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
reviewer: {
|
|
142
|
-
mode: "subagent",
|
|
143
|
-
color: "#8A7CFF",
|
|
144
|
-
maxSteps: 8,
|
|
145
|
-
description: "Internal read-only verification-gap auditor. Enabled as subagent by default but not part of the core resolver→coder path. Resolver dispatches only when it judges a verification gap exists on non-trivial changes.",
|
|
146
|
-
prompt: [
|
|
147
|
-
"You are Reviewer, a strictly read-only internal review subagent for OpenCode Resolve.",
|
|
148
|
-
"You are NOT part of the core path (resolver→coder). You are injected as an internal subagent so the resolver can dispatch you when it judges a verification gap exists on non-trivial changes.",
|
|
149
|
-
"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.",
|
|
150
|
-
"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.",
|
|
151
|
-
"Prioritize concrete bugs, behavioral regressions, security risks, missing tests, and maintainability issues.",
|
|
152
|
-
"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.",
|
|
153
|
-
"If a fix is needed, describe it precisely and recommend dispatching the coder or resolver agent. Never apply fixes yourself.",
|
|
154
|
-
].join("\n"),
|
|
155
|
-
permission: {
|
|
156
|
-
edit: "deny",
|
|
157
|
-
bash: "deny",
|
|
158
|
-
webfetch: "ask",
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
resolver: {
|
|
162
|
-
mode: "all",
|
|
163
|
-
color: "#FF7AC6",
|
|
164
|
-
maxSteps: 30,
|
|
165
|
-
description: "Primary orchestrator in the fixed-role verified loop (resolver→coder). Decomposes work into verified checkpoints, dispatches coder, verifies each, and carries forward progress. Internal subagents (explorer, reviewer, deep-reviewer) are available by default but dispatched only when justified.",
|
|
166
|
-
prompt: buildResolverPrompt(undefined),
|
|
167
|
-
permission: {
|
|
168
|
-
edit: "ask",
|
|
169
|
-
bash: "ask",
|
|
170
|
-
webfetch: "ask",
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
architect: {
|
|
174
|
-
mode: "subagent",
|
|
175
|
-
color: "#00BFFF",
|
|
176
|
-
maxSteps: 10,
|
|
177
|
-
description: "Use for complex design, decomposition, and implementation instructions before coding.",
|
|
178
|
-
prompt: [
|
|
179
|
-
"You are Architect, a design and task decomposition subagent for OpenCode Resolve.",
|
|
180
|
-
"Clarify constraints, map affected areas, and propose the simplest viable implementation path.",
|
|
181
|
-
"Prefer native OpenCode plan/build behavior; provide actionable guidance to the parent agent instead of heavy orchestration.",
|
|
182
|
-
].join("\n"),
|
|
183
|
-
permission: {
|
|
184
|
-
edit: "deny",
|
|
185
|
-
bash: "ask",
|
|
186
|
-
webfetch: "ask",
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
"gpt-coder": {
|
|
190
|
-
mode: "subagent",
|
|
191
|
-
color: "#FFB347",
|
|
192
|
-
maxSteps: 20,
|
|
193
|
-
description: "Use for difficult implementation work that needs stronger reasoning than the default coder.",
|
|
194
|
-
prompt: [
|
|
195
|
-
"You are GPT Coder, a high-reasoning implementation subagent for difficult tasks.",
|
|
196
|
-
"Use the same small-change discipline as Coder, but take extra care with design, edge cases, and verification.",
|
|
197
|
-
"Inspect before editing, implement directly, verify when practical, and report exactly what changed.",
|
|
198
|
-
].join("\n"),
|
|
199
|
-
permission: {
|
|
200
|
-
edit: "ask",
|
|
201
|
-
bash: "ask",
|
|
202
|
-
webfetch: "ask",
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
debugger: {
|
|
206
|
-
mode: "subagent",
|
|
207
|
-
color: "#FF5F57",
|
|
208
|
-
maxSteps: 14,
|
|
209
|
-
description: "Use for reproducing failures, reading logs, isolating root causes, and proposing the smallest fix.",
|
|
210
|
-
prompt: [
|
|
211
|
-
"You are Debugger, a root-cause analysis subagent for OpenCode Resolve.",
|
|
212
|
-
"Reproduce when feasible, inspect logs and stack traces, isolate the most likely cause, and recommend or apply the smallest safe fix when asked.",
|
|
213
|
-
"Separate confirmed facts from hypotheses.",
|
|
214
|
-
].join("\n"),
|
|
215
|
-
permission: {
|
|
216
|
-
edit: "ask",
|
|
217
|
-
bash: "ask",
|
|
218
|
-
webfetch: "ask",
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
researcher: {
|
|
222
|
-
mode: "subagent",
|
|
223
|
-
color: "#33C7A3",
|
|
224
|
-
maxSteps: 8,
|
|
225
|
-
description: "Use for codebase exploration and documentation-backed research before implementation.",
|
|
226
|
-
prompt: [
|
|
227
|
-
"You are Researcher, a codebase and documentation research subagent for OpenCode Resolve.",
|
|
228
|
-
"Search the repository first, then use documentation tools such as Context7 or web fetch only when needed.",
|
|
229
|
-
"Return concise findings with paths, APIs, and constraints that matter for implementation.",
|
|
230
|
-
].join("\n"),
|
|
231
|
-
permission: {
|
|
232
|
-
edit: "deny",
|
|
233
|
-
bash: "ask",
|
|
234
|
-
webfetch: "ask",
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
explorer: {
|
|
238
|
-
mode: "subagent",
|
|
239
|
-
color: "#33CCFF",
|
|
240
|
-
maxSteps: 6,
|
|
241
|
-
description: "Internal pre-change fast scout for codebase/file/pattern/doc discovery. Enabled as subagent by default but not part of the core path. Read-only; quick model.",
|
|
242
|
-
prompt: [
|
|
243
|
-
"You are Explorer, a fast codebase scout subagent for OpenCode Resolve.",
|
|
244
|
-
"Your job is to quickly discover files, patterns, APIs, and relevant code locations before implementation begins.",
|
|
245
|
-
"You MUST NOT modify the project by any means: no file edits, no writes, no shell commands that change state.",
|
|
246
|
-
"Use read-only tools (read, grep, glob, list) and documentation tools (web fetch, Context7) to find what matters.",
|
|
247
|
-
"Return concise findings with file paths, relevant code snippets, APIs, and constraints.",
|
|
248
|
-
"Be fast and targeted — the resolver needs your discoveries to plan efficiently.",
|
|
249
|
-
].join("\n"),
|
|
250
|
-
permission: {
|
|
251
|
-
edit: "deny",
|
|
252
|
-
bash: "ask",
|
|
253
|
-
webfetch: "ask",
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
"deep-reviewer": {
|
|
257
|
-
mode: "subagent",
|
|
258
|
-
color: "#6A0DAD",
|
|
259
|
-
maxSteps: 12,
|
|
260
|
-
description: "Internal post-change strong read-only review for risky/security/architecture/high-impact changes. Enabled as subagent by default but not part of the core path. Read-only; deep model.",
|
|
261
|
-
prompt: [
|
|
262
|
-
"You are Deep Reviewer, a thorough read-only review subagent for risky, security-sensitive, or high-impact changes.",
|
|
263
|
-
"You MUST NOT modify the project by any means: no file edits, no writes, no shell commands that change state, no git commits.",
|
|
264
|
-
"Use read-only tools to deeply inspect the work against requirements, security best practices, architectural soundness, and behavioral correctness.",
|
|
265
|
-
"Focus on security vulnerabilities, data integrity risks, breaking API changes, performance regressions, and architectural drift.",
|
|
266
|
-
"Return findings ordered by severity with file and line references. For each finding, explain the risk and recommend a concrete fix.",
|
|
267
|
-
"If a fix is needed, describe it precisely and recommend dispatching the coder or resolver agent. Never apply fixes yourself.",
|
|
268
|
-
].join("\n"),
|
|
269
|
-
permission: {
|
|
270
|
-
edit: "deny",
|
|
271
|
-
bash: "deny",
|
|
272
|
-
webfetch: "ask",
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
planner: {
|
|
276
|
-
mode: "subagent",
|
|
277
|
-
color: "#F4A300",
|
|
278
|
-
maxSteps: 10,
|
|
279
|
-
description: "Internal advanced planner dispatched by the resolver when the user explicitly asks for a plan, decomposition, or implementation strategy. Read-only. Returns a concrete plan; never edits code.",
|
|
280
|
-
prompt: [
|
|
281
|
-
"You are Planner, the advanced planning subagent for OpenCode Resolve.",
|
|
282
|
-
"You are dispatched by the resolver only when the user explicitly asks for a plan, decomposition, or implementation strategy — not for routine sub-task planning the resolver handles inline.",
|
|
283
|
-
"You MUST NOT modify the project: no file edits, no writes, no shell commands that change state.",
|
|
284
|
-
"Inspect the relevant code with read-only tools (read, grep, glob, list) before proposing.",
|
|
285
|
-
"Return: clear phasing, file-level boundaries per phase, verification checkpoints, risks, and explicit trade-offs. Be concrete — name files, name decisions, name the cost of each option.",
|
|
286
|
-
"Be token-efficient: produce the smallest plan that fully covers the user's intent. No filler, no boilerplate, no restating the request.",
|
|
287
|
-
].join("\n"),
|
|
288
|
-
permission: {
|
|
289
|
-
edit: "deny",
|
|
290
|
-
bash: "deny",
|
|
291
|
-
webfetch: "ask",
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
};
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./agents.js";
|
|
3
|
+
export * from "./utils.js";
|
|
4
|
+
export * from "./config.js";
|
|
5
|
+
export * from "./state.js";
|
|
6
|
+
import { getTools } from "./tools/index.js";
|
|
7
|
+
import { getHooks } from "./hooks/index.js";
|
|
8
|
+
import { createSessionState } from "./state.js";
|
|
295
9
|
export const OpencodeResolve = async ({ directory }, options) => {
|
|
10
|
+
const sessionState = createSessionState();
|
|
296
11
|
return {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
applyResolveConfig(config, resolveConfig);
|
|
300
|
-
if (resolveConfig.autoUpdate !== false && process.env.OPENCODE_RESOLVE_NO_AUTO_UPDATE !== "1") {
|
|
301
|
-
maybeAutoUpdate().catch(() => { });
|
|
302
|
-
}
|
|
303
|
-
},
|
|
12
|
+
...getHooks(directory, options, sessionState),
|
|
13
|
+
tool: getTools(sessionState)
|
|
304
14
|
};
|
|
305
15
|
};
|
|
306
|
-
async function maybeAutoUpdate() {
|
|
307
|
-
try {
|
|
308
|
-
const previous = readUpdateCheckCache();
|
|
309
|
-
if (previous && Date.now() - previous.checkedAt < UPDATE_CHECK_INTERVAL_MS) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
// ignore corrupt cache and re-check
|
|
315
|
-
}
|
|
316
|
-
let latest;
|
|
317
|
-
try {
|
|
318
|
-
const response = await fetch("https://registry.npmjs.org/opencode-resolve/latest", {
|
|
319
|
-
headers: { Accept: "application/json" },
|
|
320
|
-
signal: AbortSignal.timeout(5000),
|
|
321
|
-
});
|
|
322
|
-
if (!response.ok)
|
|
323
|
-
return;
|
|
324
|
-
const data = (await response.json());
|
|
325
|
-
if (typeof data?.version !== "string")
|
|
326
|
-
return;
|
|
327
|
-
latest = data.version;
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
try {
|
|
333
|
-
mkdirSync(dirname(UPDATE_CHECK_FILE), { recursive: true });
|
|
334
|
-
writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
// best-effort; don't block on cache write failure
|
|
338
|
-
}
|
|
339
|
-
if (!isNewerVersion(latest, PLUGIN_VERSION))
|
|
340
|
-
return;
|
|
341
|
-
console.log(`[opencode-resolve] new version v${latest} available (current: v${PLUGIN_VERSION}) — refreshing cache in background. Restart OpenCode to activate (current session stays on v${PLUGIN_VERSION}).`);
|
|
342
|
-
try {
|
|
343
|
-
spawn("sh", ["-c", `rm -rf "${PLUGIN_CACHE_DIR}" && opencode plugin opencode-resolve --global --force`], { detached: true, stdio: "ignore" }).unref();
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
// If spawn fails, the user already saw the notice and can run the command manually.
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
function readUpdateCheckCache() {
|
|
350
|
-
try {
|
|
351
|
-
const raw = readFileSync(UPDATE_CHECK_FILE, "utf8");
|
|
352
|
-
const parsed = JSON.parse(raw);
|
|
353
|
-
if (typeof parsed?.checkedAt === "number" &&
|
|
354
|
-
typeof parsed?.latest === "string") {
|
|
355
|
-
return { checkedAt: parsed.checkedAt, latest: parsed.latest };
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
// file missing or unparseable
|
|
360
|
-
}
|
|
361
|
-
return undefined;
|
|
362
|
-
}
|
|
363
|
-
function isNewerVersion(candidate, baseline) {
|
|
364
|
-
const a = candidate.split(".").map((n) => Number.parseInt(n, 10));
|
|
365
|
-
const b = baseline.split(".").map((n) => Number.parseInt(n, 10));
|
|
366
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
367
|
-
const av = Number.isFinite(a[i]) ? a[i] : 0;
|
|
368
|
-
const bv = Number.isFinite(b[i]) ? b[i] : 0;
|
|
369
|
-
if (av > bv)
|
|
370
|
-
return true;
|
|
371
|
-
if (av < bv)
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
16
|
export default OpencodeResolve;
|
|
377
|
-
async function loadResolveConfig(directory, opencodeConfig, options) {
|
|
378
|
-
const pluginOptions = normalizeResolveConfig(options ?? getPluginOptions(opencodeConfig), "plugin options");
|
|
379
|
-
const configuredPath = typeof pluginOptions.config === "string" ? pluginOptions.config : undefined;
|
|
380
|
-
const configPaths = configuredPath
|
|
381
|
-
? [resolvePath(configuredPath, directory)]
|
|
382
|
-
: [
|
|
383
|
-
join(directory, ".opencode", "resolve.json"),
|
|
384
|
-
join(directory, "opencode-resolve.json"),
|
|
385
|
-
join(homedir(), ".config", "opencode", "resolve.json"),
|
|
386
|
-
join(homedir(), ".config", "opencode", "opencode-resolve.json"),
|
|
387
|
-
];
|
|
388
|
-
const fileConfig = await readFirstJson(configPaths);
|
|
389
|
-
return mergeResolveConfig(defaultResolveConfig(), fileConfig, pluginOptions);
|
|
390
|
-
}
|
|
391
|
-
function applyResolveConfig(config, resolveConfig) {
|
|
392
|
-
const enabled = new Set(resolveConfig.enabled ?? DEFAULT_ENABLED);
|
|
393
|
-
const models = { ...DEFAULT_MODELS, ...resolveConfig.models };
|
|
394
|
-
const defaultModel = typeof config.model === "string" ? config.model : undefined;
|
|
395
|
-
const autoApprove = resolveConfig.autoApprove !== false;
|
|
396
|
-
const maxParallelSubagents = resolveConfig.maxParallelSubagents;
|
|
397
|
-
config.agent ??= {};
|
|
398
|
-
for (const name of Object.keys(DEFAULT_AGENT_CONFIG)) {
|
|
399
|
-
const override = resolveConfig.agents?.[name];
|
|
400
|
-
const isEnabled = override?.enabled ?? enabled.has(name);
|
|
401
|
-
if (!isEnabled)
|
|
402
|
-
continue;
|
|
403
|
-
const base = DEFAULT_AGENT_CONFIG[name];
|
|
404
|
-
const { enabled: _enabled, model: requestedModel, permission: userPermission, ...agentOverride } = override ?? {};
|
|
405
|
-
const model = resolveModel(requestedModel ?? models[name] ?? defaultModel, models);
|
|
406
|
-
const permission = buildPermission(base.permission, userPermission, autoApprove);
|
|
407
|
-
const agentConfig = {
|
|
408
|
-
...base,
|
|
409
|
-
...agentOverride,
|
|
410
|
-
};
|
|
411
|
-
if (name === "resolver" && agentOverride.prompt === undefined) {
|
|
412
|
-
agentConfig.prompt = buildResolverPrompt(maxParallelSubagents);
|
|
413
|
-
}
|
|
414
|
-
if (permission)
|
|
415
|
-
agentConfig.permission = permission;
|
|
416
|
-
if (model)
|
|
417
|
-
agentConfig.model = model;
|
|
418
|
-
config.agent[name] = agentConfig;
|
|
419
|
-
}
|
|
420
|
-
if (resolveConfig.context7 !== false) {
|
|
421
|
-
config.mcp ??= {};
|
|
422
|
-
config.mcp.context7 ??= {
|
|
423
|
-
type: "remote",
|
|
424
|
-
url: "https://mcp.context7.com/mcp",
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
if (resolveConfig.commands) {
|
|
428
|
-
config.command ??= {};
|
|
429
|
-
config.command["resolve"] ??= {
|
|
430
|
-
template: "Drive this task to a verified resolution end-to-end. Classify, explore when needed, dispatch focused subagents within the configured per-role limit, verify, and iterate. $ARGUMENTS",
|
|
431
|
-
description: "Run the OpenCode Resolve resolver agent end-to-end",
|
|
432
|
-
agent: "resolver",
|
|
433
|
-
subtask: true,
|
|
434
|
-
};
|
|
435
|
-
config.command["resolve-review"] ??= {
|
|
436
|
-
template: "Review the current implementation against the user's requirements. Focus on correctness, tests, security, and maintainability. Do not modify anything.",
|
|
437
|
-
description: "Run the OpenCode Resolve reviewer agent (read-only)",
|
|
438
|
-
agent: "reviewer",
|
|
439
|
-
subtask: true,
|
|
440
|
-
};
|
|
441
|
-
config.command["resolve-code"] ??= {
|
|
442
|
-
template: "Implement the requested change with the smallest correct patch, then verify it when practical. $ARGUMENTS",
|
|
443
|
-
description: "Run the OpenCode Resolve coder agent",
|
|
444
|
-
agent: "coder",
|
|
445
|
-
subtask: true,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
function defaultResolveConfig() {
|
|
450
|
-
return {
|
|
451
|
-
enabled: DEFAULT_ENABLED,
|
|
452
|
-
models: {},
|
|
453
|
-
agents: {},
|
|
454
|
-
preserveNative: true,
|
|
455
|
-
context7: true,
|
|
456
|
-
commands: false,
|
|
457
|
-
autoApprove: true,
|
|
458
|
-
autoUpdate: true,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
function mergeResolveConfig(...configs) {
|
|
462
|
-
const result = {};
|
|
463
|
-
for (const config of configs) {
|
|
464
|
-
if (!config)
|
|
465
|
-
continue;
|
|
466
|
-
result.enabled = config.enabled ?? result.enabled;
|
|
467
|
-
result.preserveNative = config.preserveNative ?? result.preserveNative;
|
|
468
|
-
result.context7 = config.context7 ?? result.context7;
|
|
469
|
-
result.commands = config.commands ?? result.commands;
|
|
470
|
-
result.autoApprove = config.autoApprove ?? result.autoApprove;
|
|
471
|
-
result.maxParallelSubagents = config.maxParallelSubagents ?? result.maxParallelSubagents;
|
|
472
|
-
result.autoUpdate = config.autoUpdate ?? result.autoUpdate;
|
|
473
|
-
result.models = { ...result.models, ...config.models };
|
|
474
|
-
result.agents = mergeAgents(result.agents, config.agents);
|
|
475
|
-
}
|
|
476
|
-
return result;
|
|
477
|
-
}
|
|
478
|
-
function mergeAgents(left, right) {
|
|
479
|
-
const result = { ...left };
|
|
480
|
-
for (const name of Object.keys(right ?? {})) {
|
|
481
|
-
result[name] = { ...result[name], ...right?.[name] };
|
|
482
|
-
}
|
|
483
|
-
return result;
|
|
484
|
-
}
|
|
485
|
-
function resolveModel(model, models) {
|
|
486
|
-
if (!model)
|
|
487
|
-
return undefined;
|
|
488
|
-
return models[model] ?? model;
|
|
489
|
-
}
|
|
490
|
-
function buildPermission(basePermission, userPermission, autoApprove) {
|
|
491
|
-
const merged = {
|
|
492
|
-
...(basePermission ?? {}),
|
|
493
|
-
...(userPermission ?? {}),
|
|
494
|
-
};
|
|
495
|
-
if (Object.keys(merged).length === 0)
|
|
496
|
-
return undefined;
|
|
497
|
-
if (!autoApprove)
|
|
498
|
-
return merged;
|
|
499
|
-
const userKeys = new Set(Object.keys(userPermission ?? {}));
|
|
500
|
-
const result = { ...merged };
|
|
501
|
-
for (const key of Object.keys(result)) {
|
|
502
|
-
if (userKeys.has(key))
|
|
503
|
-
continue;
|
|
504
|
-
const value = result[key];
|
|
505
|
-
if (value === "ask") {
|
|
506
|
-
result[key] = "allow";
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
function getPluginOptions(config) {
|
|
512
|
-
for (const entry of config.plugin ?? []) {
|
|
513
|
-
if (Array.isArray(entry) && isResolvePluginEntry(entry[0])) {
|
|
514
|
-
return entry[1] ?? {};
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return {};
|
|
518
|
-
}
|
|
519
|
-
function isResolvePluginEntry(entry) {
|
|
520
|
-
const name = basename(entry);
|
|
521
|
-
return name === "opencode-resolve" || name.startsWith("opencode-resolve@");
|
|
522
|
-
}
|
|
523
|
-
async function readFirstJson(paths) {
|
|
524
|
-
for (const path of paths) {
|
|
525
|
-
try {
|
|
526
|
-
await access(path);
|
|
527
|
-
return normalizeResolveConfig(JSON.parse(await readFile(path, "utf8")), path);
|
|
528
|
-
}
|
|
529
|
-
catch (error) {
|
|
530
|
-
if (isMissingFileError(error))
|
|
531
|
-
continue;
|
|
532
|
-
throw new Error(`Failed to read OpenCode Resolve config at ${path}: ${formatError(error)}`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return undefined;
|
|
536
|
-
}
|
|
537
|
-
function resolvePath(path, directory) {
|
|
538
|
-
if (path.startsWith("~/"))
|
|
539
|
-
return join(homedir(), path.slice(2));
|
|
540
|
-
if (isAbsolute(path))
|
|
541
|
-
return path;
|
|
542
|
-
return resolve(directory, path);
|
|
543
|
-
}
|
|
544
|
-
function normalizeResolveConfig(value, source) {
|
|
545
|
-
if (value === undefined)
|
|
546
|
-
return {};
|
|
547
|
-
const config = expectObject(value, source);
|
|
548
|
-
for (const key of Object.keys(config)) {
|
|
549
|
-
if (!VALID_TOP_LEVEL_KEYS.has(key)) {
|
|
550
|
-
throw new Error(`Unknown top-level key "${key}" in ${source}`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
const result = {};
|
|
554
|
-
if (config.enabled !== undefined) {
|
|
555
|
-
result.enabled = expectStringArray(config.enabled, `${source}.enabled`).map((name) => expectAgentName(name, `${source}.enabled`));
|
|
556
|
-
}
|
|
557
|
-
if (config.models !== undefined) {
|
|
558
|
-
const models = expectObject(config.models, `${source}.models`);
|
|
559
|
-
result.models = {};
|
|
560
|
-
for (const [key, model] of Object.entries(models)) {
|
|
561
|
-
if (!VALID_MODEL_ALIAS_SET.has(key)) {
|
|
562
|
-
throw new Error(`Unknown model alias "${key}" in ${source}.models`);
|
|
563
|
-
}
|
|
564
|
-
result.models[key] = expectString(model, `${source}.models.${key}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (config.agents !== undefined) {
|
|
568
|
-
const agents = expectObject(config.agents, `${source}.agents`);
|
|
569
|
-
result.agents = {};
|
|
570
|
-
for (const [name, agentConfig] of Object.entries(agents)) {
|
|
571
|
-
const agentName = expectAgentName(name, `${source}.agents`);
|
|
572
|
-
result.agents[agentName] = normalizeAgentConfig(agentConfig, `${source}.agents.${name}`);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (config.preserveNative !== undefined)
|
|
576
|
-
result.preserveNative = expectBoolean(config.preserveNative, `${source}.preserveNative`);
|
|
577
|
-
if (config.context7 !== undefined)
|
|
578
|
-
result.context7 = expectBoolean(config.context7, `${source}.context7`);
|
|
579
|
-
if (config.commands !== undefined)
|
|
580
|
-
result.commands = expectBoolean(config.commands, `${source}.commands`);
|
|
581
|
-
if (config.autoApprove !== undefined)
|
|
582
|
-
result.autoApprove = expectBoolean(config.autoApprove, `${source}.autoApprove`);
|
|
583
|
-
if (config.autoUpdate !== undefined)
|
|
584
|
-
result.autoUpdate = expectBoolean(config.autoUpdate, `${source}.autoUpdate`);
|
|
585
|
-
if (config.maxParallelSubagents !== undefined) {
|
|
586
|
-
const limit = expectNumber(config.maxParallelSubagents, `${source}.maxParallelSubagents`);
|
|
587
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
588
|
-
throw new Error(`${source}.maxParallelSubagents must be a positive integer`);
|
|
589
|
-
}
|
|
590
|
-
result.maxParallelSubagents = limit;
|
|
591
|
-
}
|
|
592
|
-
if (config.config !== undefined)
|
|
593
|
-
result.config = expectString(config.config, `${source}.config`);
|
|
594
|
-
return result;
|
|
595
|
-
}
|
|
596
|
-
function normalizeAgentConfig(value, source) {
|
|
597
|
-
const config = expectObject(value, source);
|
|
598
|
-
for (const key of Object.keys(config)) {
|
|
599
|
-
if (!VALID_AGENT_KEYS.has(key)) {
|
|
600
|
-
throw new Error(`Unknown agent key "${key}" in ${source}`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
const result = {};
|
|
604
|
-
if (config.enabled !== undefined)
|
|
605
|
-
result.enabled = expectBoolean(config.enabled, `${source}.enabled`);
|
|
606
|
-
if (config.model !== undefined)
|
|
607
|
-
result.model = expectString(config.model, `${source}.model`);
|
|
608
|
-
if (config.mode !== undefined) {
|
|
609
|
-
const mode = expectString(config.mode, `${source}.mode`);
|
|
610
|
-
if (!VALID_MODES.has(mode))
|
|
611
|
-
throw new Error(`Invalid mode "${mode}" in ${source}.mode`);
|
|
612
|
-
result.mode = mode;
|
|
613
|
-
}
|
|
614
|
-
if (config.description !== undefined)
|
|
615
|
-
result.description = expectString(config.description, `${source}.description`);
|
|
616
|
-
if (config.prompt !== undefined)
|
|
617
|
-
result.prompt = expectString(config.prompt, `${source}.prompt`);
|
|
618
|
-
if (config.color !== undefined)
|
|
619
|
-
result.color = expectString(config.color, `${source}.color`);
|
|
620
|
-
if (config.maxSteps !== undefined) {
|
|
621
|
-
const maxSteps = expectNumber(config.maxSteps, `${source}.maxSteps`);
|
|
622
|
-
if (!Number.isInteger(maxSteps) || maxSteps < 1)
|
|
623
|
-
throw new Error(`${source}.maxSteps must be a positive integer`);
|
|
624
|
-
result.maxSteps = maxSteps;
|
|
625
|
-
}
|
|
626
|
-
if (config.tools !== undefined)
|
|
627
|
-
result.tools = normalizeTools(config.tools, `${source}.tools`);
|
|
628
|
-
if (config.permission !== undefined)
|
|
629
|
-
result.permission = normalizePermission(config.permission, `${source}.permission`);
|
|
630
|
-
return result;
|
|
631
|
-
}
|
|
632
|
-
function normalizeTools(value, source) {
|
|
633
|
-
const tools = expectObject(value, source);
|
|
634
|
-
const result = {};
|
|
635
|
-
for (const [key, enabled] of Object.entries(tools)) {
|
|
636
|
-
result[key] = expectBoolean(enabled, `${source}.${key}`);
|
|
637
|
-
}
|
|
638
|
-
return result;
|
|
639
|
-
}
|
|
640
|
-
function normalizePermission(value, source) {
|
|
641
|
-
const permission = expectObject(value, source);
|
|
642
|
-
const result = {};
|
|
643
|
-
for (const [key, entry] of Object.entries(permission)) {
|
|
644
|
-
if (key === "bash" && isObject(entry)) {
|
|
645
|
-
result.bash = {};
|
|
646
|
-
for (const [command, commandPermission] of Object.entries(entry)) {
|
|
647
|
-
result.bash[command] = expectPermissionValue(commandPermission, `${source}.bash.${command}`);
|
|
648
|
-
}
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
const permissionValue = expectPermissionValue(entry, `${source}.${key}`);
|
|
652
|
-
if (key === "edit" || key === "bash" || key === "webfetch" || key === "doom_loop" || key === "external_directory") {
|
|
653
|
-
result[key] = permissionValue;
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
throw new Error(`Unknown permission key "${key}" in ${source}`);
|
|
657
|
-
}
|
|
658
|
-
return result;
|
|
659
|
-
}
|
|
660
|
-
function expectAgentName(value, source) {
|
|
661
|
-
if (!VALID_AGENT_NAME_SET.has(value)) {
|
|
662
|
-
throw new Error(`Unknown agent "${value}" in ${source}. Valid agents: ${VALID_AGENT_NAMES.join(", ")}`);
|
|
663
|
-
}
|
|
664
|
-
return value;
|
|
665
|
-
}
|
|
666
|
-
function expectPermissionValue(value, source) {
|
|
667
|
-
const permission = expectString(value, source);
|
|
668
|
-
if (!VALID_PERMISSION_VALUES.has(permission)) {
|
|
669
|
-
throw new Error(`${source} must be one of: ask, allow, deny`);
|
|
670
|
-
}
|
|
671
|
-
return permission;
|
|
672
|
-
}
|
|
673
|
-
function expectStringArray(value, source) {
|
|
674
|
-
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
675
|
-
throw new Error(`${source} must be an array of strings`);
|
|
676
|
-
}
|
|
677
|
-
return value;
|
|
678
|
-
}
|
|
679
|
-
function expectObject(value, source) {
|
|
680
|
-
if (!isObject(value))
|
|
681
|
-
throw new Error(`${source} must be an object`);
|
|
682
|
-
return value;
|
|
683
|
-
}
|
|
684
|
-
function expectString(value, source) {
|
|
685
|
-
if (typeof value !== "string" || value.length === 0)
|
|
686
|
-
throw new Error(`${source} must be a non-empty string`);
|
|
687
|
-
return value;
|
|
688
|
-
}
|
|
689
|
-
function expectBoolean(value, source) {
|
|
690
|
-
if (typeof value !== "boolean")
|
|
691
|
-
throw new Error(`${source} must be a boolean`);
|
|
692
|
-
return value;
|
|
693
|
-
}
|
|
694
|
-
function expectNumber(value, source) {
|
|
695
|
-
if (typeof value !== "number" || Number.isNaN(value))
|
|
696
|
-
throw new Error(`${source} must be a number`);
|
|
697
|
-
return value;
|
|
698
|
-
}
|
|
699
|
-
function isObject(value) {
|
|
700
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
701
|
-
}
|
|
702
|
-
function isMissingFileError(error) {
|
|
703
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
704
|
-
}
|
|
705
|
-
function formatError(error) {
|
|
706
|
-
return error instanceof Error ? error.message : String(error);
|
|
707
|
-
}
|