ralph-review 0.2.1 → 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.
Files changed (55) hide show
  1. package/README.md +123 -16
  2. package/package.json +6 -4
  3. package/src/cli-core.ts +51 -88
  4. package/src/cli-rrr.ts +1 -4
  5. package/src/cli.ts +1 -2
  6. package/src/commands/apply.ts +6 -14
  7. package/src/commands/config-handlers.ts +68 -69
  8. package/src/commands/config-model.ts +147 -125
  9. package/src/commands/doctor.ts +2 -4
  10. package/src/commands/fix.ts +73 -51
  11. package/src/commands/handoff-selection.ts +6 -8
  12. package/src/commands/init.ts +12 -6
  13. package/src/commands/interactive-deps.ts +18 -0
  14. package/src/commands/log.ts +12 -12
  15. package/src/commands/run.ts +32 -33
  16. package/src/commands/stop.ts +6 -13
  17. package/src/commands/update.ts +2 -4
  18. package/src/lib/agents/claude.ts +4 -16
  19. package/src/lib/agents/core.ts +16 -0
  20. package/src/lib/agents/droid.ts +4 -15
  21. package/src/lib/agents/models.ts +2 -2
  22. package/src/lib/cli-parser.ts +19 -14
  23. package/src/lib/handoff.ts +16 -7
  24. package/src/lib/logging/session-log.ts +2 -1
  25. package/src/lib/prompts/defaults/review.md +1 -1
  26. package/src/lib/prompts/protocol.ts +2 -1
  27. package/src/lib/review-workflow/findings/artifact.ts +3 -1
  28. package/src/lib/review-workflow/findings/types.ts +1 -1
  29. package/src/lib/review-workflow/remediation/prompt.ts +7 -7
  30. package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
  31. package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
  32. package/src/lib/review-workflow/results/finalize-result.ts +20 -3
  33. package/src/lib/review-workflow/run-review-cycle.ts +1 -12
  34. package/src/lib/review-workflow/session-status.ts +13 -0
  35. package/src/lib/review-workflow/shared/framed-json.ts +2 -47
  36. package/src/lib/session/state.ts +50 -38
  37. package/src/lib/structured-output.ts +24 -9
  38. package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
  39. package/src/lib/tui/dashboard/ReviewModeOverlay.tsx +91 -139
  40. package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
  41. package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
  42. package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
  43. package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
  44. package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
  45. package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
  46. package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
  47. package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
  48. package/src/lib/tui/shared/CenteredModal.tsx +44 -0
  49. package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
  50. package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
  51. package/src/lib/tui/workspace/Workspace.tsx +6 -91
  52. package/src/lib/tui/workspace/use-workspace-state.ts +44 -37
  53. package/src/lib/types/fix.ts +15 -48
  54. package/src/lib/types/guards.ts +47 -0
  55. package/src/lib/types/review.ts +5 -39
package/README.md CHANGED
@@ -13,7 +13,7 @@ Orchestrating coding agents for code review, verification, and fixing via the Ra
13
13
 
