opencode-resolve 0.1.20 → 0.3.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/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;
@@ -79,10 +71,14 @@ const MESSAGES = {
79
71
  "system.strategyPivotHeader": ({ count }) => `🔀 STRATEGY PIVOT: ${count} total failures detected.`,
80
72
  "system.strategyPivotBody": "The current approach is not working. Dispatch ARCHITECT to analyze the problem from scratch and propose a fundamentally different strategy.",
81
73
  "system.strategyPivotTail": "Then apply the new strategy. Do NOT keep retrying the same approach.",
82
- "system.ralphHeader": "🔄 Ralph Loop: heavy editing detected on same file(s):",
83
- "system.ralphKeepGoing": "Keep driving — the Ralph Loop should keep iterating until verified resolution.",
74
+ "system.ralphHeader": "🔄 Ralph Loop: heavy editing detected on same file(s). Repetition is not itself the problem — repeating the *same* edit is:",
75
+ "system.ralphKeepGoing": "Read the diff before the next edit. If the root cause really is in this file, stay here moving to an unrelated file to break the count only spreads the damage. Keep driving until verified.",
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`,
@@ -369,6 +345,11 @@ const MESSAGES = {
369
345
  "🌳 inspecting the diff",
370
346
  "🌳 listening to git",
371
347
  ],
348
+ "narration.checkpoint": [
349
+ "🛟 snapshotting the worktree before rollback",
350
+ "🛟 saving a checkpoint you can restore from",
351
+ "🛟 stashing a safety copy first",
352
+ ],
372
353
  "narration.fetch": [
373
354
  "🌐 fetching from the web",
374
355
  "🌐 grabbing the docs",
@@ -426,13 +407,14 @@ const MESSAGES = {
426
407
  "🪑 brief regroup",
427
408
  ],
428
409
  "strategy.smallerPieces": "Break the problem into smaller pieces. Edit one function at a time, verify between each.",
429
- "strategy.differentFile": "Check if the error is actually in a DIFFERENT file the real issue may be upstream.",
410
+ "strategy.differentFile": "Follow the evidence: if diagnostics point at a different file, the real cause may be upstream. Confirm that before editing anything else.",
430
411
  "strategy.readTest": "Read the test file if it exists — the test often reveals the expected behavior.",
431
412
  "strategy.checkImports": "Check imports — missing or wrong imports are a common cause of cascading errors.",
432
413
  "strategy.searchSimilar": "Use resolve-search to find similar patterns elsewhere in the codebase.",
433
414
  "strategy.rereadFile": "Re-read the file carefully. You may be missing existing code that conflicts with your edit.",
434
- "strategy.tryDifferent": "Try a completely different approach revert your last change and try a different fix.",
415
+ "strategy.tryDifferent": "The fix, not the file, is what should change. Try a different fix for the same root cause.",
435
416
  "strategy.useDiagnostics": "Use resolve-diagnostics to check current LSP errors before the next edit.",
417
+ "strategy.reviewDiff": "Run `git diff` on the hotspot file. Look for edits that repeat the same substitution or cancel each other out.",
436
418
  "strategy.suggestionLabel": "Strategy suggestion",
437
419
  },
438
420
  ko: {
@@ -459,10 +441,14 @@ const MESSAGES = {
459
441
  "system.strategyPivotHeader": ({ count }) => `🔀 전략 전환: 총 ${count}회의 실패가 감지됐어요.`,
460
442
  "system.strategyPivotBody": "지금 접근은 통하지 않아요. ARCHITECT 를 위임해서 문제를 처음부터 분석하고 근본적으로 다른 전략을 제안받으세요.",
461
443
  "system.strategyPivotTail": "그 다음 새 전략을 적용하세요. 같은 접근을 다시 시도하지 마세요.",
462
- "system.ralphHeader": "🔄 Ralph Loop: 같은 파일을 너무 자주 수정 중이에요:",
463
- "system.ralphKeepGoing": "계속 진행하세요Ralph Loop 검증된 해결이 나올 때까지 반복합니다.",
444
+ "system.ralphHeader": "🔄 Ralph Loop: 같은 파일을 반복 수정 중이에요. 반복 자체가 문제가 아니라, *같은 수정* 을 반복하는 게 문제입니다:",
445
+ "system.ralphKeepGoing": "다음 편집 전에 diff 를 읽으세요. 근본 원인이 정말 이 파일에 있다면 계속 여기서 작업하세요 횟수를 피하려고 무관한 파일로 옮겨가면 오염만 번집니다. 검증될 때까지 계속.",
464
446
  "system.sessionStats": ({ edits, calls, elapsed }) => `📊 세션 통계: ${edits}회 편집, ${calls}회 도구 호출, ${elapsed}초 경과.`,
465
447
  "system.iterationWarning": "실패와 함께 반복이 많아졌어요. 근본적으로 다른 접근을 고려하세요 — 하지만 멈추지는 마세요.",
448
+ "system.dispatchEscalate": ({ count, agent }) => `🔄 Ralph Loop: 서브에이전트(${agent ?? "subagent"})가 ${count}회 연속 실패했습니다. 같은 방식으로 재시도 금지. 먼저 루트 코즈를 진단하고, 정확한 수정으로 다시 위임하세요.`,
449
+ "system.dispatchStop": ({ count }) => `🛑 Ralph Loop: ${count}회 연속 dispatch 실패. 여기서 멈추고, 마지막 변경을 되돌리거나 안전 상태로 복구한 뒤, 구체적으로 무엇이 막혔는지 사용자에게 보고하세요.`,
450
+ "system.dispatchPivot": "🔀 전략 전환: 같은 작업의 dispatch 반복 실패. ARCHITECT 를 위임해 근본적으로 다른 접근을 설계받으세요.",
451
+ "system.awaitingVerify": ({ file }) => `⏳ 검증 대기: ${file ?? "방금 편집한 파일"} 이(가) 아직 검증되지 않았습니다. 완료 보고 전에 반드시 typecheck/lint/test 를 실행하세요. NO EVIDENCE = NOT COMPLETE.`,
466
452
  "compaction.contextHeader": ({ body }) => `프로젝트 컨텍스트 (보존): ${body}`,
467
453
  "tool.edit": "먼저 파일을 읽으세요. 가장 작은 정확한 변경을 만드세요. 편집 후 검증하세요.",
468
454
  "tool.write": "명시적으로 필요할 때만 새 파일을 만드세요. 기존 파일 편집을 우선합니다.",
@@ -560,26 +546,6 @@ const MESSAGES = {
560
546
  ({ goal }) => goal ? `🪤 디버거가 "${goal}" 함정 설치 중` : "🪤 디버거가 함정을 설치하는 중",
561
547
  ({ goal }) => goal ? `🧪 디버거가 ${goal} 를 최소 재현으로 격리` : "🧪 디버거가 최소 재현으로 격리 중",
562
548
  ],
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
549
  "dispatch.completed": [
584
550
  ({ to }) => `✅ ${to} 완료 — 리졸버에게 제어 반환`,
585
551
  ({ to }) => `✅ ${to} 보고 도착 — 리졸버가 이어 받음`,
@@ -762,6 +728,11 @@ const MESSAGES = {
762
728
  "🌳 diff 점검 중",
763
729
  "🌳 git 말 듣는 중",
764
730
  ],
731
+ "narration.checkpoint": [
732
+ "🛟 롤백 전에 작업트리 스냅샷 뜨는 중",
733
+ "🛟 복구용 체크포인트 저장 중",
734
+ "🛟 만약을 위해 안전 사본 남기는 중",
735
+ ],
765
736
  "narration.fetch": [
766
737
  "🌐 웹에서 가져오는 중",
767
738
  "🌐 문서 끌어오는 중",
@@ -819,13 +790,14 @@ const MESSAGES = {
819
790
  "🪑 잠깐 정비",
820
791
  ],
821
792
  "strategy.smallerPieces": "문제를 더 작은 단위로 쪼개세요. 한 번에 함수 하나씩 편집하고, 각 사이에 검증하세요.",
822
- "strategy.differentFile": "에러가 실제로는 다른 파일에 있는지 확인하세요 — 진짜 원인은 상류일 수 있습니다.",
793
+ "strategy.differentFile": "증거를 따라가세요: 진단이 다른 파일을 가리킨다면 진짜 원인은 상류일 수 있습니다. 다른 파일을 건드리기 전에 먼저 확인하세요.",
823
794
  "strategy.readTest": "테스트 파일이 있으면 먼저 읽으세요 — 기대 동작이 거기에 드러나 있을 때가 많습니다.",
824
795
  "strategy.checkImports": "import 를 확인하세요 — 누락되거나 잘못된 import 가 연쇄 에러의 흔한 원인입니다.",
825
796
  "strategy.searchSimilar": "resolve-search 로 코드베이스 내 유사 패턴을 찾으세요.",
826
797
  "strategy.rereadFile": "파일을 다시 차분히 읽으세요. 편집과 충돌하는 기존 코드를 놓쳤을 수 있습니다.",
827
- "strategy.tryDifferent": "완전히 다른 접근을 시도하세요 마지막 변경을 되돌리고 다른 방법으로 고치세요.",
798
+ "strategy.tryDifferent": "바꿔야 파일이 아니라 해법입니다. 같은 근본 원인에 대해 다른 수정을 시도하세요.",
828
799
  "strategy.useDiagnostics": "다음 편집 전에 resolve-diagnostics 로 현재 LSP 에러를 확인하세요.",
800
+ "strategy.reviewDiff": "핫스팟 파일에 `git diff` 를 실행하세요. 같은 치환을 반복했거나 서로 상쇄되는 편집이 있는지 확인하세요.",
829
801
  "strategy.suggestionLabel": "전략 제안",
830
802
  },
831
803
  };
package/dist/state.d.ts CHANGED
@@ -6,6 +6,8 @@ export declare const FAILURE_THRESHOLD = 10;
6
6
  export declare const STRATEGY_PIVOT_THRESHOLD = 20;
7
7
  export declare const EDIT_HOTSPOT_THRESHOLD = 10;
8
8
  export declare const EDIT_HOTSPOT_TTL_MS = 600000;
9
+ export declare const DISPATCH_STOP_THRESHOLD = 3;
10
+ export declare const DISPATCH_PIVOT_THRESHOLD = 6;
9
11
  export interface SessionState {
10
12
  storedConfig?: ResolveConfig;
11
13
  storedProjectContext?: ProjectContext;
@@ -13,6 +15,10 @@ export interface SessionState {
13
15
  errors: number;
14
16
  warnings: number;
15
17
  timestamp: number;
18
+ errorMessages?: {
19
+ line: number;
20
+ message: string;
21
+ }[];
16
22
  }>;
17
23
  failurePatterns: Map<string, {
18
24
  count: number;
@@ -27,9 +33,16 @@ export interface SessionState {
27
33
  }>;
28
34
  totalEdits: number;
29
35
  totalToolCalls: number;
36
+ totalOutputBytes: number;
30
37
  sessionStartTime: number;
31
38
  loopWarnings: string[];
32
39
  lastStrategyHint: string;
40
+ consecutiveDispatchFailures: number;
41
+ lastDispatchAgent?: string;
42
+ lastDispatchSucceeded?: boolean;
43
+ lastDispatchAt?: number;
44
+ awaitingVerify: boolean;
45
+ awaitingVerifyFile?: string;
33
46
  currentAgent?: string;
34
47
  locale: Locale;
35
48
  }
package/dist/state.js CHANGED
@@ -4,6 +4,12 @@ export const FAILURE_THRESHOLD = 10;
4
4
  export const STRATEGY_PIVOT_THRESHOLD = 20;
5
5
  export const EDIT_HOTSPOT_THRESHOLD = 10;
6
6
  export const EDIT_HOTSPOT_TTL_MS = 600_000;
7
+ // Ralph Loop: dispatch lifecycle thresholds. The resolver→coder loop must
8
+ // close: dispatch → verify → (fail: diagnose+redispatch | pass: done).
9
+ // After DISPATCH_STOP_THRESHOLD consecutive failed dispatches, force a STOP &
10
+ // report. After DISPATCH_PIVOT_THRESHOLD, force an architect rethink.
11
+ export const DISPATCH_STOP_THRESHOLD = 3;
12
+ export const DISPATCH_PIVOT_THRESHOLD = 6;
7
13
  export function createSessionState() {
8
14
  return {
9
15
  recentDiagnostics: new Map(),
@@ -13,9 +19,12 @@ export function createSessionState() {
13
19
  editHotspots: new Map(),
14
20
  totalEdits: 0,
15
21
  totalToolCalls: 0,
22
+ totalOutputBytes: 0,
16
23
  sessionStartTime: Date.now(),
17
24
  loopWarnings: [],
18
25
  lastStrategyHint: "",
26
+ consecutiveDispatchFailures: 0,
27
+ awaitingVerify: false,
19
28
  locale: "en"
20
29
  };
21
30
  }
@@ -1,4 +1,5 @@
1
1
  import { SessionState } from "../state.js";
2
+ export declare function summarizeTestFailure(stdout: string, stderr: string, runner: string | undefined): string;
2
3
  export declare function getTools(sessionState: SessionState): {
3
4
  "resolve-verify": {
4
5
  description: string;
@@ -13,9 +14,11 @@ export declare function getTools(sessionState: SessionState): {
13
14
  description: string;
14
15
  args: {
15
16
  path: import("zod").ZodOptional<import("zod").ZodString>;
17
+ context: import("zod").ZodOptional<import("zod").ZodBoolean>;
16
18
  };
17
19
  execute(args: {
18
20
  path?: string | undefined;
21
+ context?: boolean | undefined;
19
22
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
20
23
  };
21
24
  "resolve-context": {
@@ -56,11 +59,13 @@ export declare function getTools(sessionState: SessionState): {
56
59
  file: import("zod").ZodOptional<import("zod").ZodString>;
57
60
  pattern: import("zod").ZodOptional<import("zod").ZodString>;
58
61
  runner: import("zod").ZodOptional<import("zod").ZodString>;
62
+ raw: import("zod").ZodOptional<import("zod").ZodBoolean>;
59
63
  };
60
64
  execute(args: {
61
65
  file?: string | undefined;
62
66
  pattern?: string | undefined;
63
67
  runner?: string | undefined;
68
+ raw?: boolean | undefined;
64
69
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
65
70
  };
66
71
  "resolve-pattern": {
@@ -219,8 +224,12 @@ export declare function getTools(sessionState: SessionState): {
219
224
  };
220
225
  "resolve-session": {
221
226
  description: string;
222
- args: {};
223
- execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
227
+ args: {
228
+ full: import("zod").ZodOptional<import("zod").ZodBoolean>;
229
+ };
230
+ execute(args: {
231
+ full?: boolean | undefined;
232
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
224
233
  };
225
234
  "resolve-audit": {
226
235
  description: string;
@@ -6,8 +6,8 @@ import { fileURLToPath } from "node:url";
6
6
  import { dirname } from "node:path";
7
7
  import { classifyBashCommand, runCommand, sanitizeShellArg, truncateOutput, PLUGIN_VERSION } from "../utils.js";
8
8
  import { DIAGNOSTICS_TTL_MS } from "../state.js";
9
- import { VALID_PROFILES, VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
10
- const WRITE_CAPABLE_AGENTS = new Set(["resolver", "codex", "coder", "glm", "gpt-coder", "debugger"]);
9
+ import { VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
10
+ const WRITE_CAPABLE_AGENTS = new Set(["resolver", "coder", "debugger"]);
11
11
  function canWriteFromTool(ctx) {
12
12
  return typeof ctx.agent !== "string" || WRITE_CAPABLE_AGENTS.has(ctx.agent);
13
13
  }
@@ -15,6 +15,10 @@ function readOnlyToolWriteDenied(ctx, action) {
15
15
  return `Permission denied: agent '${ctx.agent ?? "unknown"}' is read-only and cannot ${action}. Dispatch resolver/coder for workspace writes.`;
16
16
  }
17
17
  function commandExecutionDenied(command) {
18
+ // Deliberately called without `permissions` — this path executes directly and
19
+ // never reaches `tool.execute.before`, so a rollback here would run without a
20
+ // checkpoint. `permissions.allowGitReset`/`allowGitClean` only relax the
21
+ // OpenCode bash tool, which is checkpointed.
18
22
  const action = classifyBashCommand(command);
19
23
  if (action === "allow")
20
24
  return undefined;
@@ -23,6 +27,60 @@ function commandExecutionDenied(command) {
23
27
  }
24
28
  return `Command is not allowlisted for direct tool execution: ${command}. Run it through OpenCode bash so the normal permission flow can decide.`;
25
29
  }
30
+ // Phase 1.2: parse Vitest/Jest/Pytest/Go test failures into a compact summary
31
+ // (failing names, expected vs received, 3 stack frames). Always returns output
32
+ // ≤ the previous raw-truncate fallback, so resolve-test is strictly net-positive.
33
+ export function summarizeTestFailure(stdout, stderr, runner) {
34
+ const raw = stderr || stdout;
35
+ const cmd = (runner ?? "").toLowerCase();
36
+ const out = `${stdout}\n${stderr}`;
37
+ // Pytest: "FAILED file::test_name - msg" + "E ..." assertion lines
38
+ if (cmd.includes("pytest") || /FAILED\s+\S+::/.test(raw)) {
39
+ const failed = [...raw.matchAll(/FAILED\s+(\S+?::\S+)\s+-\s+(.+)/g)].slice(0, 10)
40
+ .map((m) => ` ✗ ${m[1]}\n ${m[2].slice(0, 200)}`);
41
+ const asserts = [...raw.matchAll(/^E\s+(.+)/gm)].slice(0, 5).map((m) => m[1].slice(0, 200));
42
+ if (failed.length || asserts.length) {
43
+ const parts = [];
44
+ if (failed.length)
45
+ parts.push(`Failed tests:\n${failed.join("\n")}`);
46
+ if (asserts.length)
47
+ parts.push(`Assertion errors:\n ${asserts.join("\n ")}`);
48
+ return truncateOutput(parts.join("\n"), 1200);
49
+ }
50
+ }
51
+ // Go test: "--- FAIL: TestName" + " file.go:NN: msg"
52
+ if (cmd.includes("go test") || /--- FAIL:/.test(raw)) {
53
+ const failed = [...raw.matchAll(/--- FAIL:\s+(\S+)/g)].slice(0, 10).map((m) => ` ✗ ${m[1]}`);
54
+ const errs = [...raw.matchAll(/^\s+(\S+\.go):(\d+):\s+(.+)/gm)].slice(0, 5)
55
+ .map((m) => ` ${m[1]}:${m[2]}: ${m[3].slice(0, 160)}`);
56
+ if (failed.length) {
57
+ return truncateOutput(`Failed tests:\n${failed.join("\n")}\n${errs.join("\n")}`, 1200);
58
+ }
59
+ }
60
+ // Vitest / Jest
61
+ const failingNames = new Set();
62
+ for (const m of out.matchAll(/●\s+(.+)/g))
63
+ failingNames.add(m[1].trim().slice(0, 120)); // Jest suite › test
64
+ for (const m of out.matchAll(/^\s*×\s+(.+)/gm))
65
+ failingNames.add(m[1].trim().slice(0, 120)); // Vitest × test
66
+ const failFiles = [...out.matchAll(/^FAIL\s+(\S+)/gm)].map((m) => m[1]).slice(0, 10);
67
+ const expected = [...out.matchAll(/Expected[^:\n]*:\s*(.+)/g)].slice(0, 3).map((m) => m[1].slice(0, 150));
68
+ const received = [...out.matchAll(/Received[^:\n]*:\s*(.+)/g)].slice(0, 3).map((m) => m[1].slice(0, 150));
69
+ const stack = [...out.matchAll(/^\s+at\s+(.+)/gm)].slice(0, 3).map((m) => m[1].slice(0, 160));
70
+ const parts = [];
71
+ if (failFiles.length)
72
+ parts.push(`Failed files: ${failFiles.join(", ")}`);
73
+ if (failingNames.size)
74
+ parts.push(`Failed tests:\n${[...failingNames].slice(0, 10).map((n) => ` ✗ ${n}`).join("\n")}`);
75
+ if (expected.length || received.length) {
76
+ parts.push(`Expected: ${expected[0] ?? "-"}\nReceived: ${received[0] ?? "-"}`);
77
+ }
78
+ if (stack.length)
79
+ parts.push(`Stack:\n${stack.map((s) => ` at ${s}`).join("\n")}`);
80
+ // Parsed something useful → compact summary; otherwise fall back to the
81
+ // previous raw-truncate behaviour (byte-identical).
82
+ return parts.length ? truncateOutput(parts.join("\n"), 1200) : truncateOutput(raw, 1500);
83
+ }
26
84
  export function getTools(sessionState) {
27
85
  return {
28
86
  "resolve-verify": tool({
@@ -53,22 +111,53 @@ export function getTools(sessionState) {
53
111
  },
54
112
  }),
55
113
  "resolve-diagnostics": tool({
56
- description: "Get current LSP diagnostics snapshot. Returns errors and warnings per file from the language server.",
114
+ description: "Get current LSP diagnostics snapshot. Returns errors and warnings per file from the language server. When errors are present, includes the message and ±3 lines of source context so the coder can fix without a separate read.",
57
115
  args: {
58
116
  path: tool.schema.string().optional().describe("Specific file path to check. If omitted, returns all files with active diagnostics."),
117
+ context: tool.schema.boolean().optional().describe("Include ±3 lines of source context around each error (default true). Set false for counts only."),
59
118
  },
60
- async execute(args) {
119
+ async execute(args, ctx) {
61
120
  if (sessionState.recentDiagnostics.size === 0) {
62
121
  return "No active LSP diagnostics.";
63
122
  }
64
123
  const now = Date.now();
124
+ const wantContext = args.context !== false;
65
125
  const entries = [];
66
126
  for (const [filePath, diag] of sessionState.recentDiagnostics) {
67
127
  if (now - diag.timestamp > DIAGNOSTICS_TTL_MS)
68
128
  continue;
69
129
  if (args.path && filePath !== args.path)
70
130
  continue;
71
- entries.push(`${filePath}: ${diag.errors} errors, ${diag.warnings} warnings`);
131
+ const head = `${filePath}: ${diag.errors} errors, ${diag.warnings} warnings`;
132
+ // Net-positive: error-free files and context:false calls are byte-identical to before.
133
+ if (!wantContext || diag.errors === 0 || !diag.errorMessages?.length) {
134
+ entries.push(head);
135
+ continue;
136
+ }
137
+ // Errors present: show message + ±3 source lines (bounded to 3 errors/file).
138
+ const blocks = [head];
139
+ let srcLines = null;
140
+ for (const e of diag.errorMessages.slice(0, 3)) {
141
+ const ln = e.line + 1; // LSP lines are 0-based
142
+ if (srcLines === null) {
143
+ try {
144
+ srcLines = (await readFile(resolve(ctx.directory, filePath), "utf8")).split("\n");
145
+ }
146
+ catch {
147
+ srcLines = []; // unreadable: show message without context
148
+ }
149
+ }
150
+ let snippet = "";
151
+ if (srcLines.length > 0) {
152
+ const start = Math.max(0, ln - 4);
153
+ const end = Math.min(srcLines.length, ln + 3);
154
+ snippet = srcLines.slice(start, end)
155
+ .map((l, i) => `${start + i + 1}${start + i + 1 === ln ? ">" : ":"} ${l}`)
156
+ .join("\n");
157
+ }
158
+ blocks.push(` L${ln}: ${e.message}${snippet ? "\n" + snippet : ""}`);
159
+ }
160
+ entries.push(blocks.join("\n"));
72
161
  }
73
162
  if (entries.length === 0) {
74
163
  return args.path ? `No active diagnostics for ${args.path}.` : "No active LSP diagnostics.";
@@ -177,6 +266,7 @@ export function getTools(sessionState) {
177
266
  file: tool.schema.string().optional().describe("Test file path or glob pattern (e.g. 'test/plugin.test.mjs')."),
178
267
  pattern: tool.schema.string().optional().describe("Test name pattern to filter (e.g. 'GLM profile')."),
179
268
  runner: tool.schema.string().optional().describe("Override test runner command (e.g. 'vitest run', 'jest')."),
269
+ raw: tool.schema.boolean().optional().describe("Return the raw untruncated log instead of the parsed failure summary."),
180
270
  },
181
271
  async execute(args, ctx) {
182
272
  const projCtx = sessionState.storedProjectContext;
@@ -215,7 +305,11 @@ export function getTools(sessionState) {
215
305
  if (result.exitCode === 0) {
216
306
  return { output: `✅ Tests passed.\n${truncateOutput(result.stdout, 800)}`, metadata: { exitCode: 0 } };
217
307
  }
218
- return { output: `❌ Tests failed (exit ${result.exitCode}).\n${truncateOutput(result.stderr || result.stdout, 1500)}`, metadata: { exitCode: result.exitCode } };
308
+ // Phase 1.2: parse framework output into a compact summary unless raw requested.
309
+ const detail = args.raw
310
+ ? truncateOutput(result.stderr || result.stdout, 1500)
311
+ : summarizeTestFailure(result.stdout, result.stderr, testCmd);
312
+ return { output: `❌ Tests failed (exit ${result.exitCode}).\n${detail}`, metadata: { exitCode: result.exitCode } };
219
313
  }
220
314
  catch (err) {
221
315
  return `⚠️ Test runner failed: ${err instanceof Error ? err.message : String(err)}`;
@@ -480,8 +574,6 @@ export function getTools(sessionState) {
480
574
  }
481
575
  // Build resolve.json content
482
576
  const resolveConfig = {};
483
- if (sessionState.storedConfig?.profile)
484
- resolveConfig.profile = sessionState.storedConfig.profile;
485
577
  if (sessionState.storedConfig?.tier)
486
578
  resolveConfig.tier = sessionState.storedConfig.tier;
487
579
  if (projCtx?.verifyCommands.length) {
@@ -926,9 +1018,11 @@ export function getTools(sessionState) {
926
1018
  },
927
1019
  }),
928
1020
  "resolve-session": tool({
929
- description: "Show current Ralph Loop session state: profile, tier, edit count, tool call count, failure warnings, loop warnings, and elapsed time. Use when you suspect you're going in circles.",
930
- args: {},
931
- async execute(_args, ctx) {
1021
+ description: "Show current Ralph Loop session state: tier, edit count, tool call count, output bytes, failure warnings, loop warnings, and elapsed time. Use when you suspect you're going in circles.",
1022
+ args: {
1023
+ full: tool.schema.boolean().optional().describe("Include a compact changelog (recent commits + uncommitted count) so you get session + repo state in one call. Default false."),
1024
+ },
1025
+ async execute(args, ctx) {
932
1026
  const lines = [];
933
1027
  const cfg = sessionState.storedConfig;
934
1028
  const projCtx = sessionState.storedProjectContext;
@@ -936,8 +1030,7 @@ export function getTools(sessionState) {
936
1030
  lines.push(`Session duration: ${elapsed}s`);
937
1031
  lines.push(`Tool calls: ${sessionState.totalToolCalls}`);
938
1032
  lines.push(`Edits: ${sessionState.totalEdits}`);
939
- if (cfg?.profile)
940
- lines.push(`Profile: ${cfg.profile}`);
1033
+ lines.push(`Output: ${sessionState.totalOutputBytes} bytes (~${Math.round(sessionState.totalOutputBytes / 1024)}KB)`);
941
1034
  if (cfg?.tier)
942
1035
  lines.push(`Tier: ${cfg.tier}`);
943
1036
  if (projCtx?.hasTypeScript)
@@ -970,6 +1063,25 @@ export function getTools(sessionState) {
970
1063
  lines.push(` 🔄 ${w}`);
971
1064
  }
972
1065
  ctx.metadata({ title: `session: ${sessionState.totalEdits} edits, ${sessionState.totalToolCalls} tools, ${elapsed}s` });
1066
+ // Phase 1.6: opt-in `full` mode folds a compact changelog (recent
1067
+ // commits + uncommitted count) into the same response — 3 calls → 1.
1068
+ // Default (full omitted/false) leaves the output exactly as before.
1069
+ if (args.full) {
1070
+ try {
1071
+ const log = await runCommand("git log --oneline -5", ctx.directory, 5_000);
1072
+ const st = await runCommand("git status --porcelain", ctx.directory, 5_000);
1073
+ lines.push("Recent changes:");
1074
+ if (log.stdout.trim()) {
1075
+ lines.push(log.stdout.trim().split("\n").map((l) => ` ${l}`).join("\n"));
1076
+ }
1077
+ else {
1078
+ lines.push(" (no commits yet)");
1079
+ }
1080
+ const uncommitted = st.stdout.trim().split("\n").filter(Boolean).length;
1081
+ lines.push(`Uncommitted files: ${uncommitted}`);
1082
+ }
1083
+ catch { /* not a git repo */ }
1084
+ }
973
1085
  return lines.join("\n");
974
1086
  },
975
1087
  }),
@@ -1053,17 +1165,17 @@ export function getTools(sessionState) {
1053
1165
  if (!cfg) {
1054
1166
  return "No resolve config loaded. Plugin may not be initialized.";
1055
1167
  }
1056
- // 1. Profile check
1057
- if (cfg.profile) {
1058
- if (VALID_PROFILES.has(cfg.profile)) {
1059
- results.push(`✅ Profile: ${cfg.profile}`);
1168
+ // 1. Tier check
1169
+ if (cfg.tier) {
1170
+ if (VALID_TIERS.has(cfg.tier)) {
1171
+ results.push(`✅ Tier: ${cfg.tier}`);
1060
1172
  }
1061
1173
  else {
1062
- results.push(`🔴 Invalid profile: '${cfg.profile}'. Valid: ${[...VALID_PROFILES].join(", ")}`);
1174
+ results.push(`🔴 Invalid tier: '${cfg.tier}'. Valid: ${[...VALID_TIERS].join(", ")}`);
1063
1175
  }
1064
1176
  }
1065
1177
  else {
1066
- results.push("ℹ️ No profile set (using defaults)");
1178
+ results.push("ℹ️ No tier set (using defaults)");
1067
1179
  }
1068
1180
  // 2. Tier check
1069
1181
  if (cfg.tier) {
@@ -1190,8 +1302,6 @@ export function getTools(sessionState) {
1190
1302
  failures: sessionState.totalFailures,
1191
1303
  elapsedSeconds: Math.round((Date.now() - sessionState.sessionStartTime) / 1000),
1192
1304
  };
1193
- if (sessionState.storedConfig?.profile)
1194
- state.profile = sessionState.storedConfig.profile;
1195
1305
  if (sessionState.storedConfig?.tier)
1196
1306
  state.tier = sessionState.storedConfig.tier;
1197
1307
  if (sessionState.failureWarnings.length > 0)
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type PermissionValue = "ask" | "allow" | "deny";
2
- export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "codex" | "gpt" | "glm" | "architect" | "gpt-coder" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
3
- export type ModelAlias = ResolveAgentName | "glm" | "gpt" | "quick" | "deep" | "fast" | "strong" | "mini" | "codex" | "bronze" | "silver" | "gold" | "gpt-bronze" | "gpt-silver" | "gpt-gold" | "glm-bronze" | "glm-silver" | "glm-gold";
2
+ export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "architect" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
3
+ export type ModelAlias = ResolveAgentName | "quick" | "deep" | "fast" | "strong" | "mini" | "bronze" | "silver" | "gold";
4
4
  export type AgentMode = "subagent" | "primary" | "all";
5
5
  export type ResolveAgentConfig = {
6
6
  enabled?: boolean;
@@ -19,22 +19,29 @@ export type ResolveAgentConfig = {
19
19
  external_directory?: PermissionValue;
20
20
  };
21
21
  };
22
- export type ProfileName = "mix" | "glm" | "gpt";
23
22
  export type TierName = "bronze" | "silver" | "gold";
24
23
  export type LanguageSetting = "auto" | "en" | "ko";
24
+ /** Opt-in relaxations of the universal bash safety policy. Off by default. */
25
+ export type ResolvePermissions = {
26
+ /** Let resolve agents run `git reset --hard` (a checkpoint ref is written first). */
27
+ allowGitReset?: boolean;
28
+ /** Let resolve agents run `git clean -f...` (a checkpoint ref is written first). */
29
+ allowGitClean?: boolean;
30
+ };
31
+ export type RollbackKind = "reset" | "clean";
25
32
  export type ResolveConfig = {
26
- profile?: ProfileName;
27
33
  tier?: TierName;
28
34
  enabled?: ResolveAgentName[];
29
35
  models?: Partial<Record<ModelAlias, string>>;
30
36
  agents?: Partial<Record<ResolveAgentName, ResolveAgentConfig>>;
31
37
  preserveNative?: boolean;
32
- context7?: boolean;
33
38
  commands?: boolean;
34
39
  autoApprove?: boolean;
35
40
  maxParallelSubagents?: number;
36
41
  autoUpdate?: boolean;
37
42
  language?: LanguageSetting;
43
+ singleAgentMode?: boolean;
44
+ permissions?: ResolvePermissions;
38
45
  };
39
46
  export type ResolvePluginOptions = ResolveConfig & {
40
47
  config?: string;
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,5 @@
1
- import { ProjectContext, ResolveConfig } from "./types.js";
1
+ import { ProjectContext, ResolveConfig, ResolvePermissions, RollbackKind } from "./types.js";
2
2
  export declare const PLUGIN_VERSION: string;
3
- export declare const UPDATE_CHECK_INTERVAL_MS: number;
4
- export declare const UPDATE_CHECK_FILE: string;
5
- export declare const PLUGIN_CACHE_DIR: string;
6
3
  export declare function runCommand(command: string, cwd: string, timeoutMs: number): Promise<{
7
4
  stdout: string;
8
5
  stderr: string;
@@ -13,20 +10,23 @@ export declare function truncateOutput(text: string, maxLen: number): string;
13
10
  export declare function sanitizeShellArg(input: string): string;
14
11
  export declare function isMissingFileError(error: unknown): boolean;
15
12
  export declare function formatError(error: unknown): string;
16
- export declare function classifyBashCommand(pattern: string): "allow" | "deny" | "ask";
13
+ /**
14
+ * Which rollback command, if any, this bash line runs. Both are destructive and
15
+ * denied by default; `permissions.allowGitReset` / `allowGitClean` un-gate them,
16
+ * and the caller writes a checkpoint ref before execution.
17
+ */
18
+ export declare function detectRollbackCommand(pattern: string): RollbackKind | undefined;
19
+ export declare function classifyBashCommand(pattern: string, permissions?: ResolvePermissions): "allow" | "deny" | "ask";
17
20
  export declare function existsFile(path: string): Promise<boolean>;
18
21
  export declare function existsPath(path: string): Promise<boolean>;
19
22
  export declare function existsDirectory(path: string): Promise<boolean>;
20
23
  export declare function detectProjectContext(directory: string): Promise<ProjectContext>;
21
24
  export declare function collectContextFiles(rootDirectory: string, relativeDirectory: string, maxFiles?: number): Promise<string[]>;
22
25
  export declare function readPluginVersion(): string;
23
- export declare function readUpdateCheckCache(): {
24
- checkedAt: number;
25
- latest: string;
26
- } | undefined;
27
- export declare function isNewerVersion(candidate: string, baseline: string): boolean;
28
- export declare function maybeAutoUpdate(): Promise<void>;
29
26
  export declare function readFirstJson(paths: string[]): Promise<ResolveConfig | undefined>;
27
+ export declare const GIT_HARD_RESET_PATTERN: RegExp;
28
+ export declare const GIT_FORCE_CLEAN_PATTERN: RegExp;
29
+ export declare const GIT_CLEAN_IGNORED_PATTERN: RegExp;
30
30
  export declare const BANNED_COMMANDS: ReadonlyArray<RegExp>;
31
31
  export declare const DANGEROUS_BASH_PATTERNS: ReadonlyArray<RegExp>;
32
32
  export declare const ALWAYS_SAFE_COMMANDS: ReadonlyArray<string>;