@towles/tool 0.0.52 → 0.0.53
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
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { Flags } from "@oclif/core";
|
|
3
|
+
import { Args, Flags } from "@oclif/core";
|
|
4
4
|
import consola from "consola";
|
|
5
5
|
import { colors } from "consola/utils";
|
|
6
6
|
import { BaseCommand } from "../../base.js";
|
|
@@ -22,17 +22,19 @@ export default class PlanAdd extends BaseCommand {
|
|
|
22
22
|
static override examples = [
|
|
23
23
|
{
|
|
24
24
|
description: "Add a plan from a markdown file",
|
|
25
|
-
command: "<%= config.bin %> <%= command.id %>
|
|
25
|
+
command: "<%= config.bin %> <%= command.id %> docs/plans/2025-01-18-feature.md",
|
|
26
26
|
},
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
-
static override
|
|
30
|
-
|
|
31
|
-
file: Flags.string({
|
|
32
|
-
char: "f",
|
|
29
|
+
static override args = {
|
|
30
|
+
file: Args.string({
|
|
33
31
|
description: "Path to plan file (markdown)",
|
|
34
32
|
required: true,
|
|
35
33
|
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
static override flags = {
|
|
37
|
+
...BaseCommand.baseFlags,
|
|
36
38
|
stateFile: Flags.string({
|
|
37
39
|
char: "s",
|
|
38
40
|
description: `State file path (default: ${DEFAULT_STATE_FILE})`,
|
|
@@ -40,11 +42,11 @@ export default class PlanAdd extends BaseCommand {
|
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
async run(): Promise<void> {
|
|
43
|
-
const { flags } = await this.parse(PlanAdd);
|
|
45
|
+
const { args, flags } = await this.parse(PlanAdd);
|
|
44
46
|
const ralphSettings = this.settings.settings.ralphSettings;
|
|
45
47
|
const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
|
|
46
48
|
|
|
47
|
-
const planFilePath = resolve(
|
|
49
|
+
const planFilePath = resolve(args.file);
|
|
48
50
|
|
|
49
51
|
if (!existsSync(planFilePath)) {
|
|
50
52
|
this.error(`Plan file not found: ${planFilePath}`);
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
DEFAULT_STATE_FILE,
|
|
23
23
|
DEFAULT_HISTORY_FILE,
|
|
24
24
|
DEFAULT_COMPLETION_MARKER,
|
|
25
|
+
DEFAULT_TASK_DONE_MARKER,
|
|
25
26
|
CLAUDE_DEFAULT_ARGS,
|
|
26
27
|
} from "../../lib/ralph/index.js";
|
|
27
28
|
|
|
@@ -45,6 +46,7 @@ describe("ralph-loop", () => {
|
|
|
45
46
|
expect(DEFAULT_STATE_FILE).toBe("./.claude/.ralph/ralph-state.local.json");
|
|
46
47
|
expect(DEFAULT_HISTORY_FILE).toBe("./.claude/.ralph/ralph-history.local.log");
|
|
47
48
|
expect(DEFAULT_COMPLETION_MARKER).toBe("RALPH_DONE");
|
|
49
|
+
expect(DEFAULT_TASK_DONE_MARKER).toBe("TASK_DONE");
|
|
48
50
|
expect(CLAUDE_DEFAULT_ARGS).toEqual([
|
|
49
51
|
"--print",
|
|
50
52
|
"--verbose",
|
|
@@ -99,43 +101,81 @@ describe("ralph-loop", () => {
|
|
|
99
101
|
status: "ready",
|
|
100
102
|
addedAt: new Date().toISOString(),
|
|
101
103
|
};
|
|
102
|
-
const testPlanContent = "First plan content";
|
|
103
104
|
|
|
104
105
|
it("should include completion marker", () => {
|
|
105
106
|
const prompt = buildIterationPrompt({
|
|
106
107
|
completionMarker: "RALPH_DONE",
|
|
108
|
+
taskDoneMarker: "TASK_DONE",
|
|
107
109
|
plan: testPlan,
|
|
108
|
-
planContent: testPlanContent,
|
|
109
110
|
});
|
|
110
111
|
expect(prompt).toContain("RALPH_DONE");
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
it("should include plan
|
|
114
|
+
it("should include plan file path for reading", () => {
|
|
114
115
|
const prompt = buildIterationPrompt({
|
|
115
116
|
completionMarker: "RALPH_DONE",
|
|
117
|
+
taskDoneMarker: "TASK_DONE",
|
|
116
118
|
plan: testPlan,
|
|
117
|
-
planContent: testPlanContent,
|
|
118
119
|
});
|
|
119
|
-
expect(prompt).toContain("
|
|
120
|
+
expect(prompt).toContain("/tmp/first-plan.md");
|
|
121
|
+
expect(prompt).toContain("Read Plan:");
|
|
120
122
|
});
|
|
121
123
|
|
|
122
|
-
it("should include
|
|
124
|
+
it("should include instruction to update plan file", () => {
|
|
123
125
|
const prompt = buildIterationPrompt({
|
|
124
126
|
completionMarker: "RALPH_DONE",
|
|
127
|
+
taskDoneMarker: "TASK_DONE",
|
|
125
128
|
plan: testPlan,
|
|
126
|
-
planContent: testPlanContent,
|
|
127
129
|
});
|
|
128
|
-
expect(prompt).toContain("
|
|
130
|
+
expect(prompt).toContain("Update the plan");
|
|
131
|
+
expect(prompt).toContain("/tmp/first-plan.md");
|
|
129
132
|
});
|
|
130
133
|
|
|
131
134
|
it("should include custom completion marker", () => {
|
|
132
135
|
const prompt = buildIterationPrompt({
|
|
133
136
|
completionMarker: "CUSTOM_MARKER",
|
|
137
|
+
taskDoneMarker: "TASK_DONE",
|
|
134
138
|
plan: testPlan,
|
|
135
|
-
planContent: testPlanContent,
|
|
136
139
|
});
|
|
137
140
|
expect(prompt).toContain("CUSTOM_MARKER");
|
|
138
141
|
});
|
|
142
|
+
|
|
143
|
+
it("should include TASK_DONE for tasks remaining", () => {
|
|
144
|
+
const prompt = buildIterationPrompt({
|
|
145
|
+
completionMarker: "RALPH_DONE",
|
|
146
|
+
taskDoneMarker: "TASK_DONE",
|
|
147
|
+
plan: testPlan,
|
|
148
|
+
});
|
|
149
|
+
expect(prompt).toContain("TASK_DONE");
|
|
150
|
+
expect(prompt).toContain("tasks remain");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should include RALPH_DONE for plan complete", () => {
|
|
154
|
+
const prompt = buildIterationPrompt({
|
|
155
|
+
completionMarker: "RALPH_DONE",
|
|
156
|
+
taskDoneMarker: "TASK_DONE",
|
|
157
|
+
plan: testPlan,
|
|
158
|
+
});
|
|
159
|
+
expect(prompt).toContain("RALPH_DONE");
|
|
160
|
+
expect(prompt).toContain("plan is complete");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should skip commit step when skipCommit is true", () => {
|
|
164
|
+
const promptWithCommit = buildIterationPrompt({
|
|
165
|
+
completionMarker: "RALPH_DONE",
|
|
166
|
+
taskDoneMarker: "TASK_DONE",
|
|
167
|
+
plan: testPlan,
|
|
168
|
+
skipCommit: false,
|
|
169
|
+
});
|
|
170
|
+
const promptWithoutCommit = buildIterationPrompt({
|
|
171
|
+
completionMarker: "RALPH_DONE",
|
|
172
|
+
taskDoneMarker: "TASK_DONE",
|
|
173
|
+
plan: testPlan,
|
|
174
|
+
skipCommit: true,
|
|
175
|
+
});
|
|
176
|
+
expect(promptWithCommit).toContain("git commit");
|
|
177
|
+
expect(promptWithoutCommit).not.toContain("git commit");
|
|
178
|
+
});
|
|
139
179
|
});
|
|
140
180
|
|
|
141
181
|
describe("extractOutputSummary", () => {
|
|
@@ -200,6 +240,18 @@ describe("ralph-loop", () => {
|
|
|
200
240
|
it("should be case-sensitive", () => {
|
|
201
241
|
expect(detectCompletionMarker("ralph_done", "RALPH_DONE")).toBe(false);
|
|
202
242
|
});
|
|
243
|
+
|
|
244
|
+
it("should detect TASK_DONE marker", () => {
|
|
245
|
+
expect(detectCompletionMarker("finished <promise>TASK_DONE</promise>", "TASK_DONE")).toBe(
|
|
246
|
+
true,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should distinguish TASK_DONE from RALPH_DONE", () => {
|
|
251
|
+
const output = "<promise>TASK_DONE</promise>";
|
|
252
|
+
expect(detectCompletionMarker(output, "TASK_DONE")).toBe(true);
|
|
253
|
+
expect(detectCompletionMarker(output, "RALPH_DONE")).toBe(false);
|
|
254
|
+
});
|
|
203
255
|
});
|
|
204
256
|
|
|
205
257
|
describe("state transitions", () => {
|
|
@@ -275,6 +327,26 @@ describe("ralph-loop", () => {
|
|
|
275
327
|
expect(parsed2.outputSummary).toBe("second");
|
|
276
328
|
expect(parsed2.markerFound).toBe(true);
|
|
277
329
|
});
|
|
330
|
+
|
|
331
|
+
it("should include taskMarkerFound field", () => {
|
|
332
|
+
const history: IterationHistory = {
|
|
333
|
+
iteration: 1,
|
|
334
|
+
startedAt: "2026-01-19T10:00:00Z",
|
|
335
|
+
completedAt: "2026-01-19T10:01:00Z",
|
|
336
|
+
durationMs: 60000,
|
|
337
|
+
durationHuman: "1m 0s",
|
|
338
|
+
outputSummary: "test output",
|
|
339
|
+
markerFound: false,
|
|
340
|
+
taskMarkerFound: true,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
appendHistory(history, testHistoryFile);
|
|
344
|
+
|
|
345
|
+
const content = require("node:fs").readFileSync(testHistoryFile, "utf-8");
|
|
346
|
+
const parsed = JSON.parse(content.trim());
|
|
347
|
+
expect(parsed.taskMarkerFound).toBe(true);
|
|
348
|
+
expect(parsed.markerFound).toBe(false);
|
|
349
|
+
});
|
|
278
350
|
});
|
|
279
351
|
|
|
280
352
|
describe("addPlanToState", () => {
|
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
DEFAULT_LOG_FILE,
|
|
9
9
|
DEFAULT_MAX_ITERATIONS,
|
|
10
10
|
DEFAULT_COMPLETION_MARKER,
|
|
11
|
+
DEFAULT_TASK_DONE_MARKER,
|
|
11
12
|
CLAUDE_DEFAULT_ARGS,
|
|
12
13
|
loadState,
|
|
13
14
|
saveState,
|
|
14
15
|
appendHistory,
|
|
15
16
|
resolveRalphPath,
|
|
16
17
|
getRalphPaths,
|
|
17
|
-
readPlanContent,
|
|
18
18
|
} from "../../lib/ralph/state.js";
|
|
19
19
|
import {
|
|
20
20
|
buildIterationPrompt,
|
|
@@ -94,6 +94,10 @@ export default class Run extends BaseCommand {
|
|
|
94
94
|
description: "Completion marker",
|
|
95
95
|
default: DEFAULT_COMPLETION_MARKER,
|
|
96
96
|
}),
|
|
97
|
+
taskDoneMarker: Flags.string({
|
|
98
|
+
description: "Task done marker",
|
|
99
|
+
default: DEFAULT_TASK_DONE_MARKER,
|
|
100
|
+
}),
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
async run(): Promise<void> {
|
|
@@ -147,6 +151,7 @@ export default class Run extends BaseCommand {
|
|
|
147
151
|
consola.log(` State file: ${stateFile}`);
|
|
148
152
|
consola.log(` Log file: ${logFile}`);
|
|
149
153
|
consola.log(` Completion marker: ${flags.completionMarker}`);
|
|
154
|
+
consola.log(` Task done marker: ${flags.taskDoneMarker}`);
|
|
150
155
|
consola.log(` Auto-commit: ${flags.autoCommit}`);
|
|
151
156
|
consola.log(` Claude args: ${[...CLAUDE_DEFAULT_ARGS, ...extraClaudeArgs].join(" ")}`);
|
|
152
157
|
consola.log(` Remaining plans: ${remainingPlans.length}`);
|
|
@@ -154,17 +159,11 @@ export default class Run extends BaseCommand {
|
|
|
154
159
|
consola.log(colors.cyan("\nCurrent plan:"));
|
|
155
160
|
consola.log(` #${currentPlan.id}: ${currentPlan.planFilePath}`);
|
|
156
161
|
|
|
157
|
-
// Read plan content
|
|
158
|
-
const planContent = readPlanContent(currentPlan, state, stateFile);
|
|
159
|
-
if (!planContent) {
|
|
160
|
-
this.error(`Cannot read plan file: ${currentPlan.planFilePath}`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
162
|
// Show prompt preview
|
|
164
163
|
const prompt = buildIterationPrompt({
|
|
165
164
|
completionMarker: flags.completionMarker,
|
|
165
|
+
taskDoneMarker: flags.taskDoneMarker,
|
|
166
166
|
plan: currentPlan,
|
|
167
|
-
planContent,
|
|
168
167
|
skipCommit: !flags.autoCommit,
|
|
169
168
|
});
|
|
170
169
|
consola.log(colors.dim("─".repeat(60)));
|
|
@@ -248,18 +247,10 @@ export default class Run extends BaseCommand {
|
|
|
248
247
|
break;
|
|
249
248
|
}
|
|
250
249
|
|
|
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
|
-
|
|
259
250
|
const prompt = buildIterationPrompt({
|
|
260
251
|
completionMarker: flags.completionMarker,
|
|
252
|
+
taskDoneMarker: flags.taskDoneMarker,
|
|
261
253
|
plan: plan,
|
|
262
|
-
planContent,
|
|
263
254
|
skipCommit: !flags.autoCommit,
|
|
264
255
|
});
|
|
265
256
|
|
|
@@ -288,7 +279,8 @@ export default class Run extends BaseCommand {
|
|
|
288
279
|
}
|
|
289
280
|
|
|
290
281
|
const iterationEnd = new Date().toISOString();
|
|
291
|
-
const
|
|
282
|
+
const taskMarkerFound = detectCompletionMarker(iterResult.output, flags.taskDoneMarker);
|
|
283
|
+
const planMarkerFound = detectCompletionMarker(iterResult.output, flags.completionMarker);
|
|
292
284
|
|
|
293
285
|
// Calculate duration
|
|
294
286
|
const startTime = new Date(iterationStart).getTime();
|
|
@@ -305,7 +297,8 @@ export default class Run extends BaseCommand {
|
|
|
305
297
|
durationMs,
|
|
306
298
|
durationHuman,
|
|
307
299
|
outputSummary: extractOutputSummary(iterResult.output),
|
|
308
|
-
markerFound,
|
|
300
|
+
markerFound: planMarkerFound,
|
|
301
|
+
taskMarkerFound,
|
|
309
302
|
contextUsedPercent: iterResult.contextUsedPercent,
|
|
310
303
|
},
|
|
311
304
|
ralphPaths.historyFile,
|
|
@@ -314,29 +307,35 @@ export default class Run extends BaseCommand {
|
|
|
314
307
|
// Save state
|
|
315
308
|
saveState(state, stateFile);
|
|
316
309
|
|
|
310
|
+
// Log marker status
|
|
311
|
+
if (taskMarkerFound) {
|
|
312
|
+
consola.log(colors.cyan(`Task marker found - current plan done, checking for more plans`));
|
|
313
|
+
logStream.write(`Task marker found - continuing to next plan\n`);
|
|
314
|
+
}
|
|
315
|
+
|
|
317
316
|
// Log summary
|
|
318
317
|
const contextInfo =
|
|
319
318
|
iterResult.contextUsedPercent !== undefined
|
|
320
319
|
? ` | Context: ${iterResult.contextUsedPercent}%`
|
|
321
320
|
: "";
|
|
322
321
|
logStream.write(
|
|
323
|
-
`\n━━━ Iteration ${iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\
|
|
322
|
+
`\n━━━ Iteration ${iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nTask marker: ${taskMarkerFound ? "yes" : "no"}\nPlan marker: ${planMarkerFound ? "yes" : "no"}\n`,
|
|
324
323
|
);
|
|
325
324
|
consola.log(
|
|
326
325
|
colors.dim(
|
|
327
|
-
`Duration: ${durationHuman}${contextInfo} |
|
|
326
|
+
`Duration: ${durationHuman}${contextInfo} | Task: ${taskMarkerFound ? colors.green("yes") : colors.yellow("no")} | Plan: ${planMarkerFound ? colors.green("yes") : colors.yellow("no")}`,
|
|
328
327
|
),
|
|
329
328
|
);
|
|
330
329
|
|
|
331
|
-
// Check completion
|
|
332
|
-
if (
|
|
330
|
+
// Check completion (only when ALL plans done marker found)
|
|
331
|
+
if (planMarkerFound) {
|
|
333
332
|
completed = true;
|
|
334
333
|
state.status = "completed";
|
|
335
334
|
saveState(state, stateFile);
|
|
336
335
|
consola.log(
|
|
337
|
-
colors.bold(colors.green(`\n✅
|
|
336
|
+
colors.bold(colors.green(`\n✅ All plans completed after ${iteration} iteration(s)`)),
|
|
338
337
|
);
|
|
339
|
-
logStream.write(`\n✅
|
|
338
|
+
logStream.write(`\n✅ All plans completed after ${iteration} iteration(s)\n`);
|
|
340
339
|
}
|
|
341
340
|
}
|
|
342
341
|
|
|
@@ -204,32 +204,28 @@ export function extractOutputSummary(output: string, maxLength: number = 2000):
|
|
|
204
204
|
|
|
205
205
|
export interface BuildPromptOptions {
|
|
206
206
|
completionMarker: string;
|
|
207
|
+
taskDoneMarker: string;
|
|
207
208
|
plan: RalphPlan;
|
|
208
|
-
planContent: string;
|
|
209
209
|
skipCommit?: boolean;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
export function buildIterationPrompt({
|
|
213
213
|
completionMarker,
|
|
214
|
+
taskDoneMarker,
|
|
214
215
|
plan,
|
|
215
|
-
planContent,
|
|
216
216
|
skipCommit = false,
|
|
217
217
|
}: BuildPromptOptions): string {
|
|
218
218
|
let step = 1;
|
|
219
219
|
|
|
220
220
|
const prompt = `
|
|
221
|
-
<
|
|
222
|
-
${
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
${step++}. Work on the plan above.
|
|
227
|
-
${step++}. Run type checks and tests.
|
|
228
|
-
${step++}. Mark done: \`tt ralph plan done ${plan.id}\`
|
|
221
|
+
<instructions>
|
|
222
|
+
${step++}. Read Plan: \`${plan.planFilePath}\`
|
|
223
|
+
${step++}. Choose next best task to work on.
|
|
224
|
+
${step++}. Complete that task.
|
|
225
|
+
${step++}. Update the plan \`${plan.planFilePath}\` to mark that task Done with any notes.
|
|
229
226
|
${skipCommit ? "" : `${step++}. Make a git commit.`}
|
|
227
|
+
${step++}. If any tasks remain return <promise>${taskDoneMarker}</promise> else if plan is complete return <promise>${completionMarker}</promise>.
|
|
230
228
|
|
|
231
|
-
**Before ending:** Run \`tt ralph plan list\` to check remaining plans.
|
|
232
|
-
**ONLY if ALL PLANS are done** then Output: <promise>${completionMarker}</promise>
|
|
233
229
|
</instructions>
|
|
234
230
|
`;
|
|
235
231
|
return prompt.trim();
|
package/src/lib/ralph/state.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { RalphSettingsSchema } from "../../config/settings.js";
|
|
|
12
12
|
export const DEFAULT_MAX_ITERATIONS = 10;
|
|
13
13
|
export const DEFAULT_STATE_DIR = "./.claude/.ralph";
|
|
14
14
|
export const DEFAULT_COMPLETION_MARKER = "RALPH_DONE";
|
|
15
|
+
export const DEFAULT_TASK_DONE_MARKER = "TASK_DONE";
|
|
15
16
|
|
|
16
17
|
// File names within stateDir
|
|
17
18
|
const STATE_FILE_NAME = "ralph-state.local.json";
|
|
@@ -93,6 +94,7 @@ export interface IterationHistory {
|
|
|
93
94
|
durationHuman: string;
|
|
94
95
|
outputSummary: string;
|
|
95
96
|
markerFound: boolean;
|
|
97
|
+
taskMarkerFound?: boolean;
|
|
96
98
|
contextUsedPercent?: number;
|
|
97
99
|
}
|
|
98
100
|
|