14
14
  - [How It Works](#how-it-works)
15
15
  - [Reviewer and Fixer Flow](#reviewer-and-fixer-flow)
16
- - [Interactive Mode](#interactive-mode)
16
+ - [Workflows](#workflows)
17
17
  - [Prerequisites](#prerequisites)
18
18
  - [Installation](#installation)
19
19
  - [Quick Start](#quick-start)
@@ -97,24 +97,131 @@ findings remain unresolved, Ralph Review keeps the remediation worktree availabl
97
97
 
98
98
  ---
99
99
 
100
- ## Interactive Mode
100
+ ## Workflows
101
101
 
102
- Run `rr` with no command to open Interactive Mode. It shows active sessions, recent session history,
103
- review output, findings, fix results, and handoff status.
102
+ Ralph Review fits into a few common loops. Each workflow below shows the scenario
103
+ first, then the exact commands.
104
104
 
105
- Keyboard shortcuts:
105
+ ### 1. Review what you are about to commit
106
106
 
107
- | Key | Action |
108
- |-----|--------|
109
- | `r` | Start a new review session |
110
- | `f` | Fix pending findings when a session has actionable findings |
111
- | `s` | Stop a running review session |
112
- | `l` | View session logs |
113
- | `o` | Toggle the output drawer |
114
- | `Tab`, `←`, `→` | Switch panel focus |
115
- | `↑`, `↓`, `j`, `k` | Scroll the focused panel |
116
- | `h`, `?` | Toggle help |
117
- | `Esc`, `q` | Quit Interactive Mode without stopping reviews |
107
+ You just finished a feature on a working branch. Before you stage and push, you
108
+ want a second pair of eyes on staged, unstaged, and untracked changes.
109
+
110
+ ```bash
111
+ rr run --uncommitted
112
+ ```
113
+
114
+ Findings are printed and persisted. You can keep coding while the reviewer runs
115
+ in the background and come back to triage when it is done.
116
+
117
+ ### 2. Review your branch against the base
118
+
119
+ You are preparing a pull request against `main` and want the reviewer to look at
120
+ the full diff, not just your last commit.
121
+
122
+ ```bash
123
+ rr run --base main
124
+ ```
125
+
126
+ Add a focus instruction when the diff is large:
127
+
128
+ ```bash
129
+ rr run --base main "focus on auth boundaries and input validation"
130
+ ```
131
+
132
+ ### 3. Re-review a single commit
133
+
134
+ CI flagged something on a specific commit, or you are auditing a hotfix in
135
+ isolation.
136
+
137
+ ```bash
138
+ rr run --commit 9f3a2b1
139
+ ```
140
+
141
+ ### 4. Review now, fix later (the default loop)
142
+
143
+ `rr run` only reviews. Findings get a stable ID (`F001`, `F002`, ...) and a
144
+ priority (`P0` through `P3`). Triage them, then fix exactly what you want:
145
+
146
+ ```bash
147
+ rr run
148
+ rr fix --session SESSION --priority P0,P1
149
+ # or pick by ID
150
+ rr fix --session SESSION --id F003 --id F007
151
+ ```
152
+
153
+ When the fixer is done it either applies the patch to your working tree
154
+ automatically, or leaves a pending handoff that you apply explicitly:
155
+
156
+ ```bash
157
+ rr apply
158
+ ```
159
+
160
+ ### 5. Review and auto-fix in one shot
161
+
162
+ Trusted change, low risk, you want the loop to close itself:
163
+
164
+ ```bash
165
+ rr run --auto --priority P0,P1
166
+ ```
167
+
168
+ The reviewer runs first, then the fixer immediately remediates only matching
169
+ priorities in a disposable worktree and hands the result back to you.
170
+
171
+ > Auto Setup picks the reviewer model based on Factory.ai's public code review
172
+ > benchmark. See [Why these models?](#why-these-models-auto-setup-model-selection)
173
+ > below.
174
+
175
+ ### 6. Pre-PR / team review workflow
176
+
177
+ Before opening a PR, run a base-branch review and let auto-fix clean up the
178
+ obvious stuff. Anything left becomes review notes you can paste into the PR
179
+ description.
180
+
181
+ ```bash
182
+ rr run --base main --auto --priority P0
183
+ rr log -n 1 # grab the latest review log
184
+ rr log --json # or pipe into your own tooling
185
+ ```
186
+
187
+ For an org-wide loop, persisted review logs (`rr log --json --global`) make it
188
+ easy to feed findings into dashboards or follow-up issues.
189
+
190
+ ### 7. Triage with Interactive Mode
191
+
192
+ Run `rr` with no arguments to open Interactive Mode. It shows active sessions,
193
+ recent history, review output, findings, fix results, and handoff status in a
194
+ single view, which is convenient when several reviews are in flight.
195
+
196
+ ### Why these models? (Auto Setup model selection)
197
+
198
+ `rr init` with Auto Setup chooses your reviewer and fixer's model. That list is
199
+ informed by Factory.ai's public code review benchmark, which scored 13 frontier
200
+ and open-weight models against a golden set of 50 human-curated bugs across
201
+ five real-world repositories (Sentry, Grafana, Keycloak, Discourse, and
202
+ cal.com).
203
+
204
+ The reviewer model priority (highest first) is currently:
205
+
206
+ 1. GPT-5.2
207
+ 2. Claude Opus 4.6
208
+ 3. Claude Sonnet 4.6
209
+ 4. Claude Opus 4.7
210
+ 5. GLM 5.1
211
+ 6. GPT-5.3 Codex
212
+ 7. Gemini 3.1 Pro Preview
213
+ 8. Kimi K2.6
214
+
215
+ Models near the top scored highest on the benchmark for finding real bugs at a
216
+ reasonable cost. The fixer priority is tuned separately and favors models that
217
+ are strong at code edits rather than at finding issues.
218
+
219
+ For the methodology and full results, see Factory.ai's writeup:
220
+ [Which Model Reviews Code Best?](https://factory.ai/news/code-review-benchmark).
221
+
222
+ You can always override the auto selection by running `rr init` and choosing
223
+ Customize Setup, or by editing `reviewer.model` and `fixer.model` in your
224
+ configuration directly.
118
225
 
119
226
  ---
120
227
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-review",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Orchestrating coding agents for code review, verification and fixing via the ralph loop.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,15 +53,16 @@
53
53
  "publish:recover": "bun run scripts/publish.ts --recover",
54
54
  "publish:recover:execute": "bun run scripts/publish.ts --recover --execute",
55
55
  "rr": "bun run src/cli.ts",
56
- "test": "bun test",
56
+ "test": "AGENT=1 bun test",
57
57
  "prepublishOnly": "bun test",
58
58
  "typecheck": "tsc --noEmit",
59
59
  "knip": "knip-bun",
60
+ "check-duplicates": "bunx jscpd src tests --exitCode 1 --reporters ai",
60
61
  "lint": "biome check --write .",
61
62
  "lint:ci": "biome ci .",
62
63
  "lint-staged": "lint-staged",
63
- "check": "bun run typecheck && bun run knip && bun run lint && AGENT=1 bun test --coverage",
64
- "check:ci": "bun run typecheck && bun run knip && bun run lint:ci && AGENT=1 bun test --coverage",
64
+ "check": "bun run typecheck && bun run knip && bun run lint && bun run check-duplicates && AGENT=1 bun test --coverage",
65
+ "check:ci": "bun run typecheck && bun run knip && bun run lint:ci && bun run check-duplicates && AGENT=1 bun test --coverage",
65
66
  "prepare": "husky && bun run setup-hooks",
66
67
  "setup-hooks": "bun -e 'await Bun.write(\".husky/pre-commit\", \"#!/usr/bin/env sh\\n\\nbun run knip && bun run lint-staged\\n\")' && chmod +x .husky/pre-commit"
67
68
  },
@@ -70,6 +71,7 @@
70
71
  "@types/bun": "latest",
71
72
  "@types/react": "^19.0.0",
72
73
  "husky": "^9.1.7",
74
+ "jscpd": "^4.0.9",
73
75
  "knip": "^5.82.1",
74
76
  "lint-staged": "^16.2.7",
75
77
  "typescript": "^5",
package/src/cli-core.ts CHANGED
@@ -1,5 +1,52 @@
1
1
  import { type CommandDef, formatCommandHelp, formatMainHelp } from "./lib/cli-parser";
2
2
 
3
+ const RUN_REVIEW_OPTIONS: CommandDef["options"] = [
4
+ { name: "max", alias: "m", type: "number", description: "Max iterations" },
5
+ {
6
+ name: "force",
7
+ alias: "f",
8
+ type: "boolean",
9
+ description: "Run full max iterations even if no issues are found",
10
+ },
11
+ {
12
+ name: "auto",
13
+ type: "boolean",
14
+ description: "Automatically run remediation after review completes",
15
+ },
16
+ {
17
+ name: "priority",
18
+ type: "string",
19
+ placeholder: "P0,P1",
20
+ description: "Priority filter for --auto using comma-separated values",
21
+ },
22
+ ];
23
+
24
+ const FIX_SELECTION_OPTIONS: CommandDef["options"] = [
25
+ {
26
+ name: "session",
27
+ alias: "s",
28
+ type: "string",
29
+ description: "Session ID whose persisted findings should be fixed",
30
+ },
31
+ {
32
+ name: "all",
33
+ type: "boolean",
34
+ description: "Select all persisted findings for remediation",
35
+ },
36
+ {
37
+ name: "priority",
38
+ type: "string",
39
+ placeholder: "P0,P1,P2,P3",
40
+ description: "Select findings by priority (comma-separated values)",
41
+ },
42
+ {
43
+ name: "id",
44
+ type: "string",
45
+ placeholder: "F001",
46
+ description: "Select findings by ID (repeatable)",
47
+ },
48
+ ];
49
+
3
50
  export const COMMANDS: CommandDef[] = [
4
51
  {
5
52
  name: "init",
@@ -67,24 +114,7 @@ export const COMMANDS: CommandDef[] = [
67
114
  name: "run",
68
115
  description: "Run review only and persist findings for later fixing",
69
116
  options: [
70
- { name: "max", alias: "m", type: "number", description: "Max iterations" },
71
- {
72
- name: "force",
73
- alias: "f",
74
- type: "boolean",
75
- description: "Run full max iterations even if no issues are found",
76
- },
77
- {
78
- name: "auto",
79
- type: "boolean",
80
- description: "Automatically run remediation after review completes",
81
- },
82
- {
83
- name: "priority",
84
- type: "string",
85
- placeholder: "P0,P1",
86
- description: "Priority filter for --auto using comma-separated values",
87
- },
117
+ ...RUN_REVIEW_OPTIONS,
88
118
  {
89
119
  name: "base",
90
120
  type: "string",
@@ -130,31 +160,7 @@ export const COMMANDS: CommandDef[] = [
130
160
  {
131
161
  name: "fix",
132
162
  description: "Fix selected findings from a persisted review session",
133
- options: [
134
- {
135
- name: "session",
136
- alias: "s",
137
- type: "string",
138
- description: "Session ID whose persisted findings should be fixed",
139
- },
140
- {
141
- name: "all",
142
- type: "boolean",
143
- description: "Select all persisted findings for remediation",
144
- },
145
- {
146
- name: "priority",
147
- type: "string",
148
- placeholder: "P0|P1|P2|P3",
149
- description: "Select findings by priority (repeatable)",
150
- },
151
- {
152
- name: "id",
153
- type: "string",
154
- placeholder: "F001",
155
- description: "Select findings by ID (repeatable)",
156
- },
157
- ],
163
+ options: FIX_SELECTION_OPTIONS,
158
164
  examples: [
159
165
  "rr fix --session session-123 --all",
160
166
  "rr fix --session session-123 --priority P0,P1",
@@ -312,56 +318,13 @@ export const COMMANDS: CommandDef[] = [
312
318
  name: "_run-foreground",
313
319
  description: "Internal: run review session in tmux foreground",
314
320
  hidden: true,
315
- options: [
316
- { name: "max", type: "number", description: "Max iterations" },
317
- {
318
- name: "force",
319
- alias: "f",
320
- type: "boolean",
321
- description: "Run full max iterations even if no issues are found",
322
- },
323
- {
324
- name: "auto",
325
- type: "boolean",
326
- description: "Automatically run remediation after review completes",
327
- },
328
- {
329
- name: "priority",
330
- type: "string",
331
- placeholder: "P0,P1",
332
- description: "Priority filter for --auto using comma-separated values",
333
- },
334
- ],
321
+ options: RUN_REVIEW_OPTIONS,
335
322
  },
336
323
  {
337
324
  name: "_fix-foreground",
338
325
  description: "Internal: run fixer session in tmux foreground",
339
326
  hidden: true,
340
- options: [
341
- {
342
- name: "session",
343
- alias: "s",
344
- type: "string",
345
- description: "Session ID whose persisted findings should be fixed",
346
- },
347
- {
348
- name: "all",
349
- type: "boolean",
350
- description: "Select all persisted findings for remediation",
351
- },
352
- {
353
- name: "priority",
354
- type: "string",
355
- placeholder: "P0|P1|P2|P3",
356
- description: "Select findings by priority (repeatable)",
357
- },
358
- {
359
- name: "id",
360
- type: "string",
361
- placeholder: "F001",
362
- description: "Select findings by ID (repeatable)",
363
- },
364
- ],
327
+ options: FIX_SELECTION_OPTIONS,
365
328
  },
366
329
  ];
367
330
 
package/src/cli-rrr.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { printCommandHelp } from "@/cli-core";
4
+ import { CONSOLE_ERROR, CONSOLE_LOG, PROCESS_EXIT } from "@/cli-io";
4
5
  import { startReview } from "@/commands/run";
5
6
 
6
7
  export interface RrrDeps {
@@ -11,10 +12,6 @@ export interface RrrDeps {
11
12
  exit: (code: number) => void;
12
13
  }
13
14
 
14
- const CONSOLE_LOG = console.log.bind(console) as (message: string) => void;
15
- const CONSOLE_ERROR = console.error.bind(console) as (message: string) => void;
16
- const PROCESS_EXIT = process.exit.bind(process) as (code: number) => void;
17
-
18
15
  const DEFAULT_RRR_DEPS: RrrDeps = {
19
16
  printCommandHelp,
20
17
  startReview,
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as p from "@clack/prompts";
4
4
  import { getCommandDef, getVersion, parseArgs, printCommandHelp, printUsage } from "./cli-core";
5
+ import { CONSOLE_LOG, PROCESS_EXIT } from "./cli-io";
5
6
  import { runApply } from "./commands/apply";
6
7
  import { runConfig } from "./commands/config";
7
8
  import { runDoctor } from "./commands/doctor";
@@ -55,10 +56,8 @@ export interface CliDeps {
55
56
  exit: (code: number) => void;
56
57
  }
57
58
 
58
- const CONSOLE_LOG = console.log.bind(console) as (message: string) => void;
59
59
  const CLACK_ERROR = p.log.error.bind(p.log) as (message: string) => void;
60
60
  const CLACK_MESSAGE = p.log.message.bind(p.log) as (message: string) => void;
61
- const PROCESS_EXIT = process.exit.bind(process) as (code: number) => void;
62
61
  const IS_INTERACTIVE_TERMINAL = (): boolean =>
63
62
  process.stdin.isTTY === true && process.stdout.isTTY === true;
64
63
 
@@ -1,7 +1,9 @@
1
1
  import * as p from "@clack/prompts";
2
- import { getCommandDef } from "@/cli";
3
2
  import { resolvePendingHandoffSelection } from "@/commands/handoff-selection";
4
- import type { CommandDef } from "@/lib/cli-parser";
3
+ import {
4
+ createInteractiveCommandDeps,
5
+ type InteractiveCommandDeps,
6
+ } from "@/commands/interactive-deps";
5
7
  import { parseCommand } from "@/lib/cli-parser";
6
8
  import { applyPendingHandoff, listProjectPendingHandoffs } from "@/lib/handoff";
7
9
  import { appendLog } from "@/lib/logger";
@@ -10,19 +12,9 @@ interface ApplyOptions {
10
12
  session?: string;
11
13
  }
12
14
 
13
- interface ApplyDeps {
14
- getCommandDef: (name: string) => CommandDef | undefined;
15
- logError: (message: string) => void;
16
- exit: (code: number) => void;
17
- isTTY: () => boolean;
18
- }
15
+ type ApplyDeps = InteractiveCommandDeps;
19
16
 
20
- const DEFAULT_APPLY_DEPS: ApplyDeps = {
21
- getCommandDef,
22
- logError: (message: string) => p.log.error(message),
23
- exit: (code: number) => process.exit(code),
24
- isTTY: () => process.stdout.isTTY === true,
25
- };
17
+ const DEFAULT_APPLY_DEPS = createInteractiveCommandDeps();
26
18
 
27
19
  const NO_PENDING_HANDOFFS_MESSAGE = "No pending review handoffs for current working directory.";
28
20
 
@@ -39,6 +39,11 @@ interface ParsedShowArgs extends ParsedScopedArgs {
39
39
  verbose: boolean;
40
40
  }
41
41
 
42
+ type LoadedEffectiveConfig = Awaited<
43
+ ReturnType<ConfigCommandDeps["loadEffectiveConfigWithDiagnostics"]>
44
+ >;
45
+ type ValidEffectiveConfig = LoadedEffectiveConfig & { config: Config };
46
+
42
47
  const SHOW_USAGE = "Usage: rr config show [--local|--global] [--json] [--verbose]";
43
48
 
44
49
  function shellQuote(value: string): string {
@@ -61,61 +66,72 @@ function printValue(value: unknown, print: (value: string) => void): void {
61
66
  print(String(value));
62
67
  }
63
68
 
69
+ async function warnIfEffectiveConfigInvalid(
70
+ deps: ConfigCommandDeps,
71
+ remediation: string
72
+ ): Promise<ValidEffectiveConfig | null> {
73
+ const effective = await deps.loadEffectiveConfigWithDiagnostics(deps.cwd());
74
+ const errors = collectEffectiveConfigValidationErrors(effective);
75
+ if (!effective.config || errors.length > 0) {
76
+ deps.log.warn(
77
+ formatConfigValidationMessage(
78
+ getEffectiveConfigErrorHeader(effective),
79
+ errors.length > 0 ? errors : ["Configuration format is invalid."],
80
+ remediation
81
+ )
82
+ );
83
+ return null;
84
+ }
85
+
86
+ return { ...effective, config: effective.config };
87
+ }
88
+
64
89
  function parseScopedArgs(args: string[], defaultScope: ResolvedReadScope): ParsedScopedArgs {
65
90
  const positional: string[] = [];
66
- let scope = defaultScope;
67
- let sawLocal = false;
68
- let sawGlobal = false;
91
+ const state = { scope: defaultScope, sawLocal: false, sawGlobal: false };
69
92
 
70
93
  for (const arg of args) {
71
- if (arg === "--local") {
72
- if (sawGlobal) {
73
- throw new Error("Cannot use --local and --global together.");
74
- }
75
- sawLocal = true;
76
- scope = "local";
77
- continue;
94
+ if (!parseScopeFlag(arg, state)) {
95
+ positional.push(arg);
78
96
  }
97
+ }
79
98
 
80
- if (arg === "--global") {
81
- if (sawLocal) {
82
- throw new Error("Cannot use --local and --global together.");
83
- }
84
- sawGlobal = true;
85
- scope = "global";
86
- continue;
99
+ return { scope: state.scope, positional };
100
+ }
101
+
102
+ function parseScopeFlag(
103
+ arg: string,
104
+ state: { scope: ResolvedReadScope; sawLocal: boolean; sawGlobal: boolean }
105
+ ): boolean {
106
+ if (arg === "--local") {
107
+ if (state.sawGlobal) {
108
+ throw new Error("Cannot use --local and --global together.");
87
109
  }
110
+ state.sawLocal = true;
111
+ state.scope = "local";
112
+ return true;
113
+ }
88
114
 
89
- positional.push(arg);
115
+ if (arg === "--global") {
116
+ if (state.sawLocal) {
117
+ throw new Error("Cannot use --local and --global together.");
118
+ }
119
+ state.sawGlobal = true;
120
+ state.scope = "global";
121
+ return true;
90
122
  }
91
123
 
92
- return { scope, positional };
124
+ return false;
93
125
  }
94
126
 
95
127
  function parseShowArgs(args: string[]): ParsedShowArgs {
96
128
  const positional: string[] = [];
97
- let scope: ResolvedReadScope = "effective";
98
- let sawLocal = false;
99
- let sawGlobal = false;
129
+ const scopeState = { scope: "effective" as ResolvedReadScope, sawLocal: false, sawGlobal: false };
100
130
  let json = false;
101
131
  let verbose = false;
102
132
 
103
133
  for (const arg of args) {
104
- if (arg === "--local") {
105
- if (sawGlobal) {
106
- throw new Error("Cannot use --local and --global together.");
107
- }
108
- sawLocal = true;
109
- scope = "local";
110
- continue;
111
- }
112
-
113
- if (arg === "--global") {
114
- if (sawLocal) {
115
- throw new Error("Cannot use --local and --global together.");
116
- }
117
- sawGlobal = true;
118
- scope = "global";
134
+ if (parseScopeFlag(arg, scopeState)) {
119
135
  continue;
120
136
  }
121
137
 
@@ -132,7 +148,7 @@ function parseShowArgs(args: string[]): ParsedShowArgs {
132
148
  positional.push(arg);
133
149
  }
134
150
 
135
- return { scope, positional, json, verbose };
151
+ return { scope: scopeState.scope, positional, json, verbose };
136
152
  }
137
153
 
138
154
  export async function runShow(args: string[], deps: ConfigCommandDeps): Promise<void> {
@@ -296,17 +312,10 @@ export async function runSet(args: string[], deps: ConfigCommandDeps): Promise<v
296
312
  await deps.saveConfig(normalized.config);
297
313
  deps.log.success(`Updated "${key}" to ${formatValue(parsedValue)}.`);
298
314
 
299
- const effective = await deps.loadEffectiveConfigWithDiagnostics(deps.cwd());
300
- const effectiveErrors = collectEffectiveConfigValidationErrors(effective);
301
- if (!effective.config || effectiveErrors.length > 0) {
302
- deps.log.warn(
303
- formatConfigValidationMessage(
304
- getEffectiveConfigErrorHeader(effective),
305
- effectiveErrors.length > 0 ? effectiveErrors : ["Configuration format is invalid."],
306
- "Fix the repo-local override or restore compatible global values, then try again."
307
- )
308
- );
309
- }
315
+ await warnIfEffectiveConfigInvalid(
316
+ deps,
317
+ "Fix the repo-local override or restore compatible global values, then try again."
318
+ );
310
319
  }
311
320
 
312
321
  export async function runEdit(args: string[], deps: ConfigCommandDeps): Promise<void> {
@@ -347,16 +356,11 @@ export async function runEdit(args: string[], deps: ConfigCommandDeps): Promise<
347
356
  }
348
357
 
349
358
  if (parsed.scope === "local") {
350
- const effective = await deps.loadEffectiveConfigWithDiagnostics(deps.cwd());
351
- const errors = collectEffectiveConfigValidationErrors(effective);
352
- if (!effective.config || errors.length > 0) {
353
- deps.log.warn(
354
- formatConfigValidationMessage(
355
- getEffectiveConfigErrorHeader(effective),
356
- errors.length > 0 ? errors : ["Configuration format is invalid."],
357
- 'Run "rr init" and choose Repo-local config to regenerate the file, or fix it manually.'
358
- )
359
- );
359
+ const effective = await warnIfEffectiveConfigInvalid(
360
+ deps,
361
+ 'Run "rr init" and choose Repo-local config to regenerate the file, or fix it manually.'
362
+ );
363
+ if (!effective) {
360
364
  return;
361
365
  }
362
366
 
@@ -378,16 +382,11 @@ export async function runEdit(args: string[], deps: ConfigCommandDeps): Promise<
378
382
  return;
379
383
  }
380
384
 
381
- const effective = await deps.loadEffectiveConfigWithDiagnostics(deps.cwd());
382
- const effectiveErrors = collectEffectiveConfigValidationErrors(effective);
383
- if (!effective.config || effectiveErrors.length > 0) {
384
- deps.log.warn(
385
- formatConfigValidationMessage(
386
- getEffectiveConfigErrorHeader(effective),
387
- effectiveErrors.length > 0 ? effectiveErrors : ["Configuration format is invalid."],
388
- "Fix the repo-local override or restore compatible global values, then try again."
389
- )
390
- );
385
+ const effective = await warnIfEffectiveConfigInvalid(
386
+ deps,
387
+ "Fix the repo-local override or restore compatible global values, then try again."
388
+ );
389
+ if (!effective) {
391
390
  return;
392
391
  }
393
392