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.
@@ -1,7 +1,42 @@
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";
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 '${toolName}' failed ${v.count} times. Last: ${v.lastMessage}`);
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
- if (resolveConfig.autoUpdate !== false && process.env.OPENCODE_RESOLVE_NO_AUTO_UPDATE !== "1") {
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: "true",
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
- if (action !== "ask")
239
- output.status = action;
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
- const profile = sessionState.storedConfig?.profile;
247
- if (profile === "glm") {
248
- output.temperature = capTemperature(output.temperature, 0.4);
249
- if (output.maxOutputTokens === undefined || output.maxOutputTokens > 16384) {
250
- output.maxOutputTokens = 16384;
251
- }
252
- // GLM: tighter topP for deterministic output
253
- if (output.topP === undefined || output.topP > 0.9) {
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
- else if (profile === "gpt") {
258
- output.temperature = capTemperature(output.temperature, 0.7);
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: contextMessage(sessionState.currentAgent, "system.driveResolution"),
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
- // Profile and tier info
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
- if (!ctx && !hasFailures && !hasLoops)
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
- export default OpencodeResolve;
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
- export default OpencodeResolve;
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 };
@@ -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.codex" | "dispatch.glm" | "dispatch.gpt" | "dispatch.gptCoder" | "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";
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} 보고 도착 — 리졸버가 이어 받음`,