ralph-review 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Version](https://img.shields.io/github/v/tag/kenryu42/ralph-review?label=version&color=blue)](https://github.com/kenryu42/ralph-review)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-red.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- A CLI tool that orchestrates agentic review-fix cycles until your code is clean.
8
+ Orchestrating coding agents for code review, verification and fixing via the ralph loop.
9
9
 
10
10
  ---
11
11
 
@@ -131,6 +131,10 @@ Treats review findings as untrusted input — verifies every claim against actua
131
131
  ## Installation
132
132
 
133
133
  ```bash
134
+ # Homebrew (install or update)
135
+ brew install kenryu42/tap/ralph-review
136
+
137
+ # npm (install or update)
134
138
  npm install -g ralph-review
135
139
  ```
136
140
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ralph-review",
3
- "version": "0.1.4",
4
- "description": "A CLI tool that orchestrates agentic review-fix cycles until your code is clean.",
3
+ "version": "0.1.6",
4
+ "description": "Orchestrating coding agents for code review, verification and fixing via the ralph loop.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "author": {
@@ -211,11 +211,12 @@ const DEFAULT_MAX_ITERATIONS = 5;
211
211
  const DEFAULT_ITERATION_TIMEOUT_MINUTES = 30;
212
212
 
213
213
  const REVIEWER_AGENT_PRIORITY: readonly AgentType[] = ["codex", "droid", "claude", "gemini"];
214
- const FIXER_AGENT_PRIORITY: readonly AgentType[] = ["claude", "codex", "droid", "gemini"];
215
- const SIMPLIFIER_AGENT_PRIORITY: readonly AgentType[] = ["claude", "codex", "droid", "gemini"];
214
+ const FIXER_AGENT_PRIORITY: readonly AgentType[] = ["codex", "claude", "droid", "gemini"];
215
+ const SIMPLIFIER_AGENT_PRIORITY: readonly AgentType[] = ["codex", "claude", "droid", "gemini"];
216
216
 
217
217
  const MODEL_PRIORITY_MATCHERS: Record<ConfiguredRole, readonly ((model: string) => boolean)[]> = {
218
218
  reviewer: [
219
+ (model) => matchesModelId(model, "gpt-5.4"),
219
220
  (model) => matchesModelId(model, "gpt-5.3-codex"),
220
221
  (model) => matchesModelId(model, "gpt-5.2"),
221
222
  (model) => matchesModelId(model, "gpt-5.2-codex"),
@@ -223,11 +224,13 @@ const MODEL_PRIORITY_MATCHERS: Record<ConfiguredRole, readonly ((model: string)
223
224
  (model) => matchesModelId(model, "gemini-3-pro-preview"),
224
225
  ],
225
226
  fixer: [
226
- (model) => matchesModelId(model, "claude-opus-4-6"),
227
+ (model) => matchesModelId(model, "gpt-5.4"),
227
228
  (model) => matchesModelId(model, "gpt-5.3-codex"),
229
+ (model) => matchesModelId(model, "claude-opus-4-6"),
228
230
  (model) => matchesModelId(model, "gemini-3-pro-preview"),
229
231
  ],
230
232
  "code-simplifier": [
233
+ (model) => matchesModelId(model, "gpt-5.4"),
231
234
  (model) => matchesModelId(model, "claude-opus-4-6"),
232
235
  (model) => matchesModelId(model, "gpt-5.3-codex"),
233
236
  (model) => isClaudeOpus45Model(model),
@@ -754,8 +757,8 @@ export async function buildAutoInitInput(
754
757
  iterationTimeoutMinutes,
755
758
  defaultReviewType: "uncommitted",
756
759
  runSimplifierByDefault: false,
757
- runWatchByDefault: DEFAULT_CONFIG.run?.watch ?? true,
758
- soundNotificationsEnabled: DEFAULT_CONFIG.notifications?.sound.enabled ?? true,
760
+ runWatchByDefault: true,
761
+ soundNotificationsEnabled: true,
759
762
  },
760
763
  skippedAgents,
761
764
  };
@@ -1043,14 +1046,21 @@ export async function runInitWithRuntime(
1043
1046
  }
1044
1047
 
1045
1048
  const resolvedInput = requireInitInput(runtime, input);
1046
- const inputWithPreferences: InitInput = {
1047
- ...resolvedInput,
1048
- runWatchByDefault: await promptForRunWatch(runtime, resolvedInput.runWatchByDefault),
1049
- soundNotificationsEnabled: await promptForSoundNotifications(
1050
- runtime,
1051
- resolvedInput.soundNotificationsEnabled
1052
- ),
1053
- };
1049
+ const inputWithPreferences: InitInput =
1050
+ setupMode === "auto"
1051
+ ? {
1052
+ ...resolvedInput,
1053
+ runWatchByDefault: true,
1054
+ soundNotificationsEnabled: true,
1055
+ }
1056
+ : {
1057
+ ...resolvedInput,
1058
+ runWatchByDefault: await promptForRunWatch(runtime, resolvedInput.runWatchByDefault),
1059
+ soundNotificationsEnabled: await promptForSoundNotifications(
1060
+ runtime,
1061
+ resolvedInput.soundNotificationsEnabled
1062
+ ),
1063
+ };
1054
1064
 
1055
1065
  const config = buildConfig(inputWithPreferences);
1056
1066
  runtime.prompt.log.info(`Proposed configuration:\n${formatConfigDisplay(config)}`);
@@ -17,6 +17,7 @@ export const claudeModelOptions = [
17
17
  ] as const;
18
18
 
19
19
  export const codexModelOptions = [
20
+ { value: "gpt-5.4", label: "GPT-5.4" },
20
21
  { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
21
22
  { value: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
22
23
  { value: "gpt-5.2", label: "GPT-5.2" },
@@ -37,6 +38,7 @@ export const droidModelOptions = [
37
38
  { value: "gpt-5.2", label: "GPT-5.2" },
38
39
  { value: "gpt-5.2-codex", label: "GPT-5.2-Codex" },
39
40
  { value: "gpt-5.3-codex", label: "GPT-5.3-Codex" },
41
+ { value: "gpt-5.4", label: "GPT-5.4" },
40
42
  { value: "gemini-3-pro-preview", label: "Gemini 3 Pro" },
41
43
  { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" },
42
44
  { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" },
@@ -63,6 +65,7 @@ const droidReasoningLevelsByModel: Record<string, readonly ReasoningLevel[]> = {
63
65
  "gpt-5.2": ["low", "medium", "high", "xhigh"],
64
66
  "gpt-5.2-codex": ["low", "medium", "high", "xhigh"],
65
67
  "gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
68
+ "gpt-5.4": ["low", "medium", "high", "xhigh"],
66
69
  "claude-sonnet-4-5-20250929": ["low", "medium", "high"],
67
70
  "claude-sonnet-4-6": ["low", "medium", "high"],
68
71
  "claude-opus-4-5-20251101": ["low", "medium", "high"],
@@ -163,6 +163,48 @@ const DASHBOARD_SCRIPT_TEMPLATE = `
163
163
  const sortByPriority = (fixes) =>
164
164
  [...fixes].sort((a, b) => getPriorityRank(a.priority) - getPriorityRank(b.priority));
165
165
 
166
+ const formatFixRangeHunk = (lineStart, lineEnd) => {
167
+ const count = lineEnd - lineStart + 1;
168
+ if (count <= 1) {
169
+ return \`@@ -\${lineStart} +\${lineStart} @@\`;
170
+ }
171
+ return \`@@ -\${lineStart},\${count} +\${lineStart},\${count} @@\`;
172
+ };
173
+
174
+ const normalizeFixCodeLocation = (fix) => {
175
+ const vmLocation = fix?.codeLocation;
176
+ if (
177
+ vmLocation &&
178
+ typeof vmLocation.absoluteFilePath === "string" &&
179
+ Number.isInteger(vmLocation.lineStart) &&
180
+ Number.isInteger(vmLocation.lineEnd) &&
181
+ vmLocation.lineStart > 0 &&
182
+ vmLocation.lineEnd >= vmLocation.lineStart
183
+ ) {
184
+ return vmLocation;
185
+ }
186
+
187
+ const rawLocation = fix?.code_location;
188
+ const start = rawLocation?.line_range?.start;
189
+ const end = rawLocation?.line_range?.end;
190
+ if (
191
+ rawLocation &&
192
+ typeof rawLocation.absolute_file_path === "string" &&
193
+ Number.isInteger(start) &&
194
+ Number.isInteger(end) &&
195
+ start > 0 &&
196
+ end >= start
197
+ ) {
198
+ return {
199
+ absoluteFilePath: rawLocation.absolute_file_path,
200
+ lineStart: start,
201
+ lineEnd: end,
202
+ };
203
+ }
204
+
205
+ return null;
206
+ };
207
+
166
208
  const extractFixes = (entries) => {
167
209
  const fixes = [];
168
210
  const skipped = [];
@@ -298,15 +340,24 @@ const DASHBOARD_SCRIPT_TEMPLATE = `
298
340
  <div class="panel-title">Fixes Applied</div>
299
341
  \${fixes.length
300
342
  ? \`<ul class="fix-list">\${fixes
301
- .map((fix) => \`
343
+ .map((fix) => {
344
+ const location = normalizeFixCodeLocation(fix);
345
+ const filePath = fix.file || location?.absoluteFilePath || "";
346
+ const range = location
347
+ ? \`<div class="fix-range mono">\${escapeHtml(formatFixRangeHunk(location.lineStart, location.lineEnd))}</div>\`
348
+ : "";
349
+
350
+ return \`
302
351
  <li class="fix-item">
303
352
  <div class="fix-pill \${getPriorityPillClass(fix.priority)}">\${escapeHtml(fix.priority)}</div>
304
353
  <div>
305
354
  <div class="fix-title">\${escapeHtml(fix.title)}</div>
306
- <div class="fix-meta muted">\${escapeHtml(fix.file || "")}</div>
355
+ <div class="fix-meta muted">\${escapeHtml(filePath)}</div>
356
+ \${range}
307
357
  </div>
308
358
  </li>
309
- \`)
359
+ \`;
360
+ })
310
361
  .join("")}</ul>\`
311
362
  : '<div class="muted">No fixes recorded for this session.</div>'}
312
363
  </div>
@@ -359,6 +359,11 @@ export const DASHBOARD_CSS = `
359
359
  .fix-pill-default { background: var(--accent); }
360
360
  .fix-title, .skip-title { font-weight: 600; font-size: 13px; }
361
361
  .fix-meta, .skip-reason { font-size: 11px; margin-top: 4px; }
362
+ .fix-range {
363
+ margin-top: 6px;
364
+ color: rgba(237, 242, 255, 0.84);
365
+ font-size: 11px;
366
+ }
362
367
  .muted { color: var(--muted); }
363
368
  .empty { padding: 24px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 16px; background: rgba(9, 14, 23, 0.6); }
364
369
  .empty.tiny { padding: 12px; font-size: 12px; }
@@ -17,6 +17,11 @@ interface FixViewModel {
17
17
  priority: FixEntry["priority"];
18
18
  title: string;
19
19
  file: string;
20
+ codeLocation?: {
21
+ absoluteFilePath: string;
22
+ lineStart: number;
23
+ lineEnd: number;
24
+ };
20
25
  }
21
26
 
22
27
  interface SkippedViewModel {
@@ -99,16 +104,45 @@ function formatRoleDisplay(name: string, model: string, reasoning: string): stri
99
104
  return `${name} (${details.join(", ")})`;
100
105
  }
101
106
 
107
+ function toCodeLocationViewModel(fix: FixEntry): FixViewModel["codeLocation"] {
108
+ const location = fix.code_location;
109
+ if (!location) {
110
+ return undefined;
111
+ }
112
+
113
+ const lineStart = location.line_range?.start;
114
+ const lineEnd = location.line_range?.end;
115
+ if (
116
+ typeof location.absolute_file_path !== "string" ||
117
+ !Number.isInteger(lineStart) ||
118
+ !Number.isInteger(lineEnd) ||
119
+ lineStart < 1 ||
120
+ lineEnd < lineStart
121
+ ) {
122
+ return undefined;
123
+ }
124
+
125
+ return {
126
+ absoluteFilePath: location.absolute_file_path,
127
+ lineStart,
128
+ lineEnd,
129
+ };
130
+ }
131
+
102
132
  function buildSessionViewModel(session: SessionStats): SessionViewModel {
103
133
  const { fixes, skipped } = extractFixes(session.entries ?? []);
104
134
  const priorities = new Set(fixes.map((fix) => fix.priority));
105
135
  const sortedFixes = [...fixes]
106
136
  .sort((a, b) => getPriorityRank(a.priority) - getPriorityRank(b.priority))
107
- .map((fix) => ({
108
- priority: fix.priority,
109
- title: fix.title,
110
- file: fix.file ?? "",
111
- }));
137
+ .map((fix) => {
138
+ const codeLocation = toCodeLocationViewModel(fix);
139
+ return {
140
+ priority: fix.priority,
141
+ title: fix.title,
142
+ file: fix.file ?? "",
143
+ ...(codeLocation ? { codeLocation } : {}),
144
+ };
145
+ });
112
146
 
113
147
  const sortedSkipped = [...skipped]
114
148
  .sort((a, b) => getPriorityRank(a.priority) - getPriorityRank(b.priority))
@@ -3,8 +3,10 @@ import { getPriorityPillClass } from "@/lib/html/priority";
3
3
  import { escapeHtml, formatDate, formatDuration } from "@/lib/html/shared";
4
4
  import type {
5
5
  AgentSettings,
6
+ CodeLocation,
6
7
  FixEntry,
7
8
  IterationEntry,
9
+ LineRange,
8
10
  LogEntry,
9
11
  SkippedEntry,
10
12
  SystemEntry,
@@ -20,8 +22,64 @@ function isCodeSimplified(systemEntry: SystemEntry | undefined): boolean {
20
22
  return Boolean(systemEntry.codeSimplifier || systemEntry.reviewOptions?.simplifier);
21
23
  }
22
24
 
25
+ function isValidLineRange(lineRange: LineRange | undefined): lineRange is LineRange {
26
+ if (!lineRange) {
27
+ return false;
28
+ }
29
+
30
+ return (
31
+ Number.isInteger(lineRange.start) &&
32
+ Number.isInteger(lineRange.end) &&
33
+ lineRange.start > 0 &&
34
+ lineRange.end >= lineRange.start
35
+ );
36
+ }
37
+
38
+ function isValidCodeLocation(location: CodeLocation | null | undefined): location is CodeLocation {
39
+ if (!location || typeof location.absolute_file_path !== "string") {
40
+ return false;
41
+ }
42
+
43
+ return isValidLineRange(location.line_range);
44
+ }
45
+
46
+ function getFixDisplayFile(fix: FixEntry): string {
47
+ if (fix.file) {
48
+ return fix.file;
49
+ }
50
+
51
+ const location = fix.code_location;
52
+ if (isValidCodeLocation(location)) {
53
+ return location.absolute_file_path;
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ function formatFixRangeHunk(start: number, end: number): string {
60
+ const count = end - start + 1;
61
+ if (count <= 1) {
62
+ return `@@ -${start} +${start} @@`;
63
+ }
64
+ return `@@ -${start},${count} +${start},${count} @@`;
65
+ }
66
+
67
+ function renderFixRange(fix: FixEntry): string {
68
+ const location = fix.code_location;
69
+ if (!isValidCodeLocation(location)) {
70
+ return "";
71
+ }
72
+
73
+ const lineStart = location.line_range.start;
74
+ const lineEnd = location.line_range.end;
75
+
76
+ return `<div class="fix-range mono">${escapeHtml(formatFixRangeHunk(lineStart, lineEnd))}</div>`;
77
+ }
78
+
23
79
  function renderFixEntry(fix: FixEntry): string {
24
- const file = fix.file ? `<span class="muted">${escapeHtml(fix.file)}</span>` : "";
80
+ const filePath = getFixDisplayFile(fix);
81
+ const file = filePath ? `<span class="muted">${escapeHtml(filePath)}</span>` : "";
82
+ const range = renderFixRange(fix);
25
83
  const pillClass = getPriorityPillClass(fix.priority);
26
84
  return `
27
85
  <li class="fix-item">
@@ -29,6 +87,7 @@ function renderFixEntry(fix: FixEntry): string {
29
87
  <div>
30
88
  <div class="fix-title">${escapeHtml(fix.title)}</div>
31
89
  <div class="fix-meta">${file}</div>
90
+ ${range}
32
91
  </div>
33
92
  </li>
34
93
  `;
@@ -84,9 +84,15 @@ export const LOG_CSS = `
84
84
  .fix-pill-default { background: var(--accent); }
85
85
  .fix-title { font-weight: 600; }
86
86
  .fix-meta { font-size: 12px; margin-top: 4px; }
87
+ .fix-range {
88
+ margin-top: 6px;
89
+ color: rgba(237, 242, 255, 0.85);
90
+ font-size: 12px;
91
+ }
87
92
  .skip-title { font-weight: 600; }
88
93
  .skip-reason { font-size: 12px; }
89
94
  .muted { color: var(--muted); }
95
+ .mono { font-family: "Space Grotesk", monospace; }
90
96
  .callout {
91
97
  margin-top: 12px;
92
98
  padding: 12px 16px;
@@ -36,6 +36,7 @@ ${reviewOutput}
36
36
  - Prefer one aggregate command; otherwise run available lint -> typecheck -> test -> build.
37
37
  - Treat warnings as blocking.
38
38
  - Iterate fix + rerun until clean.
39
+ 6) For each APPLY item, include the applied code location when available (absolute path + line range).
39
40
 
40
41
  ## Special rule: tracking-status claims
41
42
  Claims like "file is untracked/not committed/missing from git" are SKIP in this pre-commit workflow.
@@ -69,6 +70,10 @@ ${FIX_SUMMARY_START_TOKEN}
69
70
  "title": "<one-line title>",
70
71
  "priority": "<P0 | P1 | P2 | P3>",
71
72
  "file": "<path or null>",
73
+ "code_location": {
74
+ "absolute_file_path": "<absolute path>",
75
+ "line_range": {"start": <int>, "end": <int>}
76
+ },
72
77
  "claim": "<issue claim>",
73
78
  "evidence": "<file:line / behavior>",
74
79
  "fix": "<what changed>"
@@ -95,6 +100,7 @@ JSON rules:
95
100
  - fixes MUST be []
96
101
  - skipped MUST contain only SKIP items
97
102
  - Include all APPLY items in fixes.
103
+ - For each APPLY item, include code_location when available; otherwise set code_location to null or omit it.
98
104
  - Include all SKIP items in skipped with required reason prefix.
99
105
  - Use [] when empty.
100
106
  - Priority must be exactly P0/P1/P2/P3.
@@ -80,18 +80,21 @@ export function Header({ branch, elapsed, session, projectPath, config }: Header
80
80
  flexShrink={0}
81
81
  >
82
82
  <box flexDirection="row">
83
- <box flexDirection="column" width={12}>
83
+ <box flexDirection="column" width={20}>
84
84
  <text>
85
- <span fg={TUI_COLORS.brand.logo}> {" "}</span>
85
+ <span fg={TUI_COLORS.brand.title}>{" ██████╗ ██████╗ "}</span>
86
86
  </text>
87
87
  <text>
88
- <span fg={TUI_COLORS.brand.logo}> ███████ </span>
88
+ <span fg={TUI_COLORS.brand.title}>{" ██╔══██╗██╔══██╗"}</span>
89
89
  </text>
90
90
  <text>
91
- <span fg={TUI_COLORS.brand.logo}>▐██▃█▃██▌</span>
91
+ <span fg={TUI_COLORS.brand.title}>{" ██████╔╝██████╔╝"}</span>
92
92
  </text>
93
93
  <text>
94
- <span fg={TUI_COLORS.brand.logo}> ██▅▅▅██ </span>
94
+ <span fg={TUI_COLORS.brand.title}>{" ██║ ██║██║ ██║"}</span>
95
+ </text>
96
+ <text>
97
+ <span fg={TUI_COLORS.brand.title}>{" ╚═╝ ╚═╝╚═╝ ╚═╝"}</span>
95
98
  </text>
96
99
  </box>
97
100
 
@@ -1,11 +1,13 @@
1
1
  import type { FixDecision, Priority } from "./domain";
2
2
  import { VALID_FIX_DECISIONS, VALID_PRIORITIES } from "./domain";
3
+ import type { CodeLocation } from "./review";
3
4
 
4
5
  export interface FixEntry {
5
6
  id: number;
6
7
  title: string;
7
8
  priority: Priority;
8
9
  file?: string | null;
10
+ code_location?: CodeLocation | null;
9
11
  claim: string;
10
12
  evidence: string;
11
13
  fix: string;
@@ -25,6 +27,37 @@ export interface FixSummary {
25
27
  skipped: SkippedEntry[];
26
28
  }
27
29
 
30
+ function isLineRange(value: unknown): value is CodeLocation["line_range"] {
31
+ if (typeof value !== "object" || value === null) {
32
+ return false;
33
+ }
34
+
35
+ const obj = value as Record<string, unknown>;
36
+
37
+ return (
38
+ typeof obj.start === "number" &&
39
+ Number.isInteger(obj.start) &&
40
+ obj.start > 0 &&
41
+ typeof obj.end === "number" &&
42
+ Number.isInteger(obj.end) &&
43
+ obj.end >= obj.start
44
+ );
45
+ }
46
+
47
+ function isCodeLocation(value: unknown): value is CodeLocation {
48
+ if (typeof value !== "object" || value === null) {
49
+ return false;
50
+ }
51
+
52
+ const obj = value as Record<string, unknown>;
53
+
54
+ if (typeof obj.absolute_file_path !== "string") {
55
+ return false;
56
+ }
57
+
58
+ return isLineRange(obj.line_range);
59
+ }
60
+
28
61
  function isFixEntry(value: unknown): value is FixEntry {
29
62
  if (typeof value !== "object" || value === null) {
30
63
  return false;
@@ -38,6 +71,9 @@ function isFixEntry(value: unknown): value is FixEntry {
38
71
  typeof obj.priority === "string" &&
39
72
  VALID_PRIORITIES.includes(obj.priority as Priority) &&
40
73
  (obj.file === undefined || obj.file === null || typeof obj.file === "string") &&
74
+ (obj.code_location === undefined ||
75
+ obj.code_location === null ||
76
+ isCodeLocation(obj.code_location)) &&
41
77
  typeof obj.claim === "string" &&
42
78
  typeof obj.evidence === "string" &&
43
79
  typeof obj.fix === "string"
@@ -31,9 +31,11 @@ export type {
31
31
  SystemEntry,
32
32
  } from "./log";
33
33
  export {
34
+ type CodeLocation,
34
35
  type CodexReviewSummary,
35
36
  type Finding,
36
37
  isReviewSummary,
38
+ type LineRange,
37
39
  parseCodexReviewText,
38
40
  type ReviewSummary,
39
41
  } from "./review";
@@ -1,12 +1,12 @@
1
1
  import type { OverallCorrectness } from "./domain";
2
2
  import { VALID_OVERALL_CORRECTNESS } from "./domain";
3
3
 
4
- interface LineRange {
4
+ export interface LineRange {
5
5
  start: number;
6
6
  end: number;
7
7
  }
8
8
 
9
- interface CodeLocation {
9
+ export interface CodeLocation {
10
10
  absolute_file_path: string;
11
11
  line_range: LineRange;
12
12
  }