@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 +1 -1
- package/src/commands/ralph/plan/add.ts +7 -15
- package/src/commands/ralph/plan/done.ts +1 -1
- package/src/commands/ralph/plan/list.ts +6 -5
- package/src/commands/ralph/plan/remove.ts +1 -1
- package/src/commands/ralph/run.test.ts +67 -53
- package/src/commands/ralph/run.ts +19 -1
- package/src/lib/ralph/formatter.ts +17 -11
- package/src/lib/ralph/state.ts +36 -3
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
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
|
|
47
|
+
const planFilePath = resolve(flags.file);
|
|
48
48
|
|
|
49
|
-
if (!existsSync(
|
|
50
|
-
this.error(`Plan file not found: ${
|
|
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,
|
|
59
|
+
const newPlan = addPlanToState(state, planFilePath);
|
|
66
60
|
saveState(state, stateFile);
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
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
|
|
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
|
|
85
|
-
|
|
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
|
|
94
|
-
this.log(pc.dim(` ✓ #${plan.id} ${
|
|
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.
|
|
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
|
|
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].
|
|
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
|
-
|
|
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({
|
|
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
|
|
109
|
-
const prompt = buildIterationPrompt({
|
|
110
|
-
|
|
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({
|
|
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({
|
|
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
|
|
283
|
+
const plan = addPlanToState(state, "/tmp/implement-feature.md");
|
|
267
284
|
|
|
268
285
|
expect(plan.id).toBe(1);
|
|
269
|
-
expect(plan.
|
|
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
|
|
279
|
-
const plan2 = addPlanToState(state, "plan
|
|
280
|
-
const plan3 = addPlanToState(state, "plan
|
|
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
|
-
|
|
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
|
|
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,
|
|
314
|
-
{ id: 2,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
351
|
-
{ id: 2,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
387
|
-
addPlanToState(state, "plan
|
|
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
|
|
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
|
|
422
|
+
expect(formatted).toContain('P1["#1: done-plan.md"]:::done');
|
|
406
423
|
});
|
|
407
424
|
|
|
408
|
-
it("should truncate long
|
|
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
|
|
418
|
-
expect(formatted).toContain('P1["#1:
|
|
419
|
-
// But the Plans section should have full
|
|
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**
|
|
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
|
|
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
|
|
454
|
+
const plan1 = addPlanToState(state, "/tmp/done-plan.md");
|
|
441
455
|
plan1.status = "done";
|
|
442
|
-
addPlanToState(state, "ready
|
|
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
|
|
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].
|
|
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
|
|
473
|
-
addPlanToState(state, "plan
|
|
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
|
|
490
|
-
addPlanToState(state, "plan
|
|
491
|
-
addPlanToState(state, "plan
|
|
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?.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
// Escape quotes in
|
|
115
|
-
const
|
|
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}: ${
|
|
122
|
+
lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::done`);
|
|
120
123
|
} else {
|
|
121
|
-
lines.push(` ${nodeId}["#${p.id}: ${
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
${planContent}
|
|
217
223
|
</plan>
|
|
218
224
|
|
|
219
225
|
<instructions>
|
package/src/lib/ralph/state.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
+
}
|