@towles/tool 0.0.41 → 0.0.49
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/README.md +67 -109
- package/package.json +51 -41
- package/src/commands/base.ts +3 -18
- package/src/commands/config.ts +9 -8
- package/src/commands/doctor.ts +4 -1
- package/src/commands/gh/branch-clean.ts +10 -4
- package/src/commands/gh/branch.ts +6 -3
- package/src/commands/gh/pr.ts +10 -3
- package/src/commands/graph-template.html +1214 -0
- package/src/commands/graph.test.ts +176 -0
- package/src/commands/graph.ts +970 -0
- package/src/commands/install.ts +8 -2
- package/src/commands/journal/daily-notes.ts +9 -5
- package/src/commands/journal/meeting.ts +12 -6
- package/src/commands/journal/note.ts +12 -6
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
- package/src/commands/ralph/{task → plan}/list.ts +28 -39
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +126 -189
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +8 -27
- package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
- package/src/utils/date-utils.test.ts +2 -1
- package/src/utils/date-utils.ts +2 -2
- package/LICENSE.md +0 -20
- package/src/commands/index.ts +0 -55
- package/src/commands/observe/graph.test.ts +0 -89
- package/src/commands/observe/graph.ts +0 -1640
- package/src/commands/observe/report.ts +0 -166
- package/src/commands/observe/session.ts +0 -385
- package/src/commands/observe/setup.ts +0 -180
- package/src/commands/observe/status.ts +0 -146
- package/src/commands/ralph/lib/formatter.ts +0 -298
- package/src/commands/ralph/lib/marker.ts +0 -108
- package/src/commands/ralph/marker/create.ts +0 -23
- package/src/commands/ralph/plan.ts +0 -73
- package/src/commands/ralph/progress.ts +0 -44
- package/src/commands/ralph/ralph.test.ts +0 -673
- package/src/commands/ralph/task/add.ts +0 -105
- package/src/commands/ralph/task/done.ts +0 -73
- package/src/commands/ralph/task/remove.ts +0 -62
- package/src/config/context.ts +0 -7
- package/src/constants.ts +0 -3
- package/src/utils/anthropic/types.ts +0 -158
- package/src/utils/exec.ts +0 -8
- package/src/utils/git/git.ts +0 -25
- /package/src/{commands → lib}/journal/utils.ts +0 -0
- /package/src/{commands/ralph/lib → lib/ralph}/index.ts +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { Flags } from "@oclif/core";
|
|
3
|
-
import
|
|
3
|
+
import consola from "consola";
|
|
4
|
+
import { colors } from "consola/utils";
|
|
4
5
|
import { BaseCommand } from "../base.js";
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_STATE_FILE,
|
|
@@ -13,33 +14,22 @@ import {
|
|
|
13
14
|
appendHistory,
|
|
14
15
|
resolveRalphPath,
|
|
15
16
|
getRalphPaths,
|
|
16
|
-
} from "
|
|
17
|
+
} from "../../lib/ralph/state.js";
|
|
17
18
|
import {
|
|
18
19
|
buildIterationPrompt,
|
|
19
20
|
formatDuration,
|
|
20
21
|
extractOutputSummary,
|
|
21
22
|
detectCompletionMarker,
|
|
22
|
-
|
|
23
|
-
} from "
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
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 "";
|
|
23
|
+
} from "../../lib/ralph/formatter.js";
|
|
24
|
+
import { checkClaudeCli, runIteration } from "../../lib/ralph/execution.js";
|
|
25
|
+
import type { RalphPlan } from "../../lib/ralph/state.js";
|
|
26
|
+
|
|
27
|
+
/** Get the plan to work on: focused plan or first incomplete */
|
|
28
|
+
function getCurrentPlan(plans: RalphPlan[], focusedPlanId: number | null): RalphPlan | undefined {
|
|
29
|
+
if (focusedPlanId !== null) {
|
|
30
|
+
return plans.find((p) => p.id === focusedPlanId);
|
|
42
31
|
}
|
|
32
|
+
return plans.find((p) => p.status !== "done");
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
/**
|
|
@@ -49,14 +39,23 @@ export default class Run extends BaseCommand {
|
|
|
49
39
|
static override description = "Start the autonomous ralph loop";
|
|
50
40
|
|
|
51
41
|
static override examples = [
|
|
52
|
-
"<%= config.bin %>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
{ description: "Start the autonomous loop", command: "<%= config.bin %> <%= command.id %>" },
|
|
43
|
+
{
|
|
44
|
+
description: "Limit to 20 iterations",
|
|
45
|
+
command: "<%= config.bin %> <%= command.id %> --maxIterations 20",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
description: "Focus on specific plan",
|
|
49
|
+
command: "<%= config.bin %> <%= command.id %> --planId 5",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
description: "Run without auto-committing",
|
|
53
|
+
command: "<%= config.bin %> <%= command.id %> --no-autoCommit",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
description: "Preview config without executing",
|
|
57
|
+
command: "<%= config.bin %> <%= command.id %> --dryRun",
|
|
58
|
+
},
|
|
60
59
|
];
|
|
61
60
|
|
|
62
61
|
static override flags = {
|
|
@@ -65,29 +64,20 @@ export default class Run extends BaseCommand {
|
|
|
65
64
|
char: "s",
|
|
66
65
|
description: `State file path (default: ${DEFAULT_STATE_FILE})`,
|
|
67
66
|
}),
|
|
68
|
-
|
|
69
|
-
char: "
|
|
70
|
-
description: "Focus on specific
|
|
67
|
+
planId: Flags.integer({
|
|
68
|
+
char: "p",
|
|
69
|
+
description: "Focus on specific plan ID",
|
|
71
70
|
}),
|
|
72
71
|
maxIterations: Flags.integer({
|
|
73
72
|
char: "m",
|
|
74
73
|
description: "Max iterations",
|
|
75
74
|
default: DEFAULT_MAX_ITERATIONS,
|
|
76
75
|
}),
|
|
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
76
|
autoCommit: Flags.boolean({
|
|
83
|
-
description: "Auto-commit after each completed
|
|
77
|
+
description: "Auto-commit after each completed plan",
|
|
84
78
|
default: true,
|
|
85
79
|
allowNo: true,
|
|
86
80
|
}),
|
|
87
|
-
noFork: Flags.boolean({
|
|
88
|
-
description: "Disable session forking (start fresh session)",
|
|
89
|
-
default: false,
|
|
90
|
-
}),
|
|
91
81
|
dryRun: Flags.boolean({
|
|
92
82
|
char: "n",
|
|
93
83
|
description: "Show config without executing",
|
|
@@ -103,108 +93,79 @@ export default class Run extends BaseCommand {
|
|
|
103
93
|
description: "Completion marker",
|
|
104
94
|
default: DEFAULT_COMPLETION_MARKER,
|
|
105
95
|
}),
|
|
106
|
-
label: Flags.string({
|
|
107
|
-
char: "l",
|
|
108
|
-
description: "Only run tasks with this label",
|
|
109
|
-
}),
|
|
110
96
|
};
|
|
111
97
|
|
|
112
98
|
async run(): Promise<void> {
|
|
113
99
|
const { flags } = await this.parse(Run);
|
|
114
|
-
const ralphSettings = this.settings.
|
|
100
|
+
const ralphSettings = this.settings.settings.ralphSettings;
|
|
115
101
|
const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
|
|
116
102
|
const logFile = resolveRalphPath(flags.logFile, "logFile", ralphSettings);
|
|
117
103
|
const ralphPaths = getRalphPaths(ralphSettings);
|
|
118
104
|
|
|
119
|
-
|
|
120
|
-
const addIterations = flags.addIterations;
|
|
105
|
+
const maxIterations = flags.maxIterations;
|
|
121
106
|
const extraClaudeArgs = flags.claudeArgs?.split(" ").filter(Boolean) || [];
|
|
122
|
-
const
|
|
107
|
+
const focusedPlanId = flags.planId ?? null;
|
|
123
108
|
|
|
124
109
|
// Load existing state
|
|
125
110
|
let state = loadState(stateFile);
|
|
126
111
|
|
|
127
112
|
if (!state) {
|
|
128
|
-
this.error(`No state file found at: ${stateFile}\nUse: tt ralph
|
|
113
|
+
this.error(`No state file found at: ${stateFile}\nUse: tt ralph plan add --file path.md`);
|
|
129
114
|
}
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
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}`));
|
|
116
|
+
const remainingPlans = state.plans.filter((t) => t.status !== "done");
|
|
117
|
+
if (remainingPlans.length === 0) {
|
|
118
|
+
consola.log(colors.green("✅ All plans are done!"));
|
|
152
119
|
return;
|
|
153
120
|
}
|
|
154
121
|
|
|
155
|
-
// Validate focused
|
|
156
|
-
if (
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
this.error(`
|
|
122
|
+
// Validate focused plan if specified
|
|
123
|
+
if (focusedPlanId !== null) {
|
|
124
|
+
const focusedPlan = state.plans.find((t) => t.id === focusedPlanId);
|
|
125
|
+
if (!focusedPlan) {
|
|
126
|
+
this.error(`Plan #${focusedPlanId} not found. Use: tt ralph plan list`);
|
|
160
127
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
128
|
+
if (focusedPlan.status === "done") {
|
|
129
|
+
consola.log(colors.yellow(`Plan #${focusedPlanId} is already done.`));
|
|
163
130
|
return;
|
|
164
131
|
}
|
|
165
132
|
}
|
|
166
133
|
|
|
134
|
+
// Get current plan to work on
|
|
135
|
+
const currentPlan = getCurrentPlan(state.plans, focusedPlanId);
|
|
136
|
+
if (!currentPlan) {
|
|
137
|
+
consola.log(colors.green("✅ All plans are done!"));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
167
141
|
// Dry run mode
|
|
168
142
|
if (flags.dryRun) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
}
|
|
143
|
+
consola.log(colors.bold("\n=== DRY RUN ===\n"));
|
|
144
|
+
consola.log(colors.cyan("Config:"));
|
|
145
|
+
consola.log(` Max iterations: ${maxIterations}`);
|
|
146
|
+
consola.log(` State file: ${stateFile}`);
|
|
147
|
+
consola.log(` Log file: ${logFile}`);
|
|
148
|
+
consola.log(` Completion marker: ${flags.completionMarker}`);
|
|
149
|
+
consola.log(` Auto-commit: ${flags.autoCommit}`);
|
|
150
|
+
consola.log(` Claude args: ${[...CLAUDE_DEFAULT_ARGS, ...extraClaudeArgs].join(" ")}`);
|
|
151
|
+
consola.log(` Remaining plans: ${remainingPlans.length}`);
|
|
152
|
+
|
|
153
|
+
consola.log(colors.cyan("\nCurrent plan:"));
|
|
154
|
+
consola.log(` #${currentPlan.id}: ${currentPlan.description}`);
|
|
189
155
|
|
|
190
156
|
// Show prompt preview
|
|
191
|
-
const progressContent = readLastIterations(ralphPaths.progressFile, 3);
|
|
192
|
-
const taskList = formatTasksForPrompt(remainingTasks);
|
|
193
157
|
const prompt = buildIterationPrompt({
|
|
194
158
|
completionMarker: flags.completionMarker,
|
|
195
|
-
|
|
196
|
-
focusedTaskId,
|
|
159
|
+
plan: currentPlan,
|
|
197
160
|
skipCommit: !flags.autoCommit,
|
|
198
|
-
progressContent: progressContent || undefined,
|
|
199
|
-
taskList,
|
|
200
161
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
162
|
+
consola.log(colors.dim("─".repeat(60)));
|
|
163
|
+
consola.log(colors.bold("Prompt Preview"));
|
|
164
|
+
consola.log(colors.dim("─".repeat(60)));
|
|
165
|
+
consola.log(prompt);
|
|
166
|
+
consola.log(colors.dim("─".repeat(60)));
|
|
206
167
|
|
|
207
|
-
|
|
168
|
+
consola.log(colors.bold("\n=== END DRY RUN ===\n"));
|
|
208
169
|
return;
|
|
209
170
|
}
|
|
210
171
|
|
|
@@ -216,38 +177,29 @@ export default class Run extends BaseCommand {
|
|
|
216
177
|
}
|
|
217
178
|
|
|
218
179
|
// Update state for this run
|
|
219
|
-
state.maxIterations = maxIterations;
|
|
220
180
|
state.status = "running";
|
|
221
181
|
|
|
222
182
|
// Create log stream (append mode)
|
|
223
183
|
const logStream = fs.createWriteStream(logFile, { flags: "a" });
|
|
224
184
|
|
|
225
|
-
const ready = state.
|
|
226
|
-
const done = state.
|
|
185
|
+
const ready = state.plans.filter((t) => t.status === "ready").length;
|
|
186
|
+
const done = state.plans.filter((t) => t.status === "done").length;
|
|
227
187
|
|
|
228
188
|
logStream.write(`\n${"=".repeat(60)}\n`);
|
|
229
189
|
logStream.write(`Ralph Loop Started: ${new Date().toISOString()}\n`);
|
|
230
190
|
logStream.write(`${"=".repeat(60)}\n\n`);
|
|
231
191
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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`);
|
|
192
|
+
consola.log(colors.bold(colors.blue("\nRalph Loop Starting\n")));
|
|
193
|
+
consola.log(colors.dim(`Focus: ${focusedPlanId ? `Plan #${focusedPlanId}` : "Ralph picks"}`));
|
|
194
|
+
consola.log(colors.dim(`Max iterations: ${maxIterations}`));
|
|
195
|
+
consola.log(colors.dim(`Log file: ${logFile}`));
|
|
196
|
+
consola.log(colors.dim(`Auto-commit: ${flags.autoCommit}`));
|
|
197
|
+
consola.log(colors.dim(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)`));
|
|
198
|
+
consola.log("");
|
|
199
|
+
|
|
200
|
+
logStream.write(`Focus: ${focusedPlanId ? `Plan #${focusedPlanId}` : "Ralph picks"}\n`);
|
|
249
201
|
logStream.write(`Max iterations: ${maxIterations}\n`);
|
|
250
|
-
logStream.write(`Tasks: ${state.
|
|
202
|
+
logStream.write(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)\n\n`);
|
|
251
203
|
|
|
252
204
|
// Handle SIGINT gracefully
|
|
253
205
|
let interrupted = false;
|
|
@@ -258,7 +210,7 @@ export default class Run extends BaseCommand {
|
|
|
258
210
|
}
|
|
259
211
|
interrupted = true;
|
|
260
212
|
const msg = "\n\nInterrupted. Press Ctrl+C again to force exit.\n";
|
|
261
|
-
|
|
213
|
+
consola.log(colors.yellow(msg));
|
|
262
214
|
logStream.write(msg);
|
|
263
215
|
state.status = "error";
|
|
264
216
|
saveState(state, stateFile);
|
|
@@ -266,30 +218,31 @@ export default class Run extends BaseCommand {
|
|
|
266
218
|
|
|
267
219
|
// Main loop
|
|
268
220
|
let completed = false;
|
|
221
|
+
let iteration = 0;
|
|
269
222
|
|
|
270
|
-
while (
|
|
271
|
-
|
|
272
|
-
const currentIteration = state.iteration;
|
|
223
|
+
while (iteration < maxIterations && !interrupted && !completed) {
|
|
224
|
+
iteration++;
|
|
273
225
|
|
|
274
|
-
const iterHeader = `Iteration ${
|
|
226
|
+
const iterHeader = `Iteration ${iteration}/${maxIterations}`;
|
|
275
227
|
logStream.write(`\n━━━ ${iterHeader} ━━━\n`);
|
|
276
228
|
|
|
277
229
|
const iterationStart = new Date().toISOString();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
230
|
+
// Get current plan for this iteration
|
|
231
|
+
const plan = getCurrentPlan(state.plans, focusedPlanId);
|
|
232
|
+
if (!plan) {
|
|
233
|
+
completed = true;
|
|
234
|
+
state.status = "completed";
|
|
235
|
+
saveState(state, stateFile);
|
|
236
|
+
consola.log(
|
|
237
|
+
colors.bold(colors.green(`\n✅ All plans completed after ${iteration} iteration(s)`)),
|
|
238
|
+
);
|
|
239
|
+
logStream.write(`\n✅ All plans completed after ${iteration} iteration(s)\n`);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
286
242
|
const prompt = buildIterationPrompt({
|
|
287
243
|
completionMarker: flags.completionMarker,
|
|
288
|
-
|
|
289
|
-
focusedTaskId,
|
|
244
|
+
plan: plan,
|
|
290
245
|
skipCommit: !flags.autoCommit,
|
|
291
|
-
progressContent: progressContent || undefined,
|
|
292
|
-
taskList,
|
|
293
246
|
});
|
|
294
247
|
|
|
295
248
|
// Log the prompt
|
|
@@ -297,25 +250,15 @@ export default class Run extends BaseCommand {
|
|
|
297
250
|
|
|
298
251
|
// Build claude args
|
|
299
252
|
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
253
|
|
|
310
254
|
// Print iteration header
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log(pc.dim("─".repeat(60)));
|
|
255
|
+
consola.log("");
|
|
256
|
+
consola.log(colors.bold(colors.blue(`━━━ ${iterHeader} ━━━`)));
|
|
257
|
+
consola.log(colors.dim("─".repeat(60)));
|
|
258
|
+
consola.log(colors.bold("Prompt"));
|
|
259
|
+
consola.log(colors.dim("─".repeat(60)));
|
|
260
|
+
consola.log(prompt);
|
|
261
|
+
consola.log(colors.dim("─".repeat(60)));
|
|
319
262
|
|
|
320
263
|
// Run iteration - output goes directly to stdout
|
|
321
264
|
const iterResult = await runIteration(prompt, iterClaudeArgs, logStream);
|
|
@@ -323,17 +266,7 @@ export default class Run extends BaseCommand {
|
|
|
323
266
|
// Reload state from disk to pick up changes made by child claude process
|
|
324
267
|
const freshState = loadState(stateFile);
|
|
325
268
|
if (freshState) {
|
|
326
|
-
|
|
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;
|
|
269
|
+
Object.assign(state, freshState);
|
|
337
270
|
}
|
|
338
271
|
|
|
339
272
|
const iterationEnd = new Date().toISOString();
|
|
@@ -348,7 +281,7 @@ export default class Run extends BaseCommand {
|
|
|
348
281
|
// Record history
|
|
349
282
|
appendHistory(
|
|
350
283
|
{
|
|
351
|
-
iteration
|
|
284
|
+
iteration,
|
|
352
285
|
startedAt: iterationStart,
|
|
353
286
|
completedAt: iterationEnd,
|
|
354
287
|
durationMs,
|
|
@@ -369,11 +302,11 @@ export default class Run extends BaseCommand {
|
|
|
369
302
|
? ` | Context: ${iterResult.contextUsedPercent}%`
|
|
370
303
|
: "";
|
|
371
304
|
logStream.write(
|
|
372
|
-
`\n━━━ Iteration ${
|
|
305
|
+
`\n━━━ Iteration ${iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
|
|
373
306
|
);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
`Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ?
|
|
307
|
+
consola.log(
|
|
308
|
+
colors.dim(
|
|
309
|
+
`Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ? colors.green("yes") : colors.yellow("no")}`,
|
|
377
310
|
),
|
|
378
311
|
);
|
|
379
312
|
|
|
@@ -382,8 +315,10 @@ export default class Run extends BaseCommand {
|
|
|
382
315
|
completed = true;
|
|
383
316
|
state.status = "completed";
|
|
384
317
|
saveState(state, stateFile);
|
|
385
|
-
|
|
386
|
-
|
|
318
|
+
consola.log(
|
|
319
|
+
colors.bold(colors.green(`\n✅ Plan completed after ${iteration} iteration(s)`)),
|
|
320
|
+
);
|
|
321
|
+
logStream.write(`\n✅ Plan completed after ${iteration} iteration(s)\n`);
|
|
387
322
|
}
|
|
388
323
|
}
|
|
389
324
|
|
|
@@ -394,13 +329,15 @@ export default class Run extends BaseCommand {
|
|
|
394
329
|
return;
|
|
395
330
|
}
|
|
396
331
|
|
|
397
|
-
if (!interrupted &&
|
|
332
|
+
if (!interrupted && iteration >= maxIterations) {
|
|
398
333
|
state.status = "max_iterations_reached";
|
|
399
334
|
saveState(state, stateFile);
|
|
400
|
-
|
|
401
|
-
|
|
335
|
+
consola.log(
|
|
336
|
+
colors.bold(
|
|
337
|
+
colors.yellow(`\n⚠️ Max iterations (${maxIterations}) reached without completion`),
|
|
338
|
+
),
|
|
402
339
|
);
|
|
403
|
-
|
|
340
|
+
consola.log(colors.dim(`State saved to: ${stateFile}`));
|
|
404
341
|
logStream.write(`\n⚠️ Max iterations (${maxIterations}) reached without completion\n`);
|
|
405
342
|
this.exit(1);
|
|
406
343
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { colors } from "consola/utils";
|
|
4
|
+
import { BaseCommand } from "../base.js";
|
|
5
|
+
import { DEFAULT_STATE_FILE, loadState, resolveRalphPath } from "../../lib/ralph/state.js";
|
|
6
|
+
import {
|
|
7
|
+
formatPlanAsMarkdown,
|
|
8
|
+
formatPlanAsJson,
|
|
9
|
+
copyToClipboard,
|
|
10
|
+
} from "../../lib/ralph/formatter.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Show plan summary with status, tasks, and mermaid graph
|
|
14
|
+
*/
|
|
15
|
+
export default class Show extends BaseCommand {
|
|
16
|
+
static override description = "Show plan summary with status, tasks, and mermaid graph";
|
|
17
|
+
|
|
18
|
+
static override examples = [
|
|
19
|
+
{
|
|
20
|
+
description: "Show plan summary with mermaid graph",
|
|
21
|
+
command: "<%= config.bin %> <%= command.id %>",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
description: "Output plan as JSON",
|
|
25
|
+
command: "<%= config.bin %> <%= command.id %> --format json",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
description: "Copy plan output to clipboard",
|
|
29
|
+
command: "<%= config.bin %> <%= command.id %> --copy",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
static override flags = {
|
|
34
|
+
...BaseCommand.baseFlags,
|
|
35
|
+
stateFile: Flags.string({
|
|
36
|
+
char: "s",
|
|
37
|
+
description: `State file path (default: ${DEFAULT_STATE_FILE})`,
|
|
38
|
+
}),
|
|
39
|
+
format: Flags.string({
|
|
40
|
+
char: "f",
|
|
41
|
+
description: "Output format",
|
|
42
|
+
default: "default",
|
|
43
|
+
options: ["default", "markdown", "json"],
|
|
44
|
+
}),
|
|
45
|
+
copy: Flags.boolean({
|
|
46
|
+
char: "c",
|
|
47
|
+
description: "Copy output to clipboard",
|
|
48
|
+
default: false,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async run(): Promise<void> {
|
|
53
|
+
const { flags } = await this.parse(Show);
|
|
54
|
+
const ralphSettings = this.settings.settings.ralphSettings;
|
|
55
|
+
const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
|
|
56
|
+
|
|
57
|
+
const state = loadState(stateFile);
|
|
58
|
+
|
|
59
|
+
if (!state) {
|
|
60
|
+
consola.log(colors.yellow(`No state file found at: ${stateFile}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (state.plans.length === 0) {
|
|
65
|
+
consola.log(colors.yellow("No tasks in state file."));
|
|
66
|
+
consola.log(colors.dim('Use: tt ralph plan add "description"'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let output: string;
|
|
71
|
+
|
|
72
|
+
if (flags.format === "json") {
|
|
73
|
+
output = formatPlanAsJson(state.plans, state);
|
|
74
|
+
} else {
|
|
75
|
+
output = formatPlanAsMarkdown(state.plans, state);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
consola.log(output);
|
|
79
|
+
|
|
80
|
+
if (flags.copy) {
|
|
81
|
+
if (copyToClipboard(output)) {
|
|
82
|
+
consola.log(colors.green("✓ Copied to clipboard"));
|
|
83
|
+
} else {
|
|
84
|
+
consola.log(colors.yellow("⚠ Could not copy to clipboard (xclip/xsel not installed?)"));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/config/settings.ts
CHANGED
|
@@ -2,26 +2,16 @@ import { z } from "zod/v4";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { AppInfo } from "../constants";
|
|
6
5
|
import consola from "consola";
|
|
7
6
|
import { colors } from "consola/utils";
|
|
8
7
|
|
|
8
|
+
const TOOL_NAME = "towles-tool";
|
|
9
|
+
|
|
9
10
|
/** Default config directory */
|
|
10
|
-
export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config",
|
|
11
|
+
export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config", TOOL_NAME);
|
|
11
12
|
|
|
12
13
|
/** User settings file path */
|
|
13
|
-
export const USER_SETTINGS_PATH = path.join(
|
|
14
|
-
DEFAULT_CONFIG_DIR,
|
|
15
|
-
`${AppInfo.toolName}.settings.json`,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
/** Settings filename used within configDir */
|
|
19
|
-
export const SETTINGS_FILENAME = `${AppInfo.toolName}.settings.json`;
|
|
20
|
-
|
|
21
|
-
/** Get settings file path for a given configDir */
|
|
22
|
-
export function getSettingsPath(configDir: string): string {
|
|
23
|
-
return path.join(configDir, SETTINGS_FILENAME);
|
|
24
|
-
}
|
|
14
|
+
export const USER_SETTINGS_PATH = path.join(DEFAULT_CONFIG_DIR, `${TOOL_NAME}.settings.json`);
|
|
25
15
|
|
|
26
16
|
export const RalphSettingsSchema = z.object({
|
|
27
17
|
// Base directory for ralph files (relative to cwd or absolute)
|
|
@@ -48,7 +38,7 @@ export const JournalSettingsSchema = z.object({
|
|
|
48
38
|
.string()
|
|
49
39
|
.default(path.join("journal/{yyyy}/{MM}/notes/{yyyy}-{MM}-{dd}-{title}.md")),
|
|
50
40
|
// Directory for external templates (fallback to hardcoded if not found)
|
|
51
|
-
templateDir: z.string().default(path.join(homedir(), ".config",
|
|
41
|
+
templateDir: z.string().default(path.join(homedir(), ".config", TOOL_NAME, "templates")),
|
|
52
42
|
});
|
|
53
43
|
|
|
54
44
|
export type JournalSettings = z.infer<typeof JournalSettingsSchema>;
|
|
@@ -70,15 +60,6 @@ export interface SettingsFile {
|
|
|
70
60
|
path: string;
|
|
71
61
|
}
|
|
72
62
|
|
|
73
|
-
// TODO refactor this.
|
|
74
|
-
export class LoadedSettings {
|
|
75
|
-
constructor(settingsFile: SettingsFile) {
|
|
76
|
-
this.settingsFile = settingsFile;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
readonly settingsFile: SettingsFile;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
63
|
function createDefaultSettings(): UserSettings {
|
|
83
64
|
return UserSettingsSchema.parse({});
|
|
84
65
|
}
|
|
@@ -106,7 +87,7 @@ export function saveSettings(settingsFile: SettingsFile): void {
|
|
|
106
87
|
}
|
|
107
88
|
}
|
|
108
89
|
|
|
109
|
-
export async function loadSettings(): Promise<
|
|
90
|
+
export async function loadSettings(): Promise<SettingsFile> {
|
|
110
91
|
let userSettings: UserSettings | null = null;
|
|
111
92
|
|
|
112
93
|
// Load user settings
|
|
@@ -148,8 +129,8 @@ export async function loadSettings(): Promise<LoadedSettings> {
|
|
|
148
129
|
}
|
|
149
130
|
}
|
|
150
131
|
|
|
151
|
-
return
|
|
132
|
+
return {
|
|
152
133
|
path: USER_SETTINGS_PATH,
|
|
153
134
|
settings: userSettings!,
|
|
154
|
-
}
|
|
135
|
+
};
|
|
155
136
|
}
|