ralph-review 0.1.3 → 0.1.5
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 +5 -1
- package/package.json +2 -2
- package/src/commands/init.ts +17 -10
- package/src/commands/run.ts +20 -9
- package/src/lib/agents/codex.ts +6 -6
- package/src/lib/diagnostics/checks.ts +1 -7
- package/src/lib/format.ts +15 -7
- package/src/lib/html/dashboard/script.ts +54 -3
- package/src/lib/html/dashboard/styles.ts +5 -0
- package/src/lib/html/dashboard/view-model.ts +39 -5
- package/src/lib/html/log/page.ts +60 -1
- package/src/lib/html/log/styles.ts +6 -0
- package/src/lib/prompts/fixer.ts +6 -0
- package/src/lib/prompts/review.ts +14 -3
- package/src/lib/tui/components/Header.tsx +8 -5
- package/src/lib/tui/components/SessionPanel.tsx +2 -19
- package/src/lib/types/fix.ts +36 -0
- package/src/lib/types/index.ts +2 -0
- package/src/lib/types/review.ts +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/kenryu42/ralph-review)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
135
|
+
brew install kenryu42/tap/ralph-review
|
|
136
|
+
|
|
137
|
+
# npm
|
|
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
|
-
"description": "
|
|
3
|
+
"version": "0.1.5",
|
|
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": {
|
package/src/commands/init.ts
CHANGED
|
@@ -754,8 +754,8 @@ export async function buildAutoInitInput(
|
|
|
754
754
|
iterationTimeoutMinutes,
|
|
755
755
|
defaultReviewType: "uncommitted",
|
|
756
756
|
runSimplifierByDefault: false,
|
|
757
|
-
runWatchByDefault:
|
|
758
|
-
soundNotificationsEnabled:
|
|
757
|
+
runWatchByDefault: true,
|
|
758
|
+
soundNotificationsEnabled: true,
|
|
759
759
|
},
|
|
760
760
|
skippedAgents,
|
|
761
761
|
};
|
|
@@ -1043,14 +1043,21 @@ export async function runInitWithRuntime(
|
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
1045
|
const resolvedInput = requireInitInput(runtime, input);
|
|
1046
|
-
const inputWithPreferences: InitInput =
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1046
|
+
const inputWithPreferences: InitInput =
|
|
1047
|
+
setupMode === "auto"
|
|
1048
|
+
? {
|
|
1049
|
+
...resolvedInput,
|
|
1050
|
+
runWatchByDefault: true,
|
|
1051
|
+
soundNotificationsEnabled: true,
|
|
1052
|
+
}
|
|
1053
|
+
: {
|
|
1054
|
+
...resolvedInput,
|
|
1055
|
+
runWatchByDefault: await promptForRunWatch(runtime, resolvedInput.runWatchByDefault),
|
|
1056
|
+
soundNotificationsEnabled: await promptForSoundNotifications(
|
|
1057
|
+
runtime,
|
|
1058
|
+
resolvedInput.soundNotificationsEnabled
|
|
1059
|
+
),
|
|
1060
|
+
};
|
|
1054
1061
|
|
|
1055
1062
|
const config = buildConfig(inputWithPreferences);
|
|
1056
1063
|
runtime.prompt.log.info(`Proposed configuration:\n${formatConfigDisplay(config)}`);
|
package/src/commands/run.ts
CHANGED
|
@@ -618,15 +618,26 @@ export async function startReview(
|
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
runtime.
|
|
621
|
+
if (options.base !== undefined && options.commit !== undefined) {
|
|
622
|
+
runtime.prompt.log.error("Cannot use --base and --commit together");
|
|
623
|
+
runtime.process.exit(1);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (options.uncommitted && options.base !== undefined) {
|
|
628
|
+
runtime.prompt.log.error("Cannot use --uncommitted and --base together");
|
|
629
|
+
runtime.process.exit(1);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (options.uncommitted && options.commit !== undefined) {
|
|
634
|
+
runtime.prompt.log.error("Cannot use --uncommitted and --commit together");
|
|
635
|
+
runtime.process.exit(1);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (options.uncommitted && options.custom !== undefined) {
|
|
640
|
+
runtime.prompt.log.error("Cannot use --uncommitted and --custom together");
|
|
630
641
|
runtime.process.exit(1);
|
|
631
642
|
return;
|
|
632
643
|
}
|
package/src/lib/agents/codex.ts
CHANGED
|
@@ -52,6 +52,12 @@ export const codexConfig: AgentConfig = {
|
|
|
52
52
|
|
|
53
53
|
const baseReviewArgs = withReasoningEffort(["exec", "--json"], reasoning);
|
|
54
54
|
|
|
55
|
+
if (reviewOptions?.customInstructions) {
|
|
56
|
+
const fullPrompt = prompt ? `review ${prompt}` : "review";
|
|
57
|
+
const customArgs = withReasoningEffort(["exec", "--full-auto", "--json"], reasoning);
|
|
58
|
+
return withModel([...customArgs, fullPrompt], model);
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
if (reviewOptions?.commitSha) {
|
|
56
62
|
return withModel([...baseReviewArgs, "review", "--commit", reviewOptions.commitSha], model);
|
|
57
63
|
}
|
|
@@ -60,12 +66,6 @@ export const codexConfig: AgentConfig = {
|
|
|
60
66
|
return withModel([...baseReviewArgs, "review", "--base", reviewOptions.baseBranch], model);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
if (reviewOptions?.customInstructions) {
|
|
64
|
-
const fullPrompt = prompt ? `review ${prompt}` : "review";
|
|
65
|
-
const customArgs = withReasoningEffort(["exec", "--full-auto", "--json"], reasoning);
|
|
66
|
-
return withModel([...customArgs, fullPrompt], model);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
69
|
return withModel([...baseReviewArgs, "review", "--uncommitted"], model);
|
|
70
70
|
},
|
|
71
71
|
buildEnv: defaultBuildEnv,
|
|
@@ -508,13 +508,7 @@ export async function runDiagnostics(
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
if (
|
|
512
|
-
!options.baseBranch &&
|
|
513
|
-
!options.commitSha &&
|
|
514
|
-
!options.customInstructions &&
|
|
515
|
-
insideGitRepo &&
|
|
516
|
-
!gitRepoError
|
|
517
|
-
) {
|
|
511
|
+
if (!options.baseBranch && !options.commitSha && insideGitRepo && !gitRepoError) {
|
|
518
512
|
let hasChanges = false;
|
|
519
513
|
let hasChangesError: string | null = null;
|
|
520
514
|
try {
|
package/src/lib/format.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import type { ReviewOptions } from "@/lib/types";
|
|
2
2
|
|
|
3
|
+
function formatCustomReviewType(customInstructions: string): string {
|
|
4
|
+
const instruction = customInstructions.slice(0, 40);
|
|
5
|
+
return customInstructions.length > 40 ? `custom (${instruction}...)` : `custom (${instruction})`;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export function formatReviewType(reviewOptions: ReviewOptions | undefined): string {
|
|
4
9
|
if (!reviewOptions) return "uncommitted changes";
|
|
5
10
|
|
|
6
|
-
if (reviewOptions.customInstructions) {
|
|
7
|
-
const instruction = reviewOptions.customInstructions.slice(0, 40);
|
|
8
|
-
return reviewOptions.customInstructions.length > 40
|
|
9
|
-
? `custom (${instruction}...)`
|
|
10
|
-
: `custom (${instruction})`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
11
|
if (reviewOptions.commitSha) {
|
|
14
12
|
const shortSha = reviewOptions.commitSha.slice(0, 7);
|
|
13
|
+
if (reviewOptions.customInstructions) {
|
|
14
|
+
return `commit (${shortSha}) + ${formatCustomReviewType(reviewOptions.customInstructions)}`;
|
|
15
|
+
}
|
|
15
16
|
return `commit (${shortSha})`;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
if (reviewOptions.baseBranch) {
|
|
20
|
+
if (reviewOptions.customInstructions) {
|
|
21
|
+
return `base (${reviewOptions.baseBranch}) + ${formatCustomReviewType(reviewOptions.customInstructions)}`;
|
|
22
|
+
}
|
|
19
23
|
return `base (${reviewOptions.baseBranch})`;
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
if (reviewOptions.customInstructions) {
|
|
27
|
+
return formatCustomReviewType(reviewOptions.customInstructions);
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
return "uncommitted changes";
|
|
23
31
|
}
|
|
@@ -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(
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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))
|
package/src/lib/html/log/page.ts
CHANGED
|
@@ -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
|
|
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;
|
package/src/lib/prompts/fixer.ts
CHANGED
|
@@ -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.
|
|
@@ -19,6 +19,16 @@ const BASE_BRANCH_PROMPT_BACKUP = (branch: string) =>
|
|
|
19
19
|
const COMMIT_PROMPT = (commitHash: string) =>
|
|
20
20
|
`Review the code changes for the commit ${commitHash}. Provide prioritized, actionable findings.`;
|
|
21
21
|
|
|
22
|
+
const CUSTOM_FOCUS_PROMPT = (customInstructions: string) =>
|
|
23
|
+
`Additional review focus from user instructions:\n${customInstructions}`;
|
|
24
|
+
|
|
25
|
+
function withCustomFocus(instruction: string, customInstructions?: string): string {
|
|
26
|
+
if (!customInstructions) {
|
|
27
|
+
return instruction;
|
|
28
|
+
}
|
|
29
|
+
return `${instruction}\n\n${CUSTOM_FOCUS_PROMPT(customInstructions)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
export interface ReviewerPromptOptions {
|
|
23
33
|
repoPath: string;
|
|
24
34
|
baseBranch?: string;
|
|
@@ -26,19 +36,20 @@ export interface ReviewerPromptOptions {
|
|
|
26
36
|
customInstructions?: string;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
/**
|
|
39
|
+
/** Target priority: commitSha > baseBranch > uncommitted (default), with custom focus overlay. */
|
|
30
40
|
export function createReviewerPrompt(options: ReviewerPromptOptions): string {
|
|
31
41
|
const { repoPath, baseBranch, commitSha, customInstructions } = options;
|
|
32
42
|
|
|
33
43
|
let instruction: string;
|
|
34
44
|
|
|
35
45
|
if (commitSha) {
|
|
36
|
-
instruction = COMMIT_PROMPT(commitSha);
|
|
46
|
+
instruction = withCustomFocus(COMMIT_PROMPT(commitSha), customInstructions);
|
|
37
47
|
} else if (baseBranch) {
|
|
38
48
|
const mergeBaseSha = mergeBaseWithHead(repoPath, baseBranch);
|
|
39
|
-
|
|
49
|
+
const baseInstruction = mergeBaseSha
|
|
40
50
|
? BASE_BRANCH_PROMPT(baseBranch, mergeBaseSha)
|
|
41
51
|
: BASE_BRANCH_PROMPT_BACKUP(baseBranch);
|
|
52
|
+
instruction = withCustomFocus(baseInstruction, customInstructions);
|
|
42
53
|
} else if (customInstructions) {
|
|
43
54
|
instruction = customInstructions;
|
|
44
55
|
} else {
|
|
@@ -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={
|
|
83
|
+
<box flexDirection="column" width={20}>
|
|
84
84
|
<text>
|
|
85
|
-
<span fg={TUI_COLORS.brand.
|
|
85
|
+
<span fg={TUI_COLORS.brand.title}>{" ██████╗ ██████╗ "}</span>
|
|
86
86
|
</text>
|
|
87
87
|
<text>
|
|
88
|
-
<span fg={TUI_COLORS.brand.
|
|
88
|
+
<span fg={TUI_COLORS.brand.title}>{" ██╔══██╗██╔══██╗"}</span>
|
|
89
89
|
</text>
|
|
90
90
|
<text>
|
|
91
|
-
<span fg={TUI_COLORS.brand.
|
|
91
|
+
<span fg={TUI_COLORS.brand.title}>{" ██████╔╝██████╔╝"}</span>
|
|
92
92
|
</text>
|
|
93
93
|
<text>
|
|
94
|
-
<span fg={TUI_COLORS.brand.
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import { useTerminalDimensions } from "@opentui/react";
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { formatReviewType } from "@/lib/format";
|
|
3
4
|
import type { LockData } from "@/lib/lockfile";
|
|
4
5
|
import { TUI_COLORS } from "@/lib/tui/colors";
|
|
5
6
|
import type {
|
|
@@ -81,24 +82,6 @@ function toSingleLine(value: string): string {
|
|
|
81
82
|
return value.replace(/\s+/g, " ").trim();
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function formatReviewType(reviewOptions: ReviewOptions | undefined): string {
|
|
85
|
-
if (!reviewOptions) return "uncommitted changes";
|
|
86
|
-
|
|
87
|
-
if (reviewOptions.customInstructions) {
|
|
88
|
-
return `custom (${toSingleLine(reviewOptions.customInstructions)})`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (reviewOptions.commitSha) {
|
|
92
|
-
return `commit (${toSingleLine(reviewOptions.commitSha)})`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (reviewOptions.baseBranch) {
|
|
96
|
-
return `base (${toSingleLine(reviewOptions.baseBranch)})`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return "uncommitted changes";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
85
|
interface FixListProps {
|
|
103
86
|
fixes: FixEntry[];
|
|
104
87
|
showFiles: boolean;
|
|
@@ -566,7 +549,7 @@ export function SessionPanel({
|
|
|
566
549
|
<box flexDirection="row" gap={1}>
|
|
567
550
|
<text fg={TUI_COLORS.text.muted}>Review Type:</text>
|
|
568
551
|
<text fg={TUI_COLORS.text.primary} wrapMode="none">
|
|
569
|
-
{formatReviewType(reviewOptions)}
|
|
552
|
+
{toSingleLine(formatReviewType(reviewOptions))}
|
|
570
553
|
</text>
|
|
571
554
|
</box>
|
|
572
555
|
|
package/src/lib/types/fix.ts
CHANGED
|
@@ -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"
|
package/src/lib/types/index.ts
CHANGED
package/src/lib/types/review.ts
CHANGED
|
@@ -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
|
}
|