ralph-review 0.1.7 → 0.1.8
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 +1 -1
- package/package.json +1 -1
- package/src/cli-core.ts +6 -5
- package/src/cli.ts +25 -3
- package/src/commands/config.ts +92 -27
- package/src/commands/init.ts +15 -9
- package/src/commands/run.ts +20 -20
- package/src/commands/stop.ts +1 -1
- package/src/commands/update.ts +9 -1
- package/src/lib/cli-parser.ts +29 -9
- package/src/lib/config.ts +239 -82
- package/src/lib/diagnostics/checks.ts +1 -1
- package/src/lib/diagnostics/remediation.ts +1 -1
- package/src/lib/tui/components/StatusBar.tsx +3 -3
- package/src/lib/types/config.ts +1 -1
package/README.md
CHANGED
|
@@ -162,6 +162,7 @@ rrr
|
|
|
162
162
|
|
|
163
163
|
| Command | Description |
|
|
164
164
|
|---------|-------------|
|
|
165
|
+
| `rr` | Interactive Mode |
|
|
165
166
|
| `rr init` | Configure reviewer, fixer, and simplifier agents (auto-detects installed CLIs) |
|
|
166
167
|
| `rr run` | Start review cycle in a tmux session |
|
|
167
168
|
| `rr run --base main` | Review changes against a base branch |
|
|
@@ -172,7 +173,6 @@ rrr
|
|
|
172
173
|
| `rr config show` | Print full configuration |
|
|
173
174
|
| `rr config set KEY VAL` | Update a config value (e.g. `rr config set maxIterations 8`) |
|
|
174
175
|
| `rr list` | List active review sessions |
|
|
175
|
-
| `rr status` | Show current review status |
|
|
176
176
|
| `rr stop` | Stop running review session (`--all` to stop all) |
|
|
177
177
|
| `rr log` | View review logs (`-n 5` for last 5, `--json` for JSON output) |
|
|
178
178
|
| `rr dashboard` | Open review dashboard in browser |
|
package/package.json
CHANGED
package/src/cli-core.ts
CHANGED
|
@@ -81,18 +81,18 @@ export const COMMANDS: CommandDef[] = [
|
|
|
81
81
|
description: "Disable finish sound for this run (override config)",
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
|
-
name: "
|
|
84
|
+
name: "interactive",
|
|
85
85
|
type: "boolean",
|
|
86
|
-
description: "
|
|
86
|
+
description: "Launch Interactive Mode after starting the run (override config)",
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
|
-
name: "no-
|
|
89
|
+
name: "no-interactive",
|
|
90
90
|
type: "boolean",
|
|
91
|
-
description: "Start run without
|
|
91
|
+
description: "Start run without launching Interactive Mode (override config)",
|
|
92
92
|
},
|
|
93
93
|
SIMPLIFIER_OPTION,
|
|
94
94
|
],
|
|
95
|
-
examples: ["rr run", "rr run --base main", "rr run --no-
|
|
95
|
+
examples: ["rr run", "rr run --base main", "rr run --no-interactive"],
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
name: "list",
|
|
@@ -104,6 +104,7 @@ export const COMMANDS: CommandDef[] = [
|
|
|
104
104
|
name: "status",
|
|
105
105
|
description: "Show review status",
|
|
106
106
|
examples: ["rr status"],
|
|
107
|
+
hidden: true,
|
|
107
108
|
},
|
|
108
109
|
{
|
|
109
110
|
name: "stop",
|
package/src/cli.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface CliDeps {
|
|
|
43
43
|
runDoctor: typeof runDoctor;
|
|
44
44
|
runList: typeof runList;
|
|
45
45
|
runUpdate: typeof runUpdate;
|
|
46
|
+
isInteractiveTerminal: () => boolean;
|
|
46
47
|
log: (message: string) => void;
|
|
47
48
|
logError: (message: string) => void;
|
|
48
49
|
logMessage: (message: string) => void;
|
|
@@ -53,6 +54,8 @@ const CONSOLE_LOG = console.log.bind(console) as (message: string) => void;
|
|
|
53
54
|
const CLACK_ERROR = p.log.error.bind(p.log) as (message: string) => void;
|
|
54
55
|
const CLACK_MESSAGE = p.log.message.bind(p.log) as (message: string) => void;
|
|
55
56
|
const PROCESS_EXIT = process.exit.bind(process) as (code: number) => void;
|
|
57
|
+
const IS_INTERACTIVE_TERMINAL = (): boolean =>
|
|
58
|
+
process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
56
59
|
|
|
57
60
|
const DEFAULT_CLI_DEPS: CliDeps = {
|
|
58
61
|
parseArgs,
|
|
@@ -72,6 +75,7 @@ const DEFAULT_CLI_DEPS: CliDeps = {
|
|
|
72
75
|
runDoctor,
|
|
73
76
|
runList,
|
|
74
77
|
runUpdate,
|
|
78
|
+
isInteractiveTerminal: IS_INTERACTIVE_TERMINAL,
|
|
75
79
|
log: CONSOLE_LOG,
|
|
76
80
|
logError: CLACK_ERROR,
|
|
77
81
|
logMessage: CLACK_MESSAGE,
|
|
@@ -82,6 +86,11 @@ function buildCliDeps(overrides: Partial<CliDeps>): CliDeps {
|
|
|
82
86
|
return { ...DEFAULT_CLI_DEPS, ...overrides };
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
function reportCliError(cliDeps: CliDeps, error: unknown): void {
|
|
90
|
+
cliDeps.logError(`Error: ${error}`);
|
|
91
|
+
cliDeps.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
export async function runCli(
|
|
86
95
|
args: string[] = process.argv.slice(2),
|
|
87
96
|
deps: Partial<CliDeps> = {}
|
|
@@ -94,11 +103,25 @@ export async function runCli(
|
|
|
94
103
|
return;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
if (!command) {
|
|
106
|
+
if (showHelp && !command) {
|
|
98
107
|
cliDeps.log(cliDeps.printUsage());
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
if (!command) {
|
|
112
|
+
if (commandArgs.length > 0 || !cliDeps.isInteractiveTerminal()) {
|
|
113
|
+
cliDeps.log(cliDeps.printUsage());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await cliDeps.runStatus();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
reportCliError(cliDeps, error);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
if (showHelp) {
|
|
103
126
|
const commandHelp = cliDeps.printCommandHelp(command);
|
|
104
127
|
if (commandHelp) {
|
|
@@ -183,8 +206,7 @@ export async function runCli(
|
|
|
183
206
|
return;
|
|
184
207
|
}
|
|
185
208
|
} catch (error) {
|
|
186
|
-
cliDeps
|
|
187
|
-
cliDeps.exit(1);
|
|
209
|
+
reportCliError(cliDeps, error);
|
|
188
210
|
}
|
|
189
211
|
}
|
|
190
212
|
|
package/src/commands/config.ts
CHANGED
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
configExists,
|
|
5
5
|
ensureConfigDir,
|
|
6
6
|
loadConfig,
|
|
7
|
+
loadConfigWithDiagnostics,
|
|
7
8
|
parseConfig,
|
|
9
|
+
parseConfigWithDiagnostics,
|
|
8
10
|
saveConfig,
|
|
9
11
|
} from "@/lib/config";
|
|
10
12
|
import {
|
|
@@ -41,7 +43,9 @@ export type ConfigCommandDeps = {
|
|
|
41
43
|
configExists: typeof configExists;
|
|
42
44
|
ensureConfigDir: typeof ensureConfigDir;
|
|
43
45
|
loadConfig: typeof loadConfig;
|
|
46
|
+
loadConfigWithDiagnostics: typeof loadConfigWithDiagnostics;
|
|
44
47
|
parseConfig: typeof parseConfig;
|
|
48
|
+
parseConfigWithDiagnostics: typeof parseConfigWithDiagnostics;
|
|
45
49
|
saveConfig: typeof saveConfig;
|
|
46
50
|
spawn: ConfigCommandSpawner;
|
|
47
51
|
env: Record<string, string | undefined>;
|
|
@@ -68,7 +72,7 @@ const CONFIG_KEYS = [
|
|
|
68
72
|
"defaultReview.type",
|
|
69
73
|
"defaultReview.branch",
|
|
70
74
|
"run.simplifier",
|
|
71
|
-
"run.
|
|
75
|
+
"run.interactive",
|
|
72
76
|
"retry.maxRetries",
|
|
73
77
|
"retry.baseDelayMs",
|
|
74
78
|
"retry.maxDelayMs",
|
|
@@ -251,7 +255,7 @@ export function parseConfigValue(key: ConfigKey, rawValue: string): ConfigValue
|
|
|
251
255
|
return rawValue;
|
|
252
256
|
|
|
253
257
|
case "run.simplifier":
|
|
254
|
-
case "run.
|
|
258
|
+
case "run.interactive":
|
|
255
259
|
if (rawValue !== "true" && rawValue !== "false") {
|
|
256
260
|
throw new Error(`Value for "${key}" must be "true" or "false".`);
|
|
257
261
|
}
|
|
@@ -306,8 +310,8 @@ export function getConfigValue(config: Config, key: ConfigKey): unknown {
|
|
|
306
310
|
return config.defaultReview.type === "base" ? config.defaultReview.branch : undefined;
|
|
307
311
|
case "run.simplifier":
|
|
308
312
|
return config.run?.simplifier;
|
|
309
|
-
case "run.
|
|
310
|
-
return config.run?.
|
|
313
|
+
case "run.interactive":
|
|
314
|
+
return config.run?.interactive;
|
|
311
315
|
case "retry.maxRetries":
|
|
312
316
|
return config.retry?.maxRetries;
|
|
313
317
|
case "retry.baseDelayMs":
|
|
@@ -490,13 +494,13 @@ export function setConfigValue(config: Config, key: ConfigKey, value: ConfigValu
|
|
|
490
494
|
if (typeof value !== "boolean") {
|
|
491
495
|
throw new Error(`Value for "${key}" must be "true" or "false".`);
|
|
492
496
|
}
|
|
493
|
-
next.run = { simplifier: value,
|
|
497
|
+
next.run = { simplifier: value, interactive: next.run?.interactive ?? true };
|
|
494
498
|
return next;
|
|
495
|
-
case "run.
|
|
499
|
+
case "run.interactive":
|
|
496
500
|
if (typeof value !== "boolean") {
|
|
497
501
|
throw new Error(`Value for "${key}" must be "true" or "false".`);
|
|
498
502
|
}
|
|
499
|
-
next.run = { simplifier: next.run?.simplifier ?? false,
|
|
503
|
+
next.run = { simplifier: next.run?.simplifier ?? false, interactive: value };
|
|
500
504
|
return next;
|
|
501
505
|
case "retry.maxRetries":
|
|
502
506
|
next.retry = next.retry ? { ...next.retry } : { ...DEFAULT_RETRY_CONFIG };
|
|
@@ -591,26 +595,55 @@ export function validateConfigInvariants(config: Config): string[] {
|
|
|
591
595
|
if (config.run && typeof config.run.simplifier !== "boolean") {
|
|
592
596
|
errors.push("run.simplifier must be a boolean.");
|
|
593
597
|
}
|
|
594
|
-
if (config.run && typeof config.run.
|
|
595
|
-
errors.push("run.
|
|
598
|
+
if (config.run && typeof config.run.interactive !== "boolean") {
|
|
599
|
+
errors.push("run.interactive must be a boolean.");
|
|
596
600
|
}
|
|
597
601
|
|
|
598
602
|
return errors;
|
|
599
603
|
}
|
|
600
604
|
|
|
605
|
+
function formatConfigValidationMessage(header: string, errors: string[], footer?: string): string {
|
|
606
|
+
const uniqueErrors = [...new Set(errors)];
|
|
607
|
+
const lines = [header];
|
|
608
|
+
for (const error of uniqueErrors) {
|
|
609
|
+
lines.push(`- ${error}`);
|
|
610
|
+
}
|
|
611
|
+
if (footer) {
|
|
612
|
+
lines.push(footer);
|
|
613
|
+
}
|
|
614
|
+
return lines.join("\n");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function collectConfigValidationErrors(config: Config | null, errors: string[]): string[] {
|
|
618
|
+
const combined = [...errors];
|
|
619
|
+
if (config) {
|
|
620
|
+
combined.push(...validateConfigInvariants(config));
|
|
621
|
+
}
|
|
622
|
+
return [...new Set(combined)];
|
|
623
|
+
}
|
|
624
|
+
|
|
601
625
|
async function loadExistingConfig(deps: ConfigCommandDeps): Promise<Config> {
|
|
602
626
|
if (!(await deps.configExists())) {
|
|
603
627
|
throw new Error('Configuration not found. Run "rr init" first.');
|
|
604
628
|
}
|
|
605
629
|
|
|
606
|
-
const
|
|
607
|
-
if (!
|
|
630
|
+
const loaded = await deps.loadConfigWithDiagnostics();
|
|
631
|
+
if (!loaded.exists) {
|
|
632
|
+
throw new Error('Configuration not found. Run "rr init" first.');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const errors = collectConfigValidationErrors(loaded.config, loaded.errors);
|
|
636
|
+
if (!loaded.config || errors.length > 0) {
|
|
608
637
|
throw new Error(
|
|
609
|
-
|
|
638
|
+
formatConfigValidationMessage(
|
|
639
|
+
`Invalid configuration: ${deps.configPath}`,
|
|
640
|
+
errors.length > 0 ? errors : ["Configuration format is invalid."],
|
|
641
|
+
'Run "rr init" to regenerate the file, or fix it manually.'
|
|
642
|
+
)
|
|
610
643
|
);
|
|
611
644
|
}
|
|
612
645
|
|
|
613
|
-
return config;
|
|
646
|
+
return loaded.config;
|
|
614
647
|
}
|
|
615
648
|
|
|
616
649
|
async function runShow(args: string[], deps: ConfigCommandDeps): Promise<void> {
|
|
@@ -650,17 +683,18 @@ async function runSet(args: string[], deps: ConfigCommandDeps): Promise<void> {
|
|
|
650
683
|
const current = await loadExistingConfig(deps);
|
|
651
684
|
const updated = setConfigValue(current, key, parsedValue);
|
|
652
685
|
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
686
|
+
const normalized = deps.parseConfigWithDiagnostics(updated as unknown);
|
|
687
|
+
const validationErrors = collectConfigValidationErrors(normalized.config, normalized.errors);
|
|
688
|
+
if (!normalized.config || validationErrors.length > 0) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
formatConfigValidationMessage(
|
|
691
|
+
"Updated configuration is invalid.",
|
|
692
|
+
validationErrors.length > 0 ? validationErrors : ["Configuration format is invalid."]
|
|
693
|
+
)
|
|
694
|
+
);
|
|
661
695
|
}
|
|
662
696
|
|
|
663
|
-
await deps.saveConfig(normalized);
|
|
697
|
+
await deps.saveConfig(normalized.config);
|
|
664
698
|
deps.log.success(`Updated "${key}" to ${formatValue(parsedValue)}.`);
|
|
665
699
|
}
|
|
666
700
|
|
|
@@ -695,15 +729,20 @@ async function runEdit(args: string[], deps: ConfigCommandDeps): Promise<void> {
|
|
|
695
729
|
return;
|
|
696
730
|
}
|
|
697
731
|
|
|
698
|
-
const
|
|
699
|
-
|
|
732
|
+
const loaded = await deps.loadConfigWithDiagnostics();
|
|
733
|
+
const errors = collectConfigValidationErrors(loaded.config, loaded.errors);
|
|
734
|
+
if (!loaded.config || errors.length > 0) {
|
|
700
735
|
deps.log.warn(
|
|
701
|
-
|
|
736
|
+
formatConfigValidationMessage(
|
|
737
|
+
`Invalid configuration: ${deps.configPath}`,
|
|
738
|
+
errors.length > 0 ? errors : ["Configuration format is invalid."],
|
|
739
|
+
'Run "rr init" to regenerate the file, or fix it manually.'
|
|
740
|
+
)
|
|
702
741
|
);
|
|
703
742
|
return;
|
|
704
743
|
}
|
|
705
744
|
|
|
706
|
-
await deps.saveConfig(config);
|
|
745
|
+
await deps.saveConfig(loaded.config);
|
|
707
746
|
}
|
|
708
747
|
|
|
709
748
|
const DEFAULT_CONFIG_COMMAND_DEPS: ConfigCommandDeps = {
|
|
@@ -711,7 +750,9 @@ const DEFAULT_CONFIG_COMMAND_DEPS: ConfigCommandDeps = {
|
|
|
711
750
|
configExists,
|
|
712
751
|
ensureConfigDir,
|
|
713
752
|
loadConfig,
|
|
753
|
+
loadConfigWithDiagnostics,
|
|
714
754
|
parseConfig,
|
|
755
|
+
parseConfigWithDiagnostics,
|
|
715
756
|
saveConfig,
|
|
716
757
|
spawn: Bun.spawn as unknown as ConfigCommandSpawner,
|
|
717
758
|
env: process.env as Record<string, string | undefined>,
|
|
@@ -731,7 +772,7 @@ function resolveConfigCommandDeps(overrides?: Partial<ConfigCommandDeps>): Confi
|
|
|
731
772
|
return DEFAULT_CONFIG_COMMAND_DEPS;
|
|
732
773
|
}
|
|
733
774
|
|
|
734
|
-
|
|
775
|
+
const deps: ConfigCommandDeps = {
|
|
735
776
|
...DEFAULT_CONFIG_COMMAND_DEPS,
|
|
736
777
|
...overrides,
|
|
737
778
|
log: {
|
|
@@ -739,6 +780,30 @@ function resolveConfigCommandDeps(overrides?: Partial<ConfigCommandDeps>): Confi
|
|
|
739
780
|
...overrides.log,
|
|
740
781
|
},
|
|
741
782
|
};
|
|
783
|
+
|
|
784
|
+
const loadConfigOverride = overrides.loadConfig;
|
|
785
|
+
if (loadConfigOverride && !overrides.loadConfigWithDiagnostics) {
|
|
786
|
+
deps.loadConfigWithDiagnostics = async (path = deps.configPath) => {
|
|
787
|
+
const config = (await loadConfigOverride(path)) ?? null;
|
|
788
|
+
return {
|
|
789
|
+
exists: config !== null,
|
|
790
|
+
config,
|
|
791
|
+
errors: config ? [] : ["Configuration format is invalid."],
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (overrides.parseConfig && !overrides.parseConfigWithDiagnostics) {
|
|
797
|
+
deps.parseConfigWithDiagnostics = (value) => {
|
|
798
|
+
const config = overrides.parseConfig?.(value) ?? null;
|
|
799
|
+
return {
|
|
800
|
+
config,
|
|
801
|
+
errors: config ? [] : ["Configuration format is invalid."],
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return deps;
|
|
742
807
|
}
|
|
743
808
|
|
|
744
809
|
export function createRunConfig(overrides?: Partial<ConfigCommandDeps>) {
|
package/src/commands/init.ts
CHANGED
|
@@ -53,7 +53,7 @@ interface InitInput {
|
|
|
53
53
|
defaultReviewType: "uncommitted" | "base";
|
|
54
54
|
defaultReviewBranch?: string;
|
|
55
55
|
runSimplifierByDefault: boolean;
|
|
56
|
-
|
|
56
|
+
runInteractiveByDefault: boolean;
|
|
57
57
|
soundNotificationsEnabled: boolean;
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -319,7 +319,7 @@ export function buildConfig(input: InitInput): Config {
|
|
|
319
319
|
),
|
|
320
320
|
run: {
|
|
321
321
|
simplifier: input.runSimplifierByDefault,
|
|
322
|
-
|
|
322
|
+
interactive: input.runInteractiveByDefault,
|
|
323
323
|
},
|
|
324
324
|
maxIterations: input.maxIterations,
|
|
325
325
|
iterationTimeout: input.iterationTimeoutMinutes * 60 * 1000,
|
|
@@ -537,7 +537,7 @@ function formatConfigDisplay(config: Config): string {
|
|
|
537
537
|
` Iteration timeout: ${config.iterationTimeout / 1000 / 60} minutes`,
|
|
538
538
|
` Default review: ${defaultReviewDisplay}`,
|
|
539
539
|
` Run simplifier: ${config.run?.simplifier ? "enabled" : "disabled"}`,
|
|
540
|
-
`
|
|
540
|
+
` Interactive Mode: ${(config.run?.interactive ?? true) ? "enabled" : "disabled"}`,
|
|
541
541
|
` Sound notify: ${config.notifications.sound.enabled ? "enabled" : "disabled"}`,
|
|
542
542
|
].join("\n");
|
|
543
543
|
}
|
|
@@ -757,7 +757,7 @@ export async function buildAutoInitInput(
|
|
|
757
757
|
iterationTimeoutMinutes,
|
|
758
758
|
defaultReviewType: "uncommitted",
|
|
759
759
|
runSimplifierByDefault: false,
|
|
760
|
-
|
|
760
|
+
runInteractiveByDefault: true,
|
|
761
761
|
soundNotificationsEnabled: true,
|
|
762
762
|
},
|
|
763
763
|
skippedAgents,
|
|
@@ -919,14 +919,17 @@ async function promptForCustomInitInput(
|
|
|
919
919
|
defaultReviewType: defaultReviewType as "uncommitted" | "base",
|
|
920
920
|
defaultReviewBranch: defaultReviewBranch as string | undefined,
|
|
921
921
|
runSimplifierByDefault: runSimplifierByDefault as boolean,
|
|
922
|
-
|
|
922
|
+
runInteractiveByDefault: DEFAULT_CONFIG.run?.interactive ?? true,
|
|
923
923
|
soundNotificationsEnabled: DEFAULT_CONFIG.notifications?.sound.enabled ?? true,
|
|
924
924
|
} satisfies InitInput;
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
-
async function
|
|
927
|
+
async function promptForRunInteractive(
|
|
928
|
+
runtime: InitRuntime,
|
|
929
|
+
defaultValue: boolean
|
|
930
|
+
): Promise<boolean> {
|
|
928
931
|
const shouldEnable = await runtime.prompt.confirm({
|
|
929
|
-
message: "
|
|
932
|
+
message: "Launch Interactive Mode automatically after 'rr run'?",
|
|
930
933
|
initialValue: defaultValue,
|
|
931
934
|
});
|
|
932
935
|
handleCancel(runtime, shouldEnable);
|
|
@@ -1050,12 +1053,15 @@ export async function runInitWithRuntime(
|
|
|
1050
1053
|
setupMode === "auto"
|
|
1051
1054
|
? {
|
|
1052
1055
|
...resolvedInput,
|
|
1053
|
-
|
|
1056
|
+
runInteractiveByDefault: true,
|
|
1054
1057
|
soundNotificationsEnabled: true,
|
|
1055
1058
|
}
|
|
1056
1059
|
: {
|
|
1057
1060
|
...resolvedInput,
|
|
1058
|
-
|
|
1061
|
+
runInteractiveByDefault: await promptForRunInteractive(
|
|
1062
|
+
runtime,
|
|
1063
|
+
resolvedInput.runInteractiveByDefault
|
|
1064
|
+
),
|
|
1059
1065
|
soundNotificationsEnabled: await promptForSoundNotifications(
|
|
1060
1066
|
runtime,
|
|
1061
1067
|
resolvedInput.soundNotificationsEnabled
|
package/src/commands/run.ts
CHANGED
|
@@ -33,8 +33,8 @@ export interface RunOptions {
|
|
|
33
33
|
commit?: string;
|
|
34
34
|
custom?: string;
|
|
35
35
|
simplifier?: boolean;
|
|
36
|
-
|
|
37
|
-
"no-
|
|
36
|
+
interactive?: boolean;
|
|
37
|
+
"no-interactive"?: boolean;
|
|
38
38
|
sound?: boolean;
|
|
39
39
|
"no-sound"?: boolean;
|
|
40
40
|
}
|
|
@@ -82,20 +82,20 @@ export function resolveRunSimplifierEnabled(options: RunOptions, config: Config
|
|
|
82
82
|
return options.simplifier === true || config?.run?.simplifier === true;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function
|
|
86
|
-
if (options.
|
|
87
|
-
throw new Error("Cannot use --
|
|
85
|
+
export function resolveRunInteractiveEnabled(options: RunOptions, config: Config | null): boolean {
|
|
86
|
+
if (options.interactive && options["no-interactive"]) {
|
|
87
|
+
throw new Error("Cannot use --interactive and --no-interactive together");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
if (options.
|
|
90
|
+
if (options.interactive) {
|
|
91
91
|
return true;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
if (options["no-
|
|
94
|
+
if (options["no-interactive"]) {
|
|
95
95
|
return false;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
return config?.run?.
|
|
98
|
+
return config?.run?.interactive ?? true;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export function formatRunAgentsNote(config: Config, reviewOptions: ReviewOptions): string {
|
|
@@ -377,7 +377,7 @@ async function runInBackground(
|
|
|
377
377
|
runtime.prompt.log.success(`Review started in background session: ${sessionName}`);
|
|
378
378
|
const reviewOptions: ReviewOptions = { baseBranch, commitSha, customInstructions, simplifier };
|
|
379
379
|
runtime.prompt.note(formatRunAgentsNote(config, reviewOptions), "Agents");
|
|
380
|
-
runtime.prompt.note("rr
|
|
380
|
+
runtime.prompt.note("rr - Check status\n" + "rr stop - Stop the review", "Commands");
|
|
381
381
|
} catch (error) {
|
|
382
382
|
await runtime.lockfile.removeLockfile(undefined, projectPath, { expectedSessionId: sessionId });
|
|
383
383
|
runtime.prompt.log.error(`Failed to start background session: ${error}`);
|
|
@@ -385,9 +385,9 @@ async function runInBackground(
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
function
|
|
389
|
-
runtime.prompt.log.message("
|
|
390
|
-
runtime.prompt.log.message("
|
|
388
|
+
function logInteractiveReconnectHint(runtime: RunRuntime): void {
|
|
389
|
+
runtime.prompt.log.message("Interactive Mode closed.");
|
|
390
|
+
runtime.prompt.log.message("Launch Interactive Mode: rr");
|
|
391
391
|
runtime.prompt.log.message("Stop session: rr stop");
|
|
392
392
|
}
|
|
393
393
|
|
|
@@ -692,17 +692,17 @@ export async function startReview(
|
|
|
692
692
|
}
|
|
693
693
|
|
|
694
694
|
const runSimplifier = resolveRunSimplifierEnabled(options, config);
|
|
695
|
-
let
|
|
695
|
+
let runInteractive: boolean;
|
|
696
696
|
try {
|
|
697
|
-
|
|
697
|
+
runInteractive = resolveRunInteractiveEnabled(options, config);
|
|
698
698
|
} catch (error) {
|
|
699
699
|
runtime.prompt.log.error(`${error}`);
|
|
700
700
|
runtime.process.exit(1);
|
|
701
701
|
return;
|
|
702
702
|
}
|
|
703
|
-
if (
|
|
704
|
-
runtime.prompt.log.warn("
|
|
705
|
-
|
|
703
|
+
if (runInteractive && !runtime.process.stdoutIsTTY) {
|
|
704
|
+
runtime.prompt.log.warn("Interactive Mode is disabled because stdout is not a TTY.");
|
|
705
|
+
runInteractive = false;
|
|
706
706
|
}
|
|
707
707
|
|
|
708
708
|
// Check if inside tmux - warn about nesting
|
|
@@ -722,7 +722,7 @@ export async function startReview(
|
|
|
722
722
|
soundOverride
|
|
723
723
|
);
|
|
724
724
|
|
|
725
|
-
if (!
|
|
725
|
+
if (!runInteractive) {
|
|
726
726
|
return;
|
|
727
727
|
}
|
|
728
728
|
|
|
@@ -731,8 +731,8 @@ export async function startReview(
|
|
|
731
731
|
const branch = await runtime.getGitBranch(projectPath);
|
|
732
732
|
await runtime.openSessionPanel(projectPath, branch ?? undefined);
|
|
733
733
|
} catch (error) {
|
|
734
|
-
runtime.prompt.log.warn(`Could not
|
|
734
|
+
runtime.prompt.log.warn(`Could not launch Interactive Mode: ${error}`);
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
-
|
|
737
|
+
logInteractiveReconnectHint(runtime);
|
|
738
738
|
}
|
package/src/commands/stop.ts
CHANGED
|
@@ -118,7 +118,7 @@ async function stopCurrentSession(): Promise<void> {
|
|
|
118
118
|
if (allSessions.length > 0) {
|
|
119
119
|
p.log.message(`\nThere are ${allSessions.length} other session(s) running.`);
|
|
120
120
|
p.log.message(
|
|
121
|
-
'Use "rr stop --all" to stop all running review sessions, or "rr
|
|
121
|
+
'Use "rr stop --all" to stop all running review sessions, or "rr" to see details.'
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
return;
|
package/src/commands/update.ts
CHANGED
|
@@ -15,6 +15,10 @@ interface UpdateRuntime extends SelfUpdateDependencies {
|
|
|
15
15
|
getCommandDef: (name: string) => CommandDef | undefined;
|
|
16
16
|
parseCommand: typeof parseCommand;
|
|
17
17
|
performSelfUpdate: typeof performSelfUpdate;
|
|
18
|
+
spinner: () => {
|
|
19
|
+
start: (message: string) => void;
|
|
20
|
+
stop: (message: string) => void;
|
|
21
|
+
};
|
|
18
22
|
log: {
|
|
19
23
|
error: (message: string) => void;
|
|
20
24
|
info: (message: string) => void;
|
|
@@ -37,6 +41,7 @@ function createUpdateRuntime(overrides: UpdateRuntimeOverrides = {}): UpdateRunt
|
|
|
37
41
|
getCommandDef: overrides.getCommandDef ?? getCommandDef,
|
|
38
42
|
parseCommand: overrides.parseCommand ?? parseCommand,
|
|
39
43
|
performSelfUpdate: overrides.performSelfUpdate ?? performSelfUpdate,
|
|
44
|
+
spinner: overrides.spinner ?? p.spinner,
|
|
40
45
|
log: {
|
|
41
46
|
error: overrides.log?.error ?? p.log.error,
|
|
42
47
|
info: overrides.log?.info ?? p.log.info,
|
|
@@ -106,11 +111,14 @@ export async function runUpdate(
|
|
|
106
111
|
manager: managerValue,
|
|
107
112
|
};
|
|
108
113
|
|
|
114
|
+
const spinner = runtime.spinner();
|
|
115
|
+
spinner.start("Checking for updates...");
|
|
109
116
|
try {
|
|
110
117
|
const result = await runtime.performSelfUpdate(options, runtime);
|
|
111
|
-
|
|
118
|
+
spinner.stop("Done.");
|
|
112
119
|
renderSelfUpdateResult(result, runtime);
|
|
113
120
|
} catch (error) {
|
|
121
|
+
spinner.stop("Update failed.");
|
|
114
122
|
if (error instanceof SelfUpdateError) {
|
|
115
123
|
runtime.log.error(error.message);
|
|
116
124
|
for (const note of error.notes) {
|
package/src/lib/cli-parser.ts
CHANGED
|
@@ -108,6 +108,11 @@ export interface ParseResult<T = Record<string, unknown>> {
|
|
|
108
108
|
positional: string[];
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
interface UsageEntry {
|
|
112
|
+
command: string;
|
|
113
|
+
description: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
function buildOptionMaps(options: OptionDef[]): {
|
|
112
117
|
byName: Map<string, OptionDef>;
|
|
113
118
|
byAlias: Map<string, OptionDef>;
|
|
@@ -359,31 +364,46 @@ export function formatCommandHelp(def: CommandDef): string {
|
|
|
359
364
|
return lines.join("\n");
|
|
360
365
|
}
|
|
361
366
|
|
|
367
|
+
function getCommandDisplayName(command: CommandDef): string {
|
|
368
|
+
if (command.aliases?.length) {
|
|
369
|
+
return `${command.name} (${command.aliases.join(", ")})`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return command.name;
|
|
373
|
+
}
|
|
374
|
+
|
|
362
375
|
export function formatMainHelp(commands: CommandDef[], version: string): string {
|
|
363
376
|
const lines: string[] = [];
|
|
377
|
+
const usageEntries: UsageEntry[] = [
|
|
378
|
+
{ command: "rr", description: "Launch Interactive Mode" },
|
|
379
|
+
{ command: "rr", description: "<command> [options]" },
|
|
380
|
+
{ command: "rrr", description: "Quick alias for 'rr run'" },
|
|
381
|
+
];
|
|
364
382
|
|
|
365
383
|
lines.push(
|
|
366
384
|
`${theme.accent("ralph-review")} v${theme.muted(version)} - ${theme.info("Ralph Wiggum Code Review Orchestrator")}`
|
|
367
385
|
);
|
|
368
386
|
lines.push("");
|
|
369
387
|
lines.push(`${theme.heading("USAGE:")}`);
|
|
370
|
-
|
|
371
|
-
|
|
388
|
+
const maxUsageCommandLen = Math.max(...usageEntries.map((entry) => entry.command.length));
|
|
389
|
+
for (const entry of usageEntries) {
|
|
390
|
+
const padding = " ".repeat(maxUsageCommandLen - entry.command.length + 2);
|
|
391
|
+
lines.push(` ${theme.command(entry.command)}${padding}${entry.description}`);
|
|
392
|
+
}
|
|
372
393
|
lines.push("");
|
|
373
394
|
lines.push(`${theme.heading("COMMANDS:")}`);
|
|
374
395
|
|
|
375
396
|
const publicCommands = commands.filter((c) => !c.hidden);
|
|
376
397
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const maxNameLen = Math.max(...publicCommands.map((c) => getDisplayName(c).length));
|
|
398
|
+
const maxNameLen = Math.max(
|
|
399
|
+
...publicCommands.map((command) => getCommandDisplayName(command).length)
|
|
400
|
+
);
|
|
381
401
|
|
|
382
|
-
for (const
|
|
383
|
-
const displayName =
|
|
402
|
+
for (const command of publicCommands) {
|
|
403
|
+
const displayName = getCommandDisplayName(command);
|
|
384
404
|
const coloredName = theme.command(displayName);
|
|
385
405
|
const paddedName = coloredName + " ".repeat(maxNameLen - displayName.length + 2);
|
|
386
|
-
lines.push(` ${paddedName}${
|
|
406
|
+
lines.push(` ${paddedName}${command.description}`);
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
lines.push("");
|
package/src/lib/config.ts
CHANGED
|
@@ -18,6 +18,19 @@ import {
|
|
|
18
18
|
const CONFIG_DIR = join(homedir(), ".config", "ralph-review");
|
|
19
19
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
20
20
|
export const LOGS_DIR = join(CONFIG_DIR, "logs");
|
|
21
|
+
const VALID_AGENT_VALUES = ["codex", "claude", "opencode", "droid", "gemini", "pi"] as const;
|
|
22
|
+
const VALID_AGENT_CHOICES = VALID_AGENT_VALUES.join(", ");
|
|
23
|
+
const VALID_REASONING_VALUES = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
24
|
+
const RUN_SETTING_KEYS = ["simplifier", "interactive"] as const;
|
|
25
|
+
|
|
26
|
+
export interface ConfigParseDiagnostics {
|
|
27
|
+
config: Config | null;
|
|
28
|
+
errors: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LoadedConfigDiagnostics extends ConfigParseDiagnostics {
|
|
32
|
+
exists: boolean;
|
|
33
|
+
}
|
|
21
34
|
|
|
22
35
|
function withCanonicalMetadata(
|
|
23
36
|
config: Omit<Config, "$schema" | "version"> & Partial<Pick<Config, "$schema" | "version">>
|
|
@@ -46,39 +59,67 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
46
59
|
return typeof value === "object" && value !== null;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
function
|
|
62
|
+
function parseRetryConfigWithDiagnostics(
|
|
63
|
+
value: unknown,
|
|
64
|
+
errors: string[]
|
|
65
|
+
): RetryConfig | undefined {
|
|
50
66
|
if (value === undefined) {
|
|
51
67
|
return undefined;
|
|
52
68
|
}
|
|
53
69
|
if (!isRecord(value)) {
|
|
70
|
+
errors.push("retry must be an object.");
|
|
54
71
|
return undefined;
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
const maxRetries = value.maxRetries;
|
|
58
75
|
const baseDelayMs = value.baseDelayMs;
|
|
59
76
|
const maxDelayMs = value.maxDelayMs;
|
|
77
|
+
let hasError = false;
|
|
78
|
+
|
|
79
|
+
if (typeof maxRetries !== "number") {
|
|
80
|
+
errors.push("retry.maxRetries must be a number.");
|
|
81
|
+
hasError = true;
|
|
82
|
+
}
|
|
83
|
+
if (typeof baseDelayMs !== "number") {
|
|
84
|
+
errors.push("retry.baseDelayMs must be a number.");
|
|
85
|
+
hasError = true;
|
|
86
|
+
}
|
|
87
|
+
if (typeof maxDelayMs !== "number") {
|
|
88
|
+
errors.push("retry.maxDelayMs must be a number.");
|
|
89
|
+
hasError = true;
|
|
90
|
+
}
|
|
60
91
|
|
|
61
92
|
if (
|
|
62
|
-
|
|
63
|
-
typeof
|
|
64
|
-
typeof
|
|
93
|
+
!hasError &&
|
|
94
|
+
typeof maxRetries === "number" &&
|
|
95
|
+
typeof baseDelayMs === "number" &&
|
|
96
|
+
typeof maxDelayMs === "number"
|
|
65
97
|
) {
|
|
66
|
-
return
|
|
98
|
+
return { maxRetries, baseDelayMs, maxDelayMs };
|
|
67
99
|
}
|
|
68
100
|
|
|
69
|
-
return
|
|
101
|
+
return undefined;
|
|
70
102
|
}
|
|
71
103
|
|
|
72
|
-
function
|
|
104
|
+
function parseNotificationsConfigWithDiagnostics(
|
|
105
|
+
value: unknown,
|
|
106
|
+
errors: string[]
|
|
107
|
+
): NotificationsConfig | undefined {
|
|
73
108
|
if (value === undefined) {
|
|
74
109
|
return undefined;
|
|
75
110
|
}
|
|
76
111
|
if (!isRecord(value)) {
|
|
112
|
+
errors.push("notifications must be an object.");
|
|
77
113
|
return undefined;
|
|
78
114
|
}
|
|
79
115
|
|
|
80
116
|
const sound = value.sound;
|
|
81
|
-
if (!isRecord(sound)
|
|
117
|
+
if (!isRecord(sound)) {
|
|
118
|
+
errors.push("notifications.sound must be an object.");
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (typeof sound.enabled !== "boolean") {
|
|
122
|
+
errors.push("notifications.sound.enabled must be a boolean.");
|
|
82
123
|
return undefined;
|
|
83
124
|
}
|
|
84
125
|
|
|
@@ -89,29 +130,60 @@ function parseNotificationsConfig(value: unknown): NotificationsConfig | undefin
|
|
|
89
130
|
};
|
|
90
131
|
}
|
|
91
132
|
|
|
92
|
-
function
|
|
133
|
+
function formatRunSettingChoices(): string {
|
|
134
|
+
return RUN_SETTING_KEYS.map((key) => `run.${key}`).join(", ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseRunConfigWithDiagnostics(value: unknown, errors: string[]): RunConfig | undefined {
|
|
93
138
|
if (value === undefined) {
|
|
94
139
|
return undefined;
|
|
95
140
|
}
|
|
96
141
|
if (!isRecord(value)) {
|
|
142
|
+
errors.push("run must be an object.");
|
|
97
143
|
return undefined;
|
|
98
144
|
}
|
|
99
145
|
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
const keys = Object.keys(value);
|
|
147
|
+
let hasError = false;
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
if (key === "simplifier" || key === "interactive") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
errors.push(`run.${key} is not supported. Available settings: ${formatRunSettingChoices()}.`);
|
|
154
|
+
hasError = true;
|
|
102
155
|
}
|
|
103
|
-
|
|
104
|
-
|
|
156
|
+
|
|
157
|
+
const simplifier = value.simplifier;
|
|
158
|
+
const interactive = value.interactive === undefined ? true : value.interactive;
|
|
159
|
+
|
|
160
|
+
if (typeof simplifier !== "boolean") {
|
|
161
|
+
errors.push("run.simplifier must be a boolean.");
|
|
162
|
+
hasError = true;
|
|
163
|
+
}
|
|
164
|
+
if (typeof interactive !== "boolean") {
|
|
165
|
+
errors.push("run.interactive must be a boolean.");
|
|
166
|
+
hasError = true;
|
|
105
167
|
}
|
|
106
168
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
169
|
+
if (!hasError && typeof simplifier === "boolean" && typeof interactive === "boolean") {
|
|
170
|
+
return {
|
|
171
|
+
simplifier,
|
|
172
|
+
interactive,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
111
177
|
}
|
|
112
178
|
|
|
113
|
-
function
|
|
114
|
-
if (!isRecord(value)
|
|
179
|
+
function parseDefaultReviewWithDiagnostics(value: unknown, errors: string[]): DefaultReview | null {
|
|
180
|
+
if (!isRecord(value)) {
|
|
181
|
+
errors.push('defaultReview must be an object with type "uncommitted" or "base".');
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (typeof value.type !== "string") {
|
|
186
|
+
errors.push('defaultReview.type must be "uncommitted" or "base".');
|
|
115
187
|
return null;
|
|
116
188
|
}
|
|
117
189
|
|
|
@@ -123,112 +195,197 @@ function parseDefaultReview(value: unknown): DefaultReview | null {
|
|
|
123
195
|
return { type: "base", branch: value.branch };
|
|
124
196
|
}
|
|
125
197
|
|
|
198
|
+
if (value.type === "base") {
|
|
199
|
+
errors.push(
|
|
200
|
+
'defaultReview.branch must be a non-empty string when defaultReview.type is "base".'
|
|
201
|
+
);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
errors.push('defaultReview.type must be "uncommitted" or "base".');
|
|
126
206
|
return null;
|
|
127
207
|
}
|
|
128
208
|
|
|
129
|
-
function
|
|
209
|
+
function parseAgentSettingsWithDiagnostics(
|
|
210
|
+
value: unknown,
|
|
211
|
+
path: "reviewer" | "fixer" | "code-simplifier",
|
|
212
|
+
errors: string[]
|
|
213
|
+
): AgentSettings | null {
|
|
130
214
|
if (!isRecord(value)) {
|
|
215
|
+
errors.push(`${path} must be an object.`);
|
|
131
216
|
return null;
|
|
132
217
|
}
|
|
133
218
|
|
|
134
|
-
|
|
135
|
-
|
|
219
|
+
const agent = isAgentType(value.agent) ? value.agent : undefined;
|
|
220
|
+
let reasoning: AgentSettings["reasoning"] | undefined;
|
|
221
|
+
if (value.reasoning !== undefined) {
|
|
222
|
+
reasoning = isReasoningLevel(value.reasoning) ? value.reasoning : undefined;
|
|
136
223
|
}
|
|
224
|
+
let hasError = false;
|
|
137
225
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
226
|
+
if (!agent) {
|
|
227
|
+
hasError = true;
|
|
228
|
+
errors.push(`${path}.agent must be one of: ${VALID_AGENT_CHOICES}.`);
|
|
229
|
+
}
|
|
230
|
+
if (value.reasoning !== undefined && reasoning === undefined) {
|
|
231
|
+
errors.push(`${path}.reasoning must be one of: ${VALID_REASONING_VALUES.join(", ")}.`);
|
|
232
|
+
hasError = true;
|
|
140
233
|
}
|
|
141
234
|
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
235
|
+
if (agent === "pi") {
|
|
236
|
+
const provider = value.provider;
|
|
237
|
+
const model = value.model;
|
|
238
|
+
|
|
239
|
+
if (typeof provider !== "string") {
|
|
240
|
+
errors.push(`${path}.provider must be a string when ${path}.agent is "pi".`);
|
|
241
|
+
hasError = true;
|
|
242
|
+
}
|
|
243
|
+
if (typeof model !== "string") {
|
|
244
|
+
errors.push(`${path}.model must be a string when ${path}.agent is "pi".`);
|
|
245
|
+
hasError = true;
|
|
145
246
|
}
|
|
146
247
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
248
|
+
if (!hasError && typeof provider === "string" && typeof model === "string") {
|
|
249
|
+
return {
|
|
250
|
+
agent: "pi",
|
|
251
|
+
provider,
|
|
252
|
+
model,
|
|
253
|
+
reasoning,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
154
256
|
|
|
155
|
-
if (value.provider !== undefined) {
|
|
156
257
|
return null;
|
|
157
258
|
}
|
|
259
|
+
|
|
260
|
+
if (agent && value.provider !== undefined) {
|
|
261
|
+
errors.push(`${path}.provider is only valid when ${path}.agent is "pi".`);
|
|
262
|
+
hasError = true;
|
|
263
|
+
}
|
|
158
264
|
if (value.model !== undefined && typeof value.model !== "string") {
|
|
265
|
+
errors.push(`${path}.model must be a string.`);
|
|
266
|
+
hasError = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!agent || hasError) {
|
|
159
270
|
return null;
|
|
160
271
|
}
|
|
161
272
|
|
|
162
273
|
return {
|
|
163
|
-
agent
|
|
164
|
-
model: value.model,
|
|
165
|
-
reasoning
|
|
274
|
+
agent,
|
|
275
|
+
model: typeof value.model === "string" ? value.model : undefined,
|
|
276
|
+
reasoning,
|
|
166
277
|
};
|
|
167
278
|
}
|
|
168
279
|
|
|
169
|
-
|
|
280
|
+
function uniqueErrors(errors: string[]): string[] {
|
|
281
|
+
return [...new Set(errors)];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function parseConfigWithDiagnostics(value: unknown): ConfigParseDiagnostics {
|
|
170
285
|
if (!isRecord(value)) {
|
|
171
|
-
return
|
|
286
|
+
return {
|
|
287
|
+
config: null,
|
|
288
|
+
errors: ["Configuration must be a JSON object."],
|
|
289
|
+
};
|
|
172
290
|
}
|
|
173
291
|
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
292
|
+
const errors: string[] = [];
|
|
293
|
+
const reviewer = parseAgentSettingsWithDiagnostics(value.reviewer, "reviewer", errors);
|
|
294
|
+
const fixer = parseAgentSettingsWithDiagnostics(value.fixer, "fixer", errors);
|
|
295
|
+
const codeSimplifier =
|
|
296
|
+
value["code-simplifier"] === undefined
|
|
297
|
+
? undefined
|
|
298
|
+
: parseAgentSettingsWithDiagnostics(value["code-simplifier"], "code-simplifier", errors);
|
|
299
|
+
const defaultReview = parseDefaultReviewWithDiagnostics(value.defaultReview, errors);
|
|
300
|
+
const retry = parseRetryConfigWithDiagnostics(value.retry, errors);
|
|
301
|
+
const notifications = parseNotificationsConfigWithDiagnostics(value.notifications, errors);
|
|
302
|
+
const run = parseRunConfigWithDiagnostics(value.run, errors);
|
|
303
|
+
const maxIterations = typeof value.maxIterations === "number" ? value.maxIterations : undefined;
|
|
304
|
+
const iterationTimeout =
|
|
305
|
+
typeof value.iterationTimeout === "number" ? value.iterationTimeout : undefined;
|
|
306
|
+
|
|
307
|
+
if (maxIterations === undefined) {
|
|
308
|
+
errors.push("maxIterations must be a number.");
|
|
184
309
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (value.retry !== undefined && !retry) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
if (value.notifications !== undefined && !notifications) {
|
|
192
|
-
return null;
|
|
310
|
+
if (iterationTimeout === undefined) {
|
|
311
|
+
errors.push("iterationTimeout must be a number.");
|
|
193
312
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
!reviewer ||
|
|
316
|
+
!fixer ||
|
|
317
|
+
!defaultReview ||
|
|
318
|
+
maxIterations === undefined ||
|
|
319
|
+
iterationTimeout === undefined ||
|
|
320
|
+
errors.length > 0 ||
|
|
321
|
+
(value["code-simplifier"] !== undefined && !codeSimplifier) ||
|
|
322
|
+
(value.retry !== undefined && !retry) ||
|
|
323
|
+
(value.notifications !== undefined && !notifications) ||
|
|
324
|
+
(value.run !== undefined && !run)
|
|
325
|
+
) {
|
|
326
|
+
return {
|
|
327
|
+
config: null,
|
|
328
|
+
errors: uniqueErrors(errors),
|
|
329
|
+
};
|
|
199
330
|
}
|
|
200
331
|
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
332
|
+
return {
|
|
333
|
+
config: withCanonicalMetadata({
|
|
334
|
+
reviewer,
|
|
335
|
+
fixer,
|
|
336
|
+
...(codeSimplifier ? { "code-simplifier": codeSimplifier } : {}),
|
|
337
|
+
...(run ? { run } : {}),
|
|
338
|
+
maxIterations,
|
|
339
|
+
iterationTimeout,
|
|
340
|
+
...(retry ? { retry } : {}),
|
|
341
|
+
defaultReview,
|
|
342
|
+
notifications: notifications ?? {
|
|
343
|
+
sound: { enabled: DEFAULT_NOTIFICATIONS_CONFIG.sound.enabled },
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
errors: [],
|
|
347
|
+
};
|
|
214
348
|
}
|
|
215
349
|
|
|
216
|
-
export
|
|
350
|
+
export function parseConfig(value: unknown): Config | null {
|
|
351
|
+
return parseConfigWithDiagnostics(value).config;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function loadConfigWithDiagnostics(
|
|
355
|
+
path: string = CONFIG_PATH
|
|
356
|
+
): Promise<LoadedConfigDiagnostics> {
|
|
217
357
|
const file = Bun.file(path);
|
|
218
358
|
|
|
219
359
|
if (!(await file.exists())) {
|
|
220
|
-
return
|
|
360
|
+
return {
|
|
361
|
+
exists: false,
|
|
362
|
+
config: null,
|
|
363
|
+
errors: [],
|
|
364
|
+
};
|
|
221
365
|
}
|
|
222
366
|
|
|
223
367
|
try {
|
|
224
368
|
const content = await file.text();
|
|
225
369
|
const parsed = JSON.parse(content) as unknown;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
370
|
+
const result = parseConfigWithDiagnostics(parsed);
|
|
371
|
+
return {
|
|
372
|
+
exists: true,
|
|
373
|
+
...result,
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return {
|
|
377
|
+
exists: true,
|
|
378
|
+
config: null,
|
|
379
|
+
errors: [`Invalid JSON syntax: ${error instanceof Error ? error.message : String(error)}`],
|
|
380
|
+
};
|
|
229
381
|
}
|
|
230
382
|
}
|
|
231
383
|
|
|
384
|
+
export async function loadConfig(path: string = CONFIG_PATH): Promise<Config | null> {
|
|
385
|
+
const loaded = await loadConfigWithDiagnostics(path);
|
|
386
|
+
return loaded.config;
|
|
387
|
+
}
|
|
388
|
+
|
|
232
389
|
export async function configExists(path: string = CONFIG_PATH): Promise<boolean> {
|
|
233
390
|
return await Bun.file(path).exists();
|
|
234
391
|
}
|
|
@@ -236,6 +393,6 @@ export async function configExists(path: string = CONFIG_PATH): Promise<boolean>
|
|
|
236
393
|
export const DEFAULT_CONFIG: Partial<Config> = {
|
|
237
394
|
maxIterations: 5,
|
|
238
395
|
iterationTimeout: 1800000,
|
|
239
|
-
run: { simplifier: false,
|
|
396
|
+
run: { simplifier: false, interactive: true },
|
|
240
397
|
notifications: { sound: { enabled: DEFAULT_NOTIFICATIONS_CONFIG.sound.enabled } },
|
|
241
398
|
};
|
|
@@ -558,7 +558,7 @@ export async function runDiagnostics(
|
|
|
558
558
|
? "A review is already running for this project."
|
|
559
559
|
: "No running review lock detected.",
|
|
560
560
|
remediation: hasRunningReview
|
|
561
|
-
? [runStep("rr
|
|
561
|
+
? [runStep("rr"), runStep("rr stop"), thenStep("rr run")]
|
|
562
562
|
: [],
|
|
563
563
|
fixable: hasRunningReview && isFixable("run-lockfile"),
|
|
564
564
|
});
|
|
@@ -143,7 +143,7 @@ async function fixConfig(id: string, deps: RemediationDependencies): Promise<Fix
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const LOCKFILE_FIX_NEXT_ACTIONS = ["Run: rr
|
|
146
|
+
const LOCKFILE_FIX_NEXT_ACTIONS = ["Run: rr", "Run: rr stop", "Then run: rr run"];
|
|
147
147
|
|
|
148
148
|
async function fixLockfile(deps: RemediationDependencies): Promise<FixResult> {
|
|
149
149
|
try {
|
|
@@ -19,18 +19,18 @@ export function StatusBar({ hasSession, focusedPanel }: StatusBarProps) {
|
|
|
19
19
|
<box flexDirection="row" gap={2}>
|
|
20
20
|
<text>
|
|
21
21
|
<span fg={TUI_COLORS.accent.key}>[q]</span>
|
|
22
|
-
<span fg={TUI_COLORS.text.muted}>
|
|
22
|
+
<span fg={TUI_COLORS.text.muted}> Quit</span>
|
|
23
23
|
</text>
|
|
24
24
|
{!hasSession && (
|
|
25
25
|
<text>
|
|
26
26
|
<span fg={TUI_COLORS.accent.key}>[r]</span>
|
|
27
|
-
<span fg={TUI_COLORS.text.muted}> Run</span>
|
|
27
|
+
<span fg={TUI_COLORS.text.muted}> Run Review</span>
|
|
28
28
|
</text>
|
|
29
29
|
)}
|
|
30
30
|
{hasSession && (
|
|
31
31
|
<text>
|
|
32
32
|
<span fg={TUI_COLORS.accent.key}>[s]</span>
|
|
33
|
-
<span fg={TUI_COLORS.text.muted}> Stop</span>
|
|
33
|
+
<span fg={TUI_COLORS.text.muted}> Stop Review</span>
|
|
34
34
|
</text>
|
|
35
35
|
)}
|
|
36
36
|
<text>
|