ralph-review 0.2.4 → 0.2.5

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.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Orchestrating coding agents for code review, verification and fixing via the ralph loop.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -325,6 +325,20 @@ export async function runReviewSession(
325
325
  if (entry.type === "review_iteration") {
326
326
  latestPersistedFindings = [...latestPersistedFindings, ...entry.findings];
327
327
  completedReviewIterations = entry.iteration;
328
+
329
+ if (worktree && latestPersistedFindings.length > 0) {
330
+ await deps.saveFindingsArtifact(
331
+ CONFIG_DIR,
332
+ createFindingsArtifact(
333
+ sessionId,
334
+ projectPath,
335
+ sessionPath,
336
+ worktree,
337
+ latestPersistedFindings
338
+ )
339
+ );
340
+ shouldDeleteSessionRefs = false;
341
+ }
328
342
  }
329
343
  };
330
344
 
@@ -1,5 +1,5 @@
1
1
  import { discardSessionWorktree, type GitSessionWorktree } from "@/lib/git";
2
- import { deleteSessionFiles, readLog } from "@/lib/logger";
2
+ import { appendLog, deleteSessionFiles, readLog } from "@/lib/logger";
3
3
  import {
4
4
  type ActiveSession,
5
5
  readSessionState,
@@ -27,6 +27,7 @@ interface WaitForGracefulStopDeps {
27
27
 
28
28
  interface StopActiveSessionDeps {
29
29
  readLog: typeof readLog;
30
+ appendLog: typeof appendLog;
30
31
  deleteSessionFiles: typeof deleteSessionFiles;
31
32
  updateSessionState: typeof updateSessionState;
32
33
  sendInterrupt: typeof sendInterrupt;
@@ -41,6 +42,7 @@ interface StopActiveSessionDeps {
41
42
 
42
43
  const DEFAULT_STOP_ACTIVE_SESSION_DEPS: StopActiveSessionDeps = {
43
44
  readLog,
45
+ appendLog,
44
46
  deleteSessionFiles,
45
47
  updateSessionState,
46
48
  sendInterrupt,
@@ -137,6 +139,7 @@ interface SessionIterationState {
137
139
  hasRecordedIteration: boolean;
138
140
  hasRecordedReviewProgress: boolean;
139
141
  hasSuccessfulReviewIteration: boolean;
142
+ entries: LogEntry[];
140
143
  }
141
144
 
142
145
  async function resolveSessionIterationState(
@@ -153,12 +156,58 @@ async function resolveSessionIterationState(
153
156
  hasRecordedIteration: hasRecordedIteration(entries),
154
157
  hasRecordedReviewProgress: hasRecordedReviewProgress(entries),
155
158
  hasSuccessfulReviewIteration: hasSuccessfulReviewIteration(entries),
159
+ entries,
156
160
  };
157
161
  } catch {
158
162
  return null;
159
163
  }
160
164
  }
161
165
 
166
+ function getLatestLifecycleEntry(entries: LogEntry[]): LogEntry | undefined {
167
+ return [...entries].reverse().find((entry) => entry.type !== "handoff");
168
+ }
169
+
170
+ function getLatestReviewIteration(entries: LogEntry[]) {
171
+ return entries.filter((entry) => entry.type === "review_iteration").at(-1);
172
+ }
173
+
174
+ function getAccumulatedReviewFindings(entries: LogEntry[]) {
175
+ return entries.flatMap((entry) => (entry.type === "review_iteration" ? entry.findings : []));
176
+ }
177
+
178
+ async function terminalizeForceStoppedReviewSession(
179
+ session: ActiveSession,
180
+ iterationState: SessionIterationState | null,
181
+ deps: StopActiveSessionDeps
182
+ ): Promise<void> {
183
+ if (!session.sessionPath || !iterationState?.hasRecordedReviewProgress) {
184
+ return;
185
+ }
186
+
187
+ if (getLatestLifecycleEntry(iterationState.entries)?.type === "session_end") {
188
+ return;
189
+ }
190
+
191
+ const latestReviewIteration = getLatestReviewIteration(iterationState.entries);
192
+ if (!latestReviewIteration) {
193
+ return;
194
+ }
195
+
196
+ await deps.appendLog(session.sessionPath, {
197
+ type: "session_end",
198
+ timestamp: Date.now(),
199
+ status: "interrupted",
200
+ reason: "Review stopped by user.",
201
+ iterations: latestReviewIteration.iteration,
202
+ phase: "review",
203
+ sessionStatus: "interrupted",
204
+ reviewOutcome:
205
+ getAccumulatedReviewFindings(iterationState.entries).length > 0
206
+ ? "findings-pending"
207
+ : "incomplete",
208
+ });
209
+ }
210
+
162
211
  function createCleanupWorktree(
163
212
  session: ActiveSession,
164
213
  sourceRepoPath: string
@@ -239,6 +288,15 @@ export async function stopActiveSession(
239
288
  }
240
289
 
241
290
  const finalIterationState = await resolveSessionIterationState(session, stopDeps);
291
+ let terminalizeSessionError: unknown;
292
+ if (!stoppedGracefully) {
293
+ try {
294
+ await terminalizeForceStoppedReviewSession(session, finalIterationState, stopDeps);
295
+ } catch (error) {
296
+ terminalizeSessionError = error;
297
+ }
298
+ }
299
+
242
300
  if (finalIterationState?.hasSuccessfulReviewIteration === false) {
243
301
  cleanupUnpromotedSessionWorktree(session, stopDeps);
244
302
  }
@@ -256,6 +314,10 @@ export async function stopActiveSession(
256
314
  expectedSessionId: session.sessionId,
257
315
  });
258
316
 
317
+ if (terminalizeSessionError) {
318
+ throw terminalizeSessionError;
319
+ }
320
+
259
321
  if (deleteSessionFilesError) {
260
322
  throw deleteSessionFilesError;
261
323
  }
@@ -17,9 +17,11 @@ import { StatusBar } from "./StatusBar";
17
17
  import { useDashboardRunControl } from "./use-dashboard-run-control";
18
18
  import { useDashboardStopControl } from "./use-dashboard-stop-control";
19
19
 
20
- export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: DashboardProps) {
20
+ export function Dashboard({ projectPath, branch, refreshInterval = 1000, deps }: DashboardProps) {
21
21
  const renderer = useRenderer();
22
- const state = useWorkspaceState(projectPath, branch, refreshInterval);
22
+ const resolvedUseWorkspaceState = deps?.useWorkspaceState ?? useWorkspaceState;
23
+ const ResolvedDashboardOverlays = deps?.DashboardOverlays ?? DashboardOverlays;
24
+ const state = resolvedUseWorkspaceState(projectPath, branch, refreshInterval);
23
25
  const {
24
26
  runError,
25
27
  startupMode,
@@ -29,7 +31,7 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
29
31
  spawnRunProcess,
30
32
  spawnFixProcess,
31
33
  isStartupSpawning,
32
- } = useDashboardRunControl(projectPath);
34
+ } = useDashboardRunControl(projectPath, { spawn: deps?.spawn });
33
35
  const [focusedPane, setFocusedPane] = useState<FocusedPane>("detail");
34
36
  const [outputVisible, setOutputVisible] = useState(false);
35
37
  const [showHelp, setShowHelp] = useState(false);
@@ -37,11 +39,14 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
37
39
  const [showSession, setShowSession] = useState(false);
38
40
  const [showReviewModeOverlay, setShowReviewModeOverlay] = useState(false);
39
41
  const [showStopPicker, setShowStopPicker] = useState(false);
40
- const { isStoppingRun, stopSelectedSession } = useDashboardStopControl({
41
- currentSession: state.currentSession,
42
- setShowStopPicker,
43
- onError: setRunError,
44
- });
42
+ const { isStoppingRun, stopSelectedSession } = useDashboardStopControl(
43
+ {
44
+ currentSession: state.currentSession,
45
+ setShowStopPicker,
46
+ onError: setRunError,
47
+ },
48
+ { stopActiveSession: deps?.stopActiveSession }
49
+ );
45
50
 
46
51
  const projectName = basename(projectPath);
47
52
  const isExitingRef = useRef(false);
@@ -271,7 +276,7 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
271
276
  liveRefreshError={state.liveRefreshError}
272
277
  configWarning={state.configWarning}
273
278
  />
274
- <DashboardOverlays
279
+ <ResolvedDashboardOverlays
275
280
  showHelp={showHelp}
276
281
  showRunOverlay={showRunOverlay}
277
282
  showFixFindings={showFixFindings}
@@ -15,10 +15,18 @@ export interface DashboardRunControl {
15
15
  isStartupSpawning: () => boolean;
16
16
  }
17
17
 
18
- export function useDashboardRunControl(projectPath: string): DashboardRunControl {
18
+ interface DashboardRunControlDeps {
19
+ spawn?: typeof Bun.spawn;
20
+ }
21
+
22
+ export function useDashboardRunControl(
23
+ projectPath: string,
24
+ deps?: DashboardRunControlDeps
25
+ ): DashboardRunControl {
19
26
  const [runError, setRunError] = useState<string | null>(null);
20
27
  const [startupMode, setStartupMode] = useState<DashboardStartupMode>(null);
21
28
  const isStartupSpawningRef = useRef(false);
29
+ const spawn = deps?.spawn ?? Bun.spawn;
22
30
 
23
31
  const clearRunError = useCallback(() => {
24
32
  setRunError(null);
@@ -40,7 +48,7 @@ export function useDashboardRunControl(projectPath: string): DashboardRunControl
40
48
  setStartupMode(nextStartupMode);
41
49
 
42
50
  try {
43
- const subprocess = Bun.spawn([process.execPath, CLI_PATH, command, ...argv], {
51
+ const subprocess = spawn([process.execPath, CLI_PATH, command, ...argv], {
44
52
  cwd: projectPath,
45
53
  stdin: "ignore",
46
54
  stdout: "pipe",
@@ -73,7 +81,7 @@ export function useDashboardRunControl(projectPath: string): DashboardRunControl
73
81
  setRunError(getErrorMessage(error));
74
82
  }
75
83
  },
76
- [projectPath]
84
+ [projectPath, spawn]
77
85
  );
78
86
 
79
87
  const spawnRunProcess = useCallback(
@@ -22,13 +22,17 @@ export interface DashboardStopControl {
22
22
  stopSelectedSession: (session: ActiveSession) => Promise<void>;
23
23
  }
24
24
 
25
- export function useDashboardStopControl({
26
- currentSession,
27
- setShowStopPicker,
28
- onError,
29
- }: DashboardStopControlOptions): DashboardStopControl {
25
+ interface DashboardStopControlDeps {
26
+ stopActiveSession?: typeof stopActiveSession;
27
+ }
28
+
29
+ export function useDashboardStopControl(
30
+ options: DashboardStopControlOptions,
31
+ deps?: DashboardStopControlDeps
32
+ ): DashboardStopControl {
30
33
  const [stoppingSession, setStoppingSession] = useState<StoppingSessionState | null>(null);
31
34
  const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
35
+ const stopSession = deps?.stopActiveSession ?? stopActiveSession;
32
36
 
33
37
  const clearSettleTimer = useMemo(
34
38
  () => () => {
@@ -42,7 +46,10 @@ export function useDashboardStopControl({
42
46
 
43
47
  if (
44
48
  stoppingSession &&
45
- shouldClearStoppingSessionState({ marker: stoppingSession, currentSession })
49
+ shouldClearStoppingSessionState({
50
+ marker: stoppingSession,
51
+ currentSession: options.currentSession,
52
+ })
46
53
  ) {
47
54
  clearSettleTimer();
48
55
  setStoppingSession(null);
@@ -57,8 +64,8 @@ export function useDashboardStopControl({
57
64
 
58
65
  try {
59
66
  await stopSelectedDashboardSession(session, {
60
- setShowStopPicker,
61
- stopActiveSession,
67
+ setShowStopPicker: options.setShowStopPicker,
68
+ stopActiveSession: stopSession,
62
69
  });
63
70
 
64
71
  const settled = settleStoppingSessionState(createStoppingSessionState(session));
@@ -78,10 +85,10 @@ export function useDashboardStopControl({
78
85
  } catch (error) {
79
86
  clearSettleTimer();
80
87
  setStoppingSession(null);
81
- onError(getErrorMessage(error));
88
+ options.onError(getErrorMessage(error));
82
89
  }
83
90
  },
84
- [clearSettleTimer, onError, setShowStopPicker]
91
+ [clearSettleTimer, options.onError, options.setShowStopPicker, stopSession]
85
92
  );
86
93
 
87
94
  return {
@@ -1,5 +1,15 @@
1
+ import type { stopActiveSession } from "@/lib/stop-session";
2
+ import type { DashboardOverlays } from "@/lib/tui/dashboard/DashboardOverlays";
3
+ import type { useWorkspaceState } from "@/lib/tui/workspace/use-workspace-state";
4
+
1
5
  export interface DashboardProps {
2
6
  projectPath: string;
3
7
  branch?: string;
4
8
  refreshInterval?: number;
9
+ deps?: {
10
+ useWorkspaceState?: typeof useWorkspaceState;
11
+ DashboardOverlays?: typeof DashboardOverlays;
12
+ spawn?: typeof Bun.spawn;
13
+ stopActiveSession?: typeof stopActiveSession;
14
+ };
5
15
  }