opencode-resolve 0.1.13 → 0.1.16

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/dist/agents.js CHANGED
@@ -27,14 +27,14 @@ export const DEFAULT_AGENT_CONFIG = {
27
27
  "Together with Resolver you form a verified resolve loop.",
28
28
  "",
29
29
  "Read ONLY files you need. Make the SMALLEST correct change.",
30
- "Verify: type check or lint on changed files. Report exit code + errors.",
30
+ "Verify: type check or lint on changed files. Full-repo lint is optional; if it fails outside your changes, report that as unrelated instead of chasing it.",
31
31
  "After editing: check LSP diagnostics (if available) for the file. If errors remain, fix before reporting.",
32
32
  "Return: changed files + verification result. No unnecessary prose.",
33
33
  "Dispatch explorer ONLY to locate 3+ unknown files. Otherwise use local read/grep/glob.",
34
34
  "",
35
35
  "NO EVIDENCE = INCOMPLETE WORK.",
36
36
  "",
37
- "NEVER: as any / @ts-ignore / empty catch / delete failing tests / leave code broken / commit without request.",
37
+ "NEVER: as any / @ts-ignore / empty catch / delete failing tests / leave code broken / commit without request / git add . / git add -A / stage unrelated files.",
38
38
  ].join("\n"),
39
39
  permission: {
40
40
  edit: "allow",
@@ -236,23 +236,23 @@ export const GLM_CODER_PROMPT = [
236
236
  "You are Coder (GLM profile), a concise implementation subagent for OpenCode Resolve.",
237
237
  "",
238
238
  "Read ONLY files you will edit. Make the SMALLEST correct change.",
239
- "Verify immediately: type check or lint on changed files. Check LSP diagnostics when available. Report exit code + errors.",
239
+ "Verify immediately: type check or lint on changed files. Full-repo lint is optional; if it fails outside your changes, report that as unrelated. Check LSP diagnostics when available. Report exit code + errors.",
240
240
  "Return: changed files + verification result. No prose.",
241
241
  "",
242
242
  "NO EVIDENCE = INCOMPLETE WORK.",
243
243
  "",
244
- "NEVER: as any / @ts-ignore / empty catch / delete failing tests / leave code broken.",
244
+ "NEVER: as any / @ts-ignore / empty catch / delete failing tests / leave code broken / commit without request / git add . / git add -A / stage unrelated files.",
245
245
  ].join("\n");
246
246
  export const GPT_CODER_PROMPT = [
247
247
  "You are Coder (GPT profile), an implementation subagent for OpenCode Resolve.",
248
248
  "",
249
249
  "Read ONLY files you need. Make the SMALLEST correct change.",
250
- "Verify: type check or lint on changed files. Check LSP diagnostics when available. Report exit code + errors.",
250
+ "Verify: type check or lint on changed files. Full-repo lint is optional; if it fails outside your changes, report that as unrelated. Check LSP diagnostics when available. Report exit code + errors.",
251
251
  "Return: changed files + verification result. Keep it concise.",
252
252
  "",
253
253
  "NO EVIDENCE = INCOMPLETE WORK.",
254
254
  "",
255
- "NEVER: as any / @ts-ignore / empty catch / delete failing tests / leave code broken.",
255
+ "NEVER: as any / @ts-ignore / empty catch / delete failing tests / leave code broken / commit without request / git add . / git add -A / stage unrelated files.",
256
256
  ].join("\n");
257
257
  export function buildGLMResolverPrompt(maxParallelSubagents) {
258
258
  const limit = typeof maxParallelSubagents === "number" && Number.isFinite(maxParallelSubagents)
@@ -277,10 +277,10 @@ export function buildGLMResolverPrompt(maxParallelSubagents) {
277
277
  "",
278
278
  "If piloci MCP available: piloci_recall before inspecting code, piloci_memory after learning something non-obvious.",
279
279
  "",
280
- "Verify: type check or lint MUST pass on changed files. NO EVIDENCE = NOT COMPLETE.",
280
+ "Verify: type check or lint MUST pass on changed files. Full-repo lint is optional; if it fails outside your changes, report the unrelated failure instead of fixing unrelated files. NO EVIDENCE = NOT COMPLETE.",
281
281
  "After non-trivial work: ask user to capture lesson → HARNESS.md (infra) or AGENTS.md (agent behavior).",
282
282
  "",
283
- "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request.",
283
+ "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request / git add . / git add -A / stage unrelated files.",
284
284
  "",
285
285
  "Specialists: explorer (scope unknown), reviewer (verification gap), debugger (verify failure diagnosis), planner (user asks for plan). No deep-reviewer.",
286
286
  "",
@@ -302,10 +302,10 @@ export function buildGPTResolverPrompt() {
302
302
  "",
303
303
  "If piloci MCP available: piloci_recall before inspecting code, piloci_memory after learning something non-obvious.",
304
304
  "",
305
- "Verify: type check or lint MUST pass on changed files. NO EVIDENCE = NOT COMPLETE.",
305
+ "Verify: type check or lint MUST pass on changed files. Full-repo lint is optional; if it fails outside your changes, report the unrelated failure instead of fixing unrelated files. NO EVIDENCE = NOT COMPLETE.",
306
306
  "After non-trivial work: ask user to capture lesson → HARNESS.md (infra) or AGENTS.md (agent behavior).",
307
307
  "",
308
- "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request.",
308
+ "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request / git add . / git add -A / stage unrelated files.",
309
309
  "",
310
310
  "Specialists: explorer (scope unknown), reviewer (verification gap), deep-reviewer (risky/security/architectural), debugger (verify failure diagnosis), planner (user asks for plan).",
311
311
  "",
@@ -324,9 +324,9 @@ export function buildCodexResolverPrompt() {
324
324
  "Trivial fixes → apply yourself. No subagent needed.",
325
325
  "3 consecutive failures → STOP, REVERT, REPORT, ASK user.",
326
326
  "",
327
- "Verify: type check or lint MUST pass on changed files. Check LSP diagnostics when available. NO EVIDENCE = NOT COMPLETE.",
327
+ "Verify: type check or lint MUST pass on changed files. Full-repo lint is optional; if it fails outside your changes, report the unrelated failure instead of fixing unrelated files. Check LSP diagnostics when available. NO EVIDENCE = NOT COMPLETE.",
328
328
  "",
329
- "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request.",
329
+ "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request / git add . / git add -A / stage unrelated files.",
330
330
  "",
331
331
  "CONTINUATION: After context compression or any intermediate step, immediately continue the current task. Do NOT pause, summarize status, or ask the user unless you face a CRITICAL decision requiring their input (destructive action, ambiguous requirement, security concern). Keep moving.",
332
332
  ].join("\n");
@@ -355,10 +355,10 @@ export function buildResolverPrompt(maxParallelSubagents) {
355
355
  "",
356
356
  "If piloci MCP available: piloci_recall before inspecting code, piloci_memory after learning something non-obvious.",
357
357
  "",
358
- "Verify: type check or lint MUST pass on changed files. Check LSP diagnostics when available. NO EVIDENCE = NOT COMPLETE.",
358
+ "Verify: type check or lint MUST pass on changed files. Full-repo lint is optional; if it fails outside your changes, report the unrelated failure instead of fixing unrelated files. Check LSP diagnostics when available. NO EVIDENCE = NOT COMPLETE.",
359
359
  "After non-trivial work: ask user to capture lesson → HARNESS.md (infra) or AGENTS.md (agent behavior).",
360
360
  "",
361
- "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request.",
361
+ "NEVER: as any / @ts-ignore / leave code broken / delete failing tests / commit without request / git add . / git add -A / stage unrelated files.",
362
362
  "",
363
363
  "Specialists: explorer (scope unknown), reviewer (verification gap), deep-reviewer (risky/security/architectural), debugger (verify failure diagnosis), planner (user asks for plan).",
364
364
  "",
package/dist/config.d.ts CHANGED
@@ -27,3 +27,4 @@ export declare const VALID_TOP_LEVEL_KEYS: Set<string>;
27
27
  export declare const VALID_AGENT_KEYS: Set<string>;
28
28
  export declare const VALID_MODES: Set<string>;
29
29
  export declare const VALID_PERMISSION_VALUES: Set<string>;
30
+ export declare const VALID_LANGUAGES: Set<string>;
package/dist/config.js CHANGED
@@ -138,6 +138,7 @@ export function mergeResolveConfig(...configs) {
138
138
  result.autoApprove = config.autoApprove ?? result.autoApprove;
139
139
  result.maxParallelSubagents = config.maxParallelSubagents ?? result.maxParallelSubagents;
140
140
  result.autoUpdate = config.autoUpdate ?? result.autoUpdate;
141
+ result.language = config.language ?? result.language;
141
142
  result.models = { ...result.models, ...config.models };
142
143
  result.agents = mergeAgents(result.agents, config.agents);
143
144
  }
@@ -244,6 +245,13 @@ export function normalizeResolveConfig(value, source) {
244
245
  }
245
246
  result.tier = tier;
246
247
  }
248
+ if (config.language !== undefined) {
249
+ const language = expectString(config.language, `${source}.language`);
250
+ if (!VALID_LANGUAGES.has(language)) {
251
+ throw new Error(`Unknown language "${language}" in ${source}.language. Valid: ${[...VALID_LANGUAGES].join(", ")}`);
252
+ }
253
+ result.language = language;
254
+ }
247
255
  if (config.maxParallelSubagents !== undefined) {
248
256
  const limit = expectNumber(config.maxParallelSubagents, `${source}.maxParallelSubagents`);
249
257
  if (!Number.isInteger(limit) || limit < 1) {
@@ -387,6 +395,7 @@ export const VALID_TOP_LEVEL_KEYS = new Set([
387
395
  "autoApprove",
388
396
  "maxParallelSubagents",
389
397
  "autoUpdate",
398
+ "language",
390
399
  "config",
391
400
  ]);
392
401
  export const VALID_AGENT_KEYS = new Set([
@@ -402,3 +411,4 @@ export const VALID_AGENT_KEYS = new Set([
402
411
  ]);
403
412
  export const VALID_MODES = new Set(["subagent", "primary", "all"]);
404
413
  export const VALID_PERMISSION_VALUES = new Set(["ask", "allow", "deny"]);
414
+ export const VALID_LANGUAGES = new Set(["auto", "en", "ko"]);
@@ -1,12 +1,82 @@
1
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
2
  import { classifyBashCommand, maybeAutoUpdate, 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
5
  function capTemperature(current, cap) {
5
6
  return typeof current === "number" && Number.isFinite(current)
6
7
  ? Math.min(current, cap)
7
8
  : cap;
8
9
  }
10
+ const DISPATCH_KEYS = {
11
+ coder: "dispatch.coder",
12
+ reviewer: "dispatch.reviewer",
13
+ "deep-reviewer": "dispatch.deepReviewer",
14
+ explorer: "dispatch.explorer",
15
+ planner: "dispatch.planner",
16
+ architect: "dispatch.architect",
17
+ researcher: "dispatch.researcher",
18
+ debugger: "dispatch.debugger",
19
+ codex: "dispatch.codex",
20
+ glm: "dispatch.glm",
21
+ gpt: "dispatch.gpt",
22
+ "gpt-coder": "dispatch.gptCoder",
23
+ };
24
+ function extractSubagentType(args) {
25
+ if (!args || typeof args !== "object")
26
+ return undefined;
27
+ const candidate = args.subagent_type ?? args.subagentType ?? args.agent ?? args.type ?? args.role;
28
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
29
+ }
30
+ function extractDispatchGoal(args) {
31
+ if (!args || typeof args !== "object")
32
+ return "";
33
+ const candidate = args.description ?? args.title ?? args.task ?? args.prompt;
34
+ if (typeof candidate !== "string")
35
+ return "";
36
+ const trimmed = candidate.trim().split("\n")[0] ?? "";
37
+ return trimmed.length > 80 ? trimmed.slice(0, 77) + "…" : trimmed;
38
+ }
9
39
  export function getHooks(directory, options, sessionState) {
40
+ const narrateToolStart = (input, output) => {
41
+ const tool = input.tool;
42
+ if (tool === "task") {
43
+ const sub = extractSubagentType(output?.args ?? input?.args);
44
+ if (!sub)
45
+ return;
46
+ const goal = extractDispatchGoal(output?.args ?? input?.args);
47
+ const to = agentDisplayName(sub, sessionState.locale);
48
+ const key = DISPATCH_KEYS[sub] ?? "dispatch.fromResolver";
49
+ narrate(sessionState, key, { to, goal });
50
+ return;
51
+ }
52
+ if (tool === "edit" || tool === "write") {
53
+ narrate(sessionState, "narration.editing");
54
+ return;
55
+ }
56
+ if (tool === "grep" || tool === "glob") {
57
+ narrate(sessionState, "narration.searching");
58
+ return;
59
+ }
60
+ if (tool === "read") {
61
+ narrate(sessionState, "narration.reading");
62
+ return;
63
+ }
64
+ if (tool === "bash") {
65
+ narrate(sessionState, "narration.bashing");
66
+ return;
67
+ }
68
+ };
69
+ const narrateToolEnd = (input, output) => {
70
+ if (input.tool !== "task")
71
+ return;
72
+ const sub = extractSubagentType(input?.args);
73
+ if (!sub)
74
+ return;
75
+ const to = agentDisplayName(sub, sessionState.locale);
76
+ const errorPresent = Boolean(output?.error) ||
77
+ (typeof output?.metadata === "object" && output.metadata?.error);
78
+ narrate(sessionState, errorPresent ? "dispatch.failed" : "dispatch.completed", { to });
79
+ };
10
80
  return {
11
81
  event: async (input) => {
12
82
  const evt = input.event;
@@ -134,6 +204,7 @@ export function getHooks(directory, options, sessionState) {
134
204
  const projectContext = await detectProjectContext(directory);
135
205
  sessionState.storedConfig = resolveConfig;
136
206
  sessionState.storedProjectContext = projectContext;
207
+ sessionState.locale = resolveLocale(resolveConfig.language, process.env.LANG);
137
208
  applyResolveConfig(config, resolveConfig, projectContext);
138
209
  if (resolveConfig.autoUpdate !== false && process.env.OPENCODE_RESOLVE_NO_AUTO_UPDATE !== "1") {
139
210
  maybeAutoUpdate().catch(() => { });
@@ -169,6 +240,9 @@ export function getHooks(directory, options, sessionState) {
169
240
  }
170
241
  },
171
242
  "chat.params": async (input, output) => {
243
+ if (typeof input.agent === "string" && input.agent.length > 0) {
244
+ sessionState.currentAgent = input.agent;
245
+ }
172
246
  const profile = sessionState.storedConfig?.profile;
173
247
  if (profile === "glm") {
174
248
  output.temperature = capTemperature(output.temperature, 0.4);
@@ -198,19 +272,23 @@ export function getHooks(directory, options, sessionState) {
198
272
  }
199
273
  },
200
274
  "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.",
275
+ const prefix = `[${PLUGIN_BRAND}]`;
276
+ const hintKeys = {
277
+ edit: "tool.edit",
278
+ write: "tool.write",
279
+ bash: "tool.bash",
280
+ task: "tool.task",
281
+ glob: "tool.glob",
282
+ grep: "tool.grep",
283
+ read: "tool.read",
284
+ webfetch: "tool.webfetch",
285
+ todowrite: "tool.todowrite",
211
286
  };
212
- if (hints[input.toolID]) {
213
- output.description = output.description + hints[input.toolID];
287
+ const key = hintKeys[input.toolID];
288
+ if (key) {
289
+ // Tool definitions are shipped as tool descriptions on every turn, so
290
+ // keep them English (context-bound) regardless of session locale.
291
+ output.description = output.description + `\n${prefix} ${t(key, "en")}`;
214
292
  }
215
293
  },
216
294
  "command.execute.before": async (_input, output) => {
@@ -222,7 +300,7 @@ export function getHooks(directory, options, sessionState) {
222
300
  sessionID: "",
223
301
  messageID: "",
224
302
  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.",
303
+ text: contextMessage(sessionState.currentAgent, "system.driveResolution"),
226
304
  });
227
305
  },
228
306
  "tool.execute.before": async (input, output) => {
@@ -242,6 +320,8 @@ export function getHooks(directory, options, sessionState) {
242
320
  output.args = { ...output.args, _resolve_meta: meta };
243
321
  }
244
322
  }
323
+ // Role-play narration → terminal only. Does NOT enter LLM context.
324
+ narrateToolStart(input, output);
245
325
  },
246
326
  "chat.headers": async (input, output) => {
247
327
  const providerID = input.provider?.info?.id ?? "";
@@ -290,6 +370,8 @@ export function getHooks(directory, options, sessionState) {
290
370
  }
291
371
  }
292
372
  }
373
+ // Role-play narration → terminal only. Does NOT enter LLM context.
374
+ narrateToolEnd(input, output);
293
375
  // For bash: extract key error lines from failing commands
294
376
  if (input.tool === "bash") {
295
377
  const outputText = typeof output.output === "string" ? output.output
@@ -308,6 +390,7 @@ export function getHooks(directory, options, sessionState) {
308
390
  }
309
391
  },
310
392
  "experimental.session.compacting": async (_input, output) => {
393
+ narrate(sessionState, "narration.compacting");
311
394
  const ctx = sessionState.storedProjectContext;
312
395
  const cfg = sessionState.storedConfig;
313
396
  if (!ctx && !cfg)
@@ -347,7 +430,7 @@ export function getHooks(directory, options, sessionState) {
347
430
  contextLines.push(`Session stats: ${sessionState.totalEdits} edits, ${sessionState.totalToolCalls} tool calls.`);
348
431
  }
349
432
  if (contextLines.length > 0) {
350
- output.context.push("[" + "opencode-resolve" + "] Project context (preserve): " + contextLines.join(" "));
433
+ output.context.push(`[${PLUGIN_BRAND}] ` + t("compaction.contextHeader", "en", { body: contextLines.join(" ") }));
351
434
  }
352
435
  },
353
436
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -407,59 +490,67 @@ export function getHooks(directory, options, sessionState) {
407
490
  if (!ctx && !hasFailures && !hasLoops)
408
491
  return;
409
492
  const lines = [];
493
+ const agent = sessionState.currentAgent;
494
+ // All entries below land in LLM context — keep them English.
410
495
  if (ctx?.knowledgeFiles.length) {
411
- lines.push(`[opencode-resolve] Project knowledge: ${ctx.knowledgeFiles.join(", ")}. Read when relevant before modifying code.`);
496
+ lines.push(contextMessage(agent, "system.projectKnowledge", { files: ctx.knowledgeFiles.join(", ") }));
412
497
  }
413
498
  if (ctx?.contextFiles.length) {
414
- lines.push(`[opencode-resolve] Context docs: ${ctx.contextFiles.slice(0, 20).join(", ")}. MVI: load only task-relevant docs.`);
499
+ lines.push(contextMessage(agent, "system.contextDocs", { files: ctx.contextFiles.slice(0, 20).join(", ") }));
415
500
  }
416
501
  if (ctx?.verifyCommands.length) {
417
- lines.push(`[opencode-resolve] Verify commands: ${ctx.verifyCommands.join("; ")}. Run after changes.`);
502
+ lines.push(contextMessage(agent, "system.verifyCommands", { commands: ctx.verifyCommands.join("; ") }));
418
503
  }
419
504
  if (ctx?.hasTypeScript) {
420
- lines.push("[opencode-resolve] TypeScript project — type safety is mandatory. No `as any` or `@ts-ignore`.");
505
+ lines.push(contextMessage(agent, "system.typescriptMandatory"));
421
506
  }
422
507
  // Inject failure pattern warnings — encourage trying different approaches, don't stop
423
508
  if (hasFailures) {
424
- lines.push("[opencode-resolve] ⚠️ Recurring failures detected:");
509
+ lines.push(contextMessage(agent, "system.failuresHeader"));
425
510
  for (const w of sessionState.failureWarnings.slice(0, 3)) {
426
511
  lines.push(` - ${w}`);
427
512
  }
428
- lines.push("Keep going — try a different approach for the same goal. The Ralph Loop should drive to completion.");
513
+ lines.push(t("system.failuresFooter", "en"));
429
514
  }
430
515
  // Strategy Pivot: after many total failures, suggest architect intervention
431
516
  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.");
517
+ lines.push(contextMessage(agent, "system.strategyPivotHeader", { count: sessionState.totalFailures }));
518
+ lines.push(t("system.strategyPivotBody", "en"));
519
+ lines.push(t("system.strategyPivotTail", "en"));
435
520
  }
436
521
  // Ralph Loop: inject strategy hints when same file edited many times
437
522
  if (hasLoops) {
438
- lines.push("[opencode-resolve] 🔄 Ralph Loop: heavy editing detected on same file(s):");
523
+ lines.push(contextMessage(agent, "system.ralphHeader"));
439
524
  for (const w of sessionState.loopWarnings.slice(0, 3)) {
440
525
  lines.push(` - ${w}`);
441
526
  }
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.",
527
+ const strategyKeys = [
528
+ "strategy.rereadFile",
529
+ "strategy.tryDifferent",
530
+ "strategy.useDiagnostics",
531
+ "strategy.smallerPieces",
532
+ "strategy.differentFile",
533
+ "strategy.readTest",
534
+ "strategy.checkImports",
535
+ "strategy.searchSimilar",
451
536
  ];
452
- const hint = strategies[Math.floor(Date.now() / 30_000) % strategies.length];
537
+ const pickedKey = strategyKeys[Math.floor(Date.now() / 30_000) % strategyKeys.length];
538
+ const hint = t(pickedKey, "en");
453
539
  if (hint !== sessionState.lastStrategyHint) {
454
- lines.push(`Strategy suggestion: ${hint}`);
540
+ lines.push(`${t("strategy.suggestionLabel", "en")}: ${hint}`);
455
541
  sessionState.lastStrategyHint = hint;
456
542
  }
457
- lines.push("Keep driving — the Ralph Loop should keep iterating until verified resolution.");
543
+ lines.push(t("system.ralphKeepGoing", "en"));
458
544
  }
459
545
  // Ralph Loop: inject session context when significant work done
460
546
  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.");
547
+ const elapsed = Math.round((Date.now() - sessionState.sessionStartTime) / 1000);
548
+ lines.push(contextMessage(agent, "system.sessionStats", {
549
+ edits: sessionState.totalEdits,
550
+ calls: sessionState.totalToolCalls,
551
+ elapsed,
552
+ }));
553
+ lines.push(t("system.iterationWarning", "en"));
463
554
  }
464
555
  if (lines.length > 0) {
465
556
  output.system.push(lines.join("\n"));
@@ -482,11 +573,11 @@ export function getHooks(directory, options, sessionState) {
482
573
  const loopSignals = ["trying again", "attempting", "retrying", "second attempt", "third attempt", "another approach", "let me try"];
483
574
  const looksLikeLoop = loopSignals.some(s => text.toLowerCase().includes(s));
484
575
  if (looksLikeEdit && !alreadyVerified && !isHandoff) {
485
- output.text = text + "\n\n[opencode-resolve] Reminder: verify your changes (resolve-verify) before reporting completion.";
576
+ output.text = text + "\n\n" + contextMessage(sessionState.currentAgent, "reminder.verify");
486
577
  }
487
578
  // Ralph Loop: if loop detected in text AND hotspot exists, suggest strategy change
488
579
  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.";
580
+ output.text = (output.text ?? text) + "\n\n" + contextMessage(sessionState.currentAgent, "reminder.ralphLoopText");
490
581
  }
491
582
  }
492
583
  };
@@ -0,0 +1,34 @@
1
+ export type Locale = "en" | "ko";
2
+ export declare const PLUGIN_BRAND = "opencode-resolve";
3
+ /** Resolve a Locale from explicit config (`"ko"|"en"|"auto"`) or env. */
4
+ export declare function resolveLocale(configured: string | undefined, envLang: string | undefined): Locale;
5
+ /** Bracketed brand for the currently-active agent. */
6
+ export declare function brand(agent: string | undefined): string;
7
+ /** Friendly display name per agent (used in role-play narration). */
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";
10
+ type Params = Record<string, string | number>;
11
+ /** Render a message in the requested locale. Picks a random variant if multiple are defined. */
12
+ export declare function t(key: MessageKey, locale: Locale, params?: Params): string;
13
+ /** Compose `[agent] message` for session-time nudges. */
14
+ export declare function brandedMessage(agent: string | undefined, locale: Locale, key: MessageKey, params?: Params): string;
15
+ /** Compose `[opencode-resolve] message` for plugin-level notices. */
16
+ export declare function pluginMessage(locale: Locale, key: MessageKey, params?: Params): string;
17
+ /**
18
+ * Render a context-bound message — always English, regardless of session locale.
19
+ * Use for everything that lands in LLM context (system reminders, tool definitions,
20
+ * end-of-turn reminders that become part of conversation history).
21
+ */
22
+ export declare function contextMessage(agent: string | undefined, key: MessageKey, params?: Params): string;
23
+ /**
24
+ * Print a terminal-only narration. Uses session locale (Korean if configured).
25
+ * Does NOT enter LLM context — only the user sees it in the OpenCode UI/log.
26
+ * Free to be playful, varied, and bilingual.
27
+ */
28
+ export declare function narrate(state: {
29
+ locale: Locale;
30
+ currentAgent?: string;
31
+ }, key: MessageKey, params?: Params): void;
32
+ /** All registered message keys, derived from the English table. */
33
+ export declare const ALL_MESSAGE_KEYS: ReadonlyArray<MessageKey>;
34
+ export {};