ralph-review 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-review",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A CLI tool that orchestrates agentic review-fix cycles until your code is clean.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -582,7 +582,26 @@ export async function startReview(
582
582
 
583
583
  const loadedConfig = await runtime.loadConfig();
584
584
 
585
- const hasExplicitMode = options.base || options.uncommitted || options.commit || options.custom;
585
+ if (options.custom !== undefined && options.custom.trim().length === 0) {
586
+ runtime.prompt.log.error("--custom cannot be empty");
587
+ runtime.process.exit(1);
588
+ return;
589
+ }
590
+
591
+ if (options.commit !== undefined) {
592
+ options.commit = options.commit.trim();
593
+ if (options.commit.length === 0) {
594
+ runtime.prompt.log.error("--commit cannot be empty");
595
+ runtime.process.exit(1);
596
+ return;
597
+ }
598
+ }
599
+
600
+ const hasExplicitMode =
601
+ options.base !== undefined ||
602
+ options.uncommitted === true ||
603
+ options.commit !== undefined ||
604
+ options.custom !== undefined;
586
605
  if (!hasExplicitMode) {
587
606
  if (loadedConfig?.defaultReview?.type === "base") {
588
607
  options.base = loadedConfig.defaultReview.branch;
@@ -590,15 +609,35 @@ export async function startReview(
590
609
  // else: defaults to uncommitted behavior (no base flag)
591
610
  }
592
611
 
593
- const modeOptions = [
594
- options.base && "--base",
595
- options.uncommitted && "--uncommitted",
596
- options.commit && "--commit",
597
- options.custom && "--custom",
598
- ].filter(Boolean);
612
+ if (options.base !== undefined) {
613
+ options.base = options.base.trim();
614
+ if (options.base.length === 0) {
615
+ runtime.prompt.log.error("--base cannot be empty");
616
+ runtime.process.exit(1);
617
+ return;
618
+ }
619
+ }
620
+
621
+ if (options.base !== undefined && options.commit !== undefined) {
622
+ runtime.prompt.log.error("Cannot use --base and --commit together");
623
+ runtime.process.exit(1);
624
+ return;
625
+ }
626
+
627
+ if (options.uncommitted && options.base !== undefined) {
628
+ runtime.prompt.log.error("Cannot use --uncommitted and --base together");
629
+ runtime.process.exit(1);
630
+ return;
631
+ }
632
+
633
+ if (options.uncommitted && options.commit !== undefined) {
634
+ runtime.prompt.log.error("Cannot use --uncommitted and --commit together");
635
+ runtime.process.exit(1);
636
+ return;
637
+ }
599
638
 
600
- if (modeOptions.length > 1) {
601
- runtime.prompt.log.error(`Cannot use ${modeOptions.join(" and ")} together`);
639
+ if (options.uncommitted && options.custom !== undefined) {
640
+ runtime.prompt.log.error("Cannot use --uncommitted and --custom together");
602
641
  runtime.process.exit(1);
603
642
  return;
604
643
  }
@@ -611,6 +650,7 @@ export async function startReview(
611
650
  projectPath: runtime.process.cwd(),
612
651
  baseBranch: options.base,
613
652
  commitSha: options.commit,
653
+ customInstructions: options.custom,
614
654
  capabilityDiscoveryOptions: {
615
655
  probeAgents: getDynamicProbeAgents(loadedConfig),
616
656
  },
@@ -52,6 +52,12 @@ export const codexConfig: AgentConfig = {
52
52
 
53
53
  const baseReviewArgs = withReasoningEffort(["exec", "--json"], reasoning);
54
54
 
55
+ if (reviewOptions?.customInstructions) {
56
+ const fullPrompt = prompt ? `review ${prompt}` : "review";
57
+ const customArgs = withReasoningEffort(["exec", "--full-auto", "--json"], reasoning);
58
+ return withModel([...customArgs, fullPrompt], model);
59
+ }
60
+
55
61
  if (reviewOptions?.commitSha) {
56
62
  return withModel([...baseReviewArgs, "review", "--commit", reviewOptions.commitSha], model);
57
63
  }
@@ -60,12 +66,6 @@ export const codexConfig: AgentConfig = {
60
66
  return withModel([...baseReviewArgs, "review", "--base", reviewOptions.baseBranch], model);
61
67
  }
62
68
 
63
- if (reviewOptions?.customInstructions) {
64
- const fullPrompt = prompt ? `review ${prompt}` : "review";
65
- const customArgs = withReasoningEffort(["exec", "--full-auto", "--json"], reasoning);
66
- return withModel([...customArgs, fullPrompt], model);
67
- }
68
-
69
69
  return withModel([...baseReviewArgs, "review", "--uncommitted"], model);
70
70
  },
71
71
  buildEnv: defaultBuildEnv,
@@ -20,6 +20,7 @@ interface RunDiagnosticsDependencies {
20
20
  ) => Promise<AgentCapabilitiesMap>;
21
21
  isGitRepository?: (path: string) => Promise<boolean>;
22
22
  hasUncommittedChanges?: (path: string) => Promise<boolean>;
23
+ gitRefExists?: (path: string, ref: string) => Promise<boolean>;
23
24
  cleanupStaleLockfile?: typeof cleanupStaleLockfile;
24
25
  hasActiveLockfile?: typeof hasActiveLockfile;
25
26
  isTmuxInstalled?: () => boolean;
@@ -34,6 +35,7 @@ export interface RunDiagnosticsOptions {
34
35
  projectPath?: string;
35
36
  baseBranch?: string;
36
37
  commitSha?: string;
38
+ customInstructions?: string;
37
39
  capabilitiesByAgent?: AgentCapabilitiesMap;
38
40
  capabilityDiscoveryOptions?: CapabilityDiscoveryOptions;
39
41
  dependencies?: RunDiagnosticsDependencies;
@@ -136,6 +138,15 @@ async function hasGitUncommittedChanges(path: string): Promise<boolean> {
136
138
  return result.stdout.trim().length > 0;
137
139
  }
138
140
 
141
+ async function hasGitRef(path: string, ref: string): Promise<boolean> {
142
+ if (ref.trim().length === 0) {
143
+ return false;
144
+ }
145
+
146
+ const result = await runGitInPath(path, ["rev-parse", "--verify", ref]);
147
+ return result.exitCode === 0;
148
+ }
149
+
139
150
  function buildReport(
140
151
  context: DiagnosticContext,
141
152
  items: DiagnosticItem[],
@@ -165,6 +176,7 @@ export async function runDiagnostics(
165
176
  const resolveCapabilityDiscovery = deps.discoverAgentCapabilities ?? discoverAgentCapabilities;
166
177
  const resolveIsGitRepo = deps.isGitRepository ?? isGitRepository;
167
178
  const resolveHasChanges = deps.hasUncommittedChanges ?? hasGitUncommittedChanges;
179
+ const resolveGitRefExists = deps.gitRefExists ?? hasGitRef;
168
180
  const resolveCleanupStaleLockfile = deps.cleanupStaleLockfile ?? cleanupStaleLockfile;
169
181
  const resolveHasActiveLockfile = deps.hasActiveLockfile ?? hasActiveLockfile;
170
182
  const resolveIsTmuxInstalled = deps.isTmuxInstalled ?? isTmuxInstalled;
@@ -423,6 +435,79 @@ export async function runDiagnostics(
423
435
  }
424
436
 
425
437
  if (context === "run") {
438
+ if (insideGitRepo && !gitRepoError && options.baseBranch) {
439
+ let baseRefExists = false;
440
+ let baseRefError: string | null = null;
441
+ try {
442
+ baseRefExists = await resolveGitRefExists(projectPath, options.baseBranch);
443
+ } catch (error) {
444
+ baseRefError = `${error}`;
445
+ }
446
+
447
+ if (baseRefError) {
448
+ items.push({
449
+ id: "git-base-ref",
450
+ category: "git",
451
+ title: "Base ref",
452
+ severity: "error",
453
+ summary: `Unable to validate base ref '${options.baseBranch}'.`,
454
+ details: baseRefError,
455
+ remediation: [runStep("git branch --all"), thenStep("rr run --base <existing-ref>")],
456
+ });
457
+ } else {
458
+ items.push({
459
+ id: "git-base-ref",
460
+ category: "git",
461
+ title: "Base ref",
462
+ severity: baseRefExists ? "ok" : "error",
463
+ summary: baseRefExists
464
+ ? `Base ref '${options.baseBranch}' exists.`
465
+ : `Base ref '${options.baseBranch}' was not found.`,
466
+ remediation: baseRefExists
467
+ ? []
468
+ : [runStep("git branch --all"), thenStep("rr run --base <existing-ref>")],
469
+ });
470
+ }
471
+ }
472
+
473
+ if (insideGitRepo && !gitRepoError && options.commitSha) {
474
+ let commitRefExists = false;
475
+ let commitRefError: string | null = null;
476
+ try {
477
+ commitRefExists = await resolveGitRefExists(projectPath, options.commitSha);
478
+ } catch (error) {
479
+ commitRefError = `${error}`;
480
+ }
481
+
482
+ if (commitRefError) {
483
+ items.push({
484
+ id: "git-commit-ref",
485
+ category: "git",
486
+ title: "Commit ref",
487
+ severity: "error",
488
+ summary: `Unable to validate commit ref '${options.commitSha}'.`,
489
+ details: commitRefError,
490
+ remediation: [
491
+ runStep("git rev-parse --verify <commit>"),
492
+ thenStep("rr run --commit <sha>"),
493
+ ],
494
+ });
495
+ } else {
496
+ items.push({
497
+ id: "git-commit-ref",
498
+ category: "git",
499
+ title: "Commit ref",
500
+ severity: commitRefExists ? "ok" : "error",
501
+ summary: commitRefExists
502
+ ? `Commit ref '${options.commitSha}' exists.`
503
+ : `Commit ref '${options.commitSha}' was not found.`,
504
+ remediation: commitRefExists
505
+ ? []
506
+ : [runStep("git log --oneline -n 20"), thenStep("rr run --commit <existing-sha>")],
507
+ });
508
+ }
509
+ }
510
+
426
511
  if (!options.baseBranch && !options.commitSha && insideGitRepo && !gitRepoError) {
427
512
  let hasChanges = false;
428
513
  let hasChangesError: string | null = null;
package/src/lib/format.ts CHANGED
@@ -1,23 +1,31 @@
1
1
  import type { ReviewOptions } from "@/lib/types";
2
2
 
3
+ function formatCustomReviewType(customInstructions: string): string {
4
+ const instruction = customInstructions.slice(0, 40);
5
+ return customInstructions.length > 40 ? `custom (${instruction}...)` : `custom (${instruction})`;
6
+ }
7
+
3
8
  export function formatReviewType(reviewOptions: ReviewOptions | undefined): string {
4
9
  if (!reviewOptions) return "uncommitted changes";
5
10
 
6
- if (reviewOptions.customInstructions) {
7
- const instruction = reviewOptions.customInstructions.slice(0, 40);
8
- return reviewOptions.customInstructions.length > 40
9
- ? `custom (${instruction}...)`
10
- : `custom (${instruction})`;
11
- }
12
-
13
11
  if (reviewOptions.commitSha) {
14
12
  const shortSha = reviewOptions.commitSha.slice(0, 7);
13
+ if (reviewOptions.customInstructions) {
14
+ return `commit (${shortSha}) + ${formatCustomReviewType(reviewOptions.customInstructions)}`;
15
+ }
15
16
  return `commit (${shortSha})`;
16
17
  }
17
18
 
18
19
  if (reviewOptions.baseBranch) {
20
+ if (reviewOptions.customInstructions) {
21
+ return `base (${reviewOptions.baseBranch}) + ${formatCustomReviewType(reviewOptions.customInstructions)}`;
22
+ }
19
23
  return `base (${reviewOptions.baseBranch})`;
20
24
  }
21
25
 
26
+ if (reviewOptions.customInstructions) {
27
+ return formatCustomReviewType(reviewOptions.customInstructions);
28
+ }
29
+
22
30
  return "uncommitted changes";
23
31
  }
@@ -19,6 +19,16 @@ const BASE_BRANCH_PROMPT_BACKUP = (branch: string) =>
19
19
  const COMMIT_PROMPT = (commitHash: string) =>
20
20
  `Review the code changes for the commit ${commitHash}. Provide prioritized, actionable findings.`;
21
21
 
22
+ const CUSTOM_FOCUS_PROMPT = (customInstructions: string) =>
23
+ `Additional review focus from user instructions:\n${customInstructions}`;
24
+
25
+ function withCustomFocus(instruction: string, customInstructions?: string): string {
26
+ if (!customInstructions) {
27
+ return instruction;
28
+ }
29
+ return `${instruction}\n\n${CUSTOM_FOCUS_PROMPT(customInstructions)}`;
30
+ }
31
+
22
32
  export interface ReviewerPromptOptions {
23
33
  repoPath: string;
24
34
  baseBranch?: string;
@@ -26,19 +36,20 @@ export interface ReviewerPromptOptions {
26
36
  customInstructions?: string;
27
37
  }
28
38
 
29
- /** Priority: commitSha > baseBranch > customInstructions > uncommitted (default) */
39
+ /** Target priority: commitSha > baseBranch > uncommitted (default), with custom focus overlay. */
30
40
  export function createReviewerPrompt(options: ReviewerPromptOptions): string {
31
41
  const { repoPath, baseBranch, commitSha, customInstructions } = options;
32
42
 
33
43
  let instruction: string;
34
44
 
35
45
  if (commitSha) {
36
- instruction = COMMIT_PROMPT(commitSha);
46
+ instruction = withCustomFocus(COMMIT_PROMPT(commitSha), customInstructions);
37
47
  } else if (baseBranch) {
38
48
  const mergeBaseSha = mergeBaseWithHead(repoPath, baseBranch);
39
- instruction = mergeBaseSha
49
+ const baseInstruction = mergeBaseSha
40
50
  ? BASE_BRANCH_PROMPT(baseBranch, mergeBaseSha)
41
51
  : BASE_BRANCH_PROMPT_BACKUP(baseBranch);
52
+ instruction = withCustomFocus(baseInstruction, customInstructions);
42
53
  } else if (customInstructions) {
43
54
  instruction = customInstructions;
44
55
  } else {
@@ -1,5 +1,6 @@
1
1
  import { useTerminalDimensions } from "@opentui/react";
2
2
  import { useEffect, useMemo, useRef } from "react";
3
+ import { formatReviewType } from "@/lib/format";
3
4
  import type { LockData } from "@/lib/lockfile";
4
5
  import { TUI_COLORS } from "@/lib/tui/colors";
5
6
  import type {
@@ -81,24 +82,6 @@ function toSingleLine(value: string): string {
81
82
  return value.replace(/\s+/g, " ").trim();
82
83
  }
83
84
 
84
- function formatReviewType(reviewOptions: ReviewOptions | undefined): string {
85
- if (!reviewOptions) return "uncommitted changes";
86
-
87
- if (reviewOptions.customInstructions) {
88
- return `custom (${toSingleLine(reviewOptions.customInstructions)})`;
89
- }
90
-
91
- if (reviewOptions.commitSha) {
92
- return `commit (${toSingleLine(reviewOptions.commitSha)})`;
93
- }
94
-
95
- if (reviewOptions.baseBranch) {
96
- return `base (${toSingleLine(reviewOptions.baseBranch)})`;
97
- }
98
-
99
- return "uncommitted changes";
100
- }
101
-
102
85
  interface FixListProps {
103
86
  fixes: FixEntry[];
104
87
  showFiles: boolean;
@@ -566,7 +549,7 @@ export function SessionPanel({
566
549
  <box flexDirection="row" gap={1}>
567
550
  <text fg={TUI_COLORS.text.muted}>Review Type:</text>
568
551
  <text fg={TUI_COLORS.text.primary} wrapMode="none">
569
- {formatReviewType(reviewOptions)}
552
+ {toSingleLine(formatReviewType(reviewOptions))}
570
553
  </text>
571
554
  </box>
572
555