@towles/tool 0.0.18 → 0.0.41

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +9 -10
  3. package/README.md +121 -78
  4. package/bin/run.ts +5 -0
  5. package/package.json +63 -53
  6. package/patches/prompts.patch +34 -0
  7. package/src/commands/base.ts +42 -0
  8. package/src/commands/config.test.ts +15 -0
  9. package/src/commands/config.ts +43 -0
  10. package/src/commands/doctor.ts +133 -0
  11. package/src/commands/gh/branch-clean.ts +110 -0
  12. package/src/commands/gh/branch.test.ts +124 -0
  13. package/src/commands/gh/branch.ts +132 -0
  14. package/src/commands/gh/pr.ts +168 -0
  15. package/src/commands/index.ts +55 -0
  16. package/src/commands/install.ts +148 -0
  17. package/src/commands/journal/daily-notes.ts +66 -0
  18. package/src/commands/journal/meeting.ts +83 -0
  19. package/src/commands/journal/note.ts +83 -0
  20. package/src/commands/journal/utils.ts +399 -0
  21. package/src/commands/observe/graph.test.ts +89 -0
  22. package/src/commands/observe/graph.ts +1640 -0
  23. package/src/commands/observe/report.ts +166 -0
  24. package/src/commands/observe/session.ts +385 -0
  25. package/src/commands/observe/setup.ts +180 -0
  26. package/src/commands/observe/status.ts +146 -0
  27. package/src/commands/ralph/lib/execution.ts +302 -0
  28. package/src/commands/ralph/lib/formatter.ts +298 -0
  29. package/src/commands/ralph/lib/index.ts +4 -0
  30. package/src/commands/ralph/lib/marker.ts +108 -0
  31. package/src/commands/ralph/lib/state.ts +191 -0
  32. package/src/commands/ralph/marker/create.ts +23 -0
  33. package/src/commands/ralph/plan.ts +73 -0
  34. package/src/commands/ralph/progress.ts +44 -0
  35. package/src/commands/ralph/ralph.test.ts +673 -0
  36. package/src/commands/ralph/run.ts +408 -0
  37. package/src/commands/ralph/task/add.ts +105 -0
  38. package/src/commands/ralph/task/done.ts +73 -0
  39. package/src/commands/ralph/task/list.test.ts +48 -0
  40. package/src/commands/ralph/task/list.ts +110 -0
  41. package/src/commands/ralph/task/remove.ts +62 -0
  42. package/src/config/context.ts +7 -0
  43. package/src/config/settings.ts +155 -0
  44. package/src/constants.ts +3 -0
  45. package/src/types/journal.ts +16 -0
  46. package/src/utils/anthropic/types.ts +158 -0
  47. package/src/utils/date-utils.test.ts +96 -0
  48. package/src/utils/date-utils.ts +54 -0
  49. package/src/utils/exec.ts +8 -0
  50. package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
  51. package/src/utils/git/gh-cli-wrapper.ts +54 -0
  52. package/src/utils/git/git-wrapper.test.ts +26 -0
  53. package/src/utils/git/git-wrapper.ts +15 -0
  54. package/src/utils/git/git.ts +25 -0
  55. package/src/utils/render.test.ts +71 -0
  56. package/src/utils/render.ts +34 -0
  57. package/dist/index.d.mts +0 -1
  58. package/dist/index.mjs +0 -794
