ralph-review 0.1.7 → 0.1.9

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-review",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Orchestrating coding agents for code review, verification and fixing via the ralph loop.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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: "watch",
84
+ name: "interactive",
85
85
  type: "boolean",
86
- description: "Open Session Panel after starting the run (override config)",
86
+ description: "Launch Interactive Mode after starting the run (override config)",
87
87
  },
88
88
  {
89
- name: "no-watch",
89
+ name: "no-interactive",
90
90
  type: "boolean",
91
- description: "Start run without opening Session Panel (override config)",
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-watch"],
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.logError(`Error: ${error}`);
187
- cliDeps.exit(1);
209
+ reportCliError(cliDeps, error);
188
210
  }
189
211
  }
190
212
 
@@ -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.watch",
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.watch":
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.watch":
310
- return config.run?.watch;
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, watch: next.run?.watch ?? true };
497
+ next.run = { simplifier: value, interactive: next.run?.interactive ?? true };
494
498
  return next;
495
- case "run.watch":
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, watch: value };
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.watch !== "boolean") {
595
- errors.push("run.watch must be a boolean.");
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 config = await deps.loadConfig();
607
- if (!config) {
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
- `Configuration exists but is invalid: ${deps.configPath}. Run "rr init" or fix the file manually.`
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 invariantErrors = validateConfigInvariants(updated);
654
- if (invariantErrors.length > 0) {
655
- throw new Error(invariantErrors.join("\n"));
656
- }
657
-
658
- const normalized = deps.parseConfig(updated as unknown);
659
- if (!normalized) {
660
- throw new Error("Updated configuration is invalid.");
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 config = await deps.loadConfig();
699
- if (!config) {
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
- `Configuration exists but is invalid: ${deps.configPath}. Run "rr init" or fix it manually.`
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
- return {
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>) {
@@ -53,7 +53,7 @@ interface InitInput {
53
53
  defaultReviewType: "uncommitted" | "base";
54
54
  defaultReviewBranch?: string;
55
55
  runSimplifierByDefault: boolean;
56
- runWatchByDefault: boolean;
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
- watch: input.runWatchByDefault,
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
- ` Run watch panel: ${(config.run?.watch ?? true) ? "enabled" : "disabled"}`,
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
- runWatchByDefault: true,
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
- runWatchByDefault: DEFAULT_CONFIG.run?.watch ?? true,
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 promptForRunWatch(runtime: InitRuntime, defaultValue: boolean): Promise<boolean> {
927
+ async function promptForRunInteractive(
928
+ runtime: InitRuntime,
929
+ defaultValue: boolean
930
+ ): Promise<boolean> {
928
931
  const shouldEnable = await runtime.prompt.confirm({
929
- message: "Open Session Panel automatically after 'rr run'?",
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
- runWatchByDefault: true,
1056
+ runInteractiveByDefault: true,
1054
1057
  soundNotificationsEnabled: true,
1055
1058
  }
1056
1059
  : {
1057
1060
  ...resolvedInput,
1058
- runWatchByDefault: await promptForRunWatch(runtime, resolvedInput.runWatchByDefault),
1061
+ runInteractiveByDefault: await promptForRunInteractive(
1062
+ runtime,
1063
+ resolvedInput.runInteractiveByDefault
1064
+ ),
1059
1065
  soundNotificationsEnabled: await promptForSoundNotifications(
1060
1066
  runtime,
1061
1067
  resolvedInput.soundNotificationsEnabled
@@ -33,8 +33,8 @@ export interface RunOptions {
33
33
  commit?: string;
34
34
  custom?: string;
35
35
  simplifier?: boolean;
36
- watch?: boolean;
37
- "no-watch"?: boolean;
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 resolveRunWatchEnabled(options: RunOptions, config: Config | null): boolean {
86
- if (options.watch && options["no-watch"]) {
87
- throw new Error("Cannot use --watch and --no-watch together");
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.watch) {
90
+ if (options.interactive) {
91
91
  return true;
92
92
  }
93
93
 
94
- if (options["no-watch"]) {
94
+ if (options["no-interactive"]) {
95
95
  return false;
96
96
  }
97
97
 
98
- return config?.run?.watch ?? true;
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 status - Check status\n" + "rr stop - Stop the review", "Commands");
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 logWatchReconnectHint(runtime: RunRuntime): void {
389
- runtime.prompt.log.message("Session Panel closed.");
390
- runtime.prompt.log.message("Re-open panel: rr status");
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 runWatch: boolean;
695
+ let runInteractive: boolean;
696
696
  try {
697
- runWatch = resolveRunWatchEnabled(options, config);
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 (runWatch && !runtime.process.stdoutIsTTY) {
704
- runtime.prompt.log.warn("Watch mode is disabled because stdout is not a TTY.");
705
- runWatch = false;
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 (!runWatch) {
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 open Session Panel: ${error}`);
734
+ runtime.prompt.log.warn(`Could not launch Interactive Mode: ${error}`);
735
735
  }
736
736
 
737
- logWatchReconnectHint(runtime);
737
+ logInteractiveReconnectHint(runtime);
738
738
  }
@@ -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 status" to see details.'
121
+ 'Use "rr stop --all" to stop all running review sessions, or "rr" to see details.'
122
122
  );
123
123
  }
124
124
  return;
@@ -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) {
@@ -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
- lines.push(` ${theme.command("rr")} <command> [options]`);
371
- lines.push(` ${theme.command("rrr")} Quick alias for 'rr run'`);
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
- // Calculate max display name length (including aliases like "list (ls)")
378
- const getDisplayName = (cmd: CommandDef): string =>
379
- cmd.aliases?.length ? `${cmd.name} (${cmd.aliases.join(", ")})` : cmd.name;
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 cmd of publicCommands) {
383
- const displayName = getDisplayName(cmd);
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}${cmd.description}`);
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 parseRetryConfig(value: unknown): RetryConfig | undefined {
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
- typeof maxRetries !== "number" ||
63
- typeof baseDelayMs !== "number" ||
64
- typeof maxDelayMs !== "number"
93
+ !hasError &&
94
+ typeof maxRetries === "number" &&
95
+ typeof baseDelayMs === "number" &&
96
+ typeof maxDelayMs === "number"
65
97
  ) {
66
- return undefined;
98
+ return { maxRetries, baseDelayMs, maxDelayMs };
67
99
  }
68
100
 
69
- return { maxRetries, baseDelayMs, maxDelayMs };
101
+ return undefined;
70
102
  }
71
103
 
72
- function parseNotificationsConfig(value: unknown): NotificationsConfig | undefined {
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) || typeof sound.enabled !== "boolean") {
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 parseRunConfig(value: unknown): RunConfig | undefined {
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
- if (typeof value.simplifier !== "boolean") {
101
- return undefined;
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
- if (value.watch !== undefined && typeof value.watch !== "boolean") {
104
- return undefined;
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
- return {
108
- simplifier: value.simplifier,
109
- watch: value.watch === undefined ? true : value.watch,
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 parseDefaultReview(value: unknown): DefaultReview | null {
114
- if (!isRecord(value) || typeof value.type !== "string") {
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 parseAgentSettings(value: unknown): AgentSettings | null {
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
- if (!isAgentType(value.agent)) {
135
- return null;
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 (value.reasoning !== undefined && !isReasoningLevel(value.reasoning)) {
139
- return null;
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 (value.agent === "pi") {
143
- if (typeof value.provider !== "string" || typeof value.model !== "string") {
144
- return null;
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
- return {
148
- agent: "pi",
149
- provider: value.provider,
150
- model: value.model,
151
- reasoning: value.reasoning,
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: value.agent,
164
- model: value.model,
165
- reasoning: value.reasoning,
274
+ agent,
275
+ model: typeof value.model === "string" ? value.model : undefined,
276
+ reasoning,
166
277
  };
167
278
  }
168
279
 
169
- export function parseConfig(value: unknown): Config | null {
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 null;
286
+ return {
287
+ config: null,
288
+ errors: ["Configuration must be a JSON object."],
289
+ };
172
290
  }
173
291
 
174
- const reviewer = parseAgentSettings(value.reviewer);
175
- const fixer = parseAgentSettings(value.fixer);
176
- const codeSimplifier = parseAgentSettings(value["code-simplifier"]);
177
- const defaultReview = parseDefaultReview(value.defaultReview);
178
- const retry = parseRetryConfig(value.retry);
179
- const notifications = parseNotificationsConfig(value.notifications);
180
- const run = parseRunConfig(value.run);
181
-
182
- if (!reviewer || !fixer || !defaultReview) {
183
- return null;
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 (value["code-simplifier"] !== undefined && !codeSimplifier) {
186
- return null;
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
- if (value.run !== undefined && !run) {
195
- return null;
196
- }
197
- if (typeof value.maxIterations !== "number" || typeof value.iterationTimeout !== "number") {
198
- return null;
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 withCanonicalMetadata({
202
- reviewer,
203
- fixer,
204
- ...(codeSimplifier ? { "code-simplifier": codeSimplifier } : {}),
205
- ...(run ? { run } : {}),
206
- maxIterations: value.maxIterations,
207
- iterationTimeout: value.iterationTimeout,
208
- ...(retry ? { retry } : {}),
209
- defaultReview,
210
- notifications: notifications ?? {
211
- sound: { enabled: DEFAULT_NOTIFICATIONS_CONFIG.sound.enabled },
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 async function loadConfig(path: string = CONFIG_PATH): Promise<Config | null> {
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 null;
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
- return parseConfig(parsed);
227
- } catch {
228
- return null;
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, watch: true },
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 status"), runStep("rr stop"), thenStep("rr run")]
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 status", "Run: rr stop", "Then run: rr run"];
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 {
@@ -50,6 +50,7 @@ const BREW_INSTALLED_VERSION_ERROR =
50
50
  "Could not determine the installed Homebrew version for ralph-review.";
51
51
  const BREW_LATEST_VERSION_ERROR =
52
52
  "Could not determine the latest Homebrew version for ralph-review.";
53
+ const BREW_UPDATE_ERROR = "Failed to refresh Homebrew metadata for ralph-review.";
53
54
  const NPM_INSTALLED_VERSION_ERROR =
54
55
  "Could not determine the installed npm version for ralph-review.";
55
56
  const VERSION_COMPARE_ERROR =
@@ -474,6 +475,10 @@ async function getBrewVersions(
474
475
  return parseBrewVersions(output);
475
476
  }
476
477
 
478
+ async function refreshBrewMetadata(deps: SelfUpdateDependencies): Promise<void> {
479
+ await readTextOutput(deps, ["brew", "update", "--quiet"], BREW_UPDATE_ERROR);
480
+ }
481
+
477
482
  async function performNpmSelfUpdate(
478
483
  options: SelfUpdateOptions,
479
484
  deps: SelfUpdateDependencies
@@ -521,6 +526,7 @@ async function performBrewSelfUpdate(
521
526
  deps: SelfUpdateDependencies
522
527
  ): Promise<SelfUpdateResult> {
523
528
  ensureManagerAvailable("brew", deps);
529
+ await refreshBrewMetadata(deps);
524
530
 
525
531
  const { currentVersion, latestVersion } = await getBrewVersions(deps);
526
532
  if (!hasNewerVersion(currentVersion, latestVersion)) {
@@ -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}> Close panel</span>
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>
@@ -44,7 +44,7 @@ export interface NotificationsConfig {
44
44
 
45
45
  export interface RunConfig {
46
46
  simplifier: boolean;
47
- watch: boolean;
47
+ interactive: boolean;
48
48
  }
49
49
 
50
50
  export const CONFIG_SCHEMA_URI =