ralph-review 0.2.2 → 0.2.4
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 +7 -5
- 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 +35 -20
- 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 +43 -0
- package/src/commands/list.ts +24 -7
- package/src/commands/log.ts +12 -12
- package/src/commands/run.ts +32 -33
- package/src/commands/status.ts +25 -4
- package/src/commands/stop.ts +99 -62
- 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/agents/models.ts +9 -0
- 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/ReviewModeOverlay.tsx +2 -2
- 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 +113 -61
- 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/stop.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createInteractiveCommandDeps,
|
|
3
|
+
createPromptDeps,
|
|
4
|
+
type InteractiveCommandDeps,
|
|
5
|
+
type PromptDeps,
|
|
6
|
+
} from "@/commands/interactive-deps";
|
|
3
7
|
import { parseCommand } from "@/lib/cli-parser";
|
|
4
8
|
import { listProjectPendingHandoffs } from "@/lib/handoff";
|
|
5
9
|
import { formatHandoffNote } from "@/lib/handoff-note";
|
|
@@ -22,18 +26,46 @@ interface StopOptions {
|
|
|
22
26
|
session?: string;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
type StopDeps = InteractiveCommandDeps &
|
|
30
|
+
PromptDeps & {
|
|
31
|
+
cwd: () => string;
|
|
32
|
+
computeSessionStats: typeof computeSessionStats;
|
|
33
|
+
listProjectPendingHandoffs: typeof listProjectPendingHandoffs;
|
|
34
|
+
listAllActiveSessions: typeof listAllActiveSessions;
|
|
35
|
+
listProjectActiveSessions: typeof listProjectActiveSessions;
|
|
36
|
+
removeAllSessionStates: typeof removeAllSessionStates;
|
|
37
|
+
stopActiveSession: typeof stopActiveSession;
|
|
38
|
+
updateSessionState: typeof updateSessionState;
|
|
39
|
+
sendInterrupt: typeof sendInterrupt;
|
|
40
|
+
readSessionState: typeof readSessionState;
|
|
41
|
+
sessionExists: typeof sessionExists;
|
|
42
|
+
killSession: typeof killSession;
|
|
43
|
+
removeSessionState: typeof removeSessionState;
|
|
44
|
+
listRalphSessions: typeof listRalphSessions;
|
|
45
|
+
sleep: (ms: number) => Promise<void>;
|
|
46
|
+
};
|
|
31
47
|
|
|
32
48
|
const DEFAULT_STOP_DEPS: StopDeps = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
...createInteractiveCommandDeps(),
|
|
50
|
+
...createPromptDeps(),
|
|
51
|
+
cwd: () => process.cwd(),
|
|
52
|
+
computeSessionStats,
|
|
53
|
+
listProjectPendingHandoffs,
|
|
54
|
+
listAllActiveSessions,
|
|
55
|
+
listProjectActiveSessions,
|
|
56
|
+
removeAllSessionStates,
|
|
57
|
+
stopActiveSession,
|
|
58
|
+
updateSessionState,
|
|
59
|
+
sendInterrupt,
|
|
60
|
+
readSessionState,
|
|
61
|
+
sessionExists,
|
|
62
|
+
killSession,
|
|
63
|
+
removeSessionState,
|
|
64
|
+
listRalphSessions,
|
|
65
|
+
sleep: (ms) =>
|
|
66
|
+
new Promise<void>((resolve) => {
|
|
67
|
+
setTimeout(resolve, ms);
|
|
68
|
+
}),
|
|
37
69
|
};
|
|
38
70
|
|
|
39
71
|
type ResolvedStopHandoff = {
|
|
@@ -97,12 +129,13 @@ function createLogSessionFromPath(session: ActiveSession): LogSession | null {
|
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
async function resolveStoppedSessionHandoff(
|
|
100
|
-
session: ActiveSession
|
|
132
|
+
session: ActiveSession,
|
|
133
|
+
deps: StopDeps
|
|
101
134
|
): Promise<ResolvedStopHandoff | null> {
|
|
102
135
|
const logSession = createLogSessionFromPath(session);
|
|
103
136
|
if (logSession) {
|
|
104
137
|
try {
|
|
105
|
-
const stats = await computeSessionStats(logSession);
|
|
138
|
+
const stats = await deps.computeSessionStats(logSession);
|
|
106
139
|
if (
|
|
107
140
|
(!stats.sessionId || stats.sessionId === session.sessionId) &&
|
|
108
141
|
isReportedStopHandoffStatus(stats.handoffStatus)
|
|
@@ -119,7 +152,7 @@ async function resolveStoppedSessionHandoff(
|
|
|
119
152
|
}
|
|
120
153
|
|
|
121
154
|
try {
|
|
122
|
-
const pendingHandoffs = await listProjectPendingHandoffs(undefined, session.projectPath);
|
|
155
|
+
const pendingHandoffs = await deps.listProjectPendingHandoffs(undefined, session.projectPath);
|
|
123
156
|
const matchingHandoffs = pendingHandoffs.filter(
|
|
124
157
|
(handoff) => handoff.sessionId === session.sessionId
|
|
125
158
|
);
|
|
@@ -160,9 +193,10 @@ function formatProjectScopedCommand(
|
|
|
160
193
|
|
|
161
194
|
async function resolveStoppedSessionHandoffNote(
|
|
162
195
|
session: ActiveSession,
|
|
163
|
-
currentProjectPath: string
|
|
196
|
+
currentProjectPath: string,
|
|
197
|
+
deps: StopDeps
|
|
164
198
|
): Promise<string | null> {
|
|
165
|
-
const handoff = await resolveStoppedSessionHandoff(session);
|
|
199
|
+
const handoff = await resolveStoppedSessionHandoff(session, deps);
|
|
166
200
|
if (!handoff) {
|
|
167
201
|
return null;
|
|
168
202
|
}
|
|
@@ -188,18 +222,20 @@ async function resolveStoppedSessionHandoffNote(
|
|
|
188
222
|
|
|
189
223
|
async function stopSessionWithHandoff(
|
|
190
224
|
session: ActiveSession,
|
|
191
|
-
currentProjectPath: string
|
|
225
|
+
currentProjectPath: string,
|
|
226
|
+
deps: StopDeps
|
|
192
227
|
): Promise<string | null> {
|
|
193
|
-
await stopActiveSession(session, {
|
|
194
|
-
updateSessionState,
|
|
195
|
-
sendInterrupt,
|
|
196
|
-
readSessionState,
|
|
197
|
-
sessionExists,
|
|
198
|
-
killSession,
|
|
199
|
-
removeSessionState,
|
|
228
|
+
await deps.stopActiveSession(session, {
|
|
229
|
+
updateSessionState: deps.updateSessionState,
|
|
230
|
+
sendInterrupt: deps.sendInterrupt,
|
|
231
|
+
readSessionState: deps.readSessionState,
|
|
232
|
+
sessionExists: deps.sessionExists,
|
|
233
|
+
killSession: deps.killSession,
|
|
234
|
+
removeSessionState: deps.removeSessionState,
|
|
235
|
+
sleep: deps.sleep,
|
|
200
236
|
});
|
|
201
237
|
|
|
202
|
-
return await resolveStoppedSessionHandoffNote(session, currentProjectPath);
|
|
238
|
+
return await resolveStoppedSessionHandoffNote(session, currentProjectPath, deps);
|
|
203
239
|
}
|
|
204
240
|
|
|
205
241
|
function findSessionBySelector(
|
|
@@ -240,9 +276,10 @@ function findSessionBySelector(
|
|
|
240
276
|
}
|
|
241
277
|
|
|
242
278
|
async function chooseProjectSession(
|
|
243
|
-
projectSessions: ActiveSession[]
|
|
279
|
+
projectSessions: ActiveSession[],
|
|
280
|
+
deps: StopDeps
|
|
244
281
|
): Promise<ActiveSession | null> {
|
|
245
|
-
const selection = await
|
|
282
|
+
const selection = await deps.select({
|
|
246
283
|
message: "Choose a review session to stop",
|
|
247
284
|
options: projectSessions.map((session) => ({
|
|
248
285
|
value: session.sessionId,
|
|
@@ -251,38 +288,38 @@ async function chooseProjectSession(
|
|
|
251
288
|
})),
|
|
252
289
|
});
|
|
253
290
|
|
|
254
|
-
if (
|
|
291
|
+
if (deps.isCancel(selection)) {
|
|
255
292
|
return null;
|
|
256
293
|
}
|
|
257
294
|
|
|
258
295
|
return projectSessions.find((session) => session.sessionId === selection) ?? null;
|
|
259
296
|
}
|
|
260
297
|
|
|
261
|
-
async function stopSession(session: ActiveSession): Promise<void> {
|
|
262
|
-
|
|
263
|
-
const handoffNote = await stopSessionWithHandoff(session,
|
|
264
|
-
|
|
298
|
+
async function stopSession(session: ActiveSession, deps: StopDeps): Promise<void> {
|
|
299
|
+
deps.logStep(`Stopping session: ${session.sessionName}`);
|
|
300
|
+
const handoffNote = await stopSessionWithHandoff(session, deps.cwd(), deps);
|
|
301
|
+
deps.logSuccess("Review stopped.");
|
|
265
302
|
if (handoffNote) {
|
|
266
|
-
|
|
303
|
+
deps.logMessage(`Handoff:\n${handoffNote}`);
|
|
267
304
|
}
|
|
268
305
|
}
|
|
269
306
|
|
|
270
|
-
async function stopAllSessions(): Promise<void> {
|
|
307
|
+
async function stopAllSessions(deps: StopDeps): Promise<void> {
|
|
271
308
|
const orphanStopGracePeriod = 1_000;
|
|
272
|
-
const currentProjectPath =
|
|
273
|
-
const activeSessions = await listAllActiveSessions();
|
|
274
|
-
const tmuxSessions = await listRalphSessions();
|
|
309
|
+
const currentProjectPath = deps.cwd();
|
|
310
|
+
const activeSessions = await deps.listAllActiveSessions();
|
|
311
|
+
const tmuxSessions = await deps.listRalphSessions();
|
|
275
312
|
const sessionNames = [
|
|
276
313
|
...new Set([...tmuxSessions, ...activeSessions.map((session) => session.sessionName)]),
|
|
277
314
|
];
|
|
278
315
|
|
|
279
316
|
if (sessionNames.length === 0) {
|
|
280
|
-
|
|
281
|
-
await removeAllSessionStates();
|
|
317
|
+
deps.logInfo("No active review sessions.");
|
|
318
|
+
await deps.removeAllSessionStates();
|
|
282
319
|
return;
|
|
283
320
|
}
|
|
284
321
|
|
|
285
|
-
|
|
322
|
+
deps.logStep(`Stopping ${sessionNames.length} session(s)...`);
|
|
286
323
|
|
|
287
324
|
const activeSessionsByName = new Map(
|
|
288
325
|
activeSessions.map((session) => [session.sessionName, session] as const)
|
|
@@ -292,38 +329,36 @@ async function stopAllSessions(): Promise<void> {
|
|
|
292
329
|
);
|
|
293
330
|
const activeStopPromise = Promise.all(
|
|
294
331
|
activeSessions.map(async (session) => ({
|
|
295
|
-
handoffNote: await stopSessionWithHandoff(session, currentProjectPath),
|
|
332
|
+
handoffNote: await stopSessionWithHandoff(session, currentProjectPath, deps),
|
|
296
333
|
}))
|
|
297
334
|
);
|
|
298
335
|
|
|
299
336
|
for (const sessionName of orphanSessionNames) {
|
|
300
|
-
await sendInterrupt(sessionName);
|
|
337
|
+
await deps.sendInterrupt(sessionName);
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
const stoppedActiveSessions = await activeStopPromise;
|
|
304
341
|
|
|
305
342
|
if (orphanSessionNames.length > 0) {
|
|
306
|
-
await
|
|
307
|
-
setTimeout(resolve, orphanStopGracePeriod);
|
|
308
|
-
});
|
|
343
|
+
await deps.sleep(orphanStopGracePeriod);
|
|
309
344
|
}
|
|
310
345
|
|
|
311
346
|
for (const sessionName of orphanSessionNames) {
|
|
312
|
-
await killSession(sessionName);
|
|
347
|
+
await deps.killSession(sessionName);
|
|
313
348
|
}
|
|
314
349
|
|
|
315
350
|
for (const sessionName of sessionNames) {
|
|
316
|
-
|
|
351
|
+
deps.logMessage(` Stopped: ${sessionName}`);
|
|
317
352
|
}
|
|
318
353
|
|
|
319
354
|
for (const stoppedSession of stoppedActiveSessions) {
|
|
320
355
|
if (stoppedSession.handoffNote) {
|
|
321
|
-
|
|
356
|
+
deps.logMessage(`Handoff:\n${stoppedSession.handoffNote}`);
|
|
322
357
|
}
|
|
323
358
|
}
|
|
324
359
|
|
|
325
|
-
await removeAllSessionStates();
|
|
326
|
-
|
|
360
|
+
await deps.removeAllSessionStates();
|
|
361
|
+
deps.logSuccess(`Stopped ${sessionNames.length} session(s).`);
|
|
327
362
|
}
|
|
328
363
|
|
|
329
364
|
async function stopCurrentProjectSession(
|
|
@@ -332,7 +367,7 @@ async function stopCurrentProjectSession(
|
|
|
332
367
|
deps: StopDeps
|
|
333
368
|
): Promise<void> {
|
|
334
369
|
const projectSessions = getCurrentProjectSessions(
|
|
335
|
-
await listProjectActiveSessions(undefined, projectPath),
|
|
370
|
+
await deps.listProjectActiveSessions(undefined, projectPath),
|
|
336
371
|
projectPath
|
|
337
372
|
);
|
|
338
373
|
|
|
@@ -344,17 +379,17 @@ async function stopCurrentProjectSession(
|
|
|
344
379
|
return;
|
|
345
380
|
}
|
|
346
381
|
|
|
347
|
-
await stopSession(match.session);
|
|
382
|
+
await stopSession(match.session, deps);
|
|
348
383
|
return;
|
|
349
384
|
}
|
|
350
385
|
|
|
351
386
|
if (projectSessions.length === 0) {
|
|
352
|
-
|
|
387
|
+
deps.logInfo("No active review session for current working directory.");
|
|
353
388
|
|
|
354
|
-
const allSessions = await listAllActiveSessions();
|
|
389
|
+
const allSessions = await deps.listAllActiveSessions();
|
|
355
390
|
if (allSessions.length > 0) {
|
|
356
|
-
|
|
357
|
-
|
|
391
|
+
deps.logMessage(`\nThere are ${allSessions.length} other session(s) running.`);
|
|
392
|
+
deps.logMessage(
|
|
358
393
|
'Use "rr stop --all" to stop all running review sessions, or "rr" to see details.'
|
|
359
394
|
);
|
|
360
395
|
}
|
|
@@ -364,7 +399,7 @@ async function stopCurrentProjectSession(
|
|
|
364
399
|
if (projectSessions.length === 1) {
|
|
365
400
|
const onlySession = projectSessions[0];
|
|
366
401
|
if (onlySession) {
|
|
367
|
-
await stopSession(onlySession);
|
|
402
|
+
await stopSession(onlySession, deps);
|
|
368
403
|
}
|
|
369
404
|
return;
|
|
370
405
|
}
|
|
@@ -377,12 +412,12 @@ async function stopCurrentProjectSession(
|
|
|
377
412
|
return;
|
|
378
413
|
}
|
|
379
414
|
|
|
380
|
-
const selectedSession = await chooseProjectSession(projectSessions);
|
|
415
|
+
const selectedSession = await chooseProjectSession(projectSessions, deps);
|
|
381
416
|
if (!selectedSession) {
|
|
382
417
|
return;
|
|
383
418
|
}
|
|
384
419
|
|
|
385
|
-
await stopSession(selectedSession);
|
|
420
|
+
await stopSession(selectedSession, deps);
|
|
386
421
|
}
|
|
387
422
|
|
|
388
423
|
export async function runStop(args: string[], deps: Partial<StopDeps> = {}): Promise<void> {
|
|
@@ -406,9 +441,11 @@ export async function runStop(args: string[], deps: Partial<StopDeps> = {}): Pro
|
|
|
406
441
|
}
|
|
407
442
|
|
|
408
443
|
if (options.all) {
|
|
409
|
-
await stopAllSessions();
|
|
444
|
+
await stopAllSessions(stopDeps);
|
|
410
445
|
return;
|
|
411
446
|
}
|
|
412
447
|
|
|
413
|
-
await stopCurrentProjectSession(
|
|
448
|
+
await stopCurrentProjectSession(stopDeps.cwd(), options.session, stopDeps);
|
|
414
449
|
}
|
|
450
|
+
|
|
451
|
+
export type { StopDeps };
|
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/agents/models.ts
CHANGED
|
@@ -52,6 +52,15 @@ export function registerDroidReasoningOptions(
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export function resetRegisteredReasoningOptions(): void {
|
|
56
|
+
Object.keys(codexReasoningLevelsByModel).forEach((model) => {
|
|
57
|
+
delete codexReasoningLevelsByModel[model];
|
|
58
|
+
});
|
|
59
|
+
Object.keys(droidReasoningLevelsByModel).forEach((model) => {
|
|
60
|
+
delete droidReasoningLevelsByModel[model];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
export function getDroidReasoningOptions(model: string): ReasoningLevel[] {
|
|
56
65
|
const levels = droidReasoningLevelsByModel[model];
|
|
57
66
|
return levels ? [...levels] : [];
|
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
|
}
|