opencode-resolve 0.1.19 → 0.2.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/README.ko.md +12 -48
- package/README.md +12 -48
- package/dist/agents.d.ts +3 -19
- package/dist/agents.js +45 -205
- package/dist/config.js +10 -47
- package/dist/hooks/index.js +217 -84
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/messages.d.ts +1 -1
- package/dist/messages.js +8 -48
- package/dist/state.d.ts +13 -0
- package/dist/state.js +9 -0
- package/dist/tools/index.d.ts +11 -2
- package/dist/tools/index.js +127 -21
- package/dist/types.d.ts +3 -5
- package/dist/utils.d.ts +0 -9
- package/dist/utils.js +8 -91
- package/opencode-resolve.example.json +0 -15
- package/opencode-resolve.reference.jsonc +56 -130
- package/package.json +1 -1
- package/scripts/cli.mjs +13 -9
- package/scripts/install-local.mjs +4 -18
- package/scripts/postinstall.mjs +148 -505
package/dist/state.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare const FAILURE_THRESHOLD = 10;
|
|
|
6
6
|
export declare const STRATEGY_PIVOT_THRESHOLD = 20;
|
|
7
7
|
export declare const EDIT_HOTSPOT_THRESHOLD = 10;
|
|
8
8
|
export declare const EDIT_HOTSPOT_TTL_MS = 600000;
|
|
9
|
+
export declare const DISPATCH_STOP_THRESHOLD = 3;
|
|
10
|
+
export declare const DISPATCH_PIVOT_THRESHOLD = 6;
|
|
9
11
|
export interface SessionState {
|
|
10
12
|
storedConfig?: ResolveConfig;
|
|
11
13
|
storedProjectContext?: ProjectContext;
|
|
@@ -13,6 +15,10 @@ export interface SessionState {
|
|
|
13
15
|
errors: number;
|
|
14
16
|
warnings: number;
|
|
15
17
|
timestamp: number;
|
|
18
|
+
errorMessages?: {
|
|
19
|
+
line: number;
|
|
20
|
+
message: string;
|
|
21
|
+
}[];
|
|
16
22
|
}>;
|
|
17
23
|
failurePatterns: Map<string, {
|
|
18
24
|
count: number;
|
|
@@ -27,9 +33,16 @@ export interface SessionState {
|
|
|
27
33
|
}>;
|
|
28
34
|
totalEdits: number;
|
|
29
35
|
totalToolCalls: number;
|
|
36
|
+
totalOutputBytes: number;
|
|
30
37
|
sessionStartTime: number;
|
|
31
38
|
loopWarnings: string[];
|
|
32
39
|
lastStrategyHint: string;
|
|
40
|
+
consecutiveDispatchFailures: number;
|
|
41
|
+
lastDispatchAgent?: string;
|
|
42
|
+
lastDispatchSucceeded?: boolean;
|
|
43
|
+
lastDispatchAt?: number;
|
|
44
|
+
awaitingVerify: boolean;
|
|
45
|
+
awaitingVerifyFile?: string;
|
|
33
46
|
currentAgent?: string;
|
|
34
47
|
locale: Locale;
|
|
35
48
|
}
|
package/dist/state.js
CHANGED
|
@@ -4,6 +4,12 @@ export const FAILURE_THRESHOLD = 10;
|
|
|
4
4
|
export const STRATEGY_PIVOT_THRESHOLD = 20;
|
|
5
5
|
export const EDIT_HOTSPOT_THRESHOLD = 10;
|
|
6
6
|
export const EDIT_HOTSPOT_TTL_MS = 600_000;
|
|
7
|
+
// Ralph Loop: dispatch lifecycle thresholds. The resolver→coder loop must
|
|
8
|
+
// close: dispatch → verify → (fail: diagnose+redispatch | pass: done).
|
|
9
|
+
// After DISPATCH_STOP_THRESHOLD consecutive failed dispatches, force a STOP &
|
|
10
|
+
// report. After DISPATCH_PIVOT_THRESHOLD, force an architect rethink.
|
|
11
|
+
export const DISPATCH_STOP_THRESHOLD = 3;
|
|
12
|
+
export const DISPATCH_PIVOT_THRESHOLD = 6;
|
|
7
13
|
export function createSessionState() {
|
|
8
14
|
return {
|
|
9
15
|
recentDiagnostics: new Map(),
|
|
@@ -13,9 +19,12 @@ export function createSessionState() {
|
|
|
13
19
|
editHotspots: new Map(),
|
|
14
20
|
totalEdits: 0,
|
|
15
21
|
totalToolCalls: 0,
|
|
22
|
+
totalOutputBytes: 0,
|
|
16
23
|
sessionStartTime: Date.now(),
|
|
17
24
|
loopWarnings: [],
|
|
18
25
|
lastStrategyHint: "",
|
|
26
|
+
consecutiveDispatchFailures: 0,
|
|
27
|
+
awaitingVerify: false,
|
|
19
28
|
locale: "en"
|
|
20
29
|
};
|
|
21
30
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionState } from "../state.js";
|
|
2
|
+
export declare function summarizeTestFailure(stdout: string, stderr: string, runner: string | undefined): string;
|
|
2
3
|
export declare function getTools(sessionState: SessionState): {
|
|
3
4
|
"resolve-verify": {
|
|
4
5
|
description: string;
|
|
@@ -13,9 +14,11 @@ export declare function getTools(sessionState: SessionState): {
|
|
|
13
14
|
description: string;
|
|
14
15
|
args: {
|
|
15
16
|
path: import("zod").ZodOptional<import("zod").ZodString>;
|
|
17
|
+
context: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
16
18
|
};
|
|
17
19
|
execute(args: {
|
|
18
20
|
path?: string | undefined;
|
|
21
|
+
context?: boolean | undefined;
|
|
19
22
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
20
23
|
};
|
|
21
24
|
"resolve-context": {
|
|
@@ -56,11 +59,13 @@ export declare function getTools(sessionState: SessionState): {
|
|
|
56
59
|
file: import("zod").ZodOptional<import("zod").ZodString>;
|
|
57
60
|
pattern: import("zod").ZodOptional<import("zod").ZodString>;
|
|
58
61
|
runner: import("zod").ZodOptional<import("zod").ZodString>;
|
|
62
|
+
raw: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
59
63
|
};
|
|
60
64
|
execute(args: {
|
|
61
65
|
file?: string | undefined;
|
|
62
66
|
pattern?: string | undefined;
|
|
63
67
|
runner?: string | undefined;
|
|
68
|
+
raw?: boolean | undefined;
|
|
64
69
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
65
70
|
};
|
|
66
71
|
"resolve-pattern": {
|
|
@@ -219,8 +224,12 @@ export declare function getTools(sessionState: SessionState): {
|
|
|
219
224
|
};
|
|
220
225
|
"resolve-session": {
|
|
221
226
|
description: string;
|
|
222
|
-
args: {
|
|
223
|
-
|
|
227
|
+
args: {
|
|
228
|
+
full: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
229
|
+
};
|
|
230
|
+
execute(args: {
|
|
231
|
+
full?: boolean | undefined;
|
|
232
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
224
233
|
};
|
|
225
234
|
"resolve-audit": {
|
|
226
235
|
description: string;
|
package/dist/tools/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { classifyBashCommand, runCommand, sanitizeShellArg, truncateOutput, PLUGIN_VERSION } from "../utils.js";
|
|
8
8
|
import { DIAGNOSTICS_TTL_MS } from "../state.js";
|
|
9
|
-
import {
|
|
10
|
-
const WRITE_CAPABLE_AGENTS = new Set(["resolver", "
|
|
9
|
+
import { VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
|
|
10
|
+
const WRITE_CAPABLE_AGENTS = new Set(["resolver", "coder", "debugger"]);
|
|
11
11
|
function canWriteFromTool(ctx) {
|
|
12
12
|
return typeof ctx.agent !== "string" || WRITE_CAPABLE_AGENTS.has(ctx.agent);
|
|
13
13
|
}
|
|
@@ -23,6 +23,60 @@ function commandExecutionDenied(command) {
|
|
|
23
23
|
}
|
|
24
24
|
return `Command is not allowlisted for direct tool execution: ${command}. Run it through OpenCode bash so the normal permission flow can decide.`;
|
|
25
25
|
}
|
|
26
|
+
// Phase 1.2: parse Vitest/Jest/Pytest/Go test failures into a compact summary
|
|
27
|
+
// (failing names, expected vs received, 3 stack frames). Always returns output
|
|
28
|
+
// ≤ the previous raw-truncate fallback, so resolve-test is strictly net-positive.
|
|
29
|
+
export function summarizeTestFailure(stdout, stderr, runner) {
|
|
30
|
+
const raw = stderr || stdout;
|
|
31
|
+
const cmd = (runner ?? "").toLowerCase();
|
|
32
|
+
const out = `${stdout}\n${stderr}`;
|
|
33
|
+
// Pytest: "FAILED file::test_name - msg" + "E ..." assertion lines
|
|
34
|
+
if (cmd.includes("pytest") || /FAILED\s+\S+::/.test(raw)) {
|
|
35
|
+
const failed = [...raw.matchAll(/FAILED\s+(\S+?::\S+)\s+-\s+(.+)/g)].slice(0, 10)
|
|
36
|
+
.map((m) => ` ✗ ${m[1]}\n ${m[2].slice(0, 200)}`);
|
|
37
|
+
const asserts = [...raw.matchAll(/^E\s+(.+)/gm)].slice(0, 5).map((m) => m[1].slice(0, 200));
|
|
38
|
+
if (failed.length || asserts.length) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (failed.length)
|
|
41
|
+
parts.push(`Failed tests:\n${failed.join("\n")}`);
|
|
42
|
+
if (asserts.length)
|
|
43
|
+
parts.push(`Assertion errors:\n ${asserts.join("\n ")}`);
|
|
44
|
+
return truncateOutput(parts.join("\n"), 1200);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Go test: "--- FAIL: TestName" + " file.go:NN: msg"
|
|
48
|
+
if (cmd.includes("go test") || /--- FAIL:/.test(raw)) {
|
|
49
|
+
const failed = [...raw.matchAll(/--- FAIL:\s+(\S+)/g)].slice(0, 10).map((m) => ` ✗ ${m[1]}`);
|
|
50
|
+
const errs = [...raw.matchAll(/^\s+(\S+\.go):(\d+):\s+(.+)/gm)].slice(0, 5)
|
|
51
|
+
.map((m) => ` ${m[1]}:${m[2]}: ${m[3].slice(0, 160)}`);
|
|
52
|
+
if (failed.length) {
|
|
53
|
+
return truncateOutput(`Failed tests:\n${failed.join("\n")}\n${errs.join("\n")}`, 1200);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Vitest / Jest
|
|
57
|
+
const failingNames = new Set();
|
|
58
|
+
for (const m of out.matchAll(/●\s+(.+)/g))
|
|
59
|
+
failingNames.add(m[1].trim().slice(0, 120)); // Jest suite › test
|
|
60
|
+
for (const m of out.matchAll(/^\s*×\s+(.+)/gm))
|
|
61
|
+
failingNames.add(m[1].trim().slice(0, 120)); // Vitest × test
|
|
62
|
+
const failFiles = [...out.matchAll(/^FAIL\s+(\S+)/gm)].map((m) => m[1]).slice(0, 10);
|
|
63
|
+
const expected = [...out.matchAll(/Expected[^:\n]*:\s*(.+)/g)].slice(0, 3).map((m) => m[1].slice(0, 150));
|
|
64
|
+
const received = [...out.matchAll(/Received[^:\n]*:\s*(.+)/g)].slice(0, 3).map((m) => m[1].slice(0, 150));
|
|
65
|
+
const stack = [...out.matchAll(/^\s+at\s+(.+)/gm)].slice(0, 3).map((m) => m[1].slice(0, 160));
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (failFiles.length)
|
|
68
|
+
parts.push(`Failed files: ${failFiles.join(", ")}`);
|
|
69
|
+
if (failingNames.size)
|
|
70
|
+
parts.push(`Failed tests:\n${[...failingNames].slice(0, 10).map((n) => ` ✗ ${n}`).join("\n")}`);
|
|
71
|
+
if (expected.length || received.length) {
|
|
72
|
+
parts.push(`Expected: ${expected[0] ?? "-"}\nReceived: ${received[0] ?? "-"}`);
|
|
73
|
+
}
|
|
74
|
+
if (stack.length)
|
|
75
|
+
parts.push(`Stack:\n${stack.map((s) => ` at ${s}`).join("\n")}`);
|
|
76
|
+
// Parsed something useful → compact summary; otherwise fall back to the
|
|
77
|
+
// previous raw-truncate behaviour (byte-identical).
|
|
78
|
+
return parts.length ? truncateOutput(parts.join("\n"), 1200) : truncateOutput(raw, 1500);
|
|
79
|
+
}
|
|
26
80
|
export function getTools(sessionState) {
|
|
27
81
|
return {
|
|
28
82
|
"resolve-verify": tool({
|
|
@@ -53,22 +107,53 @@ export function getTools(sessionState) {
|
|
|
53
107
|
},
|
|
54
108
|
}),
|
|
55
109
|
"resolve-diagnostics": tool({
|
|
56
|
-
description: "Get current LSP diagnostics snapshot. Returns errors and warnings per file from the language server.",
|
|
110
|
+
description: "Get current LSP diagnostics snapshot. Returns errors and warnings per file from the language server. When errors are present, includes the message and ±3 lines of source context so the coder can fix without a separate read.",
|
|
57
111
|
args: {
|
|
58
112
|
path: tool.schema.string().optional().describe("Specific file path to check. If omitted, returns all files with active diagnostics."),
|
|
113
|
+
context: tool.schema.boolean().optional().describe("Include ±3 lines of source context around each error (default true). Set false for counts only."),
|
|
59
114
|
},
|
|
60
|
-
async execute(args) {
|
|
115
|
+
async execute(args, ctx) {
|
|
61
116
|
if (sessionState.recentDiagnostics.size === 0) {
|
|
62
117
|
return "No active LSP diagnostics.";
|
|
63
118
|
}
|
|
64
119
|
const now = Date.now();
|
|
120
|
+
const wantContext = args.context !== false;
|
|
65
121
|
const entries = [];
|
|
66
122
|
for (const [filePath, diag] of sessionState.recentDiagnostics) {
|
|
67
123
|
if (now - diag.timestamp > DIAGNOSTICS_TTL_MS)
|
|
68
124
|
continue;
|
|
69
125
|
if (args.path && filePath !== args.path)
|
|
70
126
|
continue;
|
|
71
|
-
|
|
127
|
+
const head = `${filePath}: ${diag.errors} errors, ${diag.warnings} warnings`;
|
|
128
|
+
// Net-positive: error-free files and context:false calls are byte-identical to before.
|
|
129
|
+
if (!wantContext || diag.errors === 0 || !diag.errorMessages?.length) {
|
|
130
|
+
entries.push(head);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// Errors present: show message + ±3 source lines (bounded to 3 errors/file).
|
|
134
|
+
const blocks = [head];
|
|
135
|
+
let srcLines = null;
|
|
136
|
+
for (const e of diag.errorMessages.slice(0, 3)) {
|
|
137
|
+
const ln = e.line + 1; // LSP lines are 0-based
|
|
138
|
+
if (srcLines === null) {
|
|
139
|
+
try {
|
|
140
|
+
srcLines = (await readFile(resolve(ctx.directory, filePath), "utf8")).split("\n");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
srcLines = []; // unreadable: show message without context
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let snippet = "";
|
|
147
|
+
if (srcLines.length > 0) {
|
|
148
|
+
const start = Math.max(0, ln - 4);
|
|
149
|
+
const end = Math.min(srcLines.length, ln + 3);
|
|
150
|
+
snippet = srcLines.slice(start, end)
|
|
151
|
+
.map((l, i) => `${start + i + 1}${start + i + 1 === ln ? ">" : ":"} ${l}`)
|
|
152
|
+
.join("\n");
|
|
153
|
+
}
|
|
154
|
+
blocks.push(` L${ln}: ${e.message}${snippet ? "\n" + snippet : ""}`);
|
|
155
|
+
}
|
|
156
|
+
entries.push(blocks.join("\n"));
|
|
72
157
|
}
|
|
73
158
|
if (entries.length === 0) {
|
|
74
159
|
return args.path ? `No active diagnostics for ${args.path}.` : "No active LSP diagnostics.";
|
|
@@ -177,6 +262,7 @@ export function getTools(sessionState) {
|
|
|
177
262
|
file: tool.schema.string().optional().describe("Test file path or glob pattern (e.g. 'test/plugin.test.mjs')."),
|
|
178
263
|
pattern: tool.schema.string().optional().describe("Test name pattern to filter (e.g. 'GLM profile')."),
|
|
179
264
|
runner: tool.schema.string().optional().describe("Override test runner command (e.g. 'vitest run', 'jest')."),
|
|
265
|
+
raw: tool.schema.boolean().optional().describe("Return the raw untruncated log instead of the parsed failure summary."),
|
|
180
266
|
},
|
|
181
267
|
async execute(args, ctx) {
|
|
182
268
|
const projCtx = sessionState.storedProjectContext;
|
|
@@ -215,7 +301,11 @@ export function getTools(sessionState) {
|
|
|
215
301
|
if (result.exitCode === 0) {
|
|
216
302
|
return { output: `✅ Tests passed.\n${truncateOutput(result.stdout, 800)}`, metadata: { exitCode: 0 } };
|
|
217
303
|
}
|
|
218
|
-
|
|
304
|
+
// Phase 1.2: parse framework output into a compact summary unless raw requested.
|
|
305
|
+
const detail = args.raw
|
|
306
|
+
? truncateOutput(result.stderr || result.stdout, 1500)
|
|
307
|
+
: summarizeTestFailure(result.stdout, result.stderr, testCmd);
|
|
308
|
+
return { output: `❌ Tests failed (exit ${result.exitCode}).\n${detail}`, metadata: { exitCode: result.exitCode } };
|
|
219
309
|
}
|
|
220
310
|
catch (err) {
|
|
221
311
|
return `⚠️ Test runner failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -480,8 +570,6 @@ export function getTools(sessionState) {
|
|
|
480
570
|
}
|
|
481
571
|
// Build resolve.json content
|
|
482
572
|
const resolveConfig = {};
|
|
483
|
-
if (sessionState.storedConfig?.profile)
|
|
484
|
-
resolveConfig.profile = sessionState.storedConfig.profile;
|
|
485
573
|
if (sessionState.storedConfig?.tier)
|
|
486
574
|
resolveConfig.tier = sessionState.storedConfig.tier;
|
|
487
575
|
if (projCtx?.verifyCommands.length) {
|
|
@@ -926,9 +1014,11 @@ export function getTools(sessionState) {
|
|
|
926
1014
|
},
|
|
927
1015
|
}),
|
|
928
1016
|
"resolve-session": tool({
|
|
929
|
-
description: "Show current Ralph Loop session state:
|
|
930
|
-
args: {
|
|
931
|
-
|
|
1017
|
+
description: "Show current Ralph Loop session state: tier, edit count, tool call count, output bytes, failure warnings, loop warnings, and elapsed time. Use when you suspect you're going in circles.",
|
|
1018
|
+
args: {
|
|
1019
|
+
full: tool.schema.boolean().optional().describe("Include a compact changelog (recent commits + uncommitted count) so you get session + repo state in one call. Default false."),
|
|
1020
|
+
},
|
|
1021
|
+
async execute(args, ctx) {
|
|
932
1022
|
const lines = [];
|
|
933
1023
|
const cfg = sessionState.storedConfig;
|
|
934
1024
|
const projCtx = sessionState.storedProjectContext;
|
|
@@ -936,8 +1026,7 @@ export function getTools(sessionState) {
|
|
|
936
1026
|
lines.push(`Session duration: ${elapsed}s`);
|
|
937
1027
|
lines.push(`Tool calls: ${sessionState.totalToolCalls}`);
|
|
938
1028
|
lines.push(`Edits: ${sessionState.totalEdits}`);
|
|
939
|
-
|
|
940
|
-
lines.push(`Profile: ${cfg.profile}`);
|
|
1029
|
+
lines.push(`Output: ${sessionState.totalOutputBytes} bytes (~${Math.round(sessionState.totalOutputBytes / 1024)}KB)`);
|
|
941
1030
|
if (cfg?.tier)
|
|
942
1031
|
lines.push(`Tier: ${cfg.tier}`);
|
|
943
1032
|
if (projCtx?.hasTypeScript)
|
|
@@ -970,6 +1059,25 @@ export function getTools(sessionState) {
|
|
|
970
1059
|
lines.push(` 🔄 ${w}`);
|
|
971
1060
|
}
|
|
972
1061
|
ctx.metadata({ title: `session: ${sessionState.totalEdits} edits, ${sessionState.totalToolCalls} tools, ${elapsed}s` });
|
|
1062
|
+
// Phase 1.6: opt-in `full` mode folds a compact changelog (recent
|
|
1063
|
+
// commits + uncommitted count) into the same response — 3 calls → 1.
|
|
1064
|
+
// Default (full omitted/false) leaves the output exactly as before.
|
|
1065
|
+
if (args.full) {
|
|
1066
|
+
try {
|
|
1067
|
+
const log = await runCommand("git log --oneline -5", ctx.directory, 5_000);
|
|
1068
|
+
const st = await runCommand("git status --porcelain", ctx.directory, 5_000);
|
|
1069
|
+
lines.push("Recent changes:");
|
|
1070
|
+
if (log.stdout.trim()) {
|
|
1071
|
+
lines.push(log.stdout.trim().split("\n").map((l) => ` ${l}`).join("\n"));
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
lines.push(" (no commits yet)");
|
|
1075
|
+
}
|
|
1076
|
+
const uncommitted = st.stdout.trim().split("\n").filter(Boolean).length;
|
|
1077
|
+
lines.push(`Uncommitted files: ${uncommitted}`);
|
|
1078
|
+
}
|
|
1079
|
+
catch { /* not a git repo */ }
|
|
1080
|
+
}
|
|
973
1081
|
return lines.join("\n");
|
|
974
1082
|
},
|
|
975
1083
|
}),
|
|
@@ -1053,17 +1161,17 @@ export function getTools(sessionState) {
|
|
|
1053
1161
|
if (!cfg) {
|
|
1054
1162
|
return "No resolve config loaded. Plugin may not be initialized.";
|
|
1055
1163
|
}
|
|
1056
|
-
// 1.
|
|
1057
|
-
if (cfg.
|
|
1058
|
-
if (
|
|
1059
|
-
results.push(`✅
|
|
1164
|
+
// 1. Tier check
|
|
1165
|
+
if (cfg.tier) {
|
|
1166
|
+
if (VALID_TIERS.has(cfg.tier)) {
|
|
1167
|
+
results.push(`✅ Tier: ${cfg.tier}`);
|
|
1060
1168
|
}
|
|
1061
1169
|
else {
|
|
1062
|
-
results.push(`🔴 Invalid
|
|
1170
|
+
results.push(`🔴 Invalid tier: '${cfg.tier}'. Valid: ${[...VALID_TIERS].join(", ")}`);
|
|
1063
1171
|
}
|
|
1064
1172
|
}
|
|
1065
1173
|
else {
|
|
1066
|
-
results.push("ℹ️ No
|
|
1174
|
+
results.push("ℹ️ No tier set (using defaults)");
|
|
1067
1175
|
}
|
|
1068
1176
|
// 2. Tier check
|
|
1069
1177
|
if (cfg.tier) {
|
|
@@ -1190,8 +1298,6 @@ export function getTools(sessionState) {
|
|
|
1190
1298
|
failures: sessionState.totalFailures,
|
|
1191
1299
|
elapsedSeconds: Math.round((Date.now() - sessionState.sessionStartTime) / 1000),
|
|
1192
1300
|
};
|
|
1193
|
-
if (sessionState.storedConfig?.profile)
|
|
1194
|
-
state.profile = sessionState.storedConfig.profile;
|
|
1195
1301
|
if (sessionState.storedConfig?.tier)
|
|
1196
1302
|
state.tier = sessionState.storedConfig.tier;
|
|
1197
1303
|
if (sessionState.failureWarnings.length > 0)
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type PermissionValue = "ask" | "allow" | "deny";
|
|
2
|
-
export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "
|
|
3
|
-
export type ModelAlias = ResolveAgentName | "
|
|
2
|
+
export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "architect" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
|
|
3
|
+
export type ModelAlias = ResolveAgentName | "quick" | "deep" | "fast" | "strong" | "mini" | "bronze" | "silver" | "gold";
|
|
4
4
|
export type AgentMode = "subagent" | "primary" | "all";
|
|
5
5
|
export type ResolveAgentConfig = {
|
|
6
6
|
enabled?: boolean;
|
|
@@ -19,22 +19,20 @@ export type ResolveAgentConfig = {
|
|
|
19
19
|
external_directory?: PermissionValue;
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
-
export type ProfileName = "mix" | "glm" | "gpt";
|
|
23
22
|
export type TierName = "bronze" | "silver" | "gold";
|
|
24
23
|
export type LanguageSetting = "auto" | "en" | "ko";
|
|
25
24
|
export type ResolveConfig = {
|
|
26
|
-
profile?: ProfileName;
|
|
27
25
|
tier?: TierName;
|
|
28
26
|
enabled?: ResolveAgentName[];
|
|
29
27
|
models?: Partial<Record<ModelAlias, string>>;
|
|
30
28
|
agents?: Partial<Record<ResolveAgentName, ResolveAgentConfig>>;
|
|
31
29
|
preserveNative?: boolean;
|
|
32
|
-
context7?: boolean;
|
|
33
30
|
commands?: boolean;
|
|
34
31
|
autoApprove?: boolean;
|
|
35
32
|
maxParallelSubagents?: number;
|
|
36
33
|
autoUpdate?: boolean;
|
|
37
34
|
language?: LanguageSetting;
|
|
35
|
+
singleAgentMode?: boolean;
|
|
38
36
|
};
|
|
39
37
|
export type ResolvePluginOptions = ResolveConfig & {
|
|
40
38
|
config?: string;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { ProjectContext, ResolveConfig } from "./types.js";
|
|
2
2
|
export declare const PLUGIN_VERSION: string;
|
|
3
|
-
export declare const UPDATE_CHECK_INTERVAL_MS: number;
|
|
4
|
-
export declare const UPDATE_CHECK_FILE: string;
|
|
5
|
-
export declare const PLUGIN_CACHE_DIR: string;
|
|
6
3
|
export declare function runCommand(command: string, cwd: string, timeoutMs: number): Promise<{
|
|
7
4
|
stdout: string;
|
|
8
5
|
stderr: string;
|
|
@@ -20,12 +17,6 @@ export declare function existsDirectory(path: string): Promise<boolean>;
|
|
|
20
17
|
export declare function detectProjectContext(directory: string): Promise<ProjectContext>;
|
|
21
18
|
export declare function collectContextFiles(rootDirectory: string, relativeDirectory: string, maxFiles?: number): Promise<string[]>;
|
|
22
19
|
export declare function readPluginVersion(): string;
|
|
23
|
-
export declare function readUpdateCheckCache(): {
|
|
24
|
-
checkedAt: number;
|
|
25
|
-
latest: string;
|
|
26
|
-
} | undefined;
|
|
27
|
-
export declare function isNewerVersion(candidate: string, baseline: string): boolean;
|
|
28
|
-
export declare function maybeAutoUpdate(): Promise<void>;
|
|
29
20
|
export declare function readFirstJson(paths: string[]): Promise<ResolveConfig | undefined>;
|
|
30
21
|
export declare const BANNED_COMMANDS: ReadonlyArray<RegExp>;
|
|
31
22
|
export declare const DANGEROUS_BASH_PATTERNS: ReadonlyArray<RegExp>;
|
package/dist/utils.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { stat, readFile, access, readdir } from "node:fs/promises";
|
|
3
3
|
import { join, dirname, extname, relative } from "node:path";
|
|
4
|
-
import { readFileSync
|
|
5
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { normalizeResolveConfig } from "./config.js";
|
|
8
7
|
export const PLUGIN_VERSION = readPluginVersion();
|
|
9
|
-
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
-
export const UPDATE_CHECK_FILE = join(homedir(), ".cache", "opencode-resolve", "update-check.json");
|
|
11
|
-
export const PLUGIN_CACHE_DIR = join(homedir(), ".cache", "opencode", "packages", "opencode-resolve@latest");
|
|
12
8
|
export function runCommand(command, cwd, timeoutMs) {
|
|
13
9
|
return new Promise((resolve) => {
|
|
14
10
|
const proc = spawn("sh", ["-c", command], {
|
|
@@ -218,92 +214,13 @@ export function readPluginVersion() {
|
|
|
218
214
|
return "unknown";
|
|
219
215
|
}
|
|
220
216
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
// file missing or unparseable
|
|
232
|
-
}
|
|
233
|
-
return undefined;
|
|
234
|
-
}
|
|
235
|
-
export function isNewerVersion(candidate, baseline) {
|
|
236
|
-
const a = candidate.split(".").map((n) => Number.parseInt(n, 10));
|
|
237
|
-
const b = baseline.split(".").map((n) => Number.parseInt(n, 10));
|
|
238
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
239
|
-
const av = Number.isFinite(a[i]) ? a[i] : 0;
|
|
240
|
-
const bv = Number.isFinite(b[i]) ? b[i] : 0;
|
|
241
|
-
if (av > bv)
|
|
242
|
-
return true;
|
|
243
|
-
if (av < bv)
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
export async function maybeAutoUpdate() {
|
|
249
|
-
const previous = readUpdateCheckCache();
|
|
250
|
-
try {
|
|
251
|
-
if (previous && Date.now() - previous.checkedAt < UPDATE_CHECK_INTERVAL_MS) {
|
|
252
|
-
if (isNewerVersion(previous.latest, PLUGIN_VERSION)) {
|
|
253
|
-
refreshPluginCacheInBackground(previous.latest, "cached");
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
// ignore corrupt cache and re-check
|
|
260
|
-
}
|
|
261
|
-
let latest;
|
|
262
|
-
try {
|
|
263
|
-
const response = await fetch("https://registry.npmjs.org/opencode-resolve/latest", {
|
|
264
|
-
headers: { Accept: "application/json" },
|
|
265
|
-
signal: AbortSignal.timeout(5000),
|
|
266
|
-
});
|
|
267
|
-
if (!response.ok)
|
|
268
|
-
return;
|
|
269
|
-
const data = (await response.json());
|
|
270
|
-
if (typeof data?.version !== "string")
|
|
271
|
-
return;
|
|
272
|
-
latest = data.version;
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
mkdirSync(dirname(UPDATE_CHECK_FILE), { recursive: true });
|
|
279
|
-
writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
// best-effort; don't block on cache write failure
|
|
283
|
-
}
|
|
284
|
-
if (!isNewerVersion(latest, PLUGIN_VERSION))
|
|
285
|
-
return;
|
|
286
|
-
refreshPluginCacheInBackground(latest, "registry");
|
|
287
|
-
}
|
|
288
|
-
function refreshPluginCacheInBackground(latest, source) {
|
|
289
|
-
const sourceLabel = source === "cached" ? "cached latest" : "registry latest";
|
|
290
|
-
console.log(`[opencode-resolve] new version v${latest} available (${sourceLabel}, current: v${PLUGIN_VERSION}) — refreshing OpenCode plugin cache in background. Restart OpenCode to activate it.`);
|
|
291
|
-
try {
|
|
292
|
-
spawn("sh", ["-c", `rm -rf "${PLUGIN_CACHE_DIR}" && opencode plugin opencode-resolve@latest --global --force`], {
|
|
293
|
-
detached: true,
|
|
294
|
-
stdio: "ignore",
|
|
295
|
-
env: {
|
|
296
|
-
...process.env,
|
|
297
|
-
OPENCODE_RESOLVE_REFRESHING_CACHE: "1",
|
|
298
|
-
OPENCODE_RESOLVE_SKIP_POSTINSTALL: "1",
|
|
299
|
-
OPENCODE_RESOLVE_SKIP_COMPANIONS: "1",
|
|
300
|
-
},
|
|
301
|
-
}).unref();
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
// If spawn fails, the user already saw the notice and can run the command manually.
|
|
305
|
-
}
|
|
306
|
-
}
|
|
217
|
+
// Auto-update was removed: it spawned `opencode plugin opencode-resolve@latest
|
|
218
|
+
// --global --force` per `hooks.config()` call, which created hundreds of parallel
|
|
219
|
+
// OpenCode sessions whenever multiple instances loaded the plugin at once (and
|
|
220
|
+
// once flattened the user's server with 177 sessions during a test run).
|
|
221
|
+
// Users update manually: `npm i -g opencode-resolve` or via the install script.
|
|
222
|
+
// The `autoUpdate` config field is still accepted by the schema for backward
|
|
223
|
+
// compatibility, but it is now a no-op.
|
|
307
224
|
export async function readFirstJson(paths) {
|
|
308
225
|
for (const path of paths) {
|
|
309
226
|
try {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"profile": "mix",
|
|
3
2
|
"preserveNative": true,
|
|
4
|
-
"context7": true,
|
|
5
3
|
"commands": false,
|
|
6
4
|
"autoApprove": true,
|
|
7
|
-
"autoUpdate": true,
|
|
8
5
|
"language": "auto",
|
|
9
6
|
"models": {},
|
|
10
7
|
"agents": {
|
|
@@ -15,12 +12,6 @@
|
|
|
15
12
|
"resolver": {
|
|
16
13
|
"enabled": true
|
|
17
14
|
},
|
|
18
|
-
"codex": {
|
|
19
|
-
"enabled": false
|
|
20
|
-
},
|
|
21
|
-
"gpt": {
|
|
22
|
-
"enabled": false
|
|
23
|
-
},
|
|
24
15
|
"explorer": {
|
|
25
16
|
"enabled": true,
|
|
26
17
|
"mode": "subagent"
|
|
@@ -40,17 +31,11 @@
|
|
|
40
31
|
"architect": {
|
|
41
32
|
"enabled": false
|
|
42
33
|
},
|
|
43
|
-
"gpt-coder": {
|
|
44
|
-
"enabled": false
|
|
45
|
-
},
|
|
46
34
|
"debugger": {
|
|
47
35
|
"enabled": false
|
|
48
36
|
},
|
|
49
37
|
"researcher": {
|
|
50
38
|
"enabled": false
|
|
51
|
-
},
|
|
52
|
-
"glm": {
|
|
53
|
-
"enabled": false
|
|
54
39
|
}
|
|
55
40
|
}
|
|
56
41
|
}
|