Skip to content

Commit e7c3f3a

Browse files
Added Resume capability with the associated tests (#15)
* Added Resume capability with the associated tests * Biome formatting code * fix: add type annotations and fix flaky agent override test - Add LoopState type annotations to cancel.ts and resume.ts to fix Biome noImplicitAnyLet lint errors - Fix agent override test timeout by reading stdout incrementally and killing the process after collecting expected output --------- Co-authored-by: Laith Al-Saadoon <9553966+theagenticguy@users.noreply.github.com>
1 parent 3c92068 commit e7c3f3a

10 files changed

Lines changed: 931 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Features
6+
7+
* **resume:** add `ralph resume` command for resuming interrupted loops
8+
- Resume stopped loops with full context about previous work
9+
- Override loop parameters (min/max iterations, completion promise, agent)
10+
- Enhanced prompt with instructions to review files, git history, and feedback
11+
- Displays previous iteration number and quality score
12+
13+
### Bug Fixes
14+
15+
* **state:** preserve state on interruption instead of deleting
16+
- Ctrl+C (SIGINT) now saves state with iteration and feedback
17+
- Max iterations reached saves state for continuation
18+
- `ralph cancel` marks state inactive instead of deleting
19+
- Only delete state on successful task completion
20+
21+
### Changes
22+
23+
* **cancel:** `ralph cancel` now preserves state for resume instead of deleting
24+
* **loop-runner:** enhanced cleanup function to preserve state with iteration/feedback
25+
* **schema:** added `isResume` and `resumeFromIteration` fields to LoopConfig
26+
27+
### Documentation
28+
29+
* **resume:** added comprehensive RESUME_IMPLEMENTATION.md guide
30+
* **readme:** updated with resume command documentation and state preservation notes
31+
332
## 1.0.0 (2026-01-12)
433

534

README.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ The agent will:
241241
3. Continue until it outputs `<promise>COMPLETE</promise>`
242242
4. Or stop when max iterations is reached
243243

244+
If the loop is interrupted or you want to continue after it stops:
245+
246+
```bash
247+
# Resume the loop with context about previous work
248+
ralph resume
249+
```
250+
244251
## Commands
245252

246253
### `ralph init`
@@ -288,14 +295,68 @@ ralph loop "Add input validation" -p "VALIDATION COMPLETE" -n 3 -m 20
288295
ralph loop "Build feature X" -a ./my-agent.json -m 15
289296
```
290297

298+
### `ralph resume`
299+
300+
Resume a stopped Ralph loop with context about previous work.
301+
302+
```bash
303+
ralph resume [OPTIONS]
304+
```
305+
306+
**Options:**
307+
- `--min-iterations, -n` - Override minimum iterations before accepting completion
308+
- `--max-iterations, -m` - Override max iterations before auto-stop
309+
- `--completion-promise, -p` - Override phrase that signals completion
310+
- `--agent, -a` - Override agent name
311+
312+
**How it works:**
313+
314+
When you resume a stopped loop, Ralph will:
315+
1. Read the existing state file (`.kiro/ralph-loop.local.json`)
316+
2. Extract information about previous work and feedback
317+
3. Build an enhanced prompt with resume context that instructs the agent to:
318+
- Review files that were created or modified
319+
- Check git history to see what was accomplished
320+
- Review previous feedback (quality score, next steps, improvements, blockers)
321+
4. Continue the loop from where it left off
322+
323+
**Examples:**
324+
325+
```bash
326+
# Resume with existing settings
327+
ralph resume
328+
329+
# Resume but increase max iterations
330+
ralph resume -m 30
331+
332+
# Resume with a different completion promise
333+
ralph resume -p "FINISHED"
334+
335+
# Resume with all settings overridden
336+
ralph resume -n 5 -m 25 -p "DONE"
337+
```
338+
339+
**Use cases:**
340+
- Loop was interrupted (Ctrl+C)
341+
- Hit max iterations but want to continue
342+
- Need to adjust iteration limits
343+
- Want the agent to re-evaluate with fresh context
344+
291345
### `ralph cancel`
292346

293-
Cancel an active Ralph loop.
347+
Cancel an active Ralph loop and preserve state for resume.
294348

295349
```bash
296350
ralph cancel
297351
```
298352

353+
**What it does:**
354+
- Marks the loop as inactive (doesn't delete the state)
355+
- Preserves current iteration and feedback
356+
- Allows you to resume later with `ralph resume`
357+
358+
**Note:** When a loop completes successfully (outputs the completion promise), the state is deleted automatically. When interrupted (Ctrl+C) or max iterations reached, the state is preserved for resume.
359+
299360
## How It Works
300361

301362
### 1. State File
@@ -555,6 +616,7 @@ Ralph embodies several key principles:
555616

556617
## Learn More
557618

619+
- [Resume Feature Implementation Guide](RESUME_IMPLEMENTATION.md) - Complete documentation on the resume feature and state preservation
558620
- [Original technique by Geoffrey Huntley](https://ghuntley.com/ralph/)
559621
- [Ralph Orchestrator](https://github.com/mikeyobrien/ralph-orchestrator)
560622
- [Kiro CLI Documentation](https://kiro.dev/docs/cli/)

src/commands/cancel.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import { unlink } from "node:fs/promises";
77
import { log } from "@clack/prompts";
88

9-
import { stateFromJson } from "../schemas/state";
9+
import { type LoopState, stateFromJson } from "../schemas/state";
1010
import { SESSION_FILE, STATE_FILE } from "../utils/paths";
1111

1212
/**
1313
* Cancels an active Ralph Wiggum loop.
14-
* Removes the state file and session file from the .ralph directory.
14+
* Marks the state as inactive so it can be resumed later.
1515
* @returns Resolves when cancellation is complete
1616
*/
1717
export async function cancelCommand(): Promise<void> {
@@ -22,21 +22,29 @@ export async function cancelCommand(): Promise<void> {
2222
return;
2323
}
2424

25-
// Try to get iteration number for display
25+
// Read and parse the existing state
26+
let state: LoopState;
2627
let iteration: number | string = "?";
2728
try {
2829
const content = await stateFile.text();
29-
const state = stateFromJson(content);
30+
state = stateFromJson(content);
3031
iteration = state.iteration;
3132
} catch {
32-
// Ignore parse errors, just show "?"
33+
// If we can't parse the state, just delete it
34+
await unlink(STATE_FILE).catch(() => {});
35+
log.message("Cancelled Ralph loop (invalid state file)");
36+
return;
3337
}
3438

35-
// Delete state files
36-
await Promise.all([
37-
unlink(STATE_FILE).catch(() => {}),
38-
unlink(SESSION_FILE).catch(() => {}),
39-
]);
39+
// Mark state as inactive (preserves for resume)
40+
state.active = false;
41+
42+
// Write updated state back
43+
await Bun.write(STATE_FILE, JSON.stringify(state));
44+
45+
// Delete session file (not needed for resume)
46+
await unlink(SESSION_FILE).catch(() => {});
4047

41-
log.message(`Cancelled Ralph loop (was at iteration ${iteration})`);
48+
log.message(`Cancelled Ralph loop at iteration ${iteration}`);
49+
log.message("Run 'ralph resume' to continue where you left off.");
4250
}

src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
export { cancelCommand } from "./cancel";
88
export { initCommand } from "./init";
99
export { loopCommand } from "./loop";
10+
export { resumeCommand } from "./resume";

src/commands/resume.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* @fileoverview Resume command for Ralph Wiggum CLI.
3+
* Resumes a stopped loop with context about previous work.
4+
* @module commands/resume
5+
*/
6+
import { log } from "@clack/prompts";
7+
import pc from "picocolors";
8+
9+
import { runLoop } from "../core/loop-runner";
10+
import { LoopConfigSchema } from "../schemas/config";
11+
import { type LoopState, stateFromJson } from "../schemas/state";
12+
import { STATE_FILE } from "../utils/paths";
13+
14+
/**
15+
* CLI options for the resume command (raw string values from commander).
16+
*/
17+
interface ResumeOptions {
18+
/** Minimum iterations before checking completion (string from CLI) */
19+
minIterations?: string;
20+
/** Maximum iterations, 0 for unlimited (string from CLI) */
21+
maxIterations?: string;
22+
/** Phrase that signals loop completion */
23+
completionPromise?: string;
24+
/** Optional agent name override */
25+
agent?: string;
26+
}
27+
28+
/**
29+
* Resumes a stopped Ralph Wiggum loop.
30+
* Reads the existing state file, extracts information about previous work,
31+
* and continues the loop with enhanced context.
32+
* @param opts - Command options from CLI
33+
* @returns Resolves when the loop completes or is interrupted
34+
* @throws Exits process with code 1 if validation fails or no state file found
35+
*/
36+
export async function resumeCommand(opts: ResumeOptions): Promise<void> {
37+
const stateFile = Bun.file(STATE_FILE);
38+
39+
// Check if state file exists
40+
if (!(await stateFile.exists())) {
41+
log.error(
42+
pc.red(
43+
"No stopped Ralph loop found. Run 'ralph loop' to start a new loop.",
44+
),
45+
);
46+
process.exit(1);
47+
}
48+
49+
// Read and parse the existing state
50+
let existingState: LoopState;
51+
try {
52+
const content = await stateFile.text();
53+
existingState = stateFromJson(content);
54+
} catch (error) {
55+
log.error(pc.red(`Failed to parse state file: ${error}`));
56+
process.exit(1);
57+
}
58+
59+
// Display information about the previous loop
60+
log.info(pc.bold(pc.blue("Resuming Ralph loop")));
61+
log.message(` Previous iteration: ${existingState.iteration}`);
62+
log.message(` Original prompt: ${pc.dim(existingState.prompt)}`);
63+
if (existingState.previousFeedback?.qualityScore) {
64+
log.message(
65+
` Last quality score: ${existingState.previousFeedback.qualityScore}/10`,
66+
);
67+
}
68+
console.log();
69+
70+
// Build enhanced prompt with resume context
71+
const resumeContext = buildResumeContext(existingState);
72+
const enhancedPrompt = `${resumeContext}\n\nOriginal task: ${existingState.prompt}`;
73+
74+
// Parse and validate options with Zod
75+
// Use existing state values as defaults if not provided
76+
const result = LoopConfigSchema.safeParse({
77+
prompt: enhancedPrompt,
78+
minIterations: opts.minIterations
79+
? Number.parseInt(opts.minIterations, 10)
80+
: existingState.minIterations,
81+
maxIterations: opts.maxIterations
82+
? Number.parseInt(opts.maxIterations, 10)
83+
: existingState.maxIterations,
84+
completionPromise:
85+
opts.completionPromise ?? existingState.completionPromise,
86+
agentName: opts.agent ?? null,
87+
isResume: true,
88+
resumeFromIteration: existingState.iteration,
89+
});
90+
91+
if (!result.success) {
92+
// Format Zod error messages
93+
const errorMessages = result.error.issues
94+
.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`)
95+
.join("\n");
96+
log.error(pc.red(`Validation error:\n${errorMessages}`));
97+
process.exit(1);
98+
}
99+
100+
await runLoop(result.data);
101+
}
102+
103+
/**
104+
* Builds context text about the previous loop for the resume prompt.
105+
* @param state - The existing loop state
106+
* @returns Formatted context string
107+
*/
108+
function buildResumeContext(state: ReturnType<typeof stateFromJson>): string {
109+
const lines = [
110+
"RESUME CONTEXT:",
111+
"===============",
112+
`You are resuming a Ralph loop that was stopped at iteration ${state.iteration}.`,
113+
"",
114+
"Before continuing, please:",
115+
"1. Review what was accomplished in previous iterations by checking:",
116+
" - Files that were created or modified",
117+
" - Git history (git log, git diff)",
118+
" - Test results",
119+
" - Build artifacts",
120+
"",
121+
"2. Review the previous feedback to understand where you left off:",
122+
];
123+
124+
if (state.previousFeedback) {
125+
if (state.previousFeedback.qualitySummary) {
126+
lines.push(` Quality: ${state.previousFeedback.qualitySummary}`);
127+
}
128+
129+
if (
130+
state.previousFeedback.nextSteps &&
131+
state.previousFeedback.nextSteps.length > 0
132+
) {
133+
lines.push(" Planned next steps:");
134+
for (const step of state.previousFeedback.nextSteps) {
135+
lines.push(` - ${step}`);
136+
}
137+
}
138+
139+
if (
140+
state.previousFeedback.improvements &&
141+
state.previousFeedback.improvements.length > 0
142+
) {
143+
lines.push(" Areas for improvement:");
144+
for (const improvement of state.previousFeedback.improvements) {
145+
lines.push(` - ${improvement}`);
146+
}
147+
}
148+
149+
if (
150+
state.previousFeedback.blockers &&
151+
state.previousFeedback.blockers.length > 0
152+
) {
153+
lines.push(" Blockers:");
154+
for (const blocker of state.previousFeedback.blockers) {
155+
lines.push(` - ${blocker}`);
156+
}
157+
}
158+
159+
if (
160+
state.previousFeedback.ideas &&
161+
state.previousFeedback.ideas.length > 0
162+
) {
163+
lines.push(" Ideas to consider:");
164+
for (const idea of state.previousFeedback.ideas) {
165+
lines.push(` - ${idea}`);
166+
}
167+
}
168+
} else {
169+
lines.push(" (No previous feedback available)");
170+
}
171+
172+
lines.push("");
173+
lines.push(
174+
"3. Continue working toward completion of the original task below.",
175+
);
176+
lines.push("");
177+
178+
return lines.join("\n");
179+
}

0 commit comments

Comments
 (0)