ralph-review 0.2.2 → 0.2.3
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 +123 -16
- package/package.json +6 -4
- package/src/cli-core.ts +51 -88
- package/src/cli-rrr.ts +1 -4
- package/src/cli.ts +1 -2
- package/src/commands/apply.ts +6 -14
- package/src/commands/config-handlers.ts +68 -69
- package/src/commands/config-model.ts +147 -125
- package/src/commands/doctor.ts +2 -4
- package/src/commands/fix.ts +73 -51
- package/src/commands/handoff-selection.ts +6 -8
- package/src/commands/interactive-deps.ts +18 -0
- package/src/commands/log.ts +12 -12
- package/src/commands/run.ts +32 -33
- package/src/commands/stop.ts +6 -13
- package/src/commands/update.ts +2 -4
- package/src/lib/agents/claude.ts +4 -16
- package/src/lib/agents/core.ts +16 -0
- package/src/lib/agents/droid.ts +4 -15
- package/src/lib/cli-parser.ts +19 -14
- package/src/lib/handoff.ts +16 -7
- package/src/lib/logging/session-log.ts +2 -1
- package/src/lib/prompts/defaults/review.md +1 -1
- package/src/lib/prompts/protocol.ts +2 -1
- package/src/lib/review-workflow/findings/artifact.ts +3 -1
- package/src/lib/review-workflow/findings/types.ts +1 -1
- package/src/lib/review-workflow/remediation/prompt.ts +7 -7
- package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
- package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
- package/src/lib/review-workflow/results/finalize-result.ts +20 -3
- package/src/lib/review-workflow/run-review-cycle.ts +1 -12
- package/src/lib/review-workflow/session-status.ts +13 -0
- package/src/lib/review-workflow/shared/framed-json.ts +2 -47
- package/src/lib/session/state.ts +50 -38
- package/src/lib/structured-output.ts +24 -9
- package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
- package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
- package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
- package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
- package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
- package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
- package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
- package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
- package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
- package/src/lib/tui/shared/CenteredModal.tsx +44 -0
- package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
- package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
- package/src/lib/tui/workspace/Workspace.tsx +6 -91
- package/src/lib/tui/workspace/use-workspace-state.ts +44 -37
- package/src/lib/types/fix.ts +15 -48
- package/src/lib/types/guards.ts +47 -0
- package/src/lib/types/review.ts +5 -39
package/src/commands/run.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { parseCommand } from "@/lib/cli-parser";
|
|
|
5
5
|
import { loadEffectiveConfig } from "@/lib/config";
|
|
6
6
|
import { collectIssueItems, runDiagnostics } from "@/lib/diagnostics";
|
|
7
7
|
import { getTmuxInstallHint } from "@/lib/diagnostics/tmux-install";
|
|
8
|
-
import type { DiagnosticsReport } from "@/lib/diagnostics/types";
|
|
8
|
+
import type { DiagnosticItem, DiagnosticsReport } from "@/lib/diagnostics/types";
|
|
9
9
|
import { type CycleResult, runReviewCycle } from "@/lib/engine";
|
|
10
10
|
import { formatReviewType } from "@/lib/format";
|
|
11
11
|
import { formatHandoffNote } from "@/lib/handoff-note";
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@/lib/priority-list";
|
|
20
20
|
import { runFixSession } from "@/lib/review-workflow/remediation/run-fix-session";
|
|
21
21
|
import type { FixSessionResult } from "@/lib/review-workflow/remediation/types";
|
|
22
|
+
import { mapSessionStatusToFinalStatus } from "@/lib/review-workflow/session-status";
|
|
22
23
|
import {
|
|
23
24
|
createSessionId,
|
|
24
25
|
createSessionState,
|
|
@@ -108,20 +109,6 @@ export function formatRunAgentsNote(config: Config, reviewOptions: ReviewOptions
|
|
|
108
109
|
return lines.join("\n");
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function mapSessionStatusToFinalStatus(
|
|
112
|
-
status: FixSessionResult["sessionStatus"]
|
|
113
|
-
): CycleResult["finalStatus"] {
|
|
114
|
-
if (status === "failed") {
|
|
115
|
-
return "failed";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (status === "interrupted") {
|
|
119
|
-
return "interrupted";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return "completed";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
112
|
function hasAutoFixPriorityMatches(result: CycleResult, priorities: Priority[]): boolean {
|
|
126
113
|
return (
|
|
127
114
|
result.artifact?.findings.some((finding) => priorities.includes(finding.priority)) ?? false
|
|
@@ -257,6 +244,32 @@ export interface RunRuntimeOverrides
|
|
|
257
244
|
timer?: Partial<RunRuntime["timer"]>;
|
|
258
245
|
}
|
|
259
246
|
|
|
247
|
+
function createRunCommandContext(overrides: RunRuntimeOverrides): {
|
|
248
|
+
runtime: RunRuntime;
|
|
249
|
+
projectPath: string;
|
|
250
|
+
} {
|
|
251
|
+
const runtime = createRunRuntime(overrides);
|
|
252
|
+
return {
|
|
253
|
+
runtime,
|
|
254
|
+
projectPath: runtime.process.env.RR_PROJECT_PATH || runtime.process.cwd(),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function logPreflightItems(
|
|
259
|
+
runtime: RunRuntime,
|
|
260
|
+
items: DiagnosticItem[],
|
|
261
|
+
heading: string,
|
|
262
|
+
severity: "error" | "warn"
|
|
263
|
+
): void {
|
|
264
|
+
runtime.prompt.log[severity](heading);
|
|
265
|
+
for (const item of items) {
|
|
266
|
+
runtime.prompt.log.message(` ${item.summary}`);
|
|
267
|
+
item.remediation.forEach((remediation) => {
|
|
268
|
+
runtime.prompt.log.message(` -> ${remediation}`);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
260
273
|
export function createRunRuntime(overrides: RunRuntimeOverrides = {}): RunRuntime {
|
|
261
274
|
const defaults: RunRuntime = {
|
|
262
275
|
prompt: {
|
|
@@ -449,8 +462,7 @@ export async function runForeground(
|
|
|
449
462
|
args: string[] = [],
|
|
450
463
|
overrides: RunRuntimeOverrides = {}
|
|
451
464
|
): Promise<void> {
|
|
452
|
-
const runtime =
|
|
453
|
-
const projectPath = runtime.process.env.RR_PROJECT_PATH || runtime.process.cwd();
|
|
465
|
+
const { runtime, projectPath } = createRunCommandContext(overrides);
|
|
454
466
|
const config = await runtime.loadConfig(projectPath);
|
|
455
467
|
if (!config) {
|
|
456
468
|
runtime.prompt.log.error("Failed to load config");
|
|
@@ -714,8 +726,7 @@ export async function startReview(
|
|
|
714
726
|
args: string[],
|
|
715
727
|
overrides: RunRuntimeOverrides = {}
|
|
716
728
|
): Promise<void> {
|
|
717
|
-
const runtime =
|
|
718
|
-
const projectPath = runtime.process.env.RR_PROJECT_PATH || runtime.process.cwd();
|
|
729
|
+
const { runtime, projectPath } = createRunCommandContext(overrides);
|
|
719
730
|
// Parse options using command definition
|
|
720
731
|
const runDef = runtime.getCommandDef("run");
|
|
721
732
|
if (!runDef) {
|
|
@@ -856,25 +867,13 @@ export async function startReview(
|
|
|
856
867
|
const warnings = issues.filter((item) => item.severity === "warning");
|
|
857
868
|
|
|
858
869
|
if (errors.length > 0) {
|
|
859
|
-
runtime
|
|
860
|
-
for (const item of errors) {
|
|
861
|
-
runtime.prompt.log.message(` ${item.summary}`);
|
|
862
|
-
item.remediation.forEach((remediation) => {
|
|
863
|
-
runtime.prompt.log.message(` -> ${remediation}`);
|
|
864
|
-
});
|
|
865
|
-
}
|
|
870
|
+
logPreflightItems(runtime, errors, "Cannot run review:", "error");
|
|
866
871
|
runtime.process.exit(1);
|
|
867
872
|
return;
|
|
868
873
|
}
|
|
869
874
|
|
|
870
875
|
if (warnings.length > 0) {
|
|
871
|
-
runtime
|
|
872
|
-
for (const item of warnings) {
|
|
873
|
-
runtime.prompt.log.message(` ${item.summary}`);
|
|
874
|
-
item.remediation.forEach((remediation) => {
|
|
875
|
-
runtime.prompt.log.message(` -> ${remediation}`);
|
|
876
|
-
});
|
|
877
|
-
}
|
|
876
|
+
logPreflightItems(runtime, warnings, "Preflight warnings:", "warn");
|
|
878
877
|
}
|
|
879
878
|
|
|
880
879
|
const config = diagnostics.config ?? (await runtime.loadConfig(projectPath));
|
package/src/commands/stop.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createInteractiveCommandDeps,
|
|
4
|
+
type InteractiveCommandDeps,
|
|
5
|
+
} from "@/commands/interactive-deps";
|
|
3
6
|
import { parseCommand } from "@/lib/cli-parser";
|
|
4
7
|
import { listProjectPendingHandoffs } from "@/lib/handoff";
|
|
5
8
|
import { formatHandoffNote } from "@/lib/handoff-note";
|
|
@@ -22,19 +25,9 @@ interface StopOptions {
|
|
|
22
25
|
session?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
getCommandDef: typeof getCommandDef;
|
|
27
|
-
logError: (message: string) => void;
|
|
28
|
-
exit: (code: number) => void;
|
|
29
|
-
isTTY: () => boolean;
|
|
30
|
-
}
|
|
28
|
+
type StopDeps = InteractiveCommandDeps;
|
|
31
29
|
|
|
32
|
-
const DEFAULT_STOP_DEPS
|
|
33
|
-
getCommandDef,
|
|
34
|
-
logError: (message: string) => p.log.error(message),
|
|
35
|
-
exit: (code: number) => process.exit(code),
|
|
36
|
-
isTTY: () => process.stdout.isTTY === true,
|
|
37
|
-
};
|
|
30
|
+
const DEFAULT_STOP_DEPS = createInteractiveCommandDeps();
|
|
38
31
|
|
|
39
32
|
type ResolvedStopHandoff = {
|
|
40
33
|
handoffStatus: Extract<HandoffStatus, "applied-auto" | "pending-apply" | "apply-conflicted">;
|
package/src/commands/update.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { getCommandDef } from "@/cli";
|
|
3
|
+
import type { SpinnerFactory } from "@/cli-io";
|
|
3
4
|
import { type CommandDef, parseCommand } from "@/lib/cli-parser";
|
|
4
5
|
import {
|
|
5
6
|
getDefaultSelfUpdateDependencies,
|
|
@@ -16,10 +17,7 @@ interface UpdateRuntime extends SelfUpdateDependencies {
|
|
|
16
17
|
getCommandDef: (name: string) => CommandDef | undefined;
|
|
17
18
|
parseCommand: typeof parseCommand;
|
|
18
19
|
performSelfUpdate: typeof performSelfUpdate;
|
|
19
|
-
spinner:
|
|
20
|
-
start: (message: string) => void;
|
|
21
|
-
stop: (message: string) => void;
|
|
22
|
-
};
|
|
20
|
+
spinner: SpinnerFactory;
|
|
23
21
|
log: {
|
|
24
22
|
error: (message: string) => void;
|
|
25
23
|
info: (message: string) => void;
|
package/src/lib/agents/claude.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { AgentConfig, AgentRole, ReviewOptions } from "@/lib/types";
|
|
6
|
-
import { createLineFormatter, parseJsonlEvent } from "./core";
|
|
6
|
+
import { createLineFormatter, extractLastParsedValue, parseJsonlEvent } from "./core";
|
|
7
7
|
import type {
|
|
8
8
|
AssistantContentBlock,
|
|
9
9
|
AssistantEvent,
|
|
@@ -128,21 +128,9 @@ export function formatClaudeEventForDisplay(event: ClaudeStreamEvent): string |
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export function extractClaudeResult(output: string): string | null {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const lines = output.split("\n");
|
|
136
|
-
let lastResult: string | null = null;
|
|
137
|
-
|
|
138
|
-
for (const line of lines) {
|
|
139
|
-
const event = parseClaudeStreamEvent(line);
|
|
140
|
-
if (event && isResultEvent(event)) {
|
|
141
|
-
lastResult = event.result;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return lastResult;
|
|
131
|
+
return extractLastParsedValue(output, parseClaudeStreamEvent, (event) =>
|
|
132
|
+
isResultEvent(event) ? event.result : null
|
|
133
|
+
);
|
|
146
134
|
}
|
|
147
135
|
|
|
148
136
|
export const formatClaudeLine = createLineFormatter(
|
package/src/lib/agents/core.ts
CHANGED
|
@@ -46,6 +46,22 @@ export function createLineFormatter<T>(
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function extractLastParsedValue<T>(
|
|
50
|
+
output: string,
|
|
51
|
+
parser: (line: string) => T | null,
|
|
52
|
+
selectValue: (event: T) => string | null
|
|
53
|
+
): string | null {
|
|
54
|
+
if (!output.trim()) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return output.split("\n").reduce<string | null>((lastResult, line) => {
|
|
59
|
+
const event = parser(line);
|
|
60
|
+
const value = event ? selectValue(event) : null;
|
|
61
|
+
return value ?? lastResult;
|
|
62
|
+
}, null);
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export function stripSystemReminders(text: unknown): string {
|
|
50
66
|
const normalized = typeof text === "string" ? text : String(text ?? "");
|
|
51
67
|
return normalized.replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").trim();
|
package/src/lib/agents/droid.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
createLineFormatter,
|
|
13
13
|
defaultBuildEnv,
|
|
14
|
+
extractLastParsedValue,
|
|
14
15
|
parseJsonlEvent,
|
|
15
16
|
stripSystemReminders,
|
|
16
17
|
} from "./core";
|
|
@@ -109,21 +110,9 @@ export function formatDroidEventForDisplay(event: DroidStreamEvent): string | nu
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
export function extractDroidResult(output: string): string | null {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const lines = output.split("\n");
|
|
117
|
-
let lastResult: string | null = null;
|
|
118
|
-
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
const event = parseDroidStreamEvent(line);
|
|
121
|
-
if (event?.type === "completion") {
|
|
122
|
-
lastResult = event.finalText;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return lastResult;
|
|
113
|
+
return extractLastParsedValue(output, parseDroidStreamEvent, (event) =>
|
|
114
|
+
event.type === "completion" ? event.finalText : null
|
|
115
|
+
);
|
|
127
116
|
}
|
|
128
117
|
|
|
129
118
|
export const formatDroidLine = createLineFormatter(
|
package/src/lib/cli-parser.ts
CHANGED
|
@@ -162,6 +162,23 @@ function consumeOptionValue(
|
|
|
162
162
|
throw new Error(`Option --${opt.name} requires a value`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function assignOptionValue(
|
|
166
|
+
values: Record<string, unknown>,
|
|
167
|
+
opt: OptionDef,
|
|
168
|
+
argv: string[],
|
|
169
|
+
currentIndex: number,
|
|
170
|
+
inlineValue?: string
|
|
171
|
+
): number {
|
|
172
|
+
if (opt.type === "boolean") {
|
|
173
|
+
values[opt.name] = true;
|
|
174
|
+
return currentIndex;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { value, nextIndex } = consumeOptionValue(opt, argv, currentIndex, inlineValue);
|
|
178
|
+
values[opt.name] = parseValue(opt, value);
|
|
179
|
+
return nextIndex;
|
|
180
|
+
}
|
|
181
|
+
|
|
165
182
|
export function parseCommand<T = Record<string, unknown>>(
|
|
166
183
|
def: CommandDef,
|
|
167
184
|
argv: string[]
|
|
@@ -216,13 +233,7 @@ export function parseCommand<T = Record<string, unknown>>(
|
|
|
216
233
|
throw new CliError(def.name, "unknown_option", arg, validOptions, suggestion);
|
|
217
234
|
}
|
|
218
235
|
|
|
219
|
-
|
|
220
|
-
values[opt.name] = true;
|
|
221
|
-
} else {
|
|
222
|
-
const { value, nextIndex } = consumeOptionValue(opt, argv, i, inlineValue);
|
|
223
|
-
i = nextIndex;
|
|
224
|
-
values[opt.name] = parseValue(opt, value);
|
|
225
|
-
}
|
|
236
|
+
i = assignOptionValue(values, opt, argv, i, inlineValue);
|
|
226
237
|
|
|
227
238
|
i++;
|
|
228
239
|
continue;
|
|
@@ -249,13 +260,7 @@ export function parseCommand<T = Record<string, unknown>>(
|
|
|
249
260
|
throw new CliError(def.name, "unknown_option", `-${alias}`, validOptions);
|
|
250
261
|
}
|
|
251
262
|
|
|
252
|
-
|
|
253
|
-
values[opt.name] = true;
|
|
254
|
-
} else {
|
|
255
|
-
const { value, nextIndex } = consumeOptionValue(opt, argv, i);
|
|
256
|
-
i = nextIndex;
|
|
257
|
-
values[opt.name] = parseValue(opt, value);
|
|
258
|
-
}
|
|
263
|
+
i = assignOptionValue(values, opt, argv, i);
|
|
259
264
|
|
|
260
265
|
i++;
|
|
261
266
|
continue;
|
package/src/lib/handoff.ts
CHANGED
|
@@ -386,8 +386,8 @@ export async function listProjectPendingHandoffs(
|
|
|
386
386
|
);
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
|
|
390
|
-
storageRoot: string
|
|
389
|
+
async function requirePendingHandoff(
|
|
390
|
+
storageRoot: string,
|
|
391
391
|
projectPath: string,
|
|
392
392
|
handoffId: string
|
|
393
393
|
): Promise<PendingHandoffArtifact> {
|
|
@@ -396,7 +396,19 @@ export async function applyPendingHandoff(
|
|
|
396
396
|
throw new Error(`Pending review handoff "${handoffId}" was not found.`);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
return
|
|
399
|
+
return artifact;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function applyPendingHandoff(
|
|
403
|
+
storageRoot: string = CONFIG_DIR,
|
|
404
|
+
projectPath: string,
|
|
405
|
+
handoffId: string
|
|
406
|
+
): Promise<PendingHandoffArtifact> {
|
|
407
|
+
return await applyPendingHandoffArtifact(
|
|
408
|
+
storageRoot,
|
|
409
|
+
await requirePendingHandoff(storageRoot, projectPath, handoffId),
|
|
410
|
+
"manual"
|
|
411
|
+
);
|
|
400
412
|
}
|
|
401
413
|
|
|
402
414
|
export async function discardPendingHandoff(
|
|
@@ -404,10 +416,7 @@ export async function discardPendingHandoff(
|
|
|
404
416
|
projectPath: string,
|
|
405
417
|
handoffId: string
|
|
406
418
|
): Promise<PendingHandoffArtifact> {
|
|
407
|
-
const artifact = await
|
|
408
|
-
if (!artifact) {
|
|
409
|
-
throw new Error(`Pending review handoff "${handoffId}" was not found.`);
|
|
410
|
-
}
|
|
419
|
+
const artifact = await requirePendingHandoff(storageRoot, projectPath, handoffId);
|
|
411
420
|
|
|
412
421
|
if (artifact.state === "apply-conflicted") {
|
|
413
422
|
throw new Error(
|
|
@@ -517,6 +517,7 @@ function applyEntryToSummary(
|
|
|
517
517
|
const unresolvedFindings = entry.fixResults.filter(
|
|
518
518
|
(result) => result.status === "unresolved"
|
|
519
519
|
).length;
|
|
520
|
+
const skippedFindings = entry.fixResults.filter((result) => result.status === "skipped").length;
|
|
520
521
|
const failed = entry.error !== undefined;
|
|
521
522
|
const interrupted = entry.error?.message.toLowerCase().includes("interrupt") === true;
|
|
522
523
|
|
|
@@ -528,7 +529,7 @@ function applyEntryToSummary(
|
|
|
528
529
|
next.totalResolvedSelectedFindings = resolvedFindings;
|
|
529
530
|
next.totalUnresolvedSelectedFindings = unresolvedFindings;
|
|
530
531
|
next.totalFixes = resolvedFindings;
|
|
531
|
-
next.totalSkipped = unresolvedFindings;
|
|
532
|
+
next.totalSkipped = skippedFindings + unresolvedFindings;
|
|
532
533
|
|
|
533
534
|
if (entry.duration !== undefined) {
|
|
534
535
|
next.totalDuration = (summary.totalDuration ?? 0) + entry.duration;
|
|
@@ -89,6 +89,6 @@ Use this strict framing protocol:
|
|
|
89
89
|
|
|
90
90
|
* **Do not** wrap the JSON in markdown fences or extra prose.
|
|
91
91
|
* The code_location field is required and must include absolute_file_path and line_range.
|
|
92
|
-
* Line ranges must be as short as possible for interpreting the issue (avoid ranges over 5–10 lines; pick the most suitable subrange).
|
|
92
|
+
* Line ranges must use integer `start` and `end` values with `end >= start`, and must be as short as possible for interpreting the issue (avoid ranges over 5–10 lines; pick the most suitable subrange).
|
|
93
93
|
* The code_location should overlap with the diff.
|
|
94
94
|
* Do not generate a PR fix.
|
|
@@ -11,7 +11,8 @@ export function createReviewerStructuredOutputInstructions(): string {
|
|
|
11
11
|
- ${REVIEW_SUMMARY_START_TOKEN}
|
|
12
12
|
- ${REVIEW_SUMMARY_END_TOKEN}
|
|
13
13
|
- Do not include markdown fences.
|
|
14
|
-
- Do not include any text before the start token or after the end token
|
|
14
|
+
- Do not include any text before the start token or after the end token.
|
|
15
|
+
- For each finding code_location.line_range, start and end MUST be integers and end MUST be greater than or equal to start.`;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function createFixerStructuredOutputInstructions(): string {
|
|
@@ -79,7 +79,9 @@ function isFixResultArray(value: unknown): value is FindingFixResult[] {
|
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
81
|
isFindingId(entry.findingId) &&
|
|
82
|
-
(entry.status === "resolved" ||
|
|
82
|
+
(entry.status === "resolved" ||
|
|
83
|
+
entry.status === "skipped" ||
|
|
84
|
+
entry.status === "unresolved") &&
|
|
83
85
|
typeof entry.summary === "string"
|
|
84
86
|
);
|
|
85
87
|
});
|
|
@@ -213,7 +213,7 @@ ${formatSelectedFindings(options.selectedFindings)}
|
|
|
213
213
|
|
|
214
214
|
## Required workflow
|
|
215
215
|
1. Verify each selected finding independently against the real code.
|
|
216
|
-
2. Decide resolved vs unresolved for each finding before making edits.
|
|
216
|
+
2. Decide resolved vs skipped vs unresolved for each finding before making edits.
|
|
217
217
|
3. Apply fixes only for findings you can prove.
|
|
218
218
|
4. Keep edits as local and minimal as possible.
|
|
219
219
|
5. Return one result entry for every selected finding ID.
|
|
@@ -231,8 +231,8 @@ ${FIX_SUMMARY_START_TOKEN}
|
|
|
231
231
|
"decision": "<NO_CHANGES_NEEDED | APPLY_SELECTIVELY | APPLY_MOST>",
|
|
232
232
|
"results": {
|
|
233
233
|
"F001": {
|
|
234
|
-
"status": "<resolved | unresolved>",
|
|
235
|
-
"summary": "<what changed or why the finding remains unresolved>"
|
|
234
|
+
"status": "<resolved | skipped | unresolved>",
|
|
235
|
+
"summary": "<what changed or why the finding was skipped or remains unresolved>"
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
}
|
|
@@ -240,11 +240,11 @@ ${FIX_SUMMARY_END_TOKEN}
|
|
|
240
240
|
|
|
241
241
|
JSON rules:
|
|
242
242
|
- Use the selected finding IDs as the object keys under \`results\`.
|
|
243
|
-
- Return \`resolved\` only when the selected finding is
|
|
244
|
-
- Return \`
|
|
243
|
+
- Return \`resolved\` only when the selected finding is verified and fixed; intentional edits may be included in a handoff.
|
|
244
|
+
- Return \`skipped\` only when the selected finding is not actionable or not proven and does not require a code change.
|
|
245
|
+
- Return \`unresolved\` when the finding is likely actionable or you attempted a fix but cannot safely complete and verify it.
|
|
245
246
|
- You must return one result entry for every selected finding ID.
|
|
246
247
|
- Do not include any finding that was not selected.
|
|
247
|
-
-
|
|
248
|
-
- Use \`unresolved\` when the finding was unproven, out of scope, or did not require a safe change.
|
|
248
|
+
- Missing result entries will be treated as \`unresolved\`.
|
|
249
249
|
- The delimited JSON block must be the final output.`;
|
|
250
250
|
}
|
|
@@ -19,7 +19,7 @@ import type { Config } from "@/lib/types";
|
|
|
19
19
|
import type { FixDecision } from "@/lib/types/domain";
|
|
20
20
|
|
|
21
21
|
interface BatchFixerResultEntry {
|
|
22
|
-
status: "resolved" | "unresolved";
|
|
22
|
+
status: "resolved" | "skipped" | "unresolved";
|
|
23
23
|
summary: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -66,7 +66,9 @@ function isBatchFixerResultEntry(value: unknown): value is BatchFixerResultEntry
|
|
|
66
66
|
|
|
67
67
|
const candidate = value as Record<string, unknown>;
|
|
68
68
|
return (
|
|
69
|
-
(candidate.status === "resolved" ||
|
|
69
|
+
(candidate.status === "resolved" ||
|
|
70
|
+
candidate.status === "skipped" ||
|
|
71
|
+
candidate.status === "unresolved") &&
|
|
70
72
|
typeof candidate.summary === "string"
|
|
71
73
|
);
|
|
72
74
|
}
|
|
@@ -124,6 +126,30 @@ function toFixResults(
|
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
async function appendBatchFixLog(
|
|
130
|
+
deps: RunBatchFixPhaseDependencies,
|
|
131
|
+
options: RunBatchFixPhaseOptions,
|
|
132
|
+
startedAt: number,
|
|
133
|
+
fixResults: FindingFixResult[],
|
|
134
|
+
error?: unknown
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
await deps.appendLog(options.artifact.logPath, {
|
|
137
|
+
type: "batch_fix",
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
duration: Date.now() - startedAt,
|
|
140
|
+
selectedFindingIds: options.selection.selectedFindingIds,
|
|
141
|
+
fixResults,
|
|
142
|
+
...(error === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: {
|
|
145
|
+
error: {
|
|
146
|
+
phase: "fixer" as const,
|
|
147
|
+
message: error instanceof Error ? error.message : String(error),
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
export async function runBatchFixPhase(
|
|
128
154
|
options: RunBatchFixPhaseOptions,
|
|
129
155
|
deps: RunBatchFixPhaseDependencies = DEFAULT_RUN_BATCH_FIX_PHASE_DEPENDENCIES
|
|
@@ -168,13 +194,7 @@ export async function runBatchFixPhase(
|
|
|
168
194
|
const fixResults = toFixResults(options.selection.selectedFindingIds, parsed);
|
|
169
195
|
|
|
170
196
|
deps.discardCheckpoint(options.worktree.worktreeProjectPath, checkpoint);
|
|
171
|
-
await deps
|
|
172
|
-
type: "batch_fix",
|
|
173
|
-
timestamp: Date.now(),
|
|
174
|
-
duration: Date.now() - startedAt,
|
|
175
|
-
selectedFindingIds: options.selection.selectedFindingIds,
|
|
176
|
-
fixResults,
|
|
177
|
-
});
|
|
197
|
+
await appendBatchFixLog(deps, options, startedAt, fixResults);
|
|
178
198
|
|
|
179
199
|
return {
|
|
180
200
|
phase: "batch-fix",
|
|
@@ -183,17 +203,7 @@ export async function runBatchFixPhase(
|
|
|
183
203
|
};
|
|
184
204
|
} catch (error) {
|
|
185
205
|
deps.rollbackToCheckpoint(options.worktree.worktreeProjectPath, checkpoint);
|
|
186
|
-
await deps
|
|
187
|
-
type: "batch_fix",
|
|
188
|
-
timestamp: Date.now(),
|
|
189
|
-
duration: Date.now() - startedAt,
|
|
190
|
-
selectedFindingIds: options.selection.selectedFindingIds,
|
|
191
|
-
fixResults: [],
|
|
192
|
-
error: {
|
|
193
|
-
phase: "fixer",
|
|
194
|
-
message: error instanceof Error ? error.message : String(error),
|
|
195
|
-
},
|
|
196
|
-
});
|
|
206
|
+
await appendBatchFixLog(deps, options, startedAt, [], error);
|
|
197
207
|
throw error;
|
|
198
208
|
}
|
|
199
209
|
}
|