opencode-resolve 0.1.7 → 0.1.9
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 +121 -60
- package/README.md +121 -60
- package/dist/agents.d.ts +27 -0
- package/dist/agents.js +392 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +404 -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 +8 -2
- package/opencode-resolve.reference.jsonc +107 -27
- package/package.json +10 -2
- package/scripts/install-git-hooks.mjs +28 -0
- package/scripts/install-local.mjs +64 -4
- package/scripts/postinstall.mjs +552 -34
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { DIAGNOSTICS_TTL_MS, FAILURE_PATTERN_TTL_MS, FAILURE_THRESHOLD, STRATEGY_PIVOT_THRESHOLD, EDIT_HOTSPOT_TTL_MS, EDIT_HOTSPOT_THRESHOLD } from "../state.js";
|
|
2
|
+
import { classifyBashCommand, maybeAutoUpdate, detectProjectContext } from "../utils.js";
|
|
3
|
+
import { loadResolveConfig, applyResolveConfig } from "../config.js";
|
|
4
|
+
function capTemperature(current, cap) {
|
|
5
|
+
return typeof current === "number" && Number.isFinite(current)
|
|
6
|
+
? Math.min(current, cap)
|
|
7
|
+
: cap;
|
|
8
|
+
}
|
|
9
|
+
export function getHooks(directory, options, sessionState) {
|
|
10
|
+
return {
|
|
11
|
+
event: async (input) => {
|
|
12
|
+
const evt = input.event;
|
|
13
|
+
// LSP diagnostics tracking
|
|
14
|
+
if (evt.type === "lsp.client.diagnostics") {
|
|
15
|
+
const props = evt.properties;
|
|
16
|
+
if (props.path) {
|
|
17
|
+
const data = evt;
|
|
18
|
+
const diagnostics = Array.isArray(data.diagnostics) ? data.diagnostics
|
|
19
|
+
: Array.isArray(data.errors) ? data.errors
|
|
20
|
+
: [];
|
|
21
|
+
const errors = diagnostics.filter((d) => d.severity === 1 || d.severity === "error").length;
|
|
22
|
+
const warnings = diagnostics.filter((d) => d.severity === 2 || d.severity === "warning").length;
|
|
23
|
+
if (errors > 0 || warnings > 0) {
|
|
24
|
+
sessionState.recentDiagnostics.set(props.path, { errors, warnings, timestamp: Date.now() });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
sessionState.recentDiagnostics.delete(props.path);
|
|
28
|
+
}
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [key, value] of sessionState.recentDiagnostics) {
|
|
31
|
+
if (now - value.timestamp > DIAGNOSTICS_TTL_MS)
|
|
32
|
+
sessionState.recentDiagnostics.delete(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Tool execution failure tracking via message.part.updated
|
|
37
|
+
// When a tool result part appears with a non-zero exit code, track it
|
|
38
|
+
if (evt.type === "message.part.updated") {
|
|
39
|
+
const props = evt.properties;
|
|
40
|
+
const part = props.part;
|
|
41
|
+
if (part?.type === "tool-result" || part?.type === "tool-result") {
|
|
42
|
+
const exitCode = part?.metadata?.exitCode ?? part?.output?.metadata?.exitCode;
|
|
43
|
+
const toolName = part?.toolID ?? part?.tool ?? "";
|
|
44
|
+
if (exitCode !== undefined && exitCode !== 0 && typeof toolName === "string") {
|
|
45
|
+
const existing = sessionState.failurePatterns.get(toolName);
|
|
46
|
+
const msg = String(part?.output ?? part?.error ?? "").slice(0, 200);
|
|
47
|
+
if (existing) {
|
|
48
|
+
existing.count++;
|
|
49
|
+
existing.lastMessage = msg;
|
|
50
|
+
existing.timestamp = Date.now();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
sessionState.failurePatterns.set(toolName, { count: 1, lastMessage: msg, timestamp: Date.now() });
|
|
54
|
+
}
|
|
55
|
+
// Prune stale entries
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
for (const [k, v] of sessionState.failurePatterns) {
|
|
58
|
+
if (now - v.timestamp > FAILURE_PATTERN_TTL_MS)
|
|
59
|
+
sessionState.failurePatterns.delete(k);
|
|
60
|
+
}
|
|
61
|
+
// Generate warnings for recurring failures
|
|
62
|
+
sessionState.totalFailures++;
|
|
63
|
+
sessionState.failureWarnings = [];
|
|
64
|
+
for (const [, v] of sessionState.failurePatterns) {
|
|
65
|
+
if (v.count >= FAILURE_THRESHOLD) {
|
|
66
|
+
sessionState.failureWarnings.push(`Tool '${toolName}' failed ${v.count} times. Last: ${v.lastMessage}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Track session errors for recurring issues
|
|
73
|
+
if (evt.type === "session.error") {
|
|
74
|
+
const data = evt;
|
|
75
|
+
const msg = String(data?.error?.message ?? data?.message ?? "").slice(0, 200);
|
|
76
|
+
if (msg) {
|
|
77
|
+
const existing = sessionState.failurePatterns.get("session");
|
|
78
|
+
if (existing) {
|
|
79
|
+
existing.count++;
|
|
80
|
+
existing.lastMessage = msg;
|
|
81
|
+
existing.timestamp = Date.now();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
sessionState.failurePatterns.set("session", { count: 1, lastMessage: msg, timestamp: Date.now() });
|
|
85
|
+
}
|
|
86
|
+
sessionState.failureWarnings = [];
|
|
87
|
+
sessionState.totalFailures++;
|
|
88
|
+
for (const [, v] of sessionState.failurePatterns) {
|
|
89
|
+
if (v.count >= FAILURE_THRESHOLD) {
|
|
90
|
+
sessionState.failureWarnings.push(`Session error repeated ${v.count} times: ${v.lastMessage}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Ralph Loop: track edit tool calls for hotspot detection ────────
|
|
96
|
+
if (evt.type === "message.part.updated") {
|
|
97
|
+
const props = evt.properties;
|
|
98
|
+
const part = props.part;
|
|
99
|
+
if (part?.type === "tool-invocation" || part?.type === "tool-use") {
|
|
100
|
+
sessionState.totalToolCalls++;
|
|
101
|
+
const toolName = part.tool ?? part.toolName ?? "";
|
|
102
|
+
if (toolName === "edit" || toolName === "write") {
|
|
103
|
+
sessionState.totalEdits++;
|
|
104
|
+
const filePath = part.args?.filePath ?? part.args?.path ?? "";
|
|
105
|
+
if (filePath) {
|
|
106
|
+
const existing = sessionState.editHotspots.get(filePath);
|
|
107
|
+
if (existing) {
|
|
108
|
+
existing.count++;
|
|
109
|
+
existing.lastEditTime = Date.now();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sessionState.editHotspots.set(filePath, { count: 1, lastEditTime: Date.now() });
|
|
113
|
+
}
|
|
114
|
+
// Prune stale entries
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
for (const [k, v] of sessionState.editHotspots) {
|
|
117
|
+
if (now - v.lastEditTime > EDIT_HOTSPOT_TTL_MS)
|
|
118
|
+
sessionState.editHotspots.delete(k);
|
|
119
|
+
}
|
|
120
|
+
// Generate loop warnings
|
|
121
|
+
sessionState.loopWarnings = [];
|
|
122
|
+
for (const [file, data] of sessionState.editHotspots) {
|
|
123
|
+
if (data.count >= EDIT_HOTSPOT_THRESHOLD) {
|
|
124
|
+
sessionState.loopWarnings.push(`File '${file}' edited ${data.count} times — consider a different approach. Keep iterating.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
config: async (config) => {
|
|
133
|
+
const resolveConfig = await loadResolveConfig(directory, config, options);
|
|
134
|
+
const projectContext = await detectProjectContext(directory);
|
|
135
|
+
sessionState.storedConfig = resolveConfig;
|
|
136
|
+
sessionState.storedProjectContext = projectContext;
|
|
137
|
+
applyResolveConfig(config, resolveConfig, projectContext);
|
|
138
|
+
if (resolveConfig.autoUpdate !== false && process.env.OPENCODE_RESOLVE_NO_AUTO_UPDATE !== "1") {
|
|
139
|
+
maybeAutoUpdate().catch(() => { });
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"shell.env": async (_input, output) => {
|
|
143
|
+
output.env = {
|
|
144
|
+
...output.env,
|
|
145
|
+
CI: "true",
|
|
146
|
+
DEBIAN_FRONTEND: "noninteractive",
|
|
147
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
148
|
+
GIT_EDITOR: "true",
|
|
149
|
+
GIT_PAGER: "cat",
|
|
150
|
+
PAGER: "cat",
|
|
151
|
+
GCM_INTERACTIVE: "never",
|
|
152
|
+
npm_config_yes: "true",
|
|
153
|
+
PIP_NO_INPUT: "1",
|
|
154
|
+
NODE_OPTIONS: process.env.NODE_OPTIONS ?? "", // preserve existing
|
|
155
|
+
NO_COLOR: output.env?.NO_COLOR, // preserve if set
|
|
156
|
+
LANG: output.env?.LANG ?? "en_US.UTF-8", // consistent locale
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
"permission.ask": async (input, output) => {
|
|
160
|
+
if (input.type === "bash") {
|
|
161
|
+
const cmd = typeof input.pattern === "string"
|
|
162
|
+
? input.pattern
|
|
163
|
+
: Array.isArray(input.pattern)
|
|
164
|
+
? input.pattern.join(" ")
|
|
165
|
+
: "";
|
|
166
|
+
const action = classifyBashCommand(cmd);
|
|
167
|
+
if (action !== "ask")
|
|
168
|
+
output.status = action;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"chat.params": async (input, output) => {
|
|
172
|
+
const profile = sessionState.storedConfig?.profile;
|
|
173
|
+
if (profile === "glm") {
|
|
174
|
+
output.temperature = capTemperature(output.temperature, 0.4);
|
|
175
|
+
if (output.maxOutputTokens === undefined || output.maxOutputTokens > 16384) {
|
|
176
|
+
output.maxOutputTokens = 16384;
|
|
177
|
+
}
|
|
178
|
+
// GLM: tighter topP for deterministic output
|
|
179
|
+
if (output.topP === undefined || output.topP > 0.9) {
|
|
180
|
+
output.topP = 0.85;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (profile === "gpt") {
|
|
184
|
+
output.temperature = capTemperature(output.temperature, 0.7);
|
|
185
|
+
if (output.maxOutputTokens === undefined) {
|
|
186
|
+
output.maxOutputTokens = 32768;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Read-only agents: lower temperature always
|
|
190
|
+
const readOnlyAgents = new Set(["reviewer", "deep-reviewer", "explorer", "planner", "researcher", "architect"]);
|
|
191
|
+
if (readOnlyAgents.has(input.agent)) {
|
|
192
|
+
output.temperature = capTemperature(output.temperature, 0.3);
|
|
193
|
+
}
|
|
194
|
+
// Write agents: slightly higher temperature for creative problem-solving
|
|
195
|
+
const writeAgents = new Set(["coder", "resolver", "codex", "glm", "gpt-coder"]);
|
|
196
|
+
if (writeAgents.has(input.agent) && output.temperature === undefined) {
|
|
197
|
+
output.temperature = 0.5;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"tool.definition": async (input, output) => {
|
|
201
|
+
const hints = {
|
|
202
|
+
edit: "\n[opencode-resolve] Read the file first. Make the smallest correct change. Verify after editing.",
|
|
203
|
+
write: "\n[opencode-resolve] Only write new files when explicitly needed. Prefer editing existing files.",
|
|
204
|
+
bash: "\n[opencode-resolve] Commands run in non-interactive mode. No interactive editors, pagers, or REPLs. Use -c flags for scripting.",
|
|
205
|
+
task: "\n[opencode-resolve] Dispatch subagents with: TASK (atomic goal), OUTCOME (success criteria), MUST DO, MUST NOT DO, CONTEXT.",
|
|
206
|
+
glob: "\n[opencode-resolve] Use specific patterns. Avoid '**/*' unless genuinely needed — prefer scoped searches.",
|
|
207
|
+
grep: "\n[opencode-resolve] Use specific regex patterns. Combine with include filter for targeted search.",
|
|
208
|
+
read: "\n[opencode-resolve] Read only what you need. Use offset/limit for large files. Check file-info tool for quick metadata.",
|
|
209
|
+
webfetch: "\n[opencode-resolve] Only fetch URLs when you genuinely need external information. Prefer local docs and code first.",
|
|
210
|
+
todowrite: "\n[opencode-resolve] Keep todos current. Mark completed immediately. One in_progress at a time.",
|
|
211
|
+
};
|
|
212
|
+
if (hints[input.toolID]) {
|
|
213
|
+
output.description = output.description + hints[input.toolID];
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"command.execute.before": async (_input, output) => {
|
|
217
|
+
// Prepend a discipline reminder to all command executions.
|
|
218
|
+
// The parts array is typed as Part[] — TextPart requires id/sessionID/messageID.
|
|
219
|
+
// We provide placeholder values; OpenCode replaces them if needed.
|
|
220
|
+
output.parts.push({
|
|
221
|
+
id: "",
|
|
222
|
+
sessionID: "",
|
|
223
|
+
messageID: "",
|
|
224
|
+
type: "text",
|
|
225
|
+
text: "[opencode-resolve] Drive to verified resolution. Classify intent, dispatch focused subagents, verify after each, iterate on failure. Report completion only when verified.",
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
"tool.execute.before": async (input, output) => {
|
|
229
|
+
// For bash: inject hints for common mistakes
|
|
230
|
+
if (input.tool === "bash" && output.args && typeof output.args === "object") {
|
|
231
|
+
const cmd = output.args.command ?? output.args.cmd;
|
|
232
|
+
if (typeof cmd === "string" && cmd.includes("git commit") && !cmd.includes("-m")) {
|
|
233
|
+
output.args = { ...output.args, _resolve_hint: "Use 'git commit -m \"message\"' — interactive commit is blocked." };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// For write: warn about overwriting existing files
|
|
237
|
+
if (input.tool === "write" && output.args && typeof output.args === "object") {
|
|
238
|
+
const filePath = output.args.filePath ?? output.args.path;
|
|
239
|
+
if (typeof filePath === "string") {
|
|
240
|
+
const meta = { ...(output.args._resolve_meta ?? {}) };
|
|
241
|
+
meta._resolve_write_note = "Verify file contents after writing. Use edit instead of write for existing files when possible.";
|
|
242
|
+
output.args = { ...output.args, _resolve_meta: meta };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
"chat.headers": async (input, output) => {
|
|
247
|
+
const providerID = input.provider?.info?.id ?? "";
|
|
248
|
+
// For GLM providers: add retry-after hint to avoid rate limiting
|
|
249
|
+
if (providerID.includes("zai") || providerID.includes("glm") || providerID.includes("bigmodel")) {
|
|
250
|
+
output.headers["X-Custom-Retry-Strategy"] = "exponential";
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"tool.execute.after": async (input, output) => {
|
|
254
|
+
if (input.tool === "edit" || input.tool === "write") {
|
|
255
|
+
const verifyCommands = sessionState.storedProjectContext?.verifyCommands;
|
|
256
|
+
const meta = { ...(output.metadata ?? {}) };
|
|
257
|
+
if (verifyCommands && verifyCommands.length > 0) {
|
|
258
|
+
meta._resolve_verify_hint = verifyCommands[0];
|
|
259
|
+
}
|
|
260
|
+
// Attach LSP diagnostics for the edited file if available
|
|
261
|
+
const args = input.args;
|
|
262
|
+
const editedPath = args?.filePath;
|
|
263
|
+
if (editedPath) {
|
|
264
|
+
const diag = sessionState.recentDiagnostics.get(editedPath);
|
|
265
|
+
if (diag && Date.now() - diag.timestamp < DIAGNOSTICS_TTL_MS) {
|
|
266
|
+
meta._resolve_lsp_errors = diag.errors;
|
|
267
|
+
meta._resolve_lsp_warnings = diag.warnings;
|
|
268
|
+
}
|
|
269
|
+
// Ralph Loop: track edit hotspot
|
|
270
|
+
const existing = sessionState.editHotspots.get(editedPath);
|
|
271
|
+
if (existing) {
|
|
272
|
+
existing.count++;
|
|
273
|
+
existing.lastEditTime = Date.now();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
sessionState.editHotspots.set(editedPath, { count: 1, lastEditTime: Date.now() });
|
|
277
|
+
}
|
|
278
|
+
// Ralph Loop: inject loop warning into metadata
|
|
279
|
+
const hotspot = sessionState.editHotspots.get(editedPath);
|
|
280
|
+
if (hotspot && hotspot.count >= EDIT_HOTSPOT_THRESHOLD) {
|
|
281
|
+
meta._resolve_loop_warning = `This file has been edited ${hotspot.count} times. Consider a different approach.`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
output.metadata = meta;
|
|
285
|
+
// Ralph Loop: update sessionState.loopWarnings after every edit/write
|
|
286
|
+
sessionState.loopWarnings = [];
|
|
287
|
+
for (const [file, data] of sessionState.editHotspots) {
|
|
288
|
+
if (data.count >= EDIT_HOTSPOT_THRESHOLD) {
|
|
289
|
+
sessionState.loopWarnings.push(`File '${file}' edited ${data.count} times — consider a different approach for this file. Keep iterating.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// For bash: extract key error lines from failing commands
|
|
294
|
+
if (input.tool === "bash") {
|
|
295
|
+
const outputText = typeof output.output === "string" ? output.output
|
|
296
|
+
: output.output?.output ?? "";
|
|
297
|
+
const exitCode = output.metadata?.exitCode ?? output.output?.metadata?.exitCode;
|
|
298
|
+
if (exitCode && exitCode !== 0 && typeof outputText === "string") {
|
|
299
|
+
const errorLines = outputText.split("\n")
|
|
300
|
+
.filter((l) => /\b(error|Error|ERROR|fail|FAIL|FAILED|cannot|Cannot|TypeError|SyntaxError|ReferenceError)\b/.test(l))
|
|
301
|
+
.slice(0, 5);
|
|
302
|
+
if (errorLines.length > 0) {
|
|
303
|
+
const meta = { ...(output.metadata ?? {}) };
|
|
304
|
+
meta._resolve_key_errors = errorLines;
|
|
305
|
+
output.metadata = meta;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
311
|
+
const ctx = sessionState.storedProjectContext;
|
|
312
|
+
const cfg = sessionState.storedConfig;
|
|
313
|
+
if (!ctx && !cfg)
|
|
314
|
+
return;
|
|
315
|
+
const contextLines = [];
|
|
316
|
+
// Profile and tier info
|
|
317
|
+
if (cfg?.profile)
|
|
318
|
+
contextLines.push(`Profile: ${cfg.profile}.`);
|
|
319
|
+
if (cfg?.tier)
|
|
320
|
+
contextLines.push(`Tier: ${cfg.tier}.`);
|
|
321
|
+
// Project context
|
|
322
|
+
if (ctx?.knowledgeFiles.length) {
|
|
323
|
+
contextLines.push(`Project knowledge files: ${ctx.knowledgeFiles.join(", ")}.`);
|
|
324
|
+
}
|
|
325
|
+
if (ctx?.contextFiles.length) {
|
|
326
|
+
contextLines.push(`Context docs: ${ctx.contextFiles.slice(0, 20).join(", ")}.`);
|
|
327
|
+
}
|
|
328
|
+
if (ctx?.verifyCommands.length) {
|
|
329
|
+
contextLines.push(`Verify commands: ${ctx.verifyCommands.join("; ")}.`);
|
|
330
|
+
}
|
|
331
|
+
if (ctx?.hasTypeScript) {
|
|
332
|
+
contextLines.push("TypeScript project — type safety is mandatory.");
|
|
333
|
+
}
|
|
334
|
+
if (ctx?.packageManager) {
|
|
335
|
+
contextLines.push(`Package manager: ${ctx.packageManager}.`);
|
|
336
|
+
}
|
|
337
|
+
// Active failure warnings
|
|
338
|
+
if (sessionState.failureWarnings.length > 0) {
|
|
339
|
+
contextLines.push(`Active warnings: ${sessionState.failureWarnings.join("; ")}`);
|
|
340
|
+
}
|
|
341
|
+
// Ralph Loop: preserve loop state
|
|
342
|
+
if (sessionState.loopWarnings.length > 0) {
|
|
343
|
+
contextLines.push(`Loop warnings: ${sessionState.loopWarnings.join("; ")}`);
|
|
344
|
+
}
|
|
345
|
+
// Ralph Loop: preserve session stats
|
|
346
|
+
if (sessionState.totalEdits > 0) {
|
|
347
|
+
contextLines.push(`Session stats: ${sessionState.totalEdits} edits, ${sessionState.totalToolCalls} tool calls.`);
|
|
348
|
+
}
|
|
349
|
+
if (contextLines.length > 0) {
|
|
350
|
+
output.context.push("[" + "opencode-resolve" + "] Project context (preserve): " + contextLines.join(" "));
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
354
|
+
const replacements = [
|
|
355
|
+
// Exact: default OpenCode "continue" prompt
|
|
356
|
+
["Summarize the task tool output above and continue with your task.",
|
|
357
|
+
"Analyze the subtask result above. If it succeeded, continue. If it failed, diagnose and retry. Report completion only when verified."],
|
|
358
|
+
// Regex: any "Summarize ... and continue" variant
|
|
359
|
+
[/Summarize the .+ output above and continue/i,
|
|
360
|
+
"Analyze the result above. If it succeeded, continue to the next step. If it failed, diagnose root cause and retry with a fix."],
|
|
361
|
+
// Regex: generic "continue with your task" ending
|
|
362
|
+
[/continue with your task\.$/i,
|
|
363
|
+
"continue driving toward verified completion."],
|
|
364
|
+
// Regex: "I've completed..." without verification
|
|
365
|
+
[/I('ve| have) (completed|finished|done) (the )?.*\.$/i,
|
|
366
|
+
"Verify your changes pass typecheck/lint/test before reporting completion."],
|
|
367
|
+
// Regex: passive "Let me know if..."
|
|
368
|
+
[/let me know if (you|you'd like) .*/i,
|
|
369
|
+
"Proceed with the next step. If blocked, diagnose and report specifically what failed."],
|
|
370
|
+
// Regex: "Would you like me to..."
|
|
371
|
+
[/would you like me to .*/i,
|
|
372
|
+
"Proceed with the most effective next step autonomously."],
|
|
373
|
+
// Ralph Loop: detect "I'll try again" — encourage different approach, don't stop
|
|
374
|
+
[/I('ll| will) (try again|retry|attempt again|redo)/i,
|
|
375
|
+
"Diagnose the ROOT CAUSE of the failure, then apply a DIFFERENT fix. The Ralph Loop keeps going."],
|
|
376
|
+
// Regex: "I'm not sure" — uncertainty without action
|
|
377
|
+
[/I('m| am) (not sure|unsure|uncertain) .*/i,
|
|
378
|
+
"Resolve uncertainty by reading the code, checking diagnostics, or using resolve-search. Keep driving."],
|
|
379
|
+
// Regex: "This might work" — low confidence
|
|
380
|
+
[/this (might|should|could|may) work/i,
|
|
381
|
+
"CONFIRM it works by running verification. Do not assume."],
|
|
382
|
+
// Regex: "It seems to be working" — unverified claim
|
|
383
|
+
[/it (seems|appears|looks) to (be )?(working|fine|correct)/i,
|
|
384
|
+
"VERIFY with typecheck/lint/test. 'Seems to work' is not evidence."],
|
|
385
|
+
];
|
|
386
|
+
for (const msg of output.messages) {
|
|
387
|
+
for (const part of msg.parts) {
|
|
388
|
+
if (part.type !== "text")
|
|
389
|
+
continue;
|
|
390
|
+
for (const [pattern, replacement] of replacements) {
|
|
391
|
+
if (typeof pattern === "string" ? part.text === pattern : pattern.test(part.text)) {
|
|
392
|
+
part.text = replacement;
|
|
393
|
+
break; // first match wins
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
"experimental.compaction.autocontinue": async (_input, output) => {
|
|
400
|
+
// Always enable auto-continue — the resolver should keep driving
|
|
401
|
+
output.enabled = true;
|
|
402
|
+
},
|
|
403
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
404
|
+
const ctx = sessionState.storedProjectContext;
|
|
405
|
+
const hasFailures = sessionState.failureWarnings.length > 0;
|
|
406
|
+
const hasLoops = sessionState.loopWarnings.length > 0;
|
|
407
|
+
if (!ctx && !hasFailures && !hasLoops)
|
|
408
|
+
return;
|
|
409
|
+
const lines = [];
|
|
410
|
+
if (ctx?.knowledgeFiles.length) {
|
|
411
|
+
lines.push(`[opencode-resolve] Project knowledge: ${ctx.knowledgeFiles.join(", ")}. Read when relevant before modifying code.`);
|
|
412
|
+
}
|
|
413
|
+
if (ctx?.contextFiles.length) {
|
|
414
|
+
lines.push(`[opencode-resolve] Context docs: ${ctx.contextFiles.slice(0, 20).join(", ")}. MVI: load only task-relevant docs.`);
|
|
415
|
+
}
|
|
416
|
+
if (ctx?.verifyCommands.length) {
|
|
417
|
+
lines.push(`[opencode-resolve] Verify commands: ${ctx.verifyCommands.join("; ")}. Run after changes.`);
|
|
418
|
+
}
|
|
419
|
+
if (ctx?.hasTypeScript) {
|
|
420
|
+
lines.push("[opencode-resolve] TypeScript project — type safety is mandatory. No `as any` or `@ts-ignore`.");
|
|
421
|
+
}
|
|
422
|
+
// Inject failure pattern warnings — encourage trying different approaches, don't stop
|
|
423
|
+
if (hasFailures) {
|
|
424
|
+
lines.push("[opencode-resolve] ⚠️ Recurring failures detected:");
|
|
425
|
+
for (const w of sessionState.failureWarnings.slice(0, 3)) {
|
|
426
|
+
lines.push(` - ${w}`);
|
|
427
|
+
}
|
|
428
|
+
lines.push("Keep going — try a different approach for the same goal. The Ralph Loop should drive to completion.");
|
|
429
|
+
}
|
|
430
|
+
// Strategy Pivot: after many total failures, suggest architect intervention
|
|
431
|
+
if (sessionState.totalFailures >= STRATEGY_PIVOT_THRESHOLD) {
|
|
432
|
+
lines.push(`[opencode-resolve] 🔀 STRATEGY PIVOT: ${sessionState.totalFailures} total failures detected.`);
|
|
433
|
+
lines.push("The current approach is not working. Dispatch ARCHITECT to analyze the problem from scratch and propose a fundamentally different strategy.");
|
|
434
|
+
lines.push("Then apply the new strategy. Do NOT keep retrying the same approach.");
|
|
435
|
+
}
|
|
436
|
+
// Ralph Loop: inject strategy hints when same file edited many times
|
|
437
|
+
if (hasLoops) {
|
|
438
|
+
lines.push("[opencode-resolve] 🔄 Ralph Loop: heavy editing detected on same file(s):");
|
|
439
|
+
for (const w of sessionState.loopWarnings.slice(0, 3)) {
|
|
440
|
+
lines.push(` - ${w}`);
|
|
441
|
+
}
|
|
442
|
+
const strategies = [
|
|
443
|
+
"Re-read the file carefully. You may be missing existing code that conflicts with your edit.",
|
|
444
|
+
"Try a completely different approach — revert your last change and try a different fix.",
|
|
445
|
+
"Use resolve-diagnostics to check current LSP errors before the next edit.",
|
|
446
|
+
"Break the problem into smaller pieces. Edit one function at a time, verify between each.",
|
|
447
|
+
"Check if the error is actually in a DIFFERENT file — the real issue may be upstream.",
|
|
448
|
+
"Read the test file if it exists — the test often reveals the expected behavior.",
|
|
449
|
+
"Check imports — missing or wrong imports are a common cause of cascading errors.",
|
|
450
|
+
"Use resolve-search to find similar patterns elsewhere in the codebase.",
|
|
451
|
+
];
|
|
452
|
+
const hint = strategies[Math.floor(Date.now() / 30_000) % strategies.length];
|
|
453
|
+
if (hint !== sessionState.lastStrategyHint) {
|
|
454
|
+
lines.push(`Strategy suggestion: ${hint}`);
|
|
455
|
+
sessionState.lastStrategyHint = hint;
|
|
456
|
+
}
|
|
457
|
+
lines.push("Keep driving — the Ralph Loop should keep iterating until verified resolution.");
|
|
458
|
+
}
|
|
459
|
+
// Ralph Loop: inject session context when significant work done
|
|
460
|
+
if (sessionState.totalEdits >= 20 && sessionState.failureWarnings.length > 0) {
|
|
461
|
+
lines.push(`[opencode-resolve] 📊 Session stats: ${sessionState.totalEdits} edits, ${sessionState.totalToolCalls} tool calls, ${Math.round((Date.now() - sessionState.sessionStartTime) / 1000)}s elapsed.`);
|
|
462
|
+
lines.push("Significant iteration with failures. Consider a fundamentally different approach — but keep going.");
|
|
463
|
+
}
|
|
464
|
+
if (lines.length > 0) {
|
|
465
|
+
output.system.push(lines.join("\n"));
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
"experimental.text.complete": async (_input, output) => {
|
|
469
|
+
const text = output.text ?? "";
|
|
470
|
+
if (!text)
|
|
471
|
+
return;
|
|
472
|
+
// Detect if this turn involved code changes
|
|
473
|
+
const editSignals = ["```", "edit", "wrote", "changed", "created", "updated", "modified", "deleted", "removed", "added", "renamed"];
|
|
474
|
+
const looksLikeEdit = editSignals.some(s => text.toLowerCase().includes(s));
|
|
475
|
+
// Detect if verification was already mentioned
|
|
476
|
+
const verifySignals = ["verified", "pass", "✅", "tsc --noEmit", "eslint", "npm test", "vitest pass", "all tests pass", "no errors", "0 errors", "build succeeded"];
|
|
477
|
+
const alreadyVerified = verifySignals.some(s => text.toLowerCase().includes(s));
|
|
478
|
+
// Detect if the turn ended with a question or handoff (shouldn't nudge)
|
|
479
|
+
const handoffPatterns = [/\?$/, /let me know/i, /would you like/i, /what do you think/i];
|
|
480
|
+
const isHandoff = handoffPatterns.some(p => p.test(text.trim()));
|
|
481
|
+
// Ralph Loop: detect loop-like patterns in the response text
|
|
482
|
+
const loopSignals = ["trying again", "attempting", "retrying", "second attempt", "third attempt", "another approach", "let me try"];
|
|
483
|
+
const looksLikeLoop = loopSignals.some(s => text.toLowerCase().includes(s));
|
|
484
|
+
if (looksLikeEdit && !alreadyVerified && !isHandoff) {
|
|
485
|
+
output.text = text + "\n\n[opencode-resolve] Reminder: verify your changes (resolve-verify) before reporting completion.";
|
|
486
|
+
}
|
|
487
|
+
// Ralph Loop: if loop detected in text AND hotspot exists, suggest strategy change
|
|
488
|
+
if (looksLikeLoop && sessionState.loopWarnings.length > 0) {
|
|
489
|
+
output.text = (output.text ?? text) + "\n\n[opencode-resolve] 🔄 Ralph Loop: heavy iteration detected. Use resolve-diagnostics to check current state, then try a different approach. Keep driving to completion.";
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
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";
|
|
1
6
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
7
|
export declare const OpencodeResolve: Plugin;
|
|
3
8
|
export default OpencodeResolve;
|