rw-runner 0.1.0
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/README.md +133 -0
- package/package.json +54 -0
- package/src/commands/add.ts +138 -0
- package/src/commands/init.ts +46 -0
- package/src/commands/run.ts +38 -0
- package/src/commands/status.ts +77 -0
- package/src/commands/validate.ts +44 -0
- package/src/index.ts +55 -0
- package/src/lib/checker.ts +140 -0
- package/src/lib/config.ts +89 -0
- package/src/lib/loop.ts +262 -0
- package/src/lib/runner.ts +107 -0
- package/src/lib/spec.ts +294 -0
- package/src/lib/ui.ts +117 -0
- package/src/types.ts +45 -0
package/src/lib/spec.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import type { Spec, Task, Resource, Requirement, CheckType } from "../types";
|
|
3
|
+
|
|
4
|
+
const SPEC_PATH = ".ralph/spec.md";
|
|
5
|
+
const CHANGELOG_PATH = ".ralph/changelog.md";
|
|
6
|
+
|
|
7
|
+
// Check if ralph is initialized in current directory
|
|
8
|
+
export function isInitialized(): boolean {
|
|
9
|
+
return existsSync(SPEC_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Parse spec.md into structured data
|
|
13
|
+
export function parseSpec(content: string): Spec {
|
|
14
|
+
const tasks: Task[] = [];
|
|
15
|
+
const resources: Resource[] = [];
|
|
16
|
+
|
|
17
|
+
const lines = content.split("\n");
|
|
18
|
+
let section: "none" | "tasks" | "resources" = "none";
|
|
19
|
+
let currentTask: Task | null = null;
|
|
20
|
+
let currentRequirement: Requirement | null = null;
|
|
21
|
+
let resourceBuffer: { path: string; lines: string[] } | null = null;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
|
|
27
|
+
// Section detection
|
|
28
|
+
if (trimmed === "## Tasks") {
|
|
29
|
+
section = "tasks";
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (trimmed === "## Resources") {
|
|
33
|
+
// Save any pending resource
|
|
34
|
+
if (resourceBuffer) {
|
|
35
|
+
resources.push({
|
|
36
|
+
path: resourceBuffer.path,
|
|
37
|
+
description: resourceBuffer.lines.join("\n").trim(),
|
|
38
|
+
});
|
|
39
|
+
resourceBuffer = null;
|
|
40
|
+
}
|
|
41
|
+
section = "resources";
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tasks section parsing
|
|
46
|
+
if (section === "tasks") {
|
|
47
|
+
// Task line: - [ ] description, - [x] description, - [!] description
|
|
48
|
+
const taskMatch = trimmed.match(/^- \[([ x!])\] (.+)$/);
|
|
49
|
+
if (taskMatch) {
|
|
50
|
+
// Save previous task
|
|
51
|
+
if (currentTask) {
|
|
52
|
+
if (currentRequirement) {
|
|
53
|
+
currentTask.requirements.push(currentRequirement);
|
|
54
|
+
currentRequirement = null;
|
|
55
|
+
}
|
|
56
|
+
tasks.push(currentTask);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const statusChar = taskMatch[1];
|
|
60
|
+
const description = taskMatch[2];
|
|
61
|
+
let status: Task["status"] = "pending";
|
|
62
|
+
if (statusChar === "x") status = "completed";
|
|
63
|
+
if (statusChar === "!") status = "failed";
|
|
64
|
+
|
|
65
|
+
currentTask = {
|
|
66
|
+
description,
|
|
67
|
+
status,
|
|
68
|
+
requirements: [],
|
|
69
|
+
};
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Requirement line: - [run]: `command`
|
|
74
|
+
const runMatch = trimmed.match(/^- \[run\]: `(.+)`$/);
|
|
75
|
+
if (runMatch && currentTask) {
|
|
76
|
+
// Save previous requirement
|
|
77
|
+
if (currentRequirement) {
|
|
78
|
+
currentTask.requirements.push(currentRequirement);
|
|
79
|
+
}
|
|
80
|
+
currentRequirement = {
|
|
81
|
+
command: runMatch[1],
|
|
82
|
+
checks: [],
|
|
83
|
+
};
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check lines: [expect]: value, [expect-not]: value, etc.
|
|
88
|
+
const checkMatch = trimmed.match(
|
|
89
|
+
/^\[(expect|expect-not|expect-exit|eval)\]: (.+)$/
|
|
90
|
+
);
|
|
91
|
+
if (checkMatch && currentRequirement) {
|
|
92
|
+
currentRequirement.checks.push({
|
|
93
|
+
type: checkMatch[1] as CheckType,
|
|
94
|
+
value: checkMatch[2],
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resources section parsing
|
|
101
|
+
if (section === "resources") {
|
|
102
|
+
// Resource line: [file]: `path`
|
|
103
|
+
const resourceMatch = trimmed.match(/^\[file\]: `(.+)`$/);
|
|
104
|
+
if (resourceMatch) {
|
|
105
|
+
// Save previous resource
|
|
106
|
+
if (resourceBuffer) {
|
|
107
|
+
resources.push({
|
|
108
|
+
path: resourceBuffer.path,
|
|
109
|
+
description: resourceBuffer.lines.join("\n").trim(),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
resourceBuffer = {
|
|
113
|
+
path: resourceMatch[1],
|
|
114
|
+
lines: [],
|
|
115
|
+
};
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Description lines for resource
|
|
120
|
+
if (resourceBuffer && trimmed) {
|
|
121
|
+
resourceBuffer.lines.push(trimmed);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Save final task
|
|
127
|
+
if (currentTask) {
|
|
128
|
+
if (currentRequirement) {
|
|
129
|
+
currentTask.requirements.push(currentRequirement);
|
|
130
|
+
}
|
|
131
|
+
tasks.push(currentTask);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Save final resource
|
|
135
|
+
if (resourceBuffer) {
|
|
136
|
+
resources.push({
|
|
137
|
+
path: resourceBuffer.path,
|
|
138
|
+
description: resourceBuffer.lines.join("\n").trim(),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { tasks, resources };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Read and parse spec.md
|
|
146
|
+
export function readSpec(): Spec {
|
|
147
|
+
if (!existsSync(SPEC_PATH)) {
|
|
148
|
+
throw new Error("ralph not initialized. Run `ralph init` first.");
|
|
149
|
+
}
|
|
150
|
+
const content = readFileSync(SPEC_PATH, "utf-8");
|
|
151
|
+
return parseSpec(content);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Generate spec.md content from structured data
|
|
155
|
+
export function generateSpec(spec: Spec): string {
|
|
156
|
+
let content = "# Spec\n\n## Tasks\n";
|
|
157
|
+
|
|
158
|
+
for (const task of spec.tasks) {
|
|
159
|
+
const statusChar =
|
|
160
|
+
task.status === "completed" ? "x" : task.status === "failed" ? "!" : " ";
|
|
161
|
+
content += `- [${statusChar}] ${task.description}\n`;
|
|
162
|
+
|
|
163
|
+
for (const req of task.requirements) {
|
|
164
|
+
content += ` - [run]: \`${req.command}\`\n`;
|
|
165
|
+
for (const check of req.checks) {
|
|
166
|
+
content += ` [${check.type}]: ${check.value}\n`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
content += "\n## Resources\n";
|
|
172
|
+
|
|
173
|
+
for (const resource of spec.resources) {
|
|
174
|
+
content += `[file]: \`${resource.path}\`\n`;
|
|
175
|
+
content += `${resource.description}\n\n`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return content;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Write spec to file
|
|
182
|
+
export function writeSpec(spec: Spec): void {
|
|
183
|
+
const content = generateSpec(spec);
|
|
184
|
+
writeFileSync(SPEC_PATH, content);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Update a task's status
|
|
188
|
+
export function updateTaskStatus(
|
|
189
|
+
taskDescription: string,
|
|
190
|
+
status: Task["status"]
|
|
191
|
+
): void {
|
|
192
|
+
const spec = readSpec();
|
|
193
|
+
const task = spec.tasks.find((t) => t.description === taskDescription);
|
|
194
|
+
if (task) {
|
|
195
|
+
task.status = status;
|
|
196
|
+
writeSpec(spec);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Add a task to spec
|
|
201
|
+
export function addTask(task: Task): void {
|
|
202
|
+
const spec = readSpec();
|
|
203
|
+
spec.tasks.push(task);
|
|
204
|
+
writeSpec(spec);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add a resource to spec
|
|
208
|
+
export function addResource(resource: Resource): void {
|
|
209
|
+
const spec = readSpec();
|
|
210
|
+
spec.resources.push(resource);
|
|
211
|
+
writeSpec(spec);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get pending tasks
|
|
215
|
+
export function getPendingTasks(): Task[] {
|
|
216
|
+
const spec = readSpec();
|
|
217
|
+
return spec.tasks.filter((t) => t.status === "pending");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Validate spec structure
|
|
221
|
+
export function validateSpec(): { valid: boolean; errors: string[] } {
|
|
222
|
+
const errors: string[] = [];
|
|
223
|
+
|
|
224
|
+
if (!existsSync(SPEC_PATH)) {
|
|
225
|
+
return { valid: false, errors: ["spec.md not found"] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const content = readFileSync(SPEC_PATH, "utf-8");
|
|
230
|
+
|
|
231
|
+
if (!content.includes("## Tasks")) {
|
|
232
|
+
errors.push("Missing ## Tasks section");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!content.includes("## Resources")) {
|
|
236
|
+
errors.push("Missing ## Resources section");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const spec = parseSpec(content);
|
|
240
|
+
|
|
241
|
+
// Validate tasks have proper structure
|
|
242
|
+
for (const task of spec.tasks) {
|
|
243
|
+
for (const req of task.requirements) {
|
|
244
|
+
if (!req.command) {
|
|
245
|
+
errors.push(`Task "${task.description}": [run] missing command`);
|
|
246
|
+
}
|
|
247
|
+
if (req.checks.length === 0) {
|
|
248
|
+
errors.push(
|
|
249
|
+
`Task "${task.description}": [run] \`${req.command}\` has no checks`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Validate resources have descriptions
|
|
256
|
+
for (const resource of spec.resources) {
|
|
257
|
+
if (!resource.description) {
|
|
258
|
+
errors.push(`Resource "${resource.path}": missing description`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
errors.push(`Parse error: ${(e as Error).message}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { valid: errors.length === 0, errors };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Get default spec template
|
|
269
|
+
export function getSpecTemplate(): string {
|
|
270
|
+
return `# Spec
|
|
271
|
+
|
|
272
|
+
## Tasks
|
|
273
|
+
<!-- Add tasks here with: ralph add task -->
|
|
274
|
+
|
|
275
|
+
## Resources
|
|
276
|
+
<!-- Add resources here with: ralph add resource -->
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Read changelog
|
|
281
|
+
export function readChangelog(): string {
|
|
282
|
+
if (!existsSync(CHANGELOG_PATH)) {
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
return readFileSync(CHANGELOG_PATH, "utf-8");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Append to changelog
|
|
289
|
+
export function appendChangelog(entry: string): void {
|
|
290
|
+
const existing = existsSync(CHANGELOG_PATH)
|
|
291
|
+
? readFileSync(CHANGELOG_PATH, "utf-8")
|
|
292
|
+
: "# Changelog\n\n";
|
|
293
|
+
writeFileSync(CHANGELOG_PATH, existing + entry + "\n");
|
|
294
|
+
}
|
package/src/lib/ui.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
|
|
3
|
+
// Modern CLI UI utilities
|
|
4
|
+
export const ui = {
|
|
5
|
+
// Branding
|
|
6
|
+
logo: () => {
|
|
7
|
+
console.log();
|
|
8
|
+
console.log(pc.bold(pc.magenta(" ╱╲ rw")));
|
|
9
|
+
console.log(pc.dim(" ╲╱ autonomous task runner"));
|
|
10
|
+
console.log();
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
// Section headers
|
|
14
|
+
header: (text: string) => {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(pc.bold(pc.cyan(`▸ ${text}`)));
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Success messages
|
|
20
|
+
success: (text: string) => {
|
|
21
|
+
console.log(pc.green(` ✓ ${text}`));
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Error messages
|
|
25
|
+
error: (text: string) => {
|
|
26
|
+
console.log(pc.red(` ✗ ${text}`));
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Warning messages
|
|
30
|
+
warn: (text: string) => {
|
|
31
|
+
console.log(pc.yellow(` ⚠ ${text}`));
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Info messages
|
|
35
|
+
info: (text: string) => {
|
|
36
|
+
console.log(pc.dim(` ${text}`));
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Dim text
|
|
40
|
+
dim: (text: string) => {
|
|
41
|
+
console.log(pc.dim(` ${text}`));
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Task status icons
|
|
45
|
+
taskIcon: (status: "pending" | "completed" | "failed") => {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case "completed":
|
|
48
|
+
return pc.green("✓");
|
|
49
|
+
case "failed":
|
|
50
|
+
return pc.red("!");
|
|
51
|
+
case "pending":
|
|
52
|
+
return pc.dim("○");
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Divider
|
|
57
|
+
divider: () => {
|
|
58
|
+
console.log(pc.dim(" ─────────────────────────────────────────"));
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Box around text
|
|
62
|
+
box: (text: string, color: "cyan" | "green" | "red" | "yellow" = "cyan") => {
|
|
63
|
+
const colorFn = pc[color];
|
|
64
|
+
const lines = text.split("\n");
|
|
65
|
+
const maxLen = Math.max(...lines.map((l) => l.length));
|
|
66
|
+
const top = colorFn(` ╭${"─".repeat(maxLen + 2)}╮`);
|
|
67
|
+
const bottom = colorFn(` ╰${"─".repeat(maxLen + 2)}╯`);
|
|
68
|
+
const middle = lines
|
|
69
|
+
.map((l) => colorFn(" │") + ` ${l.padEnd(maxLen)} ` + colorFn("│"))
|
|
70
|
+
.join("\n");
|
|
71
|
+
console.log(top);
|
|
72
|
+
console.log(middle);
|
|
73
|
+
console.log(bottom);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Progress display
|
|
77
|
+
progress: (current: number, total: number, label: string) => {
|
|
78
|
+
const pct = Math.round((current / total) * 100);
|
|
79
|
+
const filled = Math.round((current / total) * 20);
|
|
80
|
+
const empty = 20 - filled;
|
|
81
|
+
const bar = pc.cyan("█".repeat(filled)) + pc.dim("░".repeat(empty));
|
|
82
|
+
console.log(` ${bar} ${pct}% ${pc.dim(label)}`);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Hotkey hint
|
|
86
|
+
hotkeys: () => {
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(
|
|
89
|
+
pc.dim(" Press ") +
|
|
90
|
+
pc.bold(pc.yellow("t")) +
|
|
91
|
+
pc.dim(" to takeover, ") +
|
|
92
|
+
pc.bold(pc.yellow("q")) +
|
|
93
|
+
pc.dim(" to quit")
|
|
94
|
+
);
|
|
95
|
+
console.log();
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Clear line
|
|
99
|
+
clearLine: () => {
|
|
100
|
+
process.stdout.write("\r\x1b[K");
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Newline
|
|
104
|
+
newline: () => {
|
|
105
|
+
console.log();
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Bold text
|
|
109
|
+
bold: (text: string) => pc.bold(text),
|
|
110
|
+
|
|
111
|
+
// Colored text helpers
|
|
112
|
+
cyan: (text: string) => pc.cyan(text),
|
|
113
|
+
green: (text: string) => pc.green(text),
|
|
114
|
+
red: (text: string) => pc.red(text),
|
|
115
|
+
yellow: (text: string) => pc.yellow(text),
|
|
116
|
+
magenta: (text: string) => pc.magenta(text),
|
|
117
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Task requirement check types
|
|
2
|
+
export type CheckType = "expect" | "expect-not" | "expect-exit" | "eval";
|
|
3
|
+
|
|
4
|
+
export interface Requirement {
|
|
5
|
+
command: string;
|
|
6
|
+
checks: {
|
|
7
|
+
type: CheckType;
|
|
8
|
+
value: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Task {
|
|
13
|
+
description: string;
|
|
14
|
+
status: "pending" | "completed" | "failed";
|
|
15
|
+
requirements: Requirement[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Resource {
|
|
19
|
+
path: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Spec {
|
|
24
|
+
tasks: Task[];
|
|
25
|
+
resources: Resource[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Config {
|
|
29
|
+
model: string;
|
|
30
|
+
maxRetries: number;
|
|
31
|
+
loopDelay: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CheckResult {
|
|
35
|
+
passed: boolean;
|
|
36
|
+
output: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TaskResult {
|
|
41
|
+
task: Task;
|
|
42
|
+
success: boolean;
|
|
43
|
+
attempts: number;
|
|
44
|
+
output: string;
|
|
45
|
+
}
|