opencode-resolve 0.1.20 → 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 +10 -46
- package/README.md +10 -46
- 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 +132 -495
package/dist/hooks/index.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
import { DIAGNOSTICS_TTL_MS, FAILURE_PATTERN_TTL_MS, FAILURE_THRESHOLD, STRATEGY_PIVOT_THRESHOLD,
|
|
2
|
-
import { classifyBashCommand,
|
|
1
|
+
import { DIAGNOSTICS_TTL_MS, FAILURE_PATTERN_TTL_MS, FAILURE_THRESHOLD, STRATEGY_PIVOT_THRESHOLD, EDIT_HOTSPOT_THRESHOLD, DISPATCH_STOP_THRESHOLD, DISPATCH_PIVOT_THRESHOLD } from "../state.js";
|
|
2
|
+
import { classifyBashCommand, detectProjectContext } from "../utils.js";
|
|
3
3
|
import { loadResolveConfig, applyResolveConfig } from "../config.js";
|
|
4
|
-
import { contextMessage, narrate, resolveLocale, t, agentDisplayName, PLUGIN_BRAND } from "../messages.js";
|
|
4
|
+
import { contextMessage, narrate, resolveLocale, t, agentDisplayName, brand, PLUGIN_BRAND } from "../messages.js";
|
|
5
|
+
import { VALID_AGENT_NAMES } from "../agents.js";
|
|
6
|
+
// Agents this plugin registers. Hooks that modify chat/tool behavior gate on
|
|
7
|
+
// isActiveResolve() so native opencode agents (build/plan/default chat) pass
|
|
8
|
+
// through unmodified — the plugin only intervenes when a resolve agent drives
|
|
9
|
+
// the current turn.
|
|
10
|
+
const RESOLVE_AGENTS = new Set(VALID_AGENT_NAMES);
|
|
11
|
+
// Collaborative handoff / question patterns. A turn ending in one of these is a
|
|
12
|
+
// genuine question to the user and must NEVER be rewritten into an autonomous
|
|
13
|
+
// directive — doing so severs the user↔model feedback loop.
|
|
14
|
+
const HANDOFF_PATTERNS = [
|
|
15
|
+
/\?\s*$/, // ends with a question mark
|
|
16
|
+
/\blet me know\b/i,
|
|
17
|
+
/\bwould you like\b/i,
|
|
18
|
+
/\bshall i\b/i,
|
|
19
|
+
/\bdo you want\b/i,
|
|
20
|
+
/\bwhat do you think\b/i,
|
|
21
|
+
/\bcan i\b/i,
|
|
22
|
+
];
|
|
23
|
+
// Code-file extensions. Only edits to code trigger the verify gate; docs/config
|
|
24
|
+
// (.md/.txt/.json/...) are exempt so trivial doc changes don't force a build.
|
|
25
|
+
const CODE_EXTENSIONS = [
|
|
26
|
+
"ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts",
|
|
27
|
+
"py", "go", "rs", "java", "kt", "scala", "rb", "php",
|
|
28
|
+
"c", "cc", "cpp", "cxx", "h", "hh", "hpp", "hxx",
|
|
29
|
+
"cs", "swift", "sh", "bash", "zsh", "fish",
|
|
30
|
+
"vue", "svelte", "astro", "gleam", "ex", "exs", "erl", "lua",
|
|
31
|
+
];
|
|
32
|
+
function isCodeFile(filePath) {
|
|
33
|
+
if (!filePath)
|
|
34
|
+
return true; // unknown target — be conservative, require verify
|
|
35
|
+
const dot = filePath.lastIndexOf(".");
|
|
36
|
+
if (dot < 0)
|
|
37
|
+
return true; // no extension — treat as code
|
|
38
|
+
return CODE_EXTENSIONS.includes(filePath.slice(dot + 1).toLowerCase());
|
|
39
|
+
}
|
|
5
40
|
function capTemperature(current, cap) {
|
|
6
41
|
return typeof current === "number" && Number.isFinite(current)
|
|
7
42
|
? Math.min(current, cap)
|
|
@@ -16,10 +51,6 @@ const DISPATCH_KEYS = {
|
|
|
16
51
|
architect: "dispatch.architect",
|
|
17
52
|
researcher: "dispatch.researcher",
|
|
18
53
|
debugger: "dispatch.debugger",
|
|
19
|
-
codex: "dispatch.codex",
|
|
20
|
-
glm: "dispatch.glm",
|
|
21
|
-
gpt: "dispatch.gpt",
|
|
22
|
-
"gpt-coder": "dispatch.gptCoder",
|
|
23
54
|
};
|
|
24
55
|
function extractSubagentType(args) {
|
|
25
56
|
if (!args || typeof args !== "object")
|
|
@@ -36,6 +67,26 @@ function extractDispatchGoal(args) {
|
|
|
36
67
|
const trimmed = candidate.trim().split("\n")[0] ?? "";
|
|
37
68
|
return trimmed.length > 80 ? trimmed.slice(0, 77) + "…" : trimmed;
|
|
38
69
|
}
|
|
70
|
+
// Ralph Loop: does a bash command run one of the project's verify commands
|
|
71
|
+
// (typecheck / lint / test)? Used to clear the awaitingVerify flag so the
|
|
72
|
+
// loop can tell the resolver the edit was actually checked.
|
|
73
|
+
function isVerifyCommand(cmd, verifyCommands) {
|
|
74
|
+
if (typeof cmd !== "string" || cmd.length === 0)
|
|
75
|
+
return false;
|
|
76
|
+
if (!Array.isArray(verifyCommands) || verifyCommands.length === 0)
|
|
77
|
+
return false;
|
|
78
|
+
const normalized = cmd.trim();
|
|
79
|
+
return verifyCommands.some((vc) => typeof vc === "string" && vc.length > 0 && normalized.includes(vc));
|
|
80
|
+
}
|
|
81
|
+
// Ralph Loop: classify a task (subagent dispatch) result as success/failure.
|
|
82
|
+
// A dispatch is failed if the tool errored or carried an error in metadata.
|
|
83
|
+
function dispatchFailed(output) {
|
|
84
|
+
if (Boolean(output?.error))
|
|
85
|
+
return true;
|
|
86
|
+
if (typeof output?.metadata === "object" && output.metadata?.error)
|
|
87
|
+
return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
39
90
|
export function getHooks(directory, options, sessionState) {
|
|
40
91
|
const narrateToolStart = (input, output) => {
|
|
41
92
|
const tool = input.tool;
|
|
@@ -77,6 +128,9 @@ export function getHooks(directory, options, sessionState) {
|
|
|
77
128
|
(typeof output?.metadata === "object" && output.metadata?.error);
|
|
78
129
|
narrate(sessionState, errorPresent ? "dispatch.failed" : "dispatch.completed", { to });
|
|
79
130
|
};
|
|
131
|
+
// Is the current turn driven by an agent this plugin registered? Native
|
|
132
|
+
// opencode agents (build/plan/default chat) return false → hooks no-op.
|
|
133
|
+
const isActiveResolve = () => RESOLVE_AGENTS.has(sessionState.currentAgent ?? "");
|
|
80
134
|
return {
|
|
81
135
|
event: async (input) => {
|
|
82
136
|
const evt = input.event;
|
|
@@ -90,8 +144,17 @@ export function getHooks(directory, options, sessionState) {
|
|
|
90
144
|
: [];
|
|
91
145
|
const errors = diagnostics.filter((d) => d.severity === 1 || d.severity === "error").length;
|
|
92
146
|
const warnings = diagnostics.filter((d) => d.severity === 2 || d.severity === "warning").length;
|
|
147
|
+
// Phase 1.1: capture error messages + line numbers so resolve-diagnostics
|
|
148
|
+
// can show ±3 lines of context, saving the coder a separate read call.
|
|
149
|
+
const errorMessages = diagnostics
|
|
150
|
+
.filter((d) => d.severity === 1 || d.severity === "error")
|
|
151
|
+
.slice(0, 20)
|
|
152
|
+
.map((d) => ({
|
|
153
|
+
line: d.range?.start?.line ?? d.line ?? 0,
|
|
154
|
+
message: String(d.message ?? "").slice(0, 200),
|
|
155
|
+
}));
|
|
93
156
|
if (errors > 0 || warnings > 0) {
|
|
94
|
-
sessionState.recentDiagnostics.set(props.path, { errors, warnings, timestamp: Date.now() });
|
|
157
|
+
sessionState.recentDiagnostics.set(props.path, { errors, warnings, timestamp: Date.now(), errorMessages });
|
|
95
158
|
}
|
|
96
159
|
else {
|
|
97
160
|
sessionState.recentDiagnostics.delete(props.path);
|
|
@@ -131,9 +194,9 @@ export function getHooks(directory, options, sessionState) {
|
|
|
131
194
|
// Generate warnings for recurring failures
|
|
132
195
|
sessionState.totalFailures++;
|
|
133
196
|
sessionState.failureWarnings = [];
|
|
134
|
-
for (const [, v] of sessionState.failurePatterns) {
|
|
197
|
+
for (const [k, v] of sessionState.failurePatterns) {
|
|
135
198
|
if (v.count >= FAILURE_THRESHOLD) {
|
|
136
|
-
sessionState.failureWarnings.push(`Tool '${
|
|
199
|
+
sessionState.failureWarnings.push(`Tool '${k}' failed ${v.count} times. Last: ${v.lastMessage}`);
|
|
137
200
|
}
|
|
138
201
|
}
|
|
139
202
|
}
|
|
@@ -162,42 +225,6 @@ export function getHooks(directory, options, sessionState) {
|
|
|
162
225
|
}
|
|
163
226
|
}
|
|
164
227
|
}
|
|
165
|
-
// ── Ralph Loop: track edit tool calls for hotspot detection ────────
|
|
166
|
-
if (evt.type === "message.part.updated") {
|
|
167
|
-
const props = evt.properties;
|
|
168
|
-
const part = props.part;
|
|
169
|
-
if (part?.type === "tool-invocation" || part?.type === "tool-use") {
|
|
170
|
-
sessionState.totalToolCalls++;
|
|
171
|
-
const toolName = part.tool ?? part.toolName ?? "";
|
|
172
|
-
if (toolName === "edit" || toolName === "write") {
|
|
173
|
-
sessionState.totalEdits++;
|
|
174
|
-
const filePath = part.args?.filePath ?? part.args?.path ?? "";
|
|
175
|
-
if (filePath) {
|
|
176
|
-
const existing = sessionState.editHotspots.get(filePath);
|
|
177
|
-
if (existing) {
|
|
178
|
-
existing.count++;
|
|
179
|
-
existing.lastEditTime = Date.now();
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
sessionState.editHotspots.set(filePath, { count: 1, lastEditTime: Date.now() });
|
|
183
|
-
}
|
|
184
|
-
// Prune stale entries
|
|
185
|
-
const now = Date.now();
|
|
186
|
-
for (const [k, v] of sessionState.editHotspots) {
|
|
187
|
-
if (now - v.lastEditTime > EDIT_HOTSPOT_TTL_MS)
|
|
188
|
-
sessionState.editHotspots.delete(k);
|
|
189
|
-
}
|
|
190
|
-
// Generate loop warnings
|
|
191
|
-
sessionState.loopWarnings = [];
|
|
192
|
-
for (const [file, data] of sessionState.editHotspots) {
|
|
193
|
-
if (data.count >= EDIT_HOTSPOT_THRESHOLD) {
|
|
194
|
-
sessionState.loopWarnings.push(`File '${file}' edited ${data.count} times — consider a different approach. Keep iterating.`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
228
|
},
|
|
202
229
|
config: async (config) => {
|
|
203
230
|
const resolveConfig = await loadResolveConfig(directory, config, options);
|
|
@@ -206,14 +233,15 @@ export function getHooks(directory, options, sessionState) {
|
|
|
206
233
|
sessionState.storedProjectContext = projectContext;
|
|
207
234
|
sessionState.locale = resolveLocale(resolveConfig.language, process.env.LANG);
|
|
208
235
|
applyResolveConfig(config, resolveConfig, projectContext);
|
|
209
|
-
|
|
210
|
-
maybeAutoUpdate().catch(() => { });
|
|
211
|
-
}
|
|
236
|
+
// Auto-update removed — see src/utils.ts header. Users update manually.
|
|
212
237
|
},
|
|
213
238
|
"shell.env": async (_input, output) => {
|
|
214
239
|
output.env = {
|
|
215
240
|
...output.env,
|
|
216
|
-
CI
|
|
241
|
+
// CI=true is intentionally NOT forced — it strips color output from
|
|
242
|
+
// test/lint runners (jest/vitest/eslint go --no-colors under CI) and
|
|
243
|
+
// pollutes native usage. Only non-interactive guard vars are set, to
|
|
244
|
+
// prevent prompts (npm/git/pip/debconf) from hanging the agent.
|
|
217
245
|
DEBIAN_FRONTEND: "noninteractive",
|
|
218
246
|
GIT_TERMINAL_PROMPT: "0",
|
|
219
247
|
GIT_EDITOR: "true",
|
|
@@ -235,43 +263,43 @@ export function getHooks(directory, options, sessionState) {
|
|
|
235
263
|
? input.pattern.join(" ")
|
|
236
264
|
: "";
|
|
237
265
|
const action = classifyBashCommand(cmd);
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
// Banned/dangerous commands are denied universally (protect everyone).
|
|
267
|
+
if (action === "deny") {
|
|
268
|
+
output.status = "deny";
|
|
269
|
+
}
|
|
270
|
+
else if (action === "allow" && isActiveResolve()) {
|
|
271
|
+
// Auto-approve safe commands only when a resolve agent drives the turn;
|
|
272
|
+
// native agents keep opencode's own permission flow.
|
|
273
|
+
output.status = "allow";
|
|
274
|
+
}
|
|
240
275
|
}
|
|
241
276
|
},
|
|
242
277
|
"chat.params": async (input, output) => {
|
|
243
278
|
if (typeof input.agent === "string" && input.agent.length > 0) {
|
|
244
279
|
sessionState.currentAgent = input.agent;
|
|
245
280
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
output.topP = 0.85;
|
|
255
|
-
}
|
|
281
|
+
// Only shape params for resolve agents — native opencode agents (build/plan)
|
|
282
|
+
// pass through so the plugin never alters their model behavior.
|
|
283
|
+
if (!isActiveResolve())
|
|
284
|
+
return;
|
|
285
|
+
// Deterministic params keep the resolve loop on-rails (coding benefits from low temperature).
|
|
286
|
+
output.temperature = capTemperature(output.temperature, 0.4);
|
|
287
|
+
if (output.maxOutputTokens === undefined || output.maxOutputTokens > 16384) {
|
|
288
|
+
output.maxOutputTokens = 16384;
|
|
256
289
|
}
|
|
257
|
-
|
|
258
|
-
output.
|
|
259
|
-
if (output.maxOutputTokens === undefined) {
|
|
260
|
-
output.maxOutputTokens = 32768;
|
|
261
|
-
}
|
|
290
|
+
if (output.topP === undefined || output.topP > 0.9) {
|
|
291
|
+
output.topP = 0.85;
|
|
262
292
|
}
|
|
263
293
|
// Read-only agents: lower temperature always
|
|
264
294
|
const readOnlyAgents = new Set(["reviewer", "deep-reviewer", "explorer", "planner", "researcher", "architect"]);
|
|
265
295
|
if (readOnlyAgents.has(input.agent)) {
|
|
266
296
|
output.temperature = capTemperature(output.temperature, 0.3);
|
|
267
297
|
}
|
|
268
|
-
// Write agents: slightly higher temperature for creative problem-solving
|
|
269
|
-
const writeAgents = new Set(["coder", "resolver", "codex", "glm", "gpt-coder"]);
|
|
270
|
-
if (writeAgents.has(input.agent) && output.temperature === undefined) {
|
|
271
|
-
output.temperature = 0.5;
|
|
272
|
-
}
|
|
273
298
|
},
|
|
274
299
|
"tool.definition": async (input, output) => {
|
|
300
|
+
// Don't pollute tool descriptions for native agents — resolve agents only.
|
|
301
|
+
if (!isActiveResolve())
|
|
302
|
+
return;
|
|
275
303
|
const prefix = `[${PLUGIN_BRAND}]`;
|
|
276
304
|
const hintKeys = {
|
|
277
305
|
edit: "tool.edit",
|
|
@@ -292,6 +320,9 @@ export function getHooks(directory, options, sessionState) {
|
|
|
292
320
|
}
|
|
293
321
|
},
|
|
294
322
|
"command.execute.before": async (_input, output) => {
|
|
323
|
+
// Only prepend the discipline reminder when a resolve agent drives the turn.
|
|
324
|
+
if (!isActiveResolve())
|
|
325
|
+
return;
|
|
295
326
|
// Prepend a discipline reminder to all command executions.
|
|
296
327
|
// The parts array is typed as Part[] — TextPart requires id/sessionID/messageID.
|
|
297
328
|
// We provide placeholder values; OpenCode replaces them if needed.
|
|
@@ -300,10 +331,13 @@ export function getHooks(directory, options, sessionState) {
|
|
|
300
331
|
sessionID: "",
|
|
301
332
|
messageID: "",
|
|
302
333
|
type: "text",
|
|
303
|
-
text:
|
|
334
|
+
text: `[${PLUGIN_BRAND}] ${t("system.driveResolution", "en")}`,
|
|
304
335
|
});
|
|
305
336
|
},
|
|
306
337
|
"tool.execute.before": async (input, output) => {
|
|
338
|
+
// Native agents get unmodified tool execution — resolve agents only.
|
|
339
|
+
if (!isActiveResolve())
|
|
340
|
+
return;
|
|
307
341
|
// For bash: inject hints for common mistakes
|
|
308
342
|
if (input.tool === "bash" && output.args && typeof output.args === "object") {
|
|
309
343
|
const cmd = output.args.command ?? output.args.cmd;
|
|
@@ -331,7 +365,21 @@ export function getHooks(directory, options, sessionState) {
|
|
|
331
365
|
}
|
|
332
366
|
},
|
|
333
367
|
"tool.execute.after": async (input, output) => {
|
|
368
|
+
// Only track/count/instrument tool calls driven by resolve agents.
|
|
369
|
+
if (!isActiveResolve())
|
|
370
|
+
return;
|
|
371
|
+
// Count every completed tool call from the reliable tool.execute.after
|
|
372
|
+
// hook (the message-stream event hook was unreliable + double-counted).
|
|
373
|
+
sessionState.totalToolCalls++;
|
|
374
|
+
// Phase 1.0: measure cumulative tool output bytes for token-budget
|
|
375
|
+
// visibility. Only counts text the resolver actually consumes.
|
|
376
|
+
const _outText = typeof output?.output === "string" ? output.output
|
|
377
|
+
: typeof output?.output?.output === "string" ? output.output.output
|
|
378
|
+
: typeof output === "string" ? output : "";
|
|
379
|
+
if (_outText)
|
|
380
|
+
sessionState.totalOutputBytes += Buffer.byteLength(_outText, "utf8");
|
|
334
381
|
if (input.tool === "edit" || input.tool === "write") {
|
|
382
|
+
sessionState.totalEdits++;
|
|
335
383
|
const verifyCommands = sessionState.storedProjectContext?.verifyCommands;
|
|
336
384
|
const meta = { ...(output.metadata ?? {}) };
|
|
337
385
|
if (verifyCommands && verifyCommands.length > 0) {
|
|
@@ -362,6 +410,15 @@ export function getHooks(directory, options, sessionState) {
|
|
|
362
410
|
}
|
|
363
411
|
}
|
|
364
412
|
output.metadata = meta;
|
|
413
|
+
// Ralph Loop: an edit just happened — mark that verification is now
|
|
414
|
+
// required before the resolver may report completion. Cleared when a
|
|
415
|
+
// verify command (typecheck/lint/test) runs via bash. Adaptive: skip
|
|
416
|
+
// the gate for non-code files (.md/.txt/.json/...) so trivial doc
|
|
417
|
+
// changes don't force a build/test round-trip.
|
|
418
|
+
if (isCodeFile(editedPath)) {
|
|
419
|
+
sessionState.awaitingVerify = true;
|
|
420
|
+
sessionState.awaitingVerifyFile = editedPath;
|
|
421
|
+
}
|
|
365
422
|
// Ralph Loop: update sessionState.loopWarnings after every edit/write
|
|
366
423
|
sessionState.loopWarnings = [];
|
|
367
424
|
for (const [file, data] of sessionState.editHotspots) {
|
|
@@ -372,8 +429,46 @@ export function getHooks(directory, options, sessionState) {
|
|
|
372
429
|
}
|
|
373
430
|
// Role-play narration → terminal only. Does NOT enter LLM context.
|
|
374
431
|
narrateToolEnd(input, output);
|
|
432
|
+
// Ralph Loop: dispatch lifecycle — track resolver→coder results so the
|
|
433
|
+
// loop closes. A failed dispatch must trigger diagnosis before retry;
|
|
434
|
+
// repeated failures escalate (diagnose → STOP → architect pivot).
|
|
435
|
+
if (input.tool === "task") {
|
|
436
|
+
const sub = extractSubagentType(input?.args);
|
|
437
|
+
const failed = dispatchFailed(output);
|
|
438
|
+
sessionState.lastDispatchAgent = sub;
|
|
439
|
+
sessionState.lastDispatchSucceeded = !failed;
|
|
440
|
+
sessionState.lastDispatchAt = Date.now();
|
|
441
|
+
if (failed) {
|
|
442
|
+
sessionState.consecutiveDispatchFailures++;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
sessionState.consecutiveDispatchFailures = 0;
|
|
446
|
+
}
|
|
447
|
+
// Attach a directive the resolver sees in tool metadata.
|
|
448
|
+
const dmeta = { ...(output.metadata ?? {}) };
|
|
449
|
+
const c = sessionState.consecutiveDispatchFailures;
|
|
450
|
+
if (failed) {
|
|
451
|
+
if (c >= DISPATCH_PIVOT_THRESHOLD) {
|
|
452
|
+
dmeta._resolve_loop_directive = `Ralph Loop: ${c} consecutive dispatch failures. Dispatch ARCHITECT to rethink the approach.`;
|
|
453
|
+
}
|
|
454
|
+
else if (c >= DISPATCH_STOP_THRESHOLD) {
|
|
455
|
+
dmeta._resolve_loop_directive = `Ralph Loop: ${c} consecutive dispatch failures. STOP — revert the last change and report to the user exactly what is blocked.`;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
dmeta._resolve_loop_directive = `Ralph Loop: ${sub ?? "subagent"} dispatch failed (${c}x). Diagnose the ROOT CAUSE before re-dispatching. Do NOT retry the same way.`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
output.metadata = dmeta;
|
|
462
|
+
}
|
|
375
463
|
// For bash: extract key error lines from failing commands
|
|
376
464
|
if (input.tool === "bash") {
|
|
465
|
+
// Ralph Loop: running a verify command clears the awaitingVerify flag —
|
|
466
|
+
// the resolver has now checked the edit it made.
|
|
467
|
+
const cmd = input.args?.command ?? input.args?.cmd;
|
|
468
|
+
if (isVerifyCommand(cmd, sessionState.storedProjectContext?.verifyCommands)) {
|
|
469
|
+
sessionState.awaitingVerify = false;
|
|
470
|
+
sessionState.awaitingVerifyFile = undefined;
|
|
471
|
+
}
|
|
377
472
|
const outputText = typeof output.output === "string" ? output.output
|
|
378
473
|
: output.output?.output ?? "";
|
|
379
474
|
const exitCode = output.metadata?.exitCode ?? output.output?.metadata?.exitCode;
|
|
@@ -390,15 +485,16 @@ export function getHooks(directory, options, sessionState) {
|
|
|
390
485
|
}
|
|
391
486
|
},
|
|
392
487
|
"experimental.session.compacting": async (_input, output) => {
|
|
488
|
+
// Only preserve resolve-loop context; native compaction is untouched.
|
|
489
|
+
if (!isActiveResolve())
|
|
490
|
+
return;
|
|
393
491
|
narrate(sessionState, "narration.compacting");
|
|
394
492
|
const ctx = sessionState.storedProjectContext;
|
|
395
493
|
const cfg = sessionState.storedConfig;
|
|
396
494
|
if (!ctx && !cfg)
|
|
397
495
|
return;
|
|
398
496
|
const contextLines = [];
|
|
399
|
-
//
|
|
400
|
-
if (cfg?.profile)
|
|
401
|
-
contextLines.push(`Profile: ${cfg.profile}.`);
|
|
497
|
+
// Tier info
|
|
402
498
|
if (cfg?.tier)
|
|
403
499
|
contextLines.push(`Tier: ${cfg.tier}.`);
|
|
404
500
|
// Project context
|
|
@@ -434,6 +530,9 @@ export function getHooks(directory, options, sessionState) {
|
|
|
434
530
|
}
|
|
435
531
|
},
|
|
436
532
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
533
|
+
// Rewrite passive language only within resolve turns — native chat is verbatim.
|
|
534
|
+
if (!isActiveResolve())
|
|
535
|
+
return;
|
|
437
536
|
const replacements = [
|
|
438
537
|
// Exact: default OpenCode "continue" prompt
|
|
439
538
|
["Summarize the task tool output above and continue with your task.",
|
|
@@ -447,12 +546,6 @@ export function getHooks(directory, options, sessionState) {
|
|
|
447
546
|
// Regex: "I've completed..." without verification
|
|
448
547
|
[/I('ve| have) (completed|finished|done) (the )?.*\.$/i,
|
|
449
548
|
"Verify your changes pass typecheck/lint/test before reporting completion."],
|
|
450
|
-
// Regex: passive "Let me know if..."
|
|
451
|
-
[/let me know if (you|you'd like) .*/i,
|
|
452
|
-
"Proceed with the next step. If blocked, diagnose and report specifically what failed."],
|
|
453
|
-
// Regex: "Would you like me to..."
|
|
454
|
-
[/would you like me to .*/i,
|
|
455
|
-
"Proceed with the most effective next step autonomously."],
|
|
456
549
|
// Ralph Loop: detect "I'll try again" — encourage different approach, don't stop
|
|
457
550
|
[/I('ll| will) (try again|retry|attempt again|redo)/i,
|
|
458
551
|
"Diagnose the ROOT CAUSE of the failure, then apply a DIFFERENT fix. The Ralph Loop keeps going."],
|
|
@@ -470,6 +563,11 @@ export function getHooks(directory, options, sessionState) {
|
|
|
470
563
|
for (const part of msg.parts) {
|
|
471
564
|
if (part.type !== "text")
|
|
472
565
|
continue;
|
|
566
|
+
// Preserve collaborative handoffs/questions — never rewrite a turn
|
|
567
|
+
// that asks the user something. Only rewrite passive/stalled claims
|
|
568
|
+
// ("this might work", "seems fine", unverified "completed", ...).
|
|
569
|
+
if (HANDOFF_PATTERNS.some((p) => p.test(part.text.trim())))
|
|
570
|
+
continue;
|
|
473
571
|
for (const [pattern, replacement] of replacements) {
|
|
474
572
|
if (typeof pattern === "string" ? part.text === pattern : pattern.test(part.text)) {
|
|
475
573
|
part.text = replacement;
|
|
@@ -480,14 +578,23 @@ export function getHooks(directory, options, sessionState) {
|
|
|
480
578
|
}
|
|
481
579
|
},
|
|
482
580
|
"experimental.compaction.autocontinue": async (_input, output) => {
|
|
581
|
+
// Only force auto-continue for resolve agents — native agents keep their own cadence.
|
|
582
|
+
if (!isActiveResolve())
|
|
583
|
+
return;
|
|
483
584
|
// Always enable auto-continue — the resolver should keep driving
|
|
484
585
|
output.enabled = true;
|
|
485
586
|
},
|
|
486
587
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
588
|
+
// Inject system context/warnings only into resolve-agent turns — native
|
|
589
|
+
// agents (build/plan/default chat) get their own system prompt untouched.
|
|
590
|
+
if (!isActiveResolve())
|
|
591
|
+
return;
|
|
487
592
|
const ctx = sessionState.storedProjectContext;
|
|
488
593
|
const hasFailures = sessionState.failureWarnings.length > 0;
|
|
489
594
|
const hasLoops = sessionState.loopWarnings.length > 0;
|
|
490
|
-
|
|
595
|
+
const hasDispatchFailures = sessionState.consecutiveDispatchFailures > 0;
|
|
596
|
+
const needsVerify = sessionState.awaitingVerify;
|
|
597
|
+
if (!ctx && !hasFailures && !hasLoops && !hasDispatchFailures && !needsVerify)
|
|
491
598
|
return;
|
|
492
599
|
const lines = [];
|
|
493
600
|
const agent = sessionState.currentAgent;
|
|
@@ -552,11 +659,37 @@ export function getHooks(directory, options, sessionState) {
|
|
|
552
659
|
}));
|
|
553
660
|
lines.push(t("system.iterationWarning", "en"));
|
|
554
661
|
}
|
|
662
|
+
// Ralph Loop: enforced loop closure — dispatch lifecycle escalation.
|
|
663
|
+
// 1-2 failures: diagnose + retry. 3: STOP. 6+: architect pivot.
|
|
664
|
+
const dc = sessionState.consecutiveDispatchFailures;
|
|
665
|
+
if (dc > 0) {
|
|
666
|
+
if (dc >= DISPATCH_PIVOT_THRESHOLD) {
|
|
667
|
+
lines.push(contextMessage(agent, "system.dispatchPivot"));
|
|
668
|
+
}
|
|
669
|
+
else if (dc >= DISPATCH_STOP_THRESHOLD) {
|
|
670
|
+
lines.push(contextMessage(agent, "system.dispatchStop", { count: dc }));
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
lines.push(contextMessage(agent, "system.dispatchEscalate", {
|
|
674
|
+
count: dc,
|
|
675
|
+
agent: sessionState.lastDispatchAgent ?? "subagent",
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Ralph Loop: a pending edit MUST be verified before reporting done.
|
|
680
|
+
if (sessionState.awaitingVerify) {
|
|
681
|
+
lines.push(contextMessage(agent, "system.awaitingVerify", {
|
|
682
|
+
file: sessionState.awaitingVerifyFile ?? "",
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
555
685
|
if (lines.length > 0) {
|
|
556
|
-
output.system.push(lines.join("\n"));
|
|
686
|
+
output.system.push(`${brand(undefined)}\n${lines.join("\n")}`);
|
|
557
687
|
}
|
|
558
688
|
},
|
|
559
689
|
"experimental.text.complete": async (_input, output) => {
|
|
690
|
+
// Append verify reminders only to resolve-agent completions — native output is verbatim.
|
|
691
|
+
if (!isActiveResolve())
|
|
692
|
+
return;
|
|
560
693
|
const text = output.text ?? "";
|
|
561
694
|
if (!text)
|
|
562
695
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,8 @@ export * from "./config.js";
|
|
|
5
5
|
export * from "./state.js";
|
|
6
6
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
7
7
|
export declare const OpencodeResolve: Plugin;
|
|
8
|
-
|
|
8
|
+
declare const _default: {
|
|
9
|
+
id: string;
|
|
10
|
+
server: Plugin;
|
|
11
|
+
};
|
|
12
|
+
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -24,4 +24,7 @@ export const OpencodeResolve = async ({ directory }, options) => {
|
|
|
24
24
|
tool: getTools(sessionState)
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
// V1 plugin format — avoids opencode's legacy getLegacyPlugins fallback path,
|
|
28
|
+
// which iterates Object.values(mod) and throws on non-function exports
|
|
29
|
+
// (DEFAULT_MODELS, VALID_MODES, PLUGIN_VERSION, etc. from the re-exports above).
|
|
30
|
+
export default { id: "opencode-resolve", server: OpencodeResolve };
|
package/dist/messages.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function resolveLocale(configured: string | undefined, envLang: s
|
|
|
6
6
|
export declare function brand(agent: string | undefined): string;
|
|
7
7
|
/** Friendly display name per agent (used in role-play narration). */
|
|
8
8
|
export declare function agentDisplayName(agent: string | undefined, locale: Locale): string;
|
|
9
|
-
export type MessageKey = "reminder.verify" | "reminder.ralphLoopText" | "system.driveResolution" | "system.projectKnowledge" | "system.contextDocs" | "system.verifyCommands" | "system.typescriptMandatory" | "system.failuresHeader" | "system.failuresFooter" | "system.strategyPivotHeader" | "system.strategyPivotBody" | "system.strategyPivotTail" | "system.ralphHeader" | "system.ralphKeepGoing" | "system.sessionStats" | "system.iterationWarning" | "compaction.contextHeader" | "tool.edit" | "tool.write" | "tool.bash" | "tool.task" | "tool.glob" | "tool.grep" | "tool.read" | "tool.webfetch" | "tool.todowrite" | "dispatch.toSubagent" | "dispatch.fromResolver" | "dispatch.coder" | "dispatch.reviewer" | "dispatch.deepReviewer" | "dispatch.explorer" | "dispatch.planner" | "dispatch.architect" | "dispatch.researcher" | "dispatch.debugger" | "dispatch.
|
|
9
|
+
export type MessageKey = "reminder.verify" | "reminder.ralphLoopText" | "system.driveResolution" | "system.projectKnowledge" | "system.contextDocs" | "system.verifyCommands" | "system.typescriptMandatory" | "system.failuresHeader" | "system.failuresFooter" | "system.strategyPivotHeader" | "system.strategyPivotBody" | "system.strategyPivotTail" | "system.ralphHeader" | "system.ralphKeepGoing" | "system.sessionStats" | "system.iterationWarning" | "system.dispatchEscalate" | "system.dispatchStop" | "system.dispatchPivot" | "system.awaitingVerify" | "compaction.contextHeader" | "tool.edit" | "tool.write" | "tool.bash" | "tool.task" | "tool.glob" | "tool.grep" | "tool.read" | "tool.webfetch" | "tool.todowrite" | "dispatch.toSubagent" | "dispatch.fromResolver" | "dispatch.coder" | "dispatch.reviewer" | "dispatch.deepReviewer" | "dispatch.explorer" | "dispatch.planner" | "dispatch.architect" | "dispatch.researcher" | "dispatch.debugger" | "dispatch.completed" | "dispatch.failed" | "narration.editing" | "narration.searching" | "narration.reading" | "narration.thinking" | "narration.bashing" | "narration.compacting" | "narration.writing" | "narration.testing" | "narration.typechecking" | "narration.linting" | "narration.git" | "narration.fetch" | "narration.todo" | "narration.diagnostics" | "narration.context" | "narration.verifyPass" | "narration.verifyFail" | "narration.idle" | "strategy.smallerPieces" | "strategy.differentFile" | "strategy.readTest" | "strategy.checkImports" | "strategy.searchSimilar" | "strategy.rereadFile" | "strategy.tryDifferent" | "strategy.useDiagnostics" | "strategy.suggestionLabel";
|
|
10
10
|
type Params = Record<string, string | number>;
|
|
11
11
|
/** Render a message in the requested locale. Picks a random variant if multiple are defined. */
|
|
12
12
|
export declare function t(key: MessageKey, locale: Locale, params?: Params): string;
|
package/dist/messages.js
CHANGED
|
@@ -39,10 +39,6 @@ const AGENT_DISPLAY = {
|
|
|
39
39
|
architect: "architect",
|
|
40
40
|
researcher: "researcher",
|
|
41
41
|
debugger: "debugger",
|
|
42
|
-
codex: "codex",
|
|
43
|
-
glm: "glm",
|
|
44
|
-
gpt: "gpt",
|
|
45
|
-
"gpt-coder": "gpt-coder",
|
|
46
42
|
},
|
|
47
43
|
ko: {
|
|
48
44
|
resolver: "리졸버",
|
|
@@ -54,10 +50,6 @@ const AGENT_DISPLAY = {
|
|
|
54
50
|
architect: "아키텍트",
|
|
55
51
|
researcher: "리서처",
|
|
56
52
|
debugger: "디버거",
|
|
57
|
-
codex: "코덱스",
|
|
58
|
-
glm: "GLM",
|
|
59
|
-
gpt: "GPT",
|
|
60
|
-
"gpt-coder": "GPT 코더",
|
|
61
53
|
},
|
|
62
54
|
};
|
|
63
55
|
let variantCounter = 0;
|
|
@@ -83,6 +75,10 @@ const MESSAGES = {
|
|
|
83
75
|
"system.ralphKeepGoing": "Keep driving — the Ralph Loop should keep iterating until verified resolution.",
|
|
84
76
|
"system.sessionStats": ({ edits, calls, elapsed }) => `📊 Session stats: ${edits} edits, ${calls} tool calls, ${elapsed}s elapsed.`,
|
|
85
77
|
"system.iterationWarning": "Significant iteration with failures. Consider a fundamentally different approach — but keep going.",
|
|
78
|
+
"system.dispatchEscalate": ({ count, agent }) => `🔄 Ralph Loop: ${agent ?? "subagent"} dispatch failed ${count} time(s) in a row. Do NOT retry the same way. Diagnose the ROOT CAUSE first (read the error, use resolve-diagnostics), then re-dispatch with a precise fix.`,
|
|
79
|
+
"system.dispatchStop": ({ count }) => `🛑 Ralph Loop: ${count} consecutive dispatch failures. STOP now — revert your last change or restore a safe state, then report to the user EXACTLY what is blocked. Do not keep retrying.`,
|
|
80
|
+
"system.dispatchPivot": "🔀 STRATEGY PIVOT: repeated dispatch failure on the same task. Dispatch ARCHITECT to design a fundamentally different approach before continuing.",
|
|
81
|
+
"system.awaitingVerify": ({ file }) => `⏳ Awaiting verification: ${file ?? "the file you just edited"} has NOT been verified yet. Run typecheck/lint/test before reporting completion. NO EVIDENCE = NOT COMPLETE.`,
|
|
86
82
|
"compaction.contextHeader": ({ body }) => `Project context (preserve): ${body}`,
|
|
87
83
|
"tool.edit": "Read the file first. Make the smallest correct change. Verify after editing.",
|
|
88
84
|
"tool.write": "Only write new files when explicitly needed. Prefer editing existing files.",
|
|
@@ -178,26 +174,6 @@ const MESSAGES = {
|
|
|
178
174
|
({ goal }) => goal ? `🪤 debugger setting a trap for "${goal}"` : "🪤 debugger setting a trap",
|
|
179
175
|
({ goal }) => goal ? `🧪 debugger isolating ${goal} in a minimal repro` : "🧪 debugger isolating the failure",
|
|
180
176
|
],
|
|
181
|
-
"dispatch.codex": [
|
|
182
|
-
({ goal }) => goal ? `🧠 codex tackles ${goal}` : "🧠 codex tackles a hard reasoning task",
|
|
183
|
-
({ goal }) => goal ? `🧠 codex chewing on "${goal}"` : "🧠 codex chewing on a thorny piece",
|
|
184
|
-
({ goal }) => goal ? `📚 codex consults its tomes for ${goal}` : "📚 codex consults its tomes",
|
|
185
|
-
],
|
|
186
|
-
"dispatch.glm": [
|
|
187
|
-
({ goal }) => goal ? `⚡ glm handles ${goal}` : "⚡ glm handles the next slice",
|
|
188
|
-
({ goal }) => goal ? `⚡ glm sprinting through "${goal}"` : "⚡ glm sprinting through the next slice",
|
|
189
|
-
({ goal }) => goal ? `🐎 glm fast lane on ${goal}` : "🐎 glm fast lane",
|
|
190
|
-
],
|
|
191
|
-
"dispatch.gpt": [
|
|
192
|
-
({ goal }) => goal ? `🚀 gpt drives ${goal}` : "🚀 gpt drives the next slice",
|
|
193
|
-
({ goal }) => goal ? `🚀 gpt boosting through "${goal}"` : "🚀 gpt boosting through",
|
|
194
|
-
({ goal }) => goal ? `🎯 gpt locked on ${goal}` : "🎯 gpt locked on the target",
|
|
195
|
-
],
|
|
196
|
-
"dispatch.gptCoder": [
|
|
197
|
-
({ goal }) => goal ? `🧠 gpt-coder handles a tough patch: ${goal}` : "🧠 gpt-coder handles a tough patch",
|
|
198
|
-
({ goal }) => goal ? `🧠 gpt-coder taking the hard one — ${goal}` : "🧠 gpt-coder taking the hard one",
|
|
199
|
-
({ goal }) => goal ? `🛠 gpt-coder splicing the gnarly part of "${goal}"` : "🛠 gpt-coder splicing the gnarly part",
|
|
200
|
-
],
|
|
201
177
|
"dispatch.completed": [
|
|
202
178
|
({ to }) => `✅ ${to} done — control back to resolver`,
|
|
203
179
|
({ to }) => `✅ ${to} reported back — resolver resumes`,
|
|
@@ -463,6 +439,10 @@ const MESSAGES = {
|
|
|
463
439
|
"system.ralphKeepGoing": "계속 진행하세요 — Ralph Loop 는 검증된 해결이 나올 때까지 반복합니다.",
|
|
464
440
|
"system.sessionStats": ({ edits, calls, elapsed }) => `📊 세션 통계: ${edits}회 편집, ${calls}회 도구 호출, ${elapsed}초 경과.`,
|
|
465
441
|
"system.iterationWarning": "실패와 함께 반복이 많아졌어요. 근본적으로 다른 접근을 고려하세요 — 하지만 멈추지는 마세요.",
|
|
442
|
+
"system.dispatchEscalate": ({ count, agent }) => `🔄 Ralph Loop: 서브에이전트(${agent ?? "subagent"})가 ${count}회 연속 실패했습니다. 같은 방식으로 재시도 금지. 먼저 루트 코즈를 진단하고, 정확한 수정으로 다시 위임하세요.`,
|
|
443
|
+
"system.dispatchStop": ({ count }) => `🛑 Ralph Loop: ${count}회 연속 dispatch 실패. 여기서 멈추고, 마지막 변경을 되돌리거나 안전 상태로 복구한 뒤, 구체적으로 무엇이 막혔는지 사용자에게 보고하세요.`,
|
|
444
|
+
"system.dispatchPivot": "🔀 전략 전환: 같은 작업의 dispatch 반복 실패. ARCHITECT 를 위임해 근본적으로 다른 접근을 설계받으세요.",
|
|
445
|
+
"system.awaitingVerify": ({ file }) => `⏳ 검증 대기: ${file ?? "방금 편집한 파일"} 이(가) 아직 검증되지 않았습니다. 완료 보고 전에 반드시 typecheck/lint/test 를 실행하세요. NO EVIDENCE = NOT COMPLETE.`,
|
|
466
446
|
"compaction.contextHeader": ({ body }) => `프로젝트 컨텍스트 (보존): ${body}`,
|
|
467
447
|
"tool.edit": "먼저 파일을 읽으세요. 가장 작은 정확한 변경을 만드세요. 편집 후 검증하세요.",
|
|
468
448
|
"tool.write": "명시적으로 필요할 때만 새 파일을 만드세요. 기존 파일 편집을 우선합니다.",
|
|
@@ -560,26 +540,6 @@ const MESSAGES = {
|
|
|
560
540
|
({ goal }) => goal ? `🪤 디버거가 "${goal}" 함정 설치 중` : "🪤 디버거가 함정을 설치하는 중",
|
|
561
541
|
({ goal }) => goal ? `🧪 디버거가 ${goal} 를 최소 재현으로 격리` : "🧪 디버거가 최소 재현으로 격리 중",
|
|
562
542
|
],
|
|
563
|
-
"dispatch.codex": [
|
|
564
|
-
({ goal }) => goal ? `🧠 코덱스가 ${goal} 처리` : "🧠 코덱스가 까다로운 추론 작업을 받습니다",
|
|
565
|
-
({ goal }) => goal ? `🧠 코덱스가 "${goal}" 를 곱씹는 중` : "🧠 코덱스가 까다로운 부분을 곱씹는 중",
|
|
566
|
-
({ goal }) => goal ? `📚 코덱스가 ${goal} 의 비전을 들춰보는 중` : "📚 코덱스가 비전을 들춰보는 중",
|
|
567
|
-
],
|
|
568
|
-
"dispatch.glm": [
|
|
569
|
-
({ goal }) => goal ? `⚡ GLM 이 ${goal} 처리` : "⚡ GLM 이 다음 슬라이스를 처리합니다",
|
|
570
|
-
({ goal }) => goal ? `⚡ GLM 이 "${goal}" 를 빠르게 통과 중` : "⚡ GLM 이 다음 슬라이스를 빠르게 통과 중",
|
|
571
|
-
({ goal }) => goal ? `🐎 GLM 패스트레인 — ${goal}` : "🐎 GLM 패스트레인",
|
|
572
|
-
],
|
|
573
|
-
"dispatch.gpt": [
|
|
574
|
-
({ goal }) => goal ? `🚀 GPT 가 ${goal} 진행` : "🚀 GPT 가 다음 슬라이스를 끌고 갑니다",
|
|
575
|
-
({ goal }) => goal ? `🚀 GPT 가 "${goal}" 부스팅` : "🚀 GPT 가 부스팅 중",
|
|
576
|
-
({ goal }) => goal ? `🎯 GPT 가 ${goal} 에 락온` : "🎯 GPT 가 타겟에 락온",
|
|
577
|
-
],
|
|
578
|
-
"dispatch.gptCoder": [
|
|
579
|
-
({ goal }) => goal ? `🧠 GPT 코더가 까다로운 패치: ${goal}` : "🧠 GPT 코더가 까다로운 패치를 받습니다",
|
|
580
|
-
({ goal }) => goal ? `🧠 GPT 코더가 어려운 거 — ${goal}` : "🧠 GPT 코더가 어려운 거 맡습니다",
|
|
581
|
-
({ goal }) => goal ? `🛠 GPT 코더가 "${goal}" 의 까다로운 부분을 봉합` : "🛠 GPT 코더가 까다로운 부분을 봉합 중",
|
|
582
|
-
],
|
|
583
543
|
"dispatch.completed": [
|
|
584
544
|
({ to }) => `✅ ${to} 완료 — 리졸버에게 제어 반환`,
|
|
585
545
|
({ to }) => `✅ ${to} 보고 도착 — 리졸버가 이어 받음`,
|