@@ -0,0 +1,408 @@
1
+ import * as fs from "node:fs";
2
+ import { Flags } from "@oclif/core";
3
+ import pc from "picocolors";
4
+ import { BaseCommand } from "../base.js";
5
+ import {
6
+ DEFAULT_STATE_FILE,
7
+ DEFAULT_LOG_FILE,
8
+ DEFAULT_MAX_ITERATIONS,
9
+ DEFAULT_COMPLETION_MARKER,
10
+ CLAUDE_DEFAULT_ARGS,
11
+ loadState,
12
+ saveState,
13
+ appendHistory,
14
+ resolveRalphPath,
15
+ getRalphPaths,
16
+ } from "./lib/state.js";
17
+ import {
18
+ buildIterationPrompt,
19
+ formatDuration,
20
+ extractOutputSummary,
21
+ detectCompletionMarker,
22
+ formatTasksForPrompt,
23
+ } from "./lib/formatter.js";
24
+ import { checkClaudeCli, runIteration } from "./lib/execution.js";
25
+
26
+ /**
27
+ * Read last N iterations from progress file. Only returns iteration entries,
28
+ * excluding headers/status sections that could confuse the model.
29
+ */
30
+ function readLastIterations(filePath: string, count: number): string {
31
+ if (!fs.existsSync(filePath)) return "";
32
+ try {
33
+ const content = fs.readFileSync(filePath, "utf-8");
34
+ // Split by iteration headers, keeping the delimiter
35
+ const parts = content.split(/(?=### Iteration)/g);
36
+ // Skip first part (header/status content) - only want iteration entries
37
+ const iterations = parts.filter((p) => p.startsWith("### Iteration"));
38
+ if (iterations.length === 0) return "";
39
+ return iterations.slice(-count).join("\n").trim();
40
+ } catch {
41
+ return "";
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Run the autonomous ralph loop
47
+ */
48
+ export default class Run extends BaseCommand {
49
+ static override description = "Start the autonomous ralph loop";
50
+
51
+ static override examples = [
52
+ "<%= config.bin %> ralph run",
53
+ "<%= config.bin %> ralph run --maxIterations 20",
54
+ "<%= config.bin %> ralph run --taskId 5",
55
+ "<%= config.bin %> ralph run --no-autoCommit",
56
+ "<%= config.bin %> ralph run --noFork",
57
+ "<%= config.bin %> ralph run --dryRun",
58
+ "<%= config.bin %> ralph run --addIterations 5",
59
+ "<%= config.bin %> ralph run --label backend",
60
+ ];
61
+
62
+ static override flags = {
63
+ ...BaseCommand.baseFlags,
64
+ stateFile: Flags.string({
65
+ char: "s",
66
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
67
+ }),
68
+ taskId: Flags.integer({
69
+ char: "t",
70
+ description: "Focus on specific task ID",
71
+ }),
72
+ maxIterations: Flags.integer({
73
+ char: "m",
74
+ description: "Max iterations",
75
+ default: DEFAULT_MAX_ITERATIONS,
76
+ }),
77
+ addIterations: Flags.integer({
78
+ char: "a",
79
+ description:
80
+ "Add iterations to current count (e.g., at 5/10, --addIterations 10 makes it 5/20)",
81
+ }),
82
+ autoCommit: Flags.boolean({
83
+ description: "Auto-commit after each completed task",
84
+ default: true,
85
+ allowNo: true,
86
+ }),
87
+ noFork: Flags.boolean({
88
+ description: "Disable session forking (start fresh session)",
89
+ default: false,
90
+ }),
91
+ dryRun: Flags.boolean({
92
+ char: "n",
93
+ description: "Show config without executing",
94
+ default: false,
95
+ }),
96
+ claudeArgs: Flags.string({
97
+ description: "Extra args to pass to claude CLI (space-separated)",
98
+ }),
99
+ logFile: Flags.string({
100
+ description: `Log file path (default: ${DEFAULT_LOG_FILE})`,
101
+ }),
102
+ completionMarker: Flags.string({
103
+ description: "Completion marker",
104
+ default: DEFAULT_COMPLETION_MARKER,
105
+ }),
106
+ label: Flags.string({
107
+ char: "l",
108
+ description: "Only run tasks with this label",
109
+ }),
110
+ };
111
+
112
+ async run(): Promise<void> {
113
+ const { flags } = await this.parse(Run);
114
+ const ralphSettings = this.settings.settingsFile.settings.ralphSettings;
115
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
116
+ const logFile = resolveRalphPath(flags.logFile, "logFile", ralphSettings);
117
+ const ralphPaths = getRalphPaths(ralphSettings);
118
+
119
+ let maxIterations = flags.maxIterations;
120
+ const addIterations = flags.addIterations;
121
+ const extraClaudeArgs = flags.claudeArgs?.split(" ").filter(Boolean) || [];
122
+ const focusedTaskId = flags.taskId ?? null;
123
+
124
+ // Load existing state
125
+ let state = loadState(stateFile);
126
+
127
+ if (!state) {
128
+ this.error(`No state file found at: ${stateFile}\nUse: tt ralph task add "description"`);
129
+ }
130
+
131
+ // Handle --addIterations: extend max from current iteration
132
+ if (addIterations !== undefined) {
133
+ maxIterations = state.iteration + addIterations;
134
+ console.log(
135
+ pc.cyan(
136
+ `Adding ${addIterations} iterations: ${state.iteration}/${state.maxIterations} → ${state.iteration}/${maxIterations}`,
137
+ ),
138
+ );
139
+ }
140
+
141
+ // Filter by label if specified
142
+ const labelFilter = flags.label;
143
+ let remainingTasks = state.tasks.filter((t) => t.status !== "done");
144
+ if (labelFilter) {
145
+ remainingTasks = remainingTasks.filter((t) => t.label === labelFilter);
146
+ }
147
+ if (remainingTasks.length === 0) {
148
+ const msg = labelFilter
149
+ ? `All tasks with label '${labelFilter}' are done!`
150
+ : "All tasks are done!";
151
+ console.log(pc.green(`✅ ${msg}`));
152
+ return;
153
+ }
154
+
155
+ // Validate focused task if specified
156
+ if (focusedTaskId !== null) {
157
+ const focusedTask = state.tasks.find((t) => t.id === focusedTaskId);
158
+ if (!focusedTask) {
159
+ this.error(`Task #${focusedTaskId} not found. Use: tt ralph task list`);
160
+ }
161
+ if (focusedTask.status === "done") {
162
+ console.log(pc.yellow(`Task #${focusedTaskId} is already done.`));
163
+ return;
164
+ }
165
+ }
166
+
167
+ // Dry run mode
168
+ if (flags.dryRun) {
169
+ console.log(pc.bold("\n=== DRY RUN ===\n"));
170
+ console.log(pc.cyan("Config:"));
171
+ console.log(` Focus: ${focusedTaskId ? `Task #${focusedTaskId}` : "Ralph picks"}`);
172
+ console.log(` Label filter: ${labelFilter || "(none)"}`);
173
+ console.log(` Max iterations: ${maxIterations}`);
174
+ console.log(` State file: ${stateFile}`);
175
+ console.log(` Log file: ${logFile}`);
176
+ console.log(` Completion marker: ${flags.completionMarker}`);
177
+ console.log(` Auto-commit: ${flags.autoCommit}`);
178
+ console.log(` Fork session: ${!flags.noFork}`);
179
+ console.log(` Session ID: ${state.sessionId || "(none)"}`);
180
+ console.log(` Claude args: ${[...CLAUDE_DEFAULT_ARGS, ...extraClaudeArgs].join(" ")}`);
181
+ console.log(` Remaining tasks: ${remainingTasks.length}`);
182
+
183
+ console.log(pc.cyan("\nTasks:"));
184
+ for (const t of state.tasks) {
185
+ const icon = t.status === "done" ? "✓" : "○";
186
+ const focus = focusedTaskId === t.id ? pc.cyan(" ← FOCUS") : "";
187
+ console.log(` ${icon} ${t.id}. ${t.description} (${t.status})${focus}`);
188
+ }
189
+
190
+ // Show prompt preview
191
+ const progressContent = readLastIterations(ralphPaths.progressFile, 3);
192
+ const taskList = formatTasksForPrompt(remainingTasks);
193
+ const prompt = buildIterationPrompt({
194
+ completionMarker: flags.completionMarker,
195
+ progressFile: ralphPaths.progressFile,
196
+ focusedTaskId,
197
+ skipCommit: !flags.autoCommit,
198
+ progressContent: progressContent || undefined,
199
+ taskList,
200
+ });
201
+ console.log(pc.dim("─".repeat(60)));
202
+ console.log(pc.bold("Prompt Preview"));
203
+ console.log(pc.dim("─".repeat(60)));
204
+ console.log(prompt);
205
+ console.log(pc.dim("─".repeat(60)));
206
+
207
+ console.log(pc.bold("\n=== END DRY RUN ===\n"));
208
+ return;
209
+ }
210
+
211
+ // Check claude CLI is available
212
+ if (!(await checkClaudeCli())) {
213
+ this.error(
214
+ "claude CLI not found in PATH\nInstall Claude Code: https://docs.anthropic.com/en/docs/claude-code",
215
+ );
216
+ }
217
+
218
+ // Update state for this run
219
+ state.maxIterations = maxIterations;
220
+ state.status = "running";
221
+
222
+ // Create log stream (append mode)
223
+ const logStream = fs.createWriteStream(logFile, { flags: "a" });
224
+
225
+ const ready = state.tasks.filter((t) => t.status === "ready").length;
226
+ const done = state.tasks.filter((t) => t.status === "done").length;
227
+
228
+ logStream.write(`\n${"=".repeat(60)}\n`);
229
+ logStream.write(`Ralph Loop Started: ${new Date().toISOString()}\n`);
230
+ logStream.write(`${"=".repeat(60)}\n\n`);
231
+
232
+ console.log(pc.bold(pc.blue("\nRalph Loop Starting\n")));
233
+ console.log(pc.dim(`Focus: ${focusedTaskId ? `Task #${focusedTaskId}` : "Ralph picks"}`));
234
+ if (labelFilter) {
235
+ console.log(pc.dim(`Label filter: ${labelFilter}`));
236
+ }
237
+ console.log(pc.dim(`Max iterations: ${maxIterations}`));
238
+ console.log(pc.dim(`Log file: ${logFile}`));
239
+ console.log(pc.dim(`Auto-commit: ${flags.autoCommit}`));
240
+ console.log(
241
+ pc.dim(
242
+ `Fork session: ${!flags.noFork}${state.sessionId ? ` (session: ${state.sessionId.slice(0, 8)}...)` : ""}`,
243
+ ),
244
+ );
245
+ console.log(pc.dim(`Tasks: ${state.tasks.length} (${done} done, ${ready} ready)`));
246
+ console.log();
247
+
248
+ logStream.write(`Focus: ${focusedTaskId ? `Task #${focusedTaskId}` : "Ralph picks"}\n`);
249
+ logStream.write(`Max iterations: ${maxIterations}\n`);
250
+ logStream.write(`Tasks: ${state.tasks.length} (${done} done, ${ready} ready)\n\n`);
251
+
252
+ // Handle SIGINT gracefully
253
+ let interrupted = false;
254
+ process.on("SIGINT", () => {
255
+ if (interrupted) {
256
+ logStream.end();
257
+ process.exit(130);
258
+ }
259
+ interrupted = true;
260
+ const msg = "\n\nInterrupted. Press Ctrl+C again to force exit.\n";
261
+ console.log(pc.yellow(msg));
262
+ logStream.write(msg);
263
+ state.status = "error";
264
+ saveState(state, stateFile);
265
+ });
266
+
267
+ // Main loop
268
+ let completed = false;
269
+
270
+ while (state.iteration < maxIterations && !interrupted && !completed) {
271
+ state.iteration++;
272
+ const currentIteration = state.iteration;
273
+
274
+ const iterHeader = `Iteration ${currentIteration}/${maxIterations}`;
275
+ logStream.write(`\n━━━ ${iterHeader} ━━━\n`);
276
+
277
+ const iterationStart = new Date().toISOString();
278
+ const progressContent = readLastIterations(ralphPaths.progressFile, 3);
279
+ // Reload remaining tasks for current state
280
+ const currentRemainingTasks = state.tasks.filter((t) => t.status !== "done");
281
+ const taskList = formatTasksForPrompt(
282
+ labelFilter
283
+ ? currentRemainingTasks.filter((t) => t.label === labelFilter)
284
+ : currentRemainingTasks,
285
+ );
286
+ const prompt = buildIterationPrompt({
287
+ completionMarker: flags.completionMarker,
288
+ progressFile: ralphPaths.progressFile,
289
+ focusedTaskId,
290
+ skipCommit: !flags.autoCommit,
291
+ progressContent: progressContent || undefined,
292
+ taskList,
293
+ });
294
+
295
+ // Log the prompt
296
+ logStream.write(`\n--- Prompt ---\n${prompt}\n--- End Prompt ---\n\n`);
297
+
298
+ // Build claude args
299
+ const iterClaudeArgs = [...extraClaudeArgs];
300
+ const currentTask = focusedTaskId
301
+ ? state.tasks.find((t) => t.id === focusedTaskId)
302
+ : state.tasks.find((t) => t.status === "ready");
303
+
304
+ // Fork from task's sessionId (or state-level fallback) unless disabled
305
+ const taskSessionId = currentTask?.sessionId || state.sessionId;
306
+ if (!flags.noFork && taskSessionId) {
307
+ iterClaudeArgs.push("--fork-session", taskSessionId);
308
+ }
309
+
310
+ // Print iteration header
311
+ const sessionInfo = taskSessionId ? pc.dim(` (fork: ${taskSessionId.slice(0, 8)}...)`) : "";
312
+ console.log();
313
+ console.log(pc.bold(pc.blue(`━━━ ${iterHeader}${sessionInfo} ━━━`)));
314
+ console.log(pc.dim("─".repeat(60)));
315
+ console.log(pc.bold("Prompt"));
316
+ console.log(pc.dim("─".repeat(60)));
317
+ console.log(prompt);
318
+ console.log(pc.dim("─".repeat(60)));
319
+
320
+ // Run iteration - output goes directly to stdout
321
+ const iterResult = await runIteration(prompt, iterClaudeArgs, logStream);
322
+
323
+ // Reload state from disk to pick up changes made by child claude process
324
+ const freshState = loadState(stateFile);
325
+ if (freshState) {
326
+ const currentIter = state.iteration;
327
+ Object.assign(state, freshState, { iteration: currentIter });
328
+ }
329
+
330
+ // Store session ID on the current task for future resumption
331
+ const taskToUpdate = currentTask
332
+ ? state.tasks.find((t) => t.id === currentTask.id)
333
+ : undefined;
334
+ if (iterResult.sessionId && taskToUpdate && !taskToUpdate.sessionId) {
335
+ taskToUpdate.sessionId = iterResult.sessionId;
336
+ state.sessionId = iterResult.sessionId;
337
+ }
338
+
339
+ const iterationEnd = new Date().toISOString();
340
+ const markerFound = detectCompletionMarker(iterResult.output, flags.completionMarker);
341
+
342
+ // Calculate duration
343
+ const startTime = new Date(iterationStart).getTime();
344
+ const endTime = new Date(iterationEnd).getTime();
345
+ const durationMs = endTime - startTime;
346
+ const durationHuman = formatDuration(durationMs);
347
+
348
+ // Record history
349
+ appendHistory(
350
+ {
351
+ iteration: state.iteration,
352
+ startedAt: iterationStart,
353
+ completedAt: iterationEnd,
354
+ durationMs,
355
+ durationHuman,
356
+ outputSummary: extractOutputSummary(iterResult.output),
357
+ markerFound,
358
+ contextUsedPercent: iterResult.contextUsedPercent,
359
+ },
360
+ ralphPaths.historyFile,
361
+ );
362
+
363
+ // Save state
364
+ saveState(state, stateFile);
365
+
366
+ // Log summary
367
+ const contextInfo =
368
+ iterResult.contextUsedPercent !== undefined
369
+ ? ` | Context: ${iterResult.contextUsedPercent}%`
370
+ : "";
371
+ logStream.write(
372
+ `\n━━━ Iteration ${state.iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
373
+ );
374
+ console.log(
375
+ pc.dim(
376
+ `Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ? pc.green("yes") : pc.yellow("no")}`,
377
+ ),
378
+ );
379
+
380
+ // Check completion
381
+ if (markerFound) {
382
+ completed = true;
383
+ state.status = "completed";
384
+ saveState(state, stateFile);
385
+ console.log(pc.bold(pc.green(`\n✅ Task completed after ${state.iteration} iteration(s)`)));
386
+ logStream.write(`\n✅ Task completed after ${state.iteration} iteration(s)\n`);
387
+ }
388
+ }
389
+
390
+ logStream.end();
391
+
392
+ // Final status
393
+ if (completed) {
394
+ return;
395
+ }
396
+
397
+ if (!interrupted && state.iteration >= maxIterations) {
398
+ state.status = "max_iterations_reached";
399
+ saveState(state, stateFile);
400
+ console.log(
401
+ pc.bold(pc.yellow(`\n⚠️ Max iterations (${maxIterations}) reached without completion`)),
402
+ );
403
+ console.log(pc.dim(`State saved to: ${stateFile}`));
404
+ logStream.write(`\n⚠️ Max iterations (${maxIterations}) reached without completion\n`);
405
+ this.exit(1);
406
+ }
407
+ }
408
+ }
@@ -0,0 +1,105 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { BaseCommand } from "../../base.js";
4
+ import {
5
+ DEFAULT_STATE_FILE,
6
+ DEFAULT_MAX_ITERATIONS,
7
+ loadState,
8
+ saveState,
9
+ createInitialState,
10
+ addTaskToState,
11
+ resolveRalphPath,
12
+ } from "../lib/state.js";
13
+ import { findSessionByMarker } from "../lib/marker.js";
14
+
15
+ /**
16
+ * Add a new task to ralph state
17
+ */
18
+ export default class TaskAdd extends BaseCommand {
19
+ static override description = "Add a new task";
20
+
21
+ static override examples = [
22
+ '<%= config.bin %> ralph task add "Fix the login bug"',
23
+ '<%= config.bin %> ralph task add "Implement feature X" --sessionId abc123',
24
+ '<%= config.bin %> ralph task add "Implement feature X" --findMarker RALPH_MARKER_abc123',
25
+ '<%= config.bin %> ralph task add "Backend refactor" --label backend',
26
+ ];
27
+
28
+ static override args = {
29
+ description: Args.string({
30
+ description: "Task description",
31
+ required: true,
32
+ }),
33
+ };
34
+
35
+ static override flags = {
36
+ ...BaseCommand.baseFlags,
37
+ stateFile: Flags.string({
38
+ char: "s",
39
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
40
+ }),
41
+ sessionId: Flags.string({
42
+ description: "Claude session ID for resuming from prior research",
43
+ }),
44
+ findMarker: Flags.string({
45
+ char: "m",
46
+ description: "Find session by full marker (e.g., RALPH_MARKER_abc123)",
47
+ }),
48
+ label: Flags.string({
49
+ char: "l",
50
+ description: "Label for grouping/filtering tasks",
51
+ }),
52
+ };
53
+
54
+ async run(): Promise<void> {
55
+ const { args, flags } = await this.parse(TaskAdd);
56
+ const ralphSettings = this.settings.settingsFile.settings.ralphSettings;
57
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
58
+
59
+ const description = args.description.trim();
60
+
61
+ if (!description || description.length < 3) {
62
+ this.error("Task description too short (min 3 chars)");
63
+ }
64
+
65
+ // Resolve session ID from --sessionId or --findMarker
66
+ let sessionId = flags.sessionId;
67
+ let marker: string | undefined;
68
+ if (flags.findMarker) {
69
+ if (sessionId) {
70
+ this.error("Cannot use both --sessionId and --findMarker");
71
+ }
72
+ marker = flags.findMarker;
73
+ console.log(pc.dim(`Searching for marker: ${marker}...`));
74
+ sessionId = (await findSessionByMarker(marker)) ?? undefined;
75
+ if (!sessionId) {
76
+ this.error(
77
+ `Marker not found: ${marker}\nMake sure Claude output this marker during research.`,
78
+ );
79
+ }
80
+ console.log(pc.cyan(`Found session: ${sessionId.slice(0, 8)}...`));
81
+ }
82
+
83
+ let state = loadState(stateFile);
84
+
85
+ if (!state) {
86
+ state = createInitialState(DEFAULT_MAX_ITERATIONS);
87
+ }
88
+
89
+ const newTask = addTaskToState(state, description, sessionId, marker, flags.label);
90
+ saveState(state, stateFile);
91
+
92
+ console.log(pc.green(`✓ Added task #${newTask.id}: ${newTask.description}`));
93
+ if (flags.label) {
94
+ console.log(pc.cyan(` Label: ${flags.label}`));
95
+ }
96
+ if (sessionId) {
97
+ console.log(pc.cyan(` Session: ${sessionId.slice(0, 8)}...`));
98
+ }
99
+ if (marker) {
100
+ console.log(pc.dim(` Marker: ${marker}`));
101
+ }
102
+ console.log(pc.dim(`State saved to: ${stateFile}`));
103
+ console.log(pc.dim(`Total tasks: ${state.tasks.length}`));
104
+ }
105
+ }
@@ -0,0 +1,73 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { BaseCommand } from "../../base.js";
4
+ import { DEFAULT_STATE_FILE, loadState, saveState, resolveRalphPath } from "../lib/state.js";
5
+
6
+ /**
7
+ * Mark a ralph task as done
8
+ */
9
+ export default class TaskDone extends BaseCommand {
10
+ static override description = "Mark a task as done by ID";
11
+
12
+ static override examples = [
13
+ "<%= config.bin %> ralph task done 1",
14
+ "<%= config.bin %> ralph task done 5 --stateFile custom-state.json",
15
+ ];
16
+
17
+ static override args = {
18
+ id: Args.integer({
19
+ description: "Task ID to mark done",
20
+ required: true,
21
+ }),
22
+ };
23
+
24
+ static override flags = {
25
+ ...BaseCommand.baseFlags,
26
+ stateFile: Flags.string({
27
+ char: "s",
28
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
29
+ }),
30
+ };
31
+
32
+ async run(): Promise<void> {
33
+ const { args, flags } = await this.parse(TaskDone);
34
+ const ralphSettings = this.settings.settingsFile.settings.ralphSettings;
35
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
36
+
37
+ const taskId = args.id;
38
+
39
+ if (taskId < 1) {
40
+ this.error("Invalid task ID");
41
+ }
42
+
43
+ const state = loadState(stateFile);
44
+
45
+ if (!state) {
46
+ this.error(`No state file found at: ${stateFile}`);
47
+ }
48
+
49
+ const task = state.tasks.find((t) => t.id === taskId);
50
+
51
+ if (!task) {
52
+ this.error(`Task #${taskId} not found. Use: tt ralph task list`);
53
+ }
54
+
55
+ if (task.status === "done") {
56
+ console.log(pc.yellow(`Task #${taskId} is already done.`));
57
+ return;
58
+ }
59
+
60
+ task.status = "done";
61
+ task.completedAt = new Date().toISOString();
62
+ saveState(state, stateFile);
63
+
64
+ console.log(pc.green(`✓ Marked task #${taskId} as done: ${task.description}`));
65
+
66
+ const remaining = state.tasks.filter((t) => t.status !== "done").length;
67
+ if (remaining === 0) {
68
+ console.log(pc.bold(pc.green("All tasks complete!")));
69
+ } else {
70
+ console.log(pc.dim(`Remaining tasks: ${remaining}`));
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Integration tests for oclif ralph task list command
3
+ * Note: --help output goes through oclif's own routing
4
+ */
5
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
6
+ import { runCommand } from "@oclif/test";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import { writeFileSync, unlinkSync, existsSync } from "node:fs";
10
+
11
+ describe("ralph task list command", () => {
12
+ const tempStateFile = join(tmpdir(), `ralph-test-list-${Date.now()}.json`);
13
+
14
+ beforeAll(() => {
15
+ // Create state file with one task to minimize output during tests
16
+ writeFileSync(
17
+ tempStateFile,
18
+ JSON.stringify({
19
+ version: 1,
20
+ iteration: 0,
21
+ maxIterations: 10,
22
+ status: "running",
23
+ tasks: [{ id: 1, description: "test", status: "done", addedAt: new Date().toISOString() }],
24
+ startedAt: new Date().toISOString(),
25
+ }),
26
+ );
27
+ });
28
+
29
+ afterAll(() => {
30
+ if (existsSync(tempStateFile)) unlinkSync(tempStateFile);
31
+ });
32
+
33
+ it("runs task list without error", async () => {
34
+ const { error } = await runCommand(["ralph:task:list", "-s", tempStateFile]);
35
+ expect(error).toBeUndefined();
36
+ });
37
+
38
+ it("supports --format flag", async () => {
39
+ const { error } = await runCommand([
40
+ "ralph:task:list",
41
+ "--format",
42
+ "markdown",
43
+ "-s",
44
+ tempStateFile,
45
+ ]);
46
+ expect(error).toBeUndefined();
47
+ });
48
+ });