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.
@@ -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
+ }