@towles/tool 0.0.49 → 0.0.50

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": "@towles/tool",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "CLI tool with autonomous task runner (ralph), observability, and quality-of-life commands for daily development.",
5
5
  "keywords": [
6
6
  "autonomic",
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { Flags } from "@oclif/core";
4
4
  import consola from "consola";
@@ -44,16 +44,10 @@ export default class PlanAdd extends BaseCommand {
44
44
  const ralphSettings = this.settings.settings.ralphSettings;
45
45
  const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
46
46
 
47
- const filePath = resolve(flags.file);
47
+ const planFilePath = resolve(flags.file);
48
48
 
49
- if (!existsSync(filePath)) {
50
- this.error(`Plan file not found: ${filePath}`);
51
- }
52
-
53
- const description = readFileSync(filePath, "utf-8").trim();
54
-
55
- if (!description || description.length < 3) {
56
- this.error("Plan file is empty or too short (min 3 chars)");
49
+ if (!existsSync(planFilePath)) {
50
+ this.error(`Plan file not found: ${planFilePath}`);
57
51
  }
58
52
 
59
53
  let state = loadState(stateFile);
@@ -62,13 +56,11 @@ export default class PlanAdd extends BaseCommand {
62
56
  state = createInitialState();
63
57
  }
64
58
 
65
- const newPlan = addPlanToState(state, description);
59
+ const newPlan = addPlanToState(state, planFilePath);
66
60
  saveState(state, stateFile);
67
61
 
68
- // Show truncated description for display
69
- const displayDesc = description.length > 80 ? `${description.slice(0, 80)}...` : description;
70
- consola.log(colors.green(`✓ Added plan #${newPlan.id} from ${flags.file}`));
71
- consola.log(colors.dim(` ${displayDesc.split("\n")[0]}`));
62
+ consola.log(colors.green(`✓ Added plan #${newPlan.id}`));
63
+ consola.log(colors.dim(` File: ${planFilePath}`));
72
64
  consola.log(colors.dim(`State saved to: ${stateFile}`));
73
65
  consola.log(colors.dim(`Total plans: ${state.plans.length}`));
74
66
  }
@@ -70,7 +70,7 @@ export default class PlanDone extends BaseCommand {
70
70
  plan.completedAt = new Date().toISOString();
71
71
  saveState(state, stateFile);
72
72
 
73
- consola.log(colors.green(`✓ Marked plan #${planId} as done: ${plan.description}`));
73
+ consola.log(colors.green(`✓ Marked plan #${planId} as done: ${plan.planFilePath}`));
74
74
 
75
75
  const remaining = state.plans.filter((p) => p.status !== "done").length;
76
76
  if (remaining === 0) {
@@ -75,14 +75,15 @@ export default class PlanList extends BaseCommand {
75
75
 
76
76
  // Show ready plans first (these are actionable)
77
77
  // Reserve ~10 chars for " ○ #XX " prefix
78
- const descWidth = Math.max(40, termWidth - 12);
78
+ const pathWidth = Math.max(40, termWidth - 12);
79
79
 
80
80
  if (ready.length > 0) {
81
81
  for (const plan of ready) {
82
82
  const icon = pc.dim("○");
83
83
  const id = pc.cyan(`#${plan.id}`);
84
- const desc = truncate(plan.description, descWidth);
85
- this.log(` ${icon} ${id} ${desc}`);
84
+ const filePath = truncate(plan.planFilePath, pathWidth);
85
+ const errorSuffix = plan.error ? pc.red(` ${truncate(plan.error, 30)}`) : "";
86
+ this.log(` ${icon} ${id} ${filePath}${errorSuffix}`);
86
87
  }
87
88
  }
88
89
 
@@ -90,8 +91,8 @@ export default class PlanList extends BaseCommand {
90
91
  if (done.length > 0) {
91
92
  this.log(pc.dim(` ─── ${done.length} completed ───`));
92
93
  for (const plan of done) {
93
- const desc = truncate(plan.description, descWidth - 5);
94
- this.log(pc.dim(` ✓ #${plan.id} ${desc}`));
94
+ const filePath = truncate(plan.planFilePath, pathWidth - 5);
95
+ this.log(pc.dim(` ✓ #${plan.id} ${filePath}`));
95
96
  }
96
97
  }
97
98
  this.log();
@@ -65,7 +65,7 @@ export default class PlanRemove extends BaseCommand {
65
65
  state.plans.splice(planIndex, 1);
66
66
  saveState(state, stateFile);
67
67
 
68
- consola.log(colors.green(`✓ Removed plan #${planId}: ${removedPlan.description}`));
68
+ consola.log(colors.green(`✓ Removed plan #${planId}: ${removedPlan.planFilePath}`));
69
69
  consola.log(colors.dim(`Remaining plans: ${state.plans.length}`));
70
70
  }
71
71
  }
@@ -70,14 +70,14 @@ describe("ralph-loop", () => {
70
70
  describe("saveState and loadState", () => {
71
71
  it("should save and load state correctly", () => {
72
72
  const state = createInitialState();
73
- addPlanToState(state, "test plan");
73
+ addPlanToState(state, "/tmp/test-plan.md");
74
74
 
75
75
  saveState(state, testStateFile);
76
76
  const loaded = loadState(testStateFile);
77
77
 
78
78
  expect(loaded).not.toBeNull();
79
79
  expect(loaded?.plans).toHaveLength(1);
80
- expect(loaded?.plans[0].description).toBe("test plan");
80
+ expect(loaded?.plans[0].planFilePath).toBe("/tmp/test-plan.md");
81
81
  });
82
82
 
83
83
  it("should return null for non-existent file", () => {
@@ -95,28 +95,45 @@ describe("ralph-loop", () => {
95
95
  describe("buildIterationPrompt", () => {
96
96
  const testPlan: RalphPlan = {
97
97
  id: 1,
98
- description: "First plan",
98
+ planFilePath: "/tmp/first-plan.md",
99
99
  status: "ready",
100
100
  addedAt: new Date().toISOString(),
101
101
  };
102
+ const testPlanContent = "First plan content";
102
103
 
103
104
  it("should include completion marker", () => {
104
- const prompt = buildIterationPrompt({ completionMarker: "RALPH_DONE", plan: testPlan });
105
+ const prompt = buildIterationPrompt({
106
+ completionMarker: "RALPH_DONE",
107
+ plan: testPlan,
108
+ planContent: testPlanContent,
109
+ });
105
110
  expect(prompt).toContain("RALPH_DONE");
106
111
  });
107
112
 
108
- it("should include plan id and description", () => {
109
- const prompt = buildIterationPrompt({ completionMarker: "RALPH_DONE", plan: testPlan });
110
- expect(prompt).toContain("#1: First plan");
113
+ it("should include plan content", () => {
114
+ const prompt = buildIterationPrompt({
115
+ completionMarker: "RALPH_DONE",
116
+ plan: testPlan,
117
+ planContent: testPlanContent,
118
+ });
119
+ expect(prompt).toContain("First plan content");
111
120
  });
112
121
 
113
122
  it("should include mark done command with plan id", () => {
114
- const prompt = buildIterationPrompt({ completionMarker: "RALPH_DONE", plan: testPlan });
123
+ const prompt = buildIterationPrompt({
124
+ completionMarker: "RALPH_DONE",
125
+ plan: testPlan,
126
+ planContent: testPlanContent,
127
+ });
115
128
  expect(prompt).toContain("tt ralph plan done 1");
116
129
  });
117
130
 
118
131
  it("should include custom completion marker", () => {
119
- const prompt = buildIterationPrompt({ completionMarker: "CUSTOM_MARKER", plan: testPlan });
132
+ const prompt = buildIterationPrompt({
133
+ completionMarker: "CUSTOM_MARKER",
134
+ plan: testPlan,
135
+ planContent: testPlanContent,
136
+ });
120
137
  expect(prompt).toContain("CUSTOM_MARKER");
121
138
  });
122
139
  });
@@ -263,10 +280,10 @@ describe("ralph-loop", () => {
263
280
  describe("addPlanToState", () => {
264
281
  it("should add plan with correct structure", () => {
265
282
  const state = createInitialState();
266
- const plan = addPlanToState(state, "implement feature");
283
+ const plan = addPlanToState(state, "/tmp/implement-feature.md");
267
284
 
268
285
  expect(plan.id).toBe(1);
269
- expect(plan.description).toBe("implement feature");
286
+ expect(plan.planFilePath).toBe("/tmp/implement-feature.md");
270
287
  expect(plan.status).toBe("ready");
271
288
  expect(plan.addedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
272
289
  expect(plan.completedAt).toBeUndefined();
@@ -275,9 +292,9 @@ describe("ralph-loop", () => {
275
292
  it("should increment plan IDs", () => {
276
293
  const state = createInitialState();
277
294
 
278
- const plan1 = addPlanToState(state, "plan 1");
279
- const plan2 = addPlanToState(state, "plan 2");
280
- const plan3 = addPlanToState(state, "plan 3");
295
+ const plan1 = addPlanToState(state, "/tmp/plan-1.md");
296
+ const plan2 = addPlanToState(state, "/tmp/plan-2.md");
297
+ const plan3 = addPlanToState(state, "/tmp/plan-3.md");
281
298
 
282
299
  expect(plan1.id).toBe(1);
283
300
  expect(plan2.id).toBe(2);
@@ -291,12 +308,12 @@ describe("ralph-loop", () => {
291
308
  // Simulate deleted plan by adding with gap
292
309
  state.plans.push({
293
310
  id: 5,
294
- description: "existing plan",
311
+ planFilePath: "/tmp/existing-plan.md",
295
312
  status: "done",
296
313
  addedAt: new Date().toISOString(),
297
314
  });
298
315
 
299
- const newPlan = addPlanToState(state, "new plan");
316
+ const newPlan = addPlanToState(state, "/tmp/new-plan.md");
300
317
  expect(newPlan.id).toBe(6);
301
318
  });
302
319
  });
@@ -310,8 +327,8 @@ describe("ralph-loop", () => {
310
327
 
311
328
  it("should include summary counts", () => {
312
329
  const plans: RalphPlan[] = [
313
- { id: 1, description: "plan 1", status: "done", addedAt: "" },
314
- { id: 2, description: "plan 2", status: "ready", addedAt: "" },
330
+ { id: 1, planFilePath: "/tmp/plan-1.md", status: "done", addedAt: "" },
331
+ { id: 2, planFilePath: "/tmp/plan-2.md", status: "ready", addedAt: "" },
315
332
  ];
316
333
 
317
334
  const formatted = formatPlansAsMarkdown(plans);
@@ -323,32 +340,32 @@ describe("ralph-loop", () => {
323
340
 
324
341
  it("should format done plans with checked boxes", () => {
325
342
  const plans: RalphPlan[] = [
326
- { id: 1, description: "completed plan", status: "done", addedAt: "" },
343
+ { id: 1, planFilePath: "/tmp/completed-plan.md", status: "done", addedAt: "" },
327
344
  ];
328
345
 
329
346
  const formatted = formatPlansAsMarkdown(plans);
330
347
 
331
348
  expect(formatted).toContain("## Done");
332
- expect(formatted).toContain("- [x] **#1** completed plan");
349
+ expect(formatted).toContain("- [x] **#1** /tmp/completed-plan.md");
333
350
  expect(formatted).toContain("`✓ done`");
334
351
  });
335
352
 
336
353
  it("should format ready plans with unchecked boxes", () => {
337
354
  const plans: RalphPlan[] = [
338
- { id: 2, description: "ready plan", status: "ready", addedAt: "" },
355
+ { id: 2, planFilePath: "/tmp/ready-plan.md", status: "ready", addedAt: "" },
339
356
  ];
340
357
 
341
358
  const formatted = formatPlansAsMarkdown(plans);
342
359
 
343
360
  expect(formatted).toContain("## Ready");
344
- expect(formatted).toContain("- [ ] **#2** ready plan");
361
+ expect(formatted).toContain("- [ ] **#2** /tmp/ready-plan.md");
345
362
  expect(formatted).toContain("`○ ready`");
346
363
  });
347
364
 
348
365
  it("should group plans by status section", () => {
349
366
  const plans: RalphPlan[] = [
350
- { id: 1, description: "done plan", status: "done", addedAt: "" },
351
- { id: 2, description: "ready plan", status: "ready", addedAt: "" },
367
+ { id: 1, planFilePath: "/tmp/done-plan.md", status: "done", addedAt: "" },
368
+ { id: 2, planFilePath: "/tmp/ready-plan.md", status: "ready", addedAt: "" },
352
369
  ];
353
370
 
354
371
  const formatted = formatPlansAsMarkdown(plans);
@@ -361,7 +378,7 @@ describe("ralph-loop", () => {
361
378
  describe("formatPlanAsMarkdown", () => {
362
379
  it("should include plan header and summary", () => {
363
380
  const state = createInitialState();
364
- addPlanToState(state, "plan 1");
381
+ addPlanToState(state, "/tmp/plan-1.md");
365
382
 
366
383
  const formatted = formatPlanAsMarkdown(state.plans, state);
367
384
 
@@ -373,18 +390,18 @@ describe("ralph-loop", () => {
373
390
 
374
391
  it("should include plans section", () => {
375
392
  const state = createInitialState();
376
- addPlanToState(state, "implement feature");
393
+ addPlanToState(state, "/tmp/implement-feature.md");
377
394
 
378
395
  const formatted = formatPlanAsMarkdown(state.plans, state);
379
396
 
380
397
  expect(formatted).toContain("## Plans");
381
- expect(formatted).toContain("**#1** implement feature");
398
+ expect(formatted).toContain("**#1** /tmp/implement-feature.md");
382
399
  });
383
400
 
384
401
  it("should include mermaid graph section", () => {
385
402
  const state = createInitialState();
386
- addPlanToState(state, "plan 1");
387
- addPlanToState(state, "plan 2");
403
+ addPlanToState(state, "/tmp/plan-1.md");
404
+ addPlanToState(state, "/tmp/plan-2.md");
388
405
 
389
406
  const formatted = formatPlanAsMarkdown(state.plans, state);
390
407
 
@@ -397,28 +414,25 @@ describe("ralph-loop", () => {
397
414
 
398
415
  it("should format done plans correctly in mermaid", () => {
399
416
  const state = createInitialState();
400
- const plan = addPlanToState(state, "done plan");
417
+ const plan = addPlanToState(state, "/tmp/done-plan.md");
401
418
  plan.status = "done";
402
419
 
403
420
  const formatted = formatPlanAsMarkdown(state.plans, state);
404
421
 
405
- expect(formatted).toContain('P1["#1: done plan"]:::done');
422
+ expect(formatted).toContain('P1["#1: done-plan.md"]:::done');
406
423
  });
407
424
 
408
- it("should truncate long descriptions in mermaid", () => {
425
+ it("should truncate long filenames in mermaid", () => {
409
426
  const state = createInitialState();
410
- addPlanToState(
411
- state,
412
- "This is a very long plan description that should be truncated for the mermaid graph",
413
- );
427
+ addPlanToState(state, "/tmp/this-is-a-very-long-plan-filename-that-should-be-truncated.md");
414
428
 
415
429
  const formatted = formatPlanAsMarkdown(state.plans, state);
416
430
 
417
- // Mermaid section should have truncated description
418
- expect(formatted).toContain('P1["#1: This is a very long plan de..."]');
419
- // But the Plans section should have full description
431
+ // Mermaid section should have truncated filename
432
+ expect(formatted).toContain('P1["#1: this-is-a-very-long-plan-fi..."]');
433
+ // But the Plans section should have full path
420
434
  expect(formatted).toContain(
421
- "**#1** This is a very long plan description that should be truncated for the mermaid graph",
435
+ "**#1** /tmp/this-is-a-very-long-plan-filename-that-should-be-truncated.md",
422
436
  );
423
437
  });
424
438
  });
@@ -426,7 +440,7 @@ describe("ralph-loop", () => {
426
440
  describe("formatPlanAsJson", () => {
427
441
  it("should return valid JSON", () => {
428
442
  const state = createInitialState();
429
- addPlanToState(state, "plan 1");
443
+ addPlanToState(state, "/tmp/plan-1.md");
430
444
 
431
445
  const json = formatPlanAsJson(state.plans, state);
432
446
  const parsed = JSON.parse(json);
@@ -437,9 +451,9 @@ describe("ralph-loop", () => {
437
451
 
438
452
  it("should include summary counts", () => {
439
453
  const state = createInitialState();
440
- const plan1 = addPlanToState(state, "done plan");
454
+ const plan1 = addPlanToState(state, "/tmp/done-plan.md");
441
455
  plan1.status = "done";
442
- addPlanToState(state, "ready plan");
456
+ addPlanToState(state, "/tmp/ready-plan.md");
443
457
 
444
458
  const json = formatPlanAsJson(state.plans, state);
445
459
  const parsed = JSON.parse(json);
@@ -451,7 +465,7 @@ describe("ralph-loop", () => {
451
465
 
452
466
  it("should include all plan fields", () => {
453
467
  const state = createInitialState();
454
- const plan = addPlanToState(state, "test plan");
468
+ const plan = addPlanToState(state, "/tmp/test-plan.md");
455
469
  plan.status = "done";
456
470
  plan.completedAt = "2026-01-10T12:00:00Z";
457
471
 
@@ -459,7 +473,7 @@ describe("ralph-loop", () => {
459
473
  const parsed = JSON.parse(json);
460
474
 
461
475
  expect(parsed.plans[0].id).toBe(1);
462
- expect(parsed.plans[0].description).toBe("test plan");
476
+ expect(parsed.plans[0].planFilePath).toBe("/tmp/test-plan.md");
463
477
  expect(parsed.plans[0].status).toBe("done");
464
478
  expect(parsed.plans[0].addedAt).toBeDefined();
465
479
  expect(parsed.plans[0].completedAt).toBe("2026-01-10T12:00:00Z");
@@ -469,8 +483,8 @@ describe("ralph-loop", () => {
469
483
  describe("markDone functionality", () => {
470
484
  it("should mark plan as done and add completedAt", () => {
471
485
  const state = createInitialState();
472
- addPlanToState(state, "plan 1");
473
- addPlanToState(state, "plan 2");
486
+ addPlanToState(state, "/tmp/plan-1.md");
487
+ addPlanToState(state, "/tmp/plan-2.md");
474
488
 
475
489
  // Simulate marking plan 1 as done
476
490
  const plan = state.plans.find((t) => t.id === 1);
@@ -486,18 +500,18 @@ describe("ralph-loop", () => {
486
500
 
487
501
  it("should find plan by ID", () => {
488
502
  const state = createInitialState();
489
- addPlanToState(state, "plan 1");
490
- addPlanToState(state, "plan 2");
491
- addPlanToState(state, "plan 3");
503
+ addPlanToState(state, "/tmp/plan-1.md");
504
+ addPlanToState(state, "/tmp/plan-2.md");
505
+ addPlanToState(state, "/tmp/plan-3.md");
492
506
 
493
507
  const plan = state.plans.find((p) => p.id === 2);
494
508
  expect(plan).toBeDefined();
495
- expect(plan?.description).toBe("plan 2");
509
+ expect(plan?.planFilePath).toBe("/tmp/plan-2.md");
496
510
  });
497
511
 
498
512
  it("should return undefined for non-existent plan ID", () => {
499
513
  const state = createInitialState();
500
- addPlanToState(state, "plan 1");
514
+ addPlanToState(state, "/tmp/plan-1.md");
501
515
 
502
516
  const plan = state.plans.find((t) => t.id === 99);
503
517
  expect(plan).toBeUndefined();
@@ -505,7 +519,7 @@ describe("ralph-loop", () => {
505
519
 
506
520
  it("should persist marked-done plan to file", () => {
507
521
  const state = createInitialState();
508
- addPlanToState(state, "plan 1");
522
+ addPlanToState(state, "/tmp/plan-1.md");
509
523
 
510
524
  const plan = state.plans.find((t) => t.id === 1)!;
511
525
  plan.status = "done";
@@ -14,6 +14,7 @@ import {
14
14
  appendHistory,
15
15
  resolveRalphPath,
16
16
  getRalphPaths,
17
+ readPlanContent,
17
18
  } from "../../lib/ralph/state.js";
18
19
  import {
19
20
  buildIterationPrompt,
@@ -151,12 +152,19 @@ export default class Run extends BaseCommand {
151
152
  consola.log(` Remaining plans: ${remainingPlans.length}`);
152
153
 
153
154
  consola.log(colors.cyan("\nCurrent plan:"));
154
- consola.log(` #${currentPlan.id}: ${currentPlan.description}`);
155
+ consola.log(` #${currentPlan.id}: ${currentPlan.planFilePath}`);
156
+
157
+ // Read plan content
158
+ const planContent = readPlanContent(currentPlan, state, stateFile);
159
+ if (!planContent) {
160
+ this.error(`Cannot read plan file: ${currentPlan.planFilePath}`);
161
+ }
155
162
 
156
163
  // Show prompt preview
157
164
  const prompt = buildIterationPrompt({
158
165
  completionMarker: flags.completionMarker,
159
166
  plan: currentPlan,
167
+ planContent,
160
168
  skipCommit: !flags.autoCommit,
161
169
  });
162
170
  consola.log(colors.dim("─".repeat(60)));
@@ -239,9 +247,19 @@ export default class Run extends BaseCommand {
239
247
  logStream.write(`\n✅ All plans completed after ${iteration} iteration(s)\n`);
240
248
  break;
241
249
  }
250
+
251
+ // Read plan content
252
+ const planContent = readPlanContent(plan, state, stateFile);
253
+ if (!planContent) {
254
+ consola.log(colors.yellow(`⚠ Skipping plan #${plan.id}: cannot read file`));
255
+ logStream.write(`⚠ Skipping plan #${plan.id}: cannot read file\n`);
256
+ continue;
257
+ }
258
+
242
259
  const prompt = buildIterationPrompt({
243
260
  completionMarker: flags.completionMarker,
244
261
  plan: plan,
262
+ planContent,
245
263
  skipCommit: !flags.autoCommit,
246
264
  });
247
265
 
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import * as path from "node:path";
2
3
  import type { RalphPlan, PlanStatus, RalphState } from "./state.js";
3
4
 
4
5
  // ============================================================================
@@ -61,7 +62,8 @@ export function formatPlansAsMarkdown(plans: RalphPlan[]): string {
61
62
  if (ready.length > 0) {
62
63
  lines.push("## Ready", "");
63
64
  for (const p of ready) {
64
- lines.push(`- [ ] **#${p.id}** ${p.description} ${statusBadge(p.status)}`);
65
+ const errorSuffix = p.error ? ` ⚠️ ${p.error}` : "";
66
+ lines.push(`- [ ] **#${p.id}** ${p.planFilePath} ${statusBadge(p.status)}${errorSuffix}`);
65
67
  }
66
68
  lines.push("");
67
69
  }
@@ -69,7 +71,7 @@ export function formatPlansAsMarkdown(plans: RalphPlan[]): string {
69
71
  if (done.length > 0) {
70
72
  lines.push("## Done", "");
71
73
  for (const p of done) {
72
- lines.push(`- [x] **#${p.id}** ${p.description} ${statusBadge(p.status)}`);
74
+ lines.push(`- [x] **#${p.id}** ${p.planFilePath} ${statusBadge(p.status)}`);
73
75
  }
74
76
  lines.push("");
75
77
  }
@@ -98,7 +100,8 @@ export function formatPlanAsMarkdown(plans: RalphPlan[], state: RalphState): str
98
100
  for (const p of plans) {
99
101
  const checkbox = p.status === "done" ? "[x]" : "[ ]";
100
102
  const status = p.status === "done" ? "`done`" : "`ready`";
101
- lines.push(`- ${checkbox} **#${p.id}** ${p.description} ${status}`);
103
+ const errorSuffix = p.error ? ` ⚠️ ${p.error}` : "";
104
+ lines.push(`- ${checkbox} **#${p.id}** ${p.planFilePath} ${status}${errorSuffix}`);
102
105
  }
103
106
  lines.push("");
104
107
 
@@ -109,16 +112,16 @@ export function formatPlanAsMarkdown(plans: RalphPlan[], state: RalphState): str
109
112
  lines.push(` subgraph Progress["Plans: ${done}/${plans.length} done"]`);
110
113
 
111
114
  for (const p of plans) {
112
- const shortDesc =
113
- p.description.length > 30 ? p.description.slice(0, 27) + "..." : p.description;
114
- // Escape quotes in descriptions
115
- const safeDesc = shortDesc.replace(/"/g, "'");
115
+ const filename = path.basename(p.planFilePath);
116
+ const shortName = filename.length > 30 ? filename.slice(0, 27) + "..." : filename;
117
+ // Escape quotes in filenames
118
+ const safeName = shortName.replace(/"/g, "'");
116
119
  const nodeId = `P${p.id}`;
117
120
 
118
121
  if (p.status === "done") {
119
- lines.push(` ${nodeId}["#${p.id}: ${safeDesc}"]:::done`);
122
+ lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::done`);
120
123
  } else {
121
- lines.push(` ${nodeId}["#${p.id}: ${safeDesc}"]:::ready`);
124
+ lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::ready`);
122
125
  }
123
126
  }
124
127
 
@@ -145,10 +148,11 @@ export function formatPlanAsJson(plans: RalphPlan[], state: RalphState): string
145
148
  },
146
149
  plans: plans.map((p) => ({
147
150
  id: p.id,
148
- description: p.description,
151
+ planFilePath: p.planFilePath,
149
152
  status: p.status,
150
153
  addedAt: p.addedAt,
151
154
  completedAt: p.completedAt,
155
+ error: p.error,
152
156
  })),
153
157
  },
154
158
  null,
@@ -201,19 +205,21 @@ export function extractOutputSummary(output: string, maxLength: number = 2000):
201
205
  export interface BuildPromptOptions {
202
206
  completionMarker: string;
203
207
  plan: RalphPlan;
208
+ planContent: string;
204
209
  skipCommit?: boolean;
205
210
  }
206
211
 
207
212
  export function buildIterationPrompt({
208
213
  completionMarker,
209
214
  plan,
215
+ planContent,
210
216
  skipCommit = false,
211
217
  }: BuildPromptOptions): string {
212
218
  let step = 1;
213
219
 
214
220
  const prompt = `
215
221
  <plan>
216
- #${plan.id}: ${plan.description}
222
+ ${planContent}
217
223
  </plan>
218
224
 
219
225
  <instructions>
@@ -67,10 +67,11 @@ const PlanStatusSchema = z.enum(["ready", "done", "blocked", "cancelled"]);
67
67
 
68
68
  const RalphPlanSchema = z.object({
69
69
  id: z.number(),
70
- description: z.string(),
70
+ planFilePath: z.string(),
71
71
  status: PlanStatusSchema,
72
72
  addedAt: z.string(),
73
73
  completedAt: z.string().optional(),
74
+ error: z.string().optional(),
74
75
  });
75
76
 
76
77
  const RalphStateSchema = z.object({
@@ -151,12 +152,12 @@ export function loadState(stateFile: string): RalphState | null {
151
152
  }
152
153
  }
153
154
 
154
- export function addPlanToState(state: RalphState, description: string): RalphPlan {
155
+ export function addPlanToState(state: RalphState, planFilePath: string): RalphPlan {
155
156
  const nextId = state.plans.length > 0 ? Math.max(...state.plans.map((p) => p.id)) + 1 : 1;
156
157
 
157
158
  const newPlan: RalphPlan = {
158
159
  id: nextId,
159
- description,
160
+ planFilePath,
160
161
  status: "ready",
161
162
  addedAt: new Date().toISOString(),
162
163
  };
@@ -164,3 +165,35 @@ export function addPlanToState(state: RalphState, description: string): RalphPla
164
165
  state.plans.push(newPlan);
165
166
  return newPlan;
166
167
  }
168
+
169
+ /**
170
+ * Read plan content from file. Updates plan error field and logs warning if file missing.
171
+ * @returns File content on success, null if file missing/error
172
+ */
173
+ export function readPlanContent(
174
+ plan: RalphPlan,
175
+ state: RalphState,
176
+ stateFile: string,
177
+ ): string | null {
178
+ try {
179
+ if (!fs.existsSync(plan.planFilePath)) {
180
+ const errorMsg = `Plan file not found: ${plan.planFilePath}`;
181
+ console.warn(pc.yellow(`Warning: ${errorMsg}`));
182
+ plan.error = errorMsg;
183
+ saveState(state, stateFile);
184
+ return null;
185
+ }
186
+ // Clear any previous error
187
+ if (plan.error) {
188
+ plan.error = undefined;
189
+ saveState(state, stateFile);
190
+ }
191
+ return fs.readFileSync(plan.planFilePath, "utf-8").trim();
192
+ } catch (err) {
193
+ const errorMsg = `Failed to read plan file: ${err}`;
194
+ console.warn(pc.yellow(`Warning: ${errorMsg}`));
195
+ plan.error = errorMsg;
196
+ saveState(state, stateFile);
197
+ return null;
198
+ }
199
+ }