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
@@ -37,6 +37,49 @@ import type { SessionGroupData, WorkspaceState } from "./workspace-types";
37
37
  const DEFAULT_REFRESH_INTERVAL = 1000;
38
38
  const LIVE_REFRESH_INTERVAL = TMUX_CAPTURE_MIN_INTERVAL_MS;
39
39
 
40
+ export function createInitialWorkspaceState(
41
+ overrides: Partial<WorkspaceState> = {}
42
+ ): WorkspaceState {
43
+ return {
44
+ sessionGroups: [],
45
+ allSessions: [],
46
+ projectSessions: [],
47
+ selectedSessionId: null,
48
+ currentSession: null,
49
+ logEntries: [],
50
+ fixes: [],
51
+ skipped: [],
52
+ findings: [],
53
+ storedFindings: [],
54
+ selectedFindingIds: [],
55
+ selectedFindings: [],
56
+ unselectedFindings: [],
57
+ fixResults: [],
58
+ unresolvedSelectedFindings: [],
59
+ auditRegressionFindings: [],
60
+ iterationFixes: [],
61
+ iterationSkipped: [],
62
+ iterationFindings: [],
63
+ latestReviewIteration: null,
64
+ codexReviewText: null,
65
+ tmuxOutput: "",
66
+ elapsed: 0,
67
+ maxIterations: 0,
68
+ error: null,
69
+ liveRefreshError: null,
70
+ isLoading: true,
71
+ lastSessionStats: null,
72
+ projectStats: null,
73
+ config: null,
74
+ configWarning: null,
75
+ isGitRepo: true,
76
+ currentAgent: null,
77
+ reviewOptions: undefined,
78
+ outputVisible: false,
79
+ ...overrides,
80
+ };
81
+ }
82
+
40
83
  function buildSessionGroups(
41
84
  allSessions: ActiveSession[],
42
85
  currentProjectPath: string
@@ -80,43 +123,7 @@ export function useWorkspaceState(
80
123
  _branch?: string,
81
124
  refreshInterval: number = DEFAULT_REFRESH_INTERVAL
82
125
  ): WorkspaceState {
83
- const [state, setState] = useState<WorkspaceState>({
84
- sessionGroups: [],
85
- allSessions: [],
86
- projectSessions: [],
87
- selectedSessionId: null,
88
- currentSession: null,
89
- logEntries: [],
90
- fixes: [],
91
- skipped: [],
92
- findings: [],
93
- storedFindings: [],
94
- selectedFindingIds: [],
95
- selectedFindings: [],
96
- unselectedFindings: [],
97
- fixResults: [],
98
- unresolvedSelectedFindings: [],
99
- auditRegressionFindings: [],
100
- iterationFixes: [],
101
- iterationSkipped: [],
102
- iterationFindings: [],
103
- latestReviewIteration: null,
104
- codexReviewText: null,
105
- tmuxOutput: "",
106
- elapsed: 0,
107
- maxIterations: 0,
108
- error: null,
109
- liveRefreshError: null,
110
- isLoading: true,
111
- lastSessionStats: null,
112
- projectStats: null,
113
- config: null,
114
- configWarning: null,
115
- isGitRepo: true,
116
- currentAgent: null,
117
- reviewOptions: undefined,
118
- outputVisible: false,
119
- });
126
+ const [state, setState] = useState<WorkspaceState>(() => createInitialWorkspaceState());
120
127
 
121
128
  const stateRef = useRef(state);
122
129
  stateRef.current = state;
@@ -1,5 +1,6 @@
1
1
  import type { FixDecision, Priority } from "./domain";
2
2
  import { VALID_FIX_DECISIONS, VALID_PRIORITIES } from "./domain";
3
+ import { asRecord, isCodeLocation } from "./guards";
3
4
  import type { CodeLocation } from "./review";
4
5
 
5
6
  export interface FixEntry {
@@ -26,53 +27,27 @@ export interface FixSummary {
26
27
  skipped: SkippedEntry[];
27
28
  }
28
29
 
29
- function isLineRange(value: unknown): value is CodeLocation["line_range"] {
30
- if (typeof value !== "object" || value === null) {
31
- return false;
32
- }
33
-
34
- const obj = value as Record<string, unknown>;
35
-
30
+ function hasFixEntryHeader(obj: Record<string, unknown>): boolean {
36
31
  return (
37
- typeof obj.start === "number" &&
38
- Number.isInteger(obj.start) &&
39
- obj.start > 0 &&
40
- typeof obj.end === "number" &&
41
- Number.isInteger(obj.end) &&
42
- obj.end >= obj.start
32
+ typeof obj.id === "number" &&
33
+ typeof obj.title === "string" &&
34
+ typeof obj.priority === "string" &&
35
+ VALID_PRIORITIES.includes(obj.priority as Priority)
43
36
  );
44
37
  }
45
38
 
46
- function isCodeLocation(value: unknown): value is CodeLocation {
47
- if (typeof value !== "object" || value === null) {
48
- return false;
49
- }
50
-
51
- const obj = value as Record<string, unknown>;
52
-
53
- if (typeof obj.absolute_file_path !== "string") {
54
- return false;
55
- }
56
-
57
- return isLineRange(obj.line_range);
58
- }
59
-
60
39
  function isFixEntry(value: unknown): value is FixEntry {
61
- if (typeof value !== "object" || value === null) {
40
+ const obj = asRecord(value);
41
+ if (obj === null) {
62
42
  return false;
63
43
  }
64
44
 
65
- const obj = value as Record<string, unknown>;
66
-
67
45
  return (
68
- typeof obj.id === "number" &&
69
- typeof obj.title === "string" &&
70
- typeof obj.priority === "string" &&
71
- VALID_PRIORITIES.includes(obj.priority as Priority) &&
46
+ hasFixEntryHeader(obj) &&
72
47
  (obj.file === undefined || obj.file === null || typeof obj.file === "string") &&
73
48
  (obj.code_location === undefined ||
74
49
  obj.code_location === null ||
75
- isCodeLocation(obj.code_location)) &&
50
+ isCodeLocation(obj.code_location, { requirePositive: true })) &&
76
51
  typeof obj.claim === "string" &&
77
52
  typeof obj.evidence === "string" &&
78
53
  typeof obj.fix === "string"
@@ -80,28 +55,20 @@ function isFixEntry(value: unknown): value is FixEntry {
80
55
  }
81
56
 
82
57
  function isSkippedEntry(value: unknown): value is SkippedEntry {
83
- if (typeof value !== "object" || value === null) {
58
+ const obj = asRecord(value);
59
+ if (obj === null) {
84
60
  return false;
85
61
  }
86
62
 
87
- const obj = value as Record<string, unknown>;
88
-
89
- return (
90
- typeof obj.id === "number" &&
91
- typeof obj.title === "string" &&
92
- typeof obj.priority === "string" &&
93
- VALID_PRIORITIES.includes(obj.priority as Priority) &&
94
- typeof obj.reason === "string"
95
- );
63
+ return hasFixEntryHeader(obj) && typeof obj.reason === "string";
96
64
  }
97
65
 
98
66
  export function isFixSummary(value: unknown): value is FixSummary {
99
- if (typeof value !== "object" || value === null) {
67
+ const obj = asRecord(value);
68
+ if (obj === null) {
100
69
  return false;
101
70
  }
102
71
 
103
- const obj = value as Record<string, unknown>;
104
-
105
72
  // Check decision field
106
73
  if (
107
74
  typeof obj.decision !== "string" ||
@@ -0,0 +1,47 @@
1
+ export function asRecord(value: unknown): Record<string, unknown> | null {
2
+ if (typeof value !== "object" || value === null) {
3
+ return null;
4
+ }
5
+
6
+ return value as Record<string, unknown>;
7
+ }
8
+
9
+ function isInteger(value: unknown): value is number {
10
+ return typeof value === "number" && Number.isInteger(value);
11
+ }
12
+
13
+ function asRecordWithStringField(value: unknown, field: string): Record<string, unknown> | null {
14
+ const obj = asRecord(value);
15
+ if (obj === null || typeof obj[field] !== "string") {
16
+ return null;
17
+ }
18
+
19
+ return obj;
20
+ }
21
+
22
+ function isLineRange(
23
+ value: unknown,
24
+ options: { requirePositive?: boolean } = {}
25
+ ): value is { start: number; end: number } {
26
+ const obj = asRecord(value);
27
+ if (obj === null || !isInteger(obj.start) || !isInteger(obj.end) || obj.end < obj.start) {
28
+ return false;
29
+ }
30
+
31
+ if (options.requirePositive && (obj.start <= 0 || obj.end <= 0)) {
32
+ return false;
33
+ }
34
+
35
+ return true;
36
+ }
37
+
38
+ export function isCodeLocation(
39
+ value: unknown,
40
+ options: { requirePositive?: boolean } = {}
41
+ ): value is {
42
+ absolute_file_path: string;
43
+ line_range: { start: number; end: number };
44
+ } {
45
+ const obj = asRecordWithStringField(value, "absolute_file_path");
46
+ return obj !== null && isLineRange(obj.line_range, options);
47
+ }
@@ -1,5 +1,6 @@
1
1
  import type { OverallCorrectness } from "./domain";
2
2
  import { VALID_OVERALL_CORRECTNESS } from "./domain";
3
+ import { asRecord, isCodeLocation } from "./guards";
3
4
 
4
5
  export interface LineRange {
5
6
  start: number;
@@ -32,46 +33,12 @@ export interface CodexReviewSummary {
32
33
 
33
34
  const DEFAULT_CODEX_CONFIDENCE = 0.69;
34
35
 
35
- function isLineRange(value: unknown): value is LineRange {
36
- if (typeof value !== "object" || value === null) {
37
- return false;
38
- }
39
-
40
- const obj = value as Record<string, unknown>;
41
-
42
- return (
43
- typeof obj.start === "number" &&
44
- Number.isInteger(obj.start) &&
45
- typeof obj.end === "number" &&
46
- Number.isInteger(obj.end)
47
- );
48
- }
49
-
50
- function isCodeLocation(value: unknown): value is CodeLocation {
51
- if (typeof value !== "object" || value === null) {
52
- return false;
53
- }
54
-
55
- const obj = value as Record<string, unknown>;
56
-
57
- if (typeof obj.absolute_file_path !== "string") {
58
- return false;
59
- }
60
-
61
- if (!isLineRange(obj.line_range)) {
62
- return false;
63
- }
64
-
65
- return true;
66
- }
67
-
68
36
  function isFinding(value: unknown): value is Finding {
69
- if (typeof value !== "object" || value === null) {
37
+ const obj = asRecord(value);
38
+ if (obj === null) {
70
39
  return false;
71
40
  }
72
41
 
73
- const obj = value as Record<string, unknown>;
74
-
75
42
  if (typeof obj.title !== "string" || typeof obj.body !== "string") {
76
43
  return false;
77
44
  }
@@ -99,12 +66,11 @@ function isFinding(value: unknown): value is Finding {
99
66
  }
100
67
 
101
68
  export function isReviewSummary(value: unknown): value is ReviewSummary {
102
- if (typeof value !== "object" || value === null) {
69
+ const obj = asRecord(value);
70
+ if (obj === null) {
103
71
  return false;
104
72
  }
105
73
 
106
- const obj = value as Record<string, unknown>;
107
-
108
74
  if (!Array.isArray(obj.findings)) {
109
75
  return false;
110
76
  }