@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
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
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,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 %> --file docs/plans/2025-01-18-feature.md",
25
+ command: "<%= config.bin %> <%= command.id %> docs/plans/2025-01-18-feature.md",
26
26
  },
27
27
  ];
28
28
 
29
- static override flags = {
30
- ...BaseCommand.baseFlags,
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(flags.file);
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 content", () => {
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("First plan content");
120
+ expect(prompt).toContain("/tmp/first-plan.md");
121
+ expect(prompt).toContain("Read Plan:");
120
122
  });
121
123
 
122
- it("should include mark done command with plan id", () => {
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("tt ralph plan done 1");
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 markerFound = detectCompletionMarker(iterResult.output, flags.completionMarker);
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}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
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} | Marker: ${markerFound ? colors.green("yes") : colors.yellow("no")}`,
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 (markerFound) {
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✅ Plan completed after ${iteration} iteration(s)`)),
336
+ colors.bold(colors.green(`\n✅ All plans completed after ${iteration} iteration(s)`)),
338
337
  );
339
- logStream.write(`\n✅ Plan completed after ${iteration} iteration(s)\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
- <plan>
222
- ${planContent}
223
- </plan>
224
-
225
- <instructions>
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();
@@ -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