@towles/tool 0.0.20 → 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.
- package/LICENSE +21 -0
- package/LICENSE.md +9 -10
- package/README.md +121 -78
- package/bin/run.ts +5 -0
- package/package.json +63 -53
- package/patches/prompts.patch +34 -0
- package/src/commands/base.ts +42 -0
- package/src/commands/config.test.ts +15 -0
- package/src/commands/config.ts +43 -0
- package/src/commands/doctor.ts +133 -0
- package/src/commands/gh/branch-clean.ts +110 -0
- package/src/commands/gh/branch.test.ts +124 -0
- package/src/commands/gh/branch.ts +132 -0
- package/src/commands/gh/pr.ts +168 -0
- package/src/commands/index.ts +55 -0
- package/src/commands/install.ts +148 -0
- package/src/commands/journal/daily-notes.ts +66 -0
- package/src/commands/journal/meeting.ts +83 -0
- package/src/commands/journal/note.ts +83 -0
- package/src/commands/journal/utils.ts +399 -0
- package/src/commands/observe/graph.test.ts +89 -0
- package/src/commands/observe/graph.ts +1640 -0
- package/src/commands/observe/report.ts +166 -0
- package/src/commands/observe/session.ts +385 -0
- package/src/commands/observe/setup.ts +180 -0
- package/src/commands/observe/status.ts +146 -0
- package/src/commands/ralph/lib/execution.ts +302 -0
- package/src/commands/ralph/lib/formatter.ts +298 -0
- package/src/commands/ralph/lib/index.ts +4 -0
- package/src/commands/ralph/lib/marker.ts +108 -0
- package/src/commands/ralph/lib/state.ts +191 -0
- package/src/commands/ralph/marker/create.ts +23 -0
- package/src/commands/ralph/plan.ts +73 -0
- package/src/commands/ralph/progress.ts +44 -0
- package/src/commands/ralph/ralph.test.ts +673 -0
- package/src/commands/ralph/run.ts +408 -0
- package/src/commands/ralph/task/add.ts +105 -0
- package/src/commands/ralph/task/done.ts +73 -0
- package/src/commands/ralph/task/list.test.ts +48 -0
- package/src/commands/ralph/task/list.ts +110 -0
- package/src/commands/ralph/task/remove.ts +62 -0
- package/src/config/context.ts +7 -0
- package/src/config/settings.ts +155 -0
- package/src/constants.ts +3 -0
- package/src/types/journal.ts +16 -0
- package/src/utils/anthropic/types.ts +158 -0
- package/src/utils/date-utils.test.ts +96 -0
- package/src/utils/date-utils.ts +54 -0
- package/src/utils/exec.ts +8 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
- package/src/utils/git/gh-cli-wrapper.ts +54 -0
- package/src/utils/git/git-wrapper.test.ts +26 -0
- package/src/utils/git/git-wrapper.ts +15 -0
- package/src/utils/git/git.ts +25 -0
- package/src/utils/render.test.ts +71 -0
- package/src/utils/render.ts +34 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -805
|
@@ -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
|
+
});
|