@towles/tool 0.0.53 → 0.0.55
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 +82 -72
- package/package.json +8 -7
- package/src/commands/auto-claude.ts +219 -0
- package/src/commands/doctor.ts +1 -34
- package/src/config/settings.ts +0 -10
- package/src/lib/auto-claude/config.test.ts +53 -0
- package/src/lib/auto-claude/config.ts +68 -0
- package/src/lib/auto-claude/index.ts +14 -0
- package/src/lib/auto-claude/pipeline.test.ts +14 -0
- package/src/lib/auto-claude/pipeline.ts +64 -0
- package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +28 -0
- package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +28 -0
- package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +21 -0
- package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +33 -0
- package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +31 -0
- package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +30 -0
- package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +39 -0
- package/src/lib/auto-claude/prompt-templates/index.test.ts +145 -0
- package/src/lib/auto-claude/prompt-templates/index.ts +44 -0
- package/src/lib/auto-claude/steps/create-pr.ts +93 -0
- package/src/lib/auto-claude/steps/fetch-issues.ts +64 -0
- package/src/lib/auto-claude/steps/implement.ts +63 -0
- package/src/lib/auto-claude/steps/plan-annotations.ts +54 -0
- package/src/lib/auto-claude/steps/plan-implementation.ts +14 -0
- package/src/lib/auto-claude/steps/plan.ts +14 -0
- package/src/lib/auto-claude/steps/refresh.ts +114 -0
- package/src/lib/auto-claude/steps/remove-label.ts +22 -0
- package/src/lib/auto-claude/steps/research.ts +21 -0
- package/src/lib/auto-claude/steps/review.ts +14 -0
- package/src/lib/auto-claude/utils.test.ts +136 -0
- package/src/lib/auto-claude/utils.ts +334 -0
- package/src/commands/ralph/plan/add.ts +0 -69
- package/src/commands/ralph/plan/done.ts +0 -82
- package/src/commands/ralph/plan/list.test.ts +0 -48
- package/src/commands/ralph/plan/list.ts +0 -100
- package/src/commands/ralph/plan/remove.ts +0 -71
- package/src/commands/ralph/run.test.ts +0 -607
- package/src/commands/ralph/run.ts +0 -362
- package/src/commands/ralph/show.ts +0 -88
- package/src/lib/ralph/execution.ts +0 -292
- package/src/lib/ralph/formatter.ts +0 -240
- package/src/lib/ralph/index.ts +0 -4
- package/src/lib/ralph/state.ts +0 -201
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import type { WriteStream } from "node:fs";
|
|
2
|
-
// NOTE: We use spawn instead of tinyexec for runIteration because we need
|
|
3
|
-
// real-time streaming of stdout/stderr. tinyexec waits for command completion
|
|
4
|
-
// before returning output, which doesn't work for long-running claude sessions.
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import pc from "picocolors";
|
|
7
|
-
import { x } from "tinyexec";
|
|
8
|
-
import { CLAUDE_DEFAULT_ARGS } from "./state.js";
|
|
9
|
-
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Types
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
|
-
interface StreamEvent {
|
|
15
|
-
type: string;
|
|
16
|
-
message?: {
|
|
17
|
-
content?: Array<{ type: string; text?: string }>;
|
|
18
|
-
usage?: {
|
|
19
|
-
input_tokens?: number;
|
|
20
|
-
output_tokens?: number;
|
|
21
|
-
cache_read_input_tokens?: number;
|
|
22
|
-
cache_creation_input_tokens?: number;
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
result?: string;
|
|
26
|
-
total_cost_usd?: number;
|
|
27
|
-
num_turns?: number;
|
|
28
|
-
session_id?: string;
|
|
29
|
-
usage?: {
|
|
30
|
-
input_tokens?: number;
|
|
31
|
-
output_tokens?: number;
|
|
32
|
-
cache_read_input_tokens?: number;
|
|
33
|
-
cache_creation_input_tokens?: number;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Claude model context windows (tokens)
|
|
38
|
-
const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
39
|
-
"claude-sonnet-4-20250514": 200000,
|
|
40
|
-
"claude-opus-4-20250514": 200000,
|
|
41
|
-
"claude-3-5-sonnet-20241022": 200000,
|
|
42
|
-
"claude-3-opus-20240229": 200000,
|
|
43
|
-
default: 200000,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export interface IterationResult {
|
|
47
|
-
output: string;
|
|
48
|
-
exitCode: number;
|
|
49
|
-
contextUsedPercent?: number;
|
|
50
|
-
sessionId?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface ParsedLine {
|
|
54
|
-
text: string | null;
|
|
55
|
-
tool?: { name: string; summary: string };
|
|
56
|
-
usage?: StreamEvent["usage"];
|
|
57
|
-
sessionId?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// Claude CLI Check
|
|
62
|
-
// ============================================================================
|
|
63
|
-
|
|
64
|
-
export async function checkClaudeCli(): Promise<boolean> {
|
|
65
|
-
try {
|
|
66
|
-
const result = await x("which", ["claude"]);
|
|
67
|
-
return result.exitCode === 0;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Stream Parsing
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
// Track accumulated text from assistant messages to compute deltas
|
|
78
|
-
let lastAssistantText = "";
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Reset stream parsing state between iterations.
|
|
82
|
-
*/
|
|
83
|
-
export function resetStreamState(): void {
|
|
84
|
-
lastAssistantText = "";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function summarizeTool(name: string, input: Record<string, unknown>): string {
|
|
88
|
-
switch (name) {
|
|
89
|
-
case "Read":
|
|
90
|
-
return (
|
|
91
|
-
String(input.file_path || input.path || "")
|
|
92
|
-
.split("/")
|
|
93
|
-
.pop() || "file"
|
|
94
|
-
);
|
|
95
|
-
case "Write":
|
|
96
|
-
case "Edit":
|
|
97
|
-
return (
|
|
98
|
-
String(input.file_path || input.path || "")
|
|
99
|
-
.split("/")
|
|
100
|
-
.pop() || "file"
|
|
101
|
-
);
|
|
102
|
-
case "Glob":
|
|
103
|
-
return String(input.pattern || "");
|
|
104
|
-
case "Grep":
|
|
105
|
-
return String(input.pattern || "");
|
|
106
|
-
case "Bash":
|
|
107
|
-
return String(input.command || "").substring(0, 40);
|
|
108
|
-
case "TodoWrite":
|
|
109
|
-
return "updating todos";
|
|
110
|
-
default:
|
|
111
|
-
return Object.values(input)[0]?.toString().substring(0, 30) || "";
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function parseStreamLine(line: string): ParsedLine {
|
|
116
|
-
if (!line.trim()) return { text: null };
|
|
117
|
-
try {
|
|
118
|
-
const data = JSON.parse(line) as StreamEvent & {
|
|
119
|
-
tool_use?: { name: string; input: Record<string, unknown> };
|
|
120
|
-
content_block?: { type: string; name?: string; input?: Record<string, unknown> };
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Handle tool_use events
|
|
124
|
-
if (data.type === "tool_use" && data.tool_use) {
|
|
125
|
-
const name = data.tool_use.name;
|
|
126
|
-
const summary = summarizeTool(name, data.tool_use.input || {});
|
|
127
|
-
return { text: null, tool: { name, summary } };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Handle content_block with tool_use (streaming format)
|
|
131
|
-
if (data.type === "content_block" && data.content_block?.type === "tool_use") {
|
|
132
|
-
const name = data.content_block.name || "Tool";
|
|
133
|
-
const summary = summarizeTool(name, data.content_block.input || {});
|
|
134
|
-
return { text: null, tool: { name, summary } };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Handle assistant messages with content array
|
|
138
|
-
if (data.type === "assistant" && data.message) {
|
|
139
|
-
// Check for tool_use in content blocks
|
|
140
|
-
const toolBlocks = data.message.content?.filter((c) => c.type === "tool_use") || [];
|
|
141
|
-
if (toolBlocks.length > 0) {
|
|
142
|
-
const tb = toolBlocks[toolBlocks.length - 1] as {
|
|
143
|
-
name?: string;
|
|
144
|
-
input?: Record<string, unknown>;
|
|
145
|
-
};
|
|
146
|
-
const name = tb.name || "Tool";
|
|
147
|
-
const summary = summarizeTool(name, tb.input || {});
|
|
148
|
-
return {
|
|
149
|
-
text: null,
|
|
150
|
-
tool: { name, summary },
|
|
151
|
-
usage: data.message.usage || data.usage,
|
|
152
|
-
sessionId: data.session_id,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Extract full text from content blocks
|
|
157
|
-
const fullText =
|
|
158
|
-
data.message.content
|
|
159
|
-
?.filter((c) => c.type === "text" && c.text)
|
|
160
|
-
.map((c) => c.text)
|
|
161
|
-
.join("") || "";
|
|
162
|
-
|
|
163
|
-
// Compute delta (only new portion) to avoid duplicate output
|
|
164
|
-
let delta: string | null = null;
|
|
165
|
-
if (fullText.startsWith(lastAssistantText)) {
|
|
166
|
-
delta = fullText.slice(lastAssistantText.length) || null;
|
|
167
|
-
} else {
|
|
168
|
-
// Text doesn't match prefix - new context
|
|
169
|
-
delta = fullText || null;
|
|
170
|
-
}
|
|
171
|
-
lastAssistantText = fullText;
|
|
172
|
-
|
|
173
|
-
return { text: delta, usage: data.message.usage || data.usage, sessionId: data.session_id };
|
|
174
|
-
}
|
|
175
|
-
// Capture final result with usage and session_id
|
|
176
|
-
if (data.type === "result") {
|
|
177
|
-
const resultText = data.result
|
|
178
|
-
? `\n[Result: ${data.result.substring(0, 100)}${data.result.length > 100 ? "..." : ""}]\n`
|
|
179
|
-
: null;
|
|
180
|
-
return { text: resultText, usage: data.usage, sessionId: data.session_id };
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
// Not JSON, return raw
|
|
184
|
-
return { text: line };
|
|
185
|
-
}
|
|
186
|
-
return { text: null };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ============================================================================
|
|
190
|
-
// Run Iteration
|
|
191
|
-
// ============================================================================
|
|
192
|
-
|
|
193
|
-
export async function runIteration(
|
|
194
|
-
prompt: string,
|
|
195
|
-
claudeArgs: string[],
|
|
196
|
-
logStream?: WriteStream,
|
|
197
|
-
): Promise<IterationResult> {
|
|
198
|
-
// Reset accumulated text state from previous iteration
|
|
199
|
-
resetStreamState();
|
|
200
|
-
|
|
201
|
-
// Pass task context as system prompt via --append-system-prompt
|
|
202
|
-
// 'continue' is the user prompt - required by claude CLI when using --print
|
|
203
|
-
const allArgs = [
|
|
204
|
-
...CLAUDE_DEFAULT_ARGS,
|
|
205
|
-
...claudeArgs,
|
|
206
|
-
"--append-system-prompt",
|
|
207
|
-
prompt,
|
|
208
|
-
"continue",
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
let output = "";
|
|
212
|
-
let lineBuffer = "";
|
|
213
|
-
let finalUsage: StreamEvent["usage"] | undefined;
|
|
214
|
-
let sessionId: string | undefined;
|
|
215
|
-
let lastCharWasNewline = true;
|
|
216
|
-
|
|
217
|
-
const processLine = (line: string) => {
|
|
218
|
-
const { text: parsed, tool, usage, sessionId: sid } = parseStreamLine(line);
|
|
219
|
-
if (usage) finalUsage = usage;
|
|
220
|
-
if (sid) sessionId = sid;
|
|
221
|
-
if (tool) {
|
|
222
|
-
const prefix = lastCharWasNewline ? "" : "\n";
|
|
223
|
-
const toolLine = `${prefix}${pc.yellow("⚡")} ${pc.cyan(tool.name)}: ${tool.summary}\n`;
|
|
224
|
-
process.stdout.write(toolLine);
|
|
225
|
-
logStream?.write(`${prefix}⚡ ${tool.name}: ${tool.summary}\n`);
|
|
226
|
-
lastCharWasNewline = true;
|
|
227
|
-
}
|
|
228
|
-
if (parsed) {
|
|
229
|
-
process.stdout.write(parsed);
|
|
230
|
-
logStream?.write(parsed);
|
|
231
|
-
output += parsed;
|
|
232
|
-
lastCharWasNewline = parsed.endsWith("\n");
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
return new Promise((resolve) => {
|
|
237
|
-
const proc = spawn("claude", allArgs, {
|
|
238
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
242
|
-
const text = chunk.toString();
|
|
243
|
-
lineBuffer += text;
|
|
244
|
-
|
|
245
|
-
const lines = lineBuffer.split("\n");
|
|
246
|
-
lineBuffer = lines.pop() || "";
|
|
247
|
-
|
|
248
|
-
for (const line of lines) {
|
|
249
|
-
processLine(line);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
254
|
-
const text = chunk.toString();
|
|
255
|
-
process.stderr.write(text);
|
|
256
|
-
logStream?.write(text);
|
|
257
|
-
output += text;
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
proc.on("close", (code: number | null) => {
|
|
261
|
-
if (lineBuffer) {
|
|
262
|
-
processLine(lineBuffer);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (output && !output.endsWith("\n")) {
|
|
266
|
-
process.stdout.write("\n");
|
|
267
|
-
logStream?.write("\n");
|
|
268
|
-
output += "\n";
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Calculate context usage percent
|
|
272
|
-
let contextUsedPercent: number | undefined;
|
|
273
|
-
if (finalUsage) {
|
|
274
|
-
const totalTokens =
|
|
275
|
-
(finalUsage.input_tokens || 0) +
|
|
276
|
-
(finalUsage.output_tokens || 0) +
|
|
277
|
-
(finalUsage.cache_read_input_tokens || 0) +
|
|
278
|
-
(finalUsage.cache_creation_input_tokens || 0);
|
|
279
|
-
const maxContext = MODEL_CONTEXT_WINDOWS.default;
|
|
280
|
-
contextUsedPercent = Math.round((totalTokens / maxContext) * 100);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
resolve({ output, exitCode: code ?? 0, contextUsedPercent, sessionId });
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
proc.on("error", (err: Error) => {
|
|
287
|
-
console.error(pc.red(`Error running claude: ${err}`));
|
|
288
|
-
logStream?.write(`Error running claude: ${err}\n`);
|
|
289
|
-
resolve({ output, exitCode: 1 });
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { RalphPlan, PlanStatus, RalphState } from "./state.js";
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// Clipboard Utility
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
export function copyToClipboard(text: string): boolean {
|
|
10
|
-
try {
|
|
11
|
-
const platform = process.platform;
|
|
12
|
-
if (platform === "darwin") {
|
|
13
|
-
execFileSync("pbcopy", [], { input: text });
|
|
14
|
-
} else if (platform === "linux") {
|
|
15
|
-
// Try xclip first, then xsel
|
|
16
|
-
try {
|
|
17
|
-
execFileSync("xclip", ["-selection", "clipboard"], { input: text });
|
|
18
|
-
} catch {
|
|
19
|
-
execFileSync("xsel", ["--clipboard", "--input"], { input: text });
|
|
20
|
-
}
|
|
21
|
-
} else if (platform === "win32") {
|
|
22
|
-
execFileSync("clip", [], { input: text });
|
|
23
|
-
} else {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Format plans as markdown with checkboxes and status badges.
|
|
34
|
-
*/
|
|
35
|
-
export function formatPlansAsMarkdown(plans: RalphPlan[]): string {
|
|
36
|
-
if (plans.length === 0) {
|
|
37
|
-
return "# Plans\n\nNo plans.\n";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const statusBadge = (status: PlanStatus): string => {
|
|
41
|
-
switch (status) {
|
|
42
|
-
case "done":
|
|
43
|
-
return "`✓ done`";
|
|
44
|
-
case "ready":
|
|
45
|
-
return "`○ ready`";
|
|
46
|
-
case "blocked":
|
|
47
|
-
return "`⏸ blocked`";
|
|
48
|
-
case "cancelled":
|
|
49
|
-
return "`✗ cancelled`";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const ready = plans.filter((p) => p.status === "ready");
|
|
54
|
-
const done = plans.filter((p) => p.status === "done");
|
|
55
|
-
|
|
56
|
-
const lines: string[] = ["# Plans", ""];
|
|
57
|
-
lines.push(
|
|
58
|
-
`**Total:** ${plans.length} | **Done:** ${done.length} | **Ready:** ${ready.length}`,
|
|
59
|
-
"",
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (ready.length > 0) {
|
|
63
|
-
lines.push("## Ready", "");
|
|
64
|
-
for (const p of ready) {
|
|
65
|
-
const errorSuffix = p.error ? ` ⚠️ ${p.error}` : "";
|
|
66
|
-
lines.push(`- [ ] **#${p.id}** ${p.planFilePath} ${statusBadge(p.status)}${errorSuffix}`);
|
|
67
|
-
}
|
|
68
|
-
lines.push("");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (done.length > 0) {
|
|
72
|
-
lines.push("## Done", "");
|
|
73
|
-
for (const p of done) {
|
|
74
|
-
lines.push(`- [x] **#${p.id}** ${p.planFilePath} ${statusBadge(p.status)}`);
|
|
75
|
-
}
|
|
76
|
-
lines.push("");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return lines.join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Format plans with markdown and optional mermaid graph.
|
|
84
|
-
*/
|
|
85
|
-
export function formatPlanAsMarkdown(plans: RalphPlan[], state: RalphState): string {
|
|
86
|
-
const lines: string[] = ["# Ralph Plan", ""];
|
|
87
|
-
|
|
88
|
-
// Summary section
|
|
89
|
-
const ready = plans.filter((p) => p.status === "ready").length;
|
|
90
|
-
const done = plans.filter((p) => p.status === "done").length;
|
|
91
|
-
|
|
92
|
-
lines.push("## Summary", "");
|
|
93
|
-
lines.push(`- **Status:** ${state.status}`);
|
|
94
|
-
lines.push(`- **Total:** ${plans.length}`);
|
|
95
|
-
lines.push(`- **Done:** ${done} | **Ready:** ${ready}`);
|
|
96
|
-
lines.push("");
|
|
97
|
-
|
|
98
|
-
// Plans section with checkboxes
|
|
99
|
-
lines.push("## Plans", "");
|
|
100
|
-
for (const p of plans) {
|
|
101
|
-
const checkbox = p.status === "done" ? "[x]" : "[ ]";
|
|
102
|
-
const status = p.status === "done" ? "`done`" : "`ready`";
|
|
103
|
-
const errorSuffix = p.error ? ` ⚠️ ${p.error}` : "";
|
|
104
|
-
lines.push(`- ${checkbox} **#${p.id}** ${p.planFilePath} ${status}${errorSuffix}`);
|
|
105
|
-
}
|
|
106
|
-
lines.push("");
|
|
107
|
-
|
|
108
|
-
// Mermaid graph section
|
|
109
|
-
lines.push("## Progress Graph", "");
|
|
110
|
-
lines.push("```mermaid");
|
|
111
|
-
lines.push("graph LR");
|
|
112
|
-
lines.push(` subgraph Progress["Plans: ${done}/${plans.length} done"]`);
|
|
113
|
-
|
|
114
|
-
for (const p of plans) {
|
|
115
|
-
const filename = path.basename(p.planFilePath);
|
|
116
|
-
const shortName = filename.length > 30 ? filename.slice(0, 27) + "..." : filename;
|
|
117
|
-
// Escape quotes in filenames
|
|
118
|
-
const safeName = shortName.replace(/"/g, "'");
|
|
119
|
-
const nodeId = `P${p.id}`;
|
|
120
|
-
|
|
121
|
-
if (p.status === "done") {
|
|
122
|
-
lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::done`);
|
|
123
|
-
} else {
|
|
124
|
-
lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::ready`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
lines.push(" end");
|
|
129
|
-
lines.push(" classDef done fill:#22c55e,color:#fff");
|
|
130
|
-
lines.push(" classDef ready fill:#94a3b8,color:#000");
|
|
131
|
-
lines.push("```");
|
|
132
|
-
lines.push("");
|
|
133
|
-
|
|
134
|
-
return lines.join("\n");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Format plans as JSON for programmatic consumption.
|
|
139
|
-
*/
|
|
140
|
-
export function formatPlanAsJson(plans: RalphPlan[], state: RalphState): string {
|
|
141
|
-
return JSON.stringify(
|
|
142
|
-
{
|
|
143
|
-
status: state.status,
|
|
144
|
-
summary: {
|
|
145
|
-
total: plans.length,
|
|
146
|
-
done: plans.filter((p) => p.status === "done").length,
|
|
147
|
-
ready: plans.filter((p) => p.status === "ready").length,
|
|
148
|
-
},
|
|
149
|
-
plans: plans.map((p) => ({
|
|
150
|
-
id: p.id,
|
|
151
|
-
planFilePath: p.planFilePath,
|
|
152
|
-
status: p.status,
|
|
153
|
-
addedAt: p.addedAt,
|
|
154
|
-
completedAt: p.completedAt,
|
|
155
|
-
error: p.error,
|
|
156
|
-
})),
|
|
157
|
-
},
|
|
158
|
-
null,
|
|
159
|
-
2,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ============================================================================
|
|
164
|
-
// Duration Formatting
|
|
165
|
-
// ============================================================================
|
|
166
|
-
|
|
167
|
-
export function formatDuration(ms: number): string {
|
|
168
|
-
const seconds = Math.floor(ms / 1000);
|
|
169
|
-
const minutes = Math.floor(seconds / 60);
|
|
170
|
-
const hours = Math.floor(minutes / 60);
|
|
171
|
-
|
|
172
|
-
if (hours > 0) {
|
|
173
|
-
const remainingMins = minutes % 60;
|
|
174
|
-
return `${hours}h ${remainingMins}m`;
|
|
175
|
-
}
|
|
176
|
-
if (minutes > 0) {
|
|
177
|
-
const remainingSecs = seconds % 60;
|
|
178
|
-
return `${minutes}m ${remainingSecs}s`;
|
|
179
|
-
}
|
|
180
|
-
return `${seconds}s`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ============================================================================
|
|
184
|
-
// Output Summary
|
|
185
|
-
// ============================================================================
|
|
186
|
-
|
|
187
|
-
export function extractOutputSummary(output: string, maxLength: number = 2000): string {
|
|
188
|
-
const lines = output
|
|
189
|
-
.split("\n")
|
|
190
|
-
.filter((l) => l.trim())
|
|
191
|
-
.slice(-5);
|
|
192
|
-
let summary = lines.join(" ").trim();
|
|
193
|
-
|
|
194
|
-
if (summary.length > maxLength) {
|
|
195
|
-
summary = summary.substring(0, maxLength) + "...";
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return summary || "(no output)";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ============================================================================
|
|
202
|
-
// Prompt Building
|
|
203
|
-
// ============================================================================
|
|
204
|
-
|
|
205
|
-
export interface BuildPromptOptions {
|
|
206
|
-
completionMarker: string;
|
|
207
|
-
taskDoneMarker: string;
|
|
208
|
-
plan: RalphPlan;
|
|
209
|
-
skipCommit?: boolean;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function buildIterationPrompt({
|
|
213
|
-
completionMarker,
|
|
214
|
-
taskDoneMarker,
|
|
215
|
-
plan,
|
|
216
|
-
skipCommit = false,
|
|
217
|
-
}: BuildPromptOptions): string {
|
|
218
|
-
let step = 1;
|
|
219
|
-
|
|
220
|
-
const prompt = `
|
|
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.
|
|
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>.
|
|
228
|
-
|
|
229
|
-
</instructions>
|
|
230
|
-
`;
|
|
231
|
-
return prompt.trim();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ============================================================================
|
|
235
|
-
// Marker Detection
|
|
236
|
-
// ============================================================================
|
|
237
|
-
|
|
238
|
-
export function detectCompletionMarker(output: string, marker: string): boolean {
|
|
239
|
-
return output.includes(marker);
|
|
240
|
-
}
|
package/src/lib/ralph/index.ts
DELETED