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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isAgentType,
|
|
9
9
|
isReasoningLevel,
|
|
10
10
|
type ReasoningLevel,
|
|
11
|
+
type RetryOverrideConfig,
|
|
11
12
|
} from "@/lib/types";
|
|
12
13
|
|
|
13
14
|
type ConfigRole = "reviewer" | "fixer";
|
|
@@ -116,6 +117,40 @@ function parseInteger(value: string, key: ConfigKey): number {
|
|
|
116
117
|
return parsed;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
function parseBoundedIntegerUpdate(
|
|
121
|
+
key: ParsedScalarConfigUpdate["key"],
|
|
122
|
+
rawValue: string,
|
|
123
|
+
minimum: number
|
|
124
|
+
): ParsedScalarConfigUpdate {
|
|
125
|
+
const parsed = parseInteger(requireNonNullRawValue(key, rawValue), key);
|
|
126
|
+
if (parsed < minimum) {
|
|
127
|
+
throw new Error(`Value for "${key}" must be greater than or equal to ${minimum}.`);
|
|
128
|
+
}
|
|
129
|
+
return { key, value: parsed } as ParsedScalarConfigUpdate;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function requireNumberConfigValue(key: ConfigKey, value: ConfigValue, requirement: string): number {
|
|
133
|
+
if (typeof value === "number") {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(`Value for "${key}" must be an integer ${requirement}.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requirePiRoleSettings<T extends AgentSettings | AgentOverrideSettings>(
|
|
141
|
+
current: T | undefined,
|
|
142
|
+
role: ConfigRole,
|
|
143
|
+
command: string
|
|
144
|
+
): T {
|
|
145
|
+
if (!current || current.agent !== "pi") {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Cannot set "${role}.agent" to "pi" in a single-key update. Run "${command}" for pi setup.`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return current;
|
|
152
|
+
}
|
|
153
|
+
|
|
119
154
|
function requireNonNullRawValue(key: ConfigKey, rawValue: string): string {
|
|
120
155
|
if (rawValue === "null") {
|
|
121
156
|
throw new Error(`Value "null" is not allowed for "${key}".`);
|
|
@@ -205,36 +240,20 @@ export function parseConfigUpdate(key: ConfigKey, rawValue: string): ParsedConfi
|
|
|
205
240
|
return createRoleReasoningUpdate(key, rawValue);
|
|
206
241
|
|
|
207
242
|
case "maxIterations": {
|
|
208
|
-
|
|
209
|
-
if (parsed <= 0) {
|
|
210
|
-
throw new Error(`Value for "${key}" must be greater than 0.`);
|
|
211
|
-
}
|
|
212
|
-
return { key, value: parsed };
|
|
243
|
+
return parseBoundedIntegerUpdate(key, rawValue, 1);
|
|
213
244
|
}
|
|
214
245
|
|
|
215
246
|
case "iterationTimeout": {
|
|
216
|
-
|
|
217
|
-
if (parsed <= 0) {
|
|
218
|
-
throw new Error(`Value for "${key}" must be greater than 0.`);
|
|
219
|
-
}
|
|
220
|
-
return { key, value: parsed };
|
|
247
|
+
return parseBoundedIntegerUpdate(key, rawValue, 1);
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
case "retry.maxRetries": {
|
|
224
|
-
|
|
225
|
-
if (parsed < 0) {
|
|
226
|
-
throw new Error(`Value for "${key}" must be greater than or equal to 0.`);
|
|
227
|
-
}
|
|
228
|
-
return { key, value: parsed };
|
|
251
|
+
return parseBoundedIntegerUpdate(key, rawValue, 0);
|
|
229
252
|
}
|
|
230
253
|
|
|
231
254
|
case "retry.baseDelayMs":
|
|
232
255
|
case "retry.maxDelayMs": {
|
|
233
|
-
|
|
234
|
-
if (parsed <= 0) {
|
|
235
|
-
throw new Error(`Value for "${key}" must be greater than 0.`);
|
|
236
|
-
}
|
|
237
|
-
return { key, value: parsed };
|
|
256
|
+
return parseBoundedIntegerUpdate(key, rawValue, 1);
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
case "defaultReview.type":
|
|
@@ -329,18 +348,9 @@ function ensureRoleForMutation(
|
|
|
329
348
|
function applyRoleAgentUpdate(config: Config, role: ConfigRole, nextAgent: AgentType): Config {
|
|
330
349
|
const current = readRoleSettings(role, config);
|
|
331
350
|
if (nextAgent === "pi") {
|
|
332
|
-
|
|
333
|
-
throw new Error(
|
|
334
|
-
`Cannot set "${role}.agent" to "pi" in a single-key update. Run "rr init" for pi setup.`
|
|
335
|
-
);
|
|
336
|
-
}
|
|
351
|
+
const piSettings = requirePiRoleSettings(current, role, "rr init");
|
|
337
352
|
|
|
338
|
-
writeRoleSettings(role, config,
|
|
339
|
-
agent: "pi",
|
|
340
|
-
provider: current.provider,
|
|
341
|
-
model: current.model,
|
|
342
|
-
reasoning: current.reasoning,
|
|
343
|
-
});
|
|
353
|
+
writeRoleSettings(role, config, piSettings);
|
|
344
354
|
return config;
|
|
345
355
|
}
|
|
346
356
|
|
|
@@ -362,6 +372,86 @@ function applyRoleAgentUpdate(config: Config, role: ConfigRole, nextAgent: Agent
|
|
|
362
372
|
return config;
|
|
363
373
|
}
|
|
364
374
|
|
|
375
|
+
function setRequiredRetryValue(
|
|
376
|
+
config: Config,
|
|
377
|
+
field: keyof typeof DEFAULT_RETRY_CONFIG,
|
|
378
|
+
value: number
|
|
379
|
+
): void {
|
|
380
|
+
config.retry = config.retry ? { ...config.retry } : { ...DEFAULT_RETRY_CONFIG };
|
|
381
|
+
config.retry[field] = value;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function setOverrideRetryValue(
|
|
385
|
+
config: ConfigOverride,
|
|
386
|
+
field: keyof RetryOverrideConfig,
|
|
387
|
+
value: number
|
|
388
|
+
): void {
|
|
389
|
+
config.retry = {
|
|
390
|
+
...(config.retry && config.retry !== null ? config.retry : {}),
|
|
391
|
+
[field]: value,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function applyRoleProviderUpdate(
|
|
396
|
+
settings: AgentSettings | AgentOverrideSettings,
|
|
397
|
+
role: ConfigRole,
|
|
398
|
+
key: RoleConfigKey,
|
|
399
|
+
value: ConfigValue,
|
|
400
|
+
clearNonPiProvider: boolean
|
|
401
|
+
): void {
|
|
402
|
+
if (value === null) {
|
|
403
|
+
if (settings.agent !== "pi") {
|
|
404
|
+
if (clearNonPiProvider) {
|
|
405
|
+
delete settings.provider;
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
throw new Error(`Cannot unset "${role}.provider" while "${role}.agent" is "pi".`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (typeof value !== "string") {
|
|
413
|
+
throw new Error(`Value for "${key}" must be a string or null.`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (settings.agent !== "pi") {
|
|
417
|
+
throw new Error(`"${role}.provider" is only valid when "${role}.agent" is "pi".`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
settings.provider = value;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function applyRoleModelUpdate(
|
|
424
|
+
settings: AgentSettings | AgentOverrideSettings,
|
|
425
|
+
role: ConfigRole,
|
|
426
|
+
key: RoleConfigKey,
|
|
427
|
+
value: ConfigValue,
|
|
428
|
+
assignNullForNonPi: boolean
|
|
429
|
+
): void {
|
|
430
|
+
if (settings.agent === "pi") {
|
|
431
|
+
if (value === null || typeof value !== "string") {
|
|
432
|
+
throw new Error(`Cannot unset "${role}.model" while "${role}.agent" is "pi".`);
|
|
433
|
+
}
|
|
434
|
+
settings.model = value;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (value === null) {
|
|
439
|
+
if (assignNullForNonPi) {
|
|
440
|
+
settings.model = value;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
delete settings.model;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (typeof value === "string") {
|
|
448
|
+
settings.model = value;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
throw new Error(`Value for "${key}" must be a string or null.`);
|
|
453
|
+
}
|
|
454
|
+
|
|
365
455
|
export function setConfigValue(config: Config, key: ConfigKey, value: ConfigValue): Config {
|
|
366
456
|
const next = structuredClone(config) as Config;
|
|
367
457
|
|
|
@@ -385,49 +475,12 @@ export function setConfigValue(config: Config, key: ConfigKey, value: ConfigValu
|
|
|
385
475
|
const settings = ensureRoleForMutation(next, role, field);
|
|
386
476
|
|
|
387
477
|
if (field === "provider") {
|
|
388
|
-
|
|
389
|
-
if (settings.agent !== "pi") {
|
|
390
|
-
return next;
|
|
391
|
-
}
|
|
392
|
-
throw new Error(`Cannot unset "${role}.provider" while "${role}.agent" is "pi".`);
|
|
393
|
-
}
|
|
394
|
-
if (typeof value !== "string") {
|
|
395
|
-
throw new Error(`Value for "${key}" must be a string or null.`);
|
|
396
|
-
}
|
|
397
|
-
if (settings.agent !== "pi") {
|
|
398
|
-
throw new Error(`"${role}.provider" is only valid when "${role}.agent" is "pi".`);
|
|
399
|
-
}
|
|
400
|
-
settings.provider = value;
|
|
478
|
+
applyRoleProviderUpdate(settings, role, key as RoleConfigKey, value, false);
|
|
401
479
|
return next;
|
|
402
480
|
}
|
|
403
481
|
|
|
404
482
|
if (field === "model") {
|
|
405
|
-
|
|
406
|
-
if (value === null || typeof value !== "string") {
|
|
407
|
-
throw new Error(`Cannot unset "${role}.model" while "${role}.agent" is "pi".`);
|
|
408
|
-
}
|
|
409
|
-
settings.model = value;
|
|
410
|
-
return next;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (value === null) {
|
|
414
|
-
delete settings.model;
|
|
415
|
-
} else if (typeof value === "string") {
|
|
416
|
-
settings.model = value;
|
|
417
|
-
} else {
|
|
418
|
-
throw new Error(`Value for "${key}" must be a string or null.`);
|
|
419
|
-
}
|
|
420
|
-
return next;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (settings.agent === "pi") {
|
|
424
|
-
if (value === null) {
|
|
425
|
-
delete settings.reasoning;
|
|
426
|
-
} else if (typeof value === "string" && isReasoningLevel(value)) {
|
|
427
|
-
settings.reasoning = value;
|
|
428
|
-
} else {
|
|
429
|
-
throw new Error(`Value for "${key}" must be one of: low, medium, high, xhigh, max.`);
|
|
430
|
-
}
|
|
483
|
+
applyRoleModelUpdate(settings, role, key as RoleConfigKey, value, false);
|
|
431
484
|
return next;
|
|
432
485
|
}
|
|
433
486
|
|
|
@@ -479,25 +532,25 @@ export function setConfigValue(config: Config, key: ConfigKey, value: ConfigValu
|
|
|
479
532
|
next.defaultReview = { type: "base", branch: value };
|
|
480
533
|
return next;
|
|
481
534
|
case "retry.maxRetries":
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
535
|
+
setRequiredRetryValue(
|
|
536
|
+
next,
|
|
537
|
+
"maxRetries",
|
|
538
|
+
requireNumberConfigValue(key, value, "greater than or equal to 0")
|
|
539
|
+
);
|
|
487
540
|
return next;
|
|
488
541
|
case "retry.baseDelayMs":
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
542
|
+
setRequiredRetryValue(
|
|
543
|
+
next,
|
|
544
|
+
"baseDelayMs",
|
|
545
|
+
requireNumberConfigValue(key, value, "greater than 0")
|
|
546
|
+
);
|
|
494
547
|
return next;
|
|
495
548
|
case "retry.maxDelayMs":
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
549
|
+
setRequiredRetryValue(
|
|
550
|
+
next,
|
|
551
|
+
"maxDelayMs",
|
|
552
|
+
requireNumberConfigValue(key, value, "greater than 0")
|
|
553
|
+
);
|
|
501
554
|
return next;
|
|
502
555
|
case "notifications.sound.enabled":
|
|
503
556
|
if (typeof value !== "boolean") {
|
|
@@ -552,17 +605,13 @@ function applyOverrideRoleAgentUpdate(
|
|
|
552
605
|
): ConfigOverride {
|
|
553
606
|
const current = readOverrideRoleSettings(role, config);
|
|
554
607
|
if (nextAgent === "pi") {
|
|
555
|
-
|
|
556
|
-
throw new Error(
|
|
557
|
-
`Cannot set "${role}.agent" to "pi" in a single-key update. Run "rr init --local" for pi setup.`
|
|
558
|
-
);
|
|
559
|
-
}
|
|
608
|
+
const piSettings = requirePiRoleSettings(current, role, "rr init --local");
|
|
560
609
|
|
|
561
610
|
writeOverrideRoleSettings(role, config, {
|
|
562
611
|
agent: "pi",
|
|
563
|
-
provider:
|
|
564
|
-
model:
|
|
565
|
-
reasoning:
|
|
612
|
+
provider: piSettings.provider,
|
|
613
|
+
model: piSettings.model,
|
|
614
|
+
reasoning: piSettings.reasoning,
|
|
566
615
|
});
|
|
567
616
|
return config;
|
|
568
617
|
}
|
|
@@ -600,30 +649,12 @@ export function setConfigOverrideValue(
|
|
|
600
649
|
const settings = ensureOverrideRoleForMutation(next, role);
|
|
601
650
|
|
|
602
651
|
if (field === "provider") {
|
|
603
|
-
|
|
604
|
-
if (settings.agent !== "pi") {
|
|
605
|
-
delete settings.provider;
|
|
606
|
-
return next;
|
|
607
|
-
}
|
|
608
|
-
throw new Error(`Cannot unset "${role}.provider" while "${role}.agent" is "pi".`);
|
|
609
|
-
}
|
|
610
|
-
if (settings.agent !== "pi") {
|
|
611
|
-
throw new Error(`"${role}.provider" is only valid when "${role}.agent" is "pi".`);
|
|
612
|
-
}
|
|
613
|
-
settings.provider = value;
|
|
652
|
+
applyRoleProviderUpdate(settings, role, update.key, value, true);
|
|
614
653
|
return next;
|
|
615
654
|
}
|
|
616
655
|
|
|
617
656
|
if (field === "model") {
|
|
618
|
-
|
|
619
|
-
if (value === null) {
|
|
620
|
-
throw new Error(`Cannot unset "${role}.model" while "${role}.agent" is "pi".`);
|
|
621
|
-
}
|
|
622
|
-
settings.model = value;
|
|
623
|
-
return next;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
settings.model = value;
|
|
657
|
+
applyRoleModelUpdate(settings, role, update.key, value, true);
|
|
627
658
|
return next;
|
|
628
659
|
}
|
|
629
660
|
|
|
@@ -657,22 +688,13 @@ export function setConfigOverrideValue(
|
|
|
657
688
|
next.defaultReview = { type: "base", branch: update.value };
|
|
658
689
|
return next;
|
|
659
690
|
case "retry.maxRetries":
|
|
660
|
-
next
|
|
661
|
-
...(next.retry && next.retry !== null ? next.retry : {}),
|
|
662
|
-
maxRetries: update.value,
|
|
663
|
-
};
|
|
691
|
+
setOverrideRetryValue(next, "maxRetries", update.value);
|
|
664
692
|
return next;
|
|
665
693
|
case "retry.baseDelayMs":
|
|
666
|
-
next
|
|
667
|
-
...(next.retry && next.retry !== null ? next.retry : {}),
|
|
668
|
-
baseDelayMs: update.value,
|
|
669
|
-
};
|
|
694
|
+
setOverrideRetryValue(next, "baseDelayMs", update.value);
|
|
670
695
|
return next;
|
|
671
696
|
case "retry.maxDelayMs":
|
|
672
|
-
next
|
|
673
|
-
...(next.retry && next.retry !== null ? next.retry : {}),
|
|
674
|
-
maxDelayMs: update.value,
|
|
675
|
-
};
|
|
697
|
+
setOverrideRetryValue(next, "maxDelayMs", update.value);
|
|
676
698
|
return next;
|
|
677
699
|
case "notifications.sound.enabled":
|
|
678
700
|
next.notifications = {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
+
import type { SpinnerFactory } from "@/cli-io";
|
|
2
3
|
import { runDiagnostics } from "@/lib/diagnostics";
|
|
3
4
|
import type { FixResult, RemediationDependencies } from "@/lib/diagnostics/remediation";
|
|
4
5
|
import { applyFixes as defaultApplyFixes, isFixable } from "@/lib/diagnostics/remediation";
|
|
@@ -17,10 +18,7 @@ interface DoctorRuntime {
|
|
|
17
18
|
) => Promise<FixResult[]>;
|
|
18
19
|
intro: (message: string) => void;
|
|
19
20
|
note: (message: string, title: string) => void;
|
|
20
|
-
spinner:
|
|
21
|
-
start: (message: string) => void;
|
|
22
|
-
stop: (message: string) => void;
|
|
23
|
-
};
|
|
21
|
+
spinner: SpinnerFactory;
|
|
24
22
|
log: {
|
|
25
23
|
error: (message: string) => void;
|
|
26
24
|
warn: (message: string) => void;
|
package/src/commands/fix.ts
CHANGED
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
} from "@/lib/priority-list";
|
|
13
13
|
import { loadFindingsArtifactBySessionId } from "@/lib/review-workflow/findings/artifact";
|
|
14
14
|
import type { FindingId, FindingsArtifact } from "@/lib/review-workflow/findings/types";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
promptForFixSelection,
|
|
17
|
+
runFixSession,
|
|
18
|
+
} from "@/lib/review-workflow/remediation/run-fix-session";
|
|
16
19
|
import {
|
|
17
20
|
createSessionState,
|
|
18
21
|
HEARTBEAT_INTERVAL_MS,
|
|
@@ -67,30 +70,19 @@ export interface FixCommandDeps {
|
|
|
67
70
|
exit: (code: number) => void;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
options: artifact.findings.map((finding) => ({
|
|
75
|
-
value: finding.id,
|
|
76
|
-
label: `${finding.id} [${finding.priority}] ${finding.title}`,
|
|
77
|
-
hint: `${finding.filePath}:${finding.startLine}-${finding.endLine}`,
|
|
78
|
-
})),
|
|
79
|
-
required: false,
|
|
80
|
-
})
|
|
81
|
-
.then((selection) => {
|
|
82
|
-
if (p.isCancel(selection)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
73
|
+
interface LoadedFixArtifact {
|
|
74
|
+
parsed: ParsedFixCommandOptions;
|
|
75
|
+
artifact: FindingsArtifact;
|
|
76
|
+
}
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
interface PreparedFixCommand extends LoadedFixArtifact {
|
|
79
|
+
commandDeps: FixCommandDeps;
|
|
88
80
|
}
|
|
89
81
|
|
|
90
82
|
const DEFAULT_FIX_COMMAND_DEPS: FixCommandDeps = {
|
|
91
83
|
loadConfig: loadEffectiveConfig,
|
|
92
84
|
loadFindingsArtifactBySessionId,
|
|
93
|
-
promptForSelection:
|
|
85
|
+
promptForSelection: promptForFixSelection,
|
|
94
86
|
runFixSession,
|
|
95
87
|
isTTY: () => process.stdout.isTTY === true,
|
|
96
88
|
isTmuxInstalled,
|
|
@@ -332,31 +324,69 @@ export function parseFixCommandOptions(args: string[]): ParsedFixCommandOptions
|
|
|
332
324
|
};
|
|
333
325
|
}
|
|
334
326
|
|
|
335
|
-
|
|
336
|
-
args: string[]
|
|
337
|
-
|
|
338
|
-
): Promise<
|
|
339
|
-
const commandDeps = { ...DEFAULT_FIX_COMMAND_DEPS, ...deps };
|
|
340
|
-
|
|
327
|
+
async function loadFixCommandArtifact(
|
|
328
|
+
args: string[],
|
|
329
|
+
commandDeps: FixCommandDeps
|
|
330
|
+
): Promise<LoadedFixArtifact | null> {
|
|
341
331
|
let parsed: ParsedFixCommandOptions;
|
|
342
332
|
try {
|
|
343
333
|
parsed = parseFixCommandOptions(args);
|
|
344
334
|
} catch (error) {
|
|
345
335
|
commandDeps.logError(`${error}`);
|
|
346
336
|
commandDeps.exit(1);
|
|
347
|
-
return;
|
|
337
|
+
return null;
|
|
348
338
|
}
|
|
349
339
|
|
|
350
340
|
const artifact = await commandDeps.loadFindingsArtifactBySessionId(CONFIG_DIR, parsed.sessionId);
|
|
351
341
|
if (!artifact) {
|
|
352
342
|
commandDeps.logError(`Findings artifact not found for session ${parsed.sessionId}`);
|
|
353
343
|
commandDeps.exit(1);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { parsed, artifact };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function prepareFixCommand(
|
|
351
|
+
args: string[],
|
|
352
|
+
deps: Partial<FixCommandDeps>
|
|
353
|
+
): Promise<PreparedFixCommand | null> {
|
|
354
|
+
const commandDeps = { ...DEFAULT_FIX_COMMAND_DEPS, ...deps };
|
|
355
|
+
const loaded = await loadFixCommandArtifact(args, commandDeps);
|
|
356
|
+
return loaded ? { commandDeps, ...loaded } : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function withPreparedFixCommand(
|
|
360
|
+
args: string[],
|
|
361
|
+
deps: Partial<FixCommandDeps>,
|
|
362
|
+
run: (prepared: PreparedFixCommand) => Promise<void>
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const prepared = await prepareFixCommand(args, deps);
|
|
365
|
+
if (!prepared) {
|
|
354
366
|
return;
|
|
355
367
|
}
|
|
356
368
|
|
|
357
|
-
|
|
369
|
+
await run(prepared);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function loadRequiredFixConfig(
|
|
373
|
+
commandDeps: FixCommandDeps,
|
|
374
|
+
projectPath: string
|
|
375
|
+
): ReturnType<FixCommandDeps["loadConfig"]> {
|
|
376
|
+
const config = await commandDeps.loadConfig(projectPath);
|
|
377
|
+
if (!config) {
|
|
358
378
|
commandDeps.logError("Failed to load configuration");
|
|
359
379
|
commandDeps.exit(1);
|
|
380
|
+
}
|
|
381
|
+
return config;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function runPreparedFix({
|
|
385
|
+
commandDeps,
|
|
386
|
+
parsed,
|
|
387
|
+
artifact,
|
|
388
|
+
}: PreparedFixCommand): Promise<void> {
|
|
389
|
+
if (!(await loadRequiredFixConfig(commandDeps, artifact.projectPath))) {
|
|
360
390
|
return;
|
|
361
391
|
}
|
|
362
392
|
|
|
@@ -423,33 +453,18 @@ export async function runFix(
|
|
|
423
453
|
}
|
|
424
454
|
}
|
|
425
455
|
|
|
426
|
-
export
|
|
427
|
-
args
|
|
428
|
-
|
|
429
|
-
): Promise<void> {
|
|
430
|
-
const commandDeps = { ...DEFAULT_FIX_COMMAND_DEPS, ...deps };
|
|
431
|
-
|
|
432
|
-
let parsed: ParsedFixCommandOptions;
|
|
433
|
-
try {
|
|
434
|
-
parsed = parseFixCommandOptions(args);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
commandDeps.logError(`${error}`);
|
|
437
|
-
commandDeps.exit(1);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const artifact = await commandDeps.loadFindingsArtifactBySessionId(CONFIG_DIR, parsed.sessionId);
|
|
442
|
-
if (!artifact) {
|
|
443
|
-
commandDeps.logError(`Findings artifact not found for session ${parsed.sessionId}`);
|
|
444
|
-
commandDeps.exit(1);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
456
|
+
export function runFix(args: string[] = [], deps: Partial<FixCommandDeps> = {}): Promise<void> {
|
|
457
|
+
return withPreparedFixCommand(args, deps, runPreparedFix);
|
|
458
|
+
}
|
|
447
459
|
|
|
460
|
+
async function runPreparedFixForeground({
|
|
461
|
+
commandDeps,
|
|
462
|
+
parsed,
|
|
463
|
+
artifact,
|
|
464
|
+
}: PreparedFixCommand): Promise<void> {
|
|
448
465
|
const projectPath = commandDeps.env.RR_PROJECT_PATH || artifact.projectPath || commandDeps.cwd();
|
|
449
|
-
const config = await commandDeps
|
|
466
|
+
const config = await loadRequiredFixConfig(commandDeps, projectPath);
|
|
450
467
|
if (!config) {
|
|
451
|
-
commandDeps.logError("Failed to load configuration");
|
|
452
|
-
commandDeps.exit(1);
|
|
453
468
|
return;
|
|
454
469
|
}
|
|
455
470
|
|
|
@@ -593,3 +608,10 @@ export async function runFixForeground(
|
|
|
593
608
|
});
|
|
594
609
|
}
|
|
595
610
|
}
|
|
611
|
+
|
|
612
|
+
export function runFixForeground(
|
|
613
|
+
args: string[] = [],
|
|
614
|
+
deps: Partial<FixCommandDeps> = {}
|
|
615
|
+
): Promise<void> {
|
|
616
|
+
return withPreparedFixCommand(args, deps, runPreparedFixForeground);
|
|
617
|
+
}
|
|
@@ -2,6 +2,10 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import type { PendingHandoffArtifact } from "@/lib/handoff";
|
|
3
3
|
|
|
4
4
|
type HandoffAction = "apply" | "discard";
|
|
5
|
+
type HandoffSelect = (input: {
|
|
6
|
+
message: string;
|
|
7
|
+
options: Array<{ value: string; label: string; hint: string }>;
|
|
8
|
+
}) => Promise<unknown>;
|
|
5
9
|
|
|
6
10
|
interface SelectableHandoff {
|
|
7
11
|
handoffId: string;
|
|
@@ -14,10 +18,7 @@ interface ResolvePendingHandoffSelectionOptions {
|
|
|
14
18
|
selector?: string;
|
|
15
19
|
action: HandoffAction;
|
|
16
20
|
isTTY: boolean;
|
|
17
|
-
select?:
|
|
18
|
-
message: string;
|
|
19
|
-
options: Array<{ value: string; label: string; hint: string }>;
|
|
20
|
-
}) => Promise<unknown>;
|
|
21
|
+
select?: HandoffSelect;
|
|
21
22
|
isCancel?: (value: unknown) => boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -105,10 +106,7 @@ async function resolveHandoffSelection<T extends SelectableHandoff>(options: {
|
|
|
105
106
|
action: HandoffAction;
|
|
106
107
|
isTTY: boolean;
|
|
107
108
|
multipleHandoffsMessage: string;
|
|
108
|
-
select?:
|
|
109
|
-
message: string;
|
|
110
|
-
options: Array<{ value: string; label: string; hint: string }>;
|
|
111
|
-
}) => Promise<unknown>;
|
|
109
|
+
select?: HandoffSelect;
|
|
112
110
|
isCancel?: (value: unknown) => boolean;
|
|
113
111
|
}): Promise<HandoffSelectionResult<T>> {
|
|
114
112
|
if (options.selector) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { getCommandDef } from "@/cli";
|
|
3
|
+
|
|
4
|
+
export interface InteractiveCommandDeps {
|
|
5
|
+
getCommandDef: typeof getCommandDef;
|
|
6
|
+
logError: (message: string) => void;
|
|
7
|
+
exit: (code: number) => void;
|
|
8
|
+
isTTY: () => boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createInteractiveCommandDeps(): InteractiveCommandDeps {
|
|
12
|
+
return {
|
|
13
|
+
getCommandDef,
|
|
14
|
+
logError: (message) => p.log.error(message),
|
|
15
|
+
exit: (code) => process.exit(code),
|
|
16
|
+
isTTY: () => process.stdout.isTTY === true,
|
|
17
|
+
};
|
|
18
|
+
}
|
package/src/commands/log.ts
CHANGED
|
@@ -82,6 +82,16 @@ function isUnknownEmptySession(session: SessionStats): boolean {
|
|
|
82
82
|
return session.status === "unknown" && session.iterations === 0;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function printNoProjectSessions(projectName: string, json: boolean): void {
|
|
86
|
+
if (json) {
|
|
87
|
+
console.log(JSON.stringify({ project: projectName, sessions: [] }, null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
p.log.info("No review sessions found for current working directory.");
|
|
92
|
+
p.log.message('Start a review with "rr run" first.');
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
function formatDate(timestamp: number): string {
|
|
86
96
|
return new Date(timestamp).toLocaleString();
|
|
87
97
|
}
|
|
@@ -412,12 +422,7 @@ export async function runLog(args: string[]): Promise<void> {
|
|
|
412
422
|
const projectSessions = await listProjectLogSessions(CONFIG_DIR, currentProjectPath);
|
|
413
423
|
|
|
414
424
|
if (projectSessions.length === 0) {
|
|
415
|
-
|
|
416
|
-
console.log(JSON.stringify({ project: projectName, sessions: [] }, null, 2));
|
|
417
|
-
} else {
|
|
418
|
-
p.log.info("No review sessions found for current working directory.");
|
|
419
|
-
p.log.message('Start a review with "rr run" first.');
|
|
420
|
-
}
|
|
425
|
+
printNoProjectSessions(projectName, options.json);
|
|
421
426
|
return;
|
|
422
427
|
}
|
|
423
428
|
|
|
@@ -451,12 +456,7 @@ export async function runLog(args: string[]): Promise<void> {
|
|
|
451
456
|
}
|
|
452
457
|
|
|
453
458
|
if (sessionStats.length === 0) {
|
|
454
|
-
|
|
455
|
-
console.log(JSON.stringify({ project: projectName, sessions: [] }, null, 2));
|
|
456
|
-
} else {
|
|
457
|
-
p.log.info("No review sessions found for current working directory.");
|
|
458
|
-
p.log.message('Start a review with "rr run" first.');
|
|
459
|
-
}
|
|
459
|
+
printNoProjectSessions(projectName, options.json);
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
462
|
|