ralphflow 0.3.0 → 0.4.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/README.md +67 -27
- package/dist/chunk-GVOJO5IN.js +274 -0
- package/dist/ralphflow.js +301 -233
- package/dist/server-O6J52DZT.js +323 -0
- package/package.json +7 -2
- package/src/dashboard/ui/index.html +838 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +19 -11
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +7 -5
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +4 -2
- package/src/templates/research/loops/00-discovery-loop/prompt.md +7 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +7 -5
- package/src/templates/research/loops/02-story-loop/prompt.md +4 -2
- package/src/templates/research/loops/03-document-loop/prompt.md +4 -2
package/dist/ralphflow.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getDb,
|
|
4
|
+
incrementIteration,
|
|
5
|
+
isLoopComplete,
|
|
6
|
+
listFlows,
|
|
7
|
+
loadConfig,
|
|
8
|
+
markLoopComplete,
|
|
9
|
+
markLoopRunning,
|
|
10
|
+
resolveFlowDir,
|
|
11
|
+
resolveLoop,
|
|
12
|
+
showStatus
|
|
13
|
+
} from "./chunk-GVOJO5IN.js";
|
|
2
14
|
|
|
3
15
|
// src/cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
16
|
+
import { Command as Command6 } from "commander";
|
|
17
|
+
import chalk8 from "chalk";
|
|
6
18
|
|
|
7
19
|
// src/cli/init.ts
|
|
8
20
|
import { Command } from "commander";
|
|
@@ -72,7 +84,7 @@ async function initProject(cwd, options = {}) {
|
|
|
72
84
|
return;
|
|
73
85
|
}
|
|
74
86
|
if (existsSync2(ralphFlowDir)) {
|
|
75
|
-
const existing =
|
|
87
|
+
const existing = listFlows2(ralphFlowDir);
|
|
76
88
|
if (existing.length > 0 && !options.template) {
|
|
77
89
|
console.log();
|
|
78
90
|
console.log(chalk.bold(" Existing flows:"));
|
|
@@ -115,7 +127,7 @@ async function initProject(cwd, options = {}) {
|
|
|
115
127
|
console.log(chalk.dim(` Next: npx ralphflow run story --flow ${flowName}`));
|
|
116
128
|
console.log();
|
|
117
129
|
}
|
|
118
|
-
function
|
|
130
|
+
function listFlows2(ralphFlowDir) {
|
|
119
131
|
try {
|
|
120
132
|
return readdirSync(ralphFlowDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
121
133
|
} catch {
|
|
@@ -141,91 +153,10 @@ import { Command as Command2 } from "commander";
|
|
|
141
153
|
import chalk4 from "chalk";
|
|
142
154
|
|
|
143
155
|
// src/core/runner.ts
|
|
144
|
-
import { readFileSync as
|
|
145
|
-
import { join as
|
|
156
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync, readdirSync as readdirSync2, unlinkSync } from "fs";
|
|
157
|
+
import { join as join3, basename } from "path";
|
|
146
158
|
import chalk3 from "chalk";
|
|
147
159
|
|
|
148
|
-
// src/core/config.ts
|
|
149
|
-
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
|
|
150
|
-
import { join as join3 } from "path";
|
|
151
|
-
import { parse as parseYaml } from "yaml";
|
|
152
|
-
var LOOP_ALIASES = {
|
|
153
|
-
// code-implementation aliases
|
|
154
|
-
story: "story-loop",
|
|
155
|
-
stories: "story-loop",
|
|
156
|
-
tasks: "tasks-loop",
|
|
157
|
-
task: "tasks-loop",
|
|
158
|
-
delivery: "delivery-loop",
|
|
159
|
-
deliver: "delivery-loop",
|
|
160
|
-
// research aliases
|
|
161
|
-
discovery: "discovery-loop",
|
|
162
|
-
discover: "discovery-loop",
|
|
163
|
-
research: "research-loop",
|
|
164
|
-
document: "document-loop",
|
|
165
|
-
doc: "document-loop"
|
|
166
|
-
};
|
|
167
|
-
function listFlows2(cwd) {
|
|
168
|
-
const baseDir = join3(cwd, ".ralph-flow");
|
|
169
|
-
if (!existsSync3(baseDir)) return [];
|
|
170
|
-
return readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).filter((d) => existsSync3(join3(baseDir, d.name, "ralphflow.yaml"))).map((d) => d.name);
|
|
171
|
-
}
|
|
172
|
-
function resolveFlowDir(cwd, flowName) {
|
|
173
|
-
const baseDir = join3(cwd, ".ralph-flow");
|
|
174
|
-
if (!existsSync3(baseDir)) {
|
|
175
|
-
throw new Error("No .ralph-flow/ found. Run `npx ralphflow init` first.");
|
|
176
|
-
}
|
|
177
|
-
const flows = listFlows2(cwd);
|
|
178
|
-
if (flows.length === 0) {
|
|
179
|
-
throw new Error("No flows found in .ralph-flow/. Run `npx ralphflow init` first.");
|
|
180
|
-
}
|
|
181
|
-
if (flowName) {
|
|
182
|
-
if (!flows.includes(flowName)) {
|
|
183
|
-
throw new Error(`Flow "${flowName}" not found. Available: ${flows.join(", ")}`);
|
|
184
|
-
}
|
|
185
|
-
return join3(baseDir, flowName);
|
|
186
|
-
}
|
|
187
|
-
if (flows.length === 1) {
|
|
188
|
-
return join3(baseDir, flows[0]);
|
|
189
|
-
}
|
|
190
|
-
throw new Error(
|
|
191
|
-
`Multiple flows found: ${flows.join(", ")}. Use --flow <name> to specify which one.`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
function loadConfig(flowDir) {
|
|
195
|
-
const configPath = join3(flowDir, "ralphflow.yaml");
|
|
196
|
-
if (!existsSync3(configPath)) {
|
|
197
|
-
throw new Error(`No ralphflow.yaml found in ${flowDir}`);
|
|
198
|
-
}
|
|
199
|
-
const raw = readFileSync2(configPath, "utf-8");
|
|
200
|
-
const config = parseYaml(raw);
|
|
201
|
-
if (!config.name) {
|
|
202
|
-
throw new Error('ralphflow.yaml: missing required field "name"');
|
|
203
|
-
}
|
|
204
|
-
if (!config.loops || Object.keys(config.loops).length === 0) {
|
|
205
|
-
throw new Error('ralphflow.yaml: missing required field "loops"');
|
|
206
|
-
}
|
|
207
|
-
if (!config.dir) {
|
|
208
|
-
config.dir = ".ralph-flow";
|
|
209
|
-
}
|
|
210
|
-
return config;
|
|
211
|
-
}
|
|
212
|
-
function resolveLoop(config, name) {
|
|
213
|
-
if (config.loops[name]) {
|
|
214
|
-
return { key: name, loop: config.loops[name] };
|
|
215
|
-
}
|
|
216
|
-
const aliased = LOOP_ALIASES[name.toLowerCase()];
|
|
217
|
-
if (aliased && config.loops[aliased]) {
|
|
218
|
-
return { key: aliased, loop: config.loops[aliased] };
|
|
219
|
-
}
|
|
220
|
-
for (const [key, loop] of Object.entries(config.loops)) {
|
|
221
|
-
if (key.startsWith(name) || loop.name.toLowerCase().includes(name.toLowerCase())) {
|
|
222
|
-
return { key, loop };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const available = Object.keys(config.loops).join(", ");
|
|
226
|
-
throw new Error(`Unknown loop "${name}". Available: ${available}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
160
|
// src/core/claude.ts
|
|
230
161
|
import { spawn } from "child_process";
|
|
231
162
|
async function spawnClaude(options) {
|
|
@@ -255,8 +186,8 @@ async function spawnClaude(options) {
|
|
|
255
186
|
|
|
256
187
|
// src/core/runner.ts
|
|
257
188
|
function agentsDir(flowDir, loop) {
|
|
258
|
-
const loopDir =
|
|
259
|
-
return
|
|
189
|
+
const loopDir = join3(flowDir, loop.tracker, "..");
|
|
190
|
+
return join3(loopDir, ".agents");
|
|
260
191
|
}
|
|
261
192
|
function isProcessAlive(pid) {
|
|
262
193
|
try {
|
|
@@ -267,13 +198,13 @@ function isProcessAlive(pid) {
|
|
|
267
198
|
}
|
|
268
199
|
}
|
|
269
200
|
function cleanStaleAgents(dir) {
|
|
270
|
-
if (!
|
|
271
|
-
for (const file of
|
|
201
|
+
if (!existsSync3(dir)) return;
|
|
202
|
+
for (const file of readdirSync2(dir)) {
|
|
272
203
|
if (!file.endsWith(".lock")) continue;
|
|
273
|
-
const pidStr =
|
|
204
|
+
const pidStr = readFileSync2(join3(dir, file), "utf-8").trim();
|
|
274
205
|
const pid = parseInt(pidStr, 10);
|
|
275
206
|
if (isNaN(pid) || !isProcessAlive(pid)) {
|
|
276
|
-
unlinkSync(
|
|
207
|
+
unlinkSync(join3(dir, file));
|
|
277
208
|
}
|
|
278
209
|
}
|
|
279
210
|
}
|
|
@@ -281,8 +212,8 @@ function acquireAgentId(dir, maxAgents) {
|
|
|
281
212
|
mkdirSync2(dir, { recursive: true });
|
|
282
213
|
cleanStaleAgents(dir);
|
|
283
214
|
for (let n = 1; n <= maxAgents; n++) {
|
|
284
|
-
const lockFile =
|
|
285
|
-
if (!
|
|
215
|
+
const lockFile = join3(dir, `agent-${n}.lock`);
|
|
216
|
+
if (!existsSync3(lockFile)) {
|
|
286
217
|
writeFileSync(lockFile, String(process.pid));
|
|
287
218
|
return `agent-${n}`;
|
|
288
219
|
}
|
|
@@ -290,17 +221,39 @@ function acquireAgentId(dir, maxAgents) {
|
|
|
290
221
|
throw new Error(`All ${maxAgents} agent slots are occupied. Wait for one to finish or increase max_agents.`);
|
|
291
222
|
}
|
|
292
223
|
function releaseAgentId(dir, agentName) {
|
|
293
|
-
const lockFile =
|
|
224
|
+
const lockFile = join3(dir, `${agentName}.lock`);
|
|
294
225
|
try {
|
|
295
226
|
unlinkSync(lockFile);
|
|
296
227
|
} catch {
|
|
297
228
|
}
|
|
298
229
|
}
|
|
299
230
|
function checkTrackerForCompletion(flowDir, loop) {
|
|
300
|
-
const trackerPath =
|
|
301
|
-
if (!
|
|
302
|
-
const content =
|
|
303
|
-
return content.includes(`<promise>${loop.completion}</promise>`);
|
|
231
|
+
const trackerPath = join3(flowDir, loop.tracker);
|
|
232
|
+
if (!existsSync3(trackerPath)) return false;
|
|
233
|
+
const content = readFileSync2(trackerPath, "utf-8");
|
|
234
|
+
return content.includes(`<promise>${loop.completion}</promise>`) || content.includes(loop.completion);
|
|
235
|
+
}
|
|
236
|
+
function checkTrackerMetadataCompletion(flowDir, loop) {
|
|
237
|
+
const trackerPath = join3(flowDir, loop.tracker);
|
|
238
|
+
if (!existsSync3(trackerPath)) return false;
|
|
239
|
+
const content = readFileSync2(trackerPath, "utf-8");
|
|
240
|
+
const completedMatch = content.match(/^- completed_(?:tasks|stories): \[(.+)\]$/m);
|
|
241
|
+
if (!completedMatch) return false;
|
|
242
|
+
const completedItems = completedMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
243
|
+
if (completedItems.length === 0) return false;
|
|
244
|
+
const inProgressLines = content.match(/\{[^}]*status:\s*in_progress[^}]*\}/g);
|
|
245
|
+
if (inProgressLines && inProgressLines.length > 0) return false;
|
|
246
|
+
const pendingLines = content.match(/\{[^}]*status:\s*pending[^}]*\}/g);
|
|
247
|
+
if (pendingLines && pendingLines.length > 0) return false;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
function checkTrackerCheckboxes(flowDir, loop) {
|
|
251
|
+
const trackerPath = join3(flowDir, loop.tracker);
|
|
252
|
+
if (!existsSync3(trackerPath)) return false;
|
|
253
|
+
const content = readFileSync2(trackerPath, "utf-8");
|
|
254
|
+
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
255
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
256
|
+
return checked > 0 && unchecked === 0;
|
|
304
257
|
}
|
|
305
258
|
async function runLoop(loopName, options) {
|
|
306
259
|
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
@@ -341,8 +294,18 @@ async function runLoop(loopName, options) {
|
|
|
341
294
|
cleanup();
|
|
342
295
|
}
|
|
343
296
|
}
|
|
344
|
-
async function iterationLoop(loop, flowDir, options, agentName) {
|
|
297
|
+
async function iterationLoop(loop, flowDir, options, agentName, db, flowName) {
|
|
298
|
+
const loopKey = loop.name;
|
|
345
299
|
for (let i = 1; i <= options.maxIterations; i++) {
|
|
300
|
+
if (db && flowName && isLoopComplete(db, flowName, loopKey)) {
|
|
301
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 already complete`));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerCheckboxes(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop)) {
|
|
305
|
+
if (db && flowName) markLoopComplete(db, flowName, loopKey);
|
|
306
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete`));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
346
309
|
const label = agentName ? chalk3.dim(` [${agentName}] Iteration ${i}/${options.maxIterations}`) : chalk3.dim(` Iteration ${i}/${options.maxIterations}`);
|
|
347
310
|
console.log(label);
|
|
348
311
|
const prompt = readPrompt(loop, flowDir, agentName);
|
|
@@ -351,7 +314,9 @@ async function iterationLoop(loop, flowDir, options, agentName) {
|
|
|
351
314
|
model: options.model,
|
|
352
315
|
cwd: options.cwd
|
|
353
316
|
});
|
|
354
|
-
if (
|
|
317
|
+
if (db && flowName) incrementIteration(db, flowName, loopKey);
|
|
318
|
+
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerCheckboxes(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop)) {
|
|
319
|
+
if (db && flowName) markLoopComplete(db, flowName, loopKey);
|
|
355
320
|
console.log();
|
|
356
321
|
console.log(chalk3.green(` Loop complete: ${loop.completion}`));
|
|
357
322
|
return;
|
|
@@ -371,9 +336,56 @@ async function iterationLoop(loop, flowDir, options, agentName) {
|
|
|
371
336
|
}
|
|
372
337
|
console.log(chalk3.yellow(` Max iterations (${options.maxIterations}) reached.`));
|
|
373
338
|
}
|
|
339
|
+
async function runAllLoops(options) {
|
|
340
|
+
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
341
|
+
const config = loadConfig(flowDir);
|
|
342
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
343
|
+
console.log();
|
|
344
|
+
console.log(chalk3.bold(" RalphFlow \u2014 Running all loops"));
|
|
345
|
+
console.log();
|
|
346
|
+
for (const [key, loop] of sortedLoops) {
|
|
347
|
+
console.log(chalk3.bold(` Starting: ${loop.name}`));
|
|
348
|
+
await iterationLoop(loop, flowDir, options);
|
|
349
|
+
}
|
|
350
|
+
console.log(chalk3.green(" All loops complete."));
|
|
351
|
+
}
|
|
352
|
+
async function runE2E(options) {
|
|
353
|
+
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
354
|
+
const config = loadConfig(flowDir);
|
|
355
|
+
const flowName = options.flow || basename(flowDir);
|
|
356
|
+
const db = getDb(options.cwd);
|
|
357
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
358
|
+
console.log();
|
|
359
|
+
console.log(chalk3.bold(" RalphFlow \u2014 E2E"));
|
|
360
|
+
console.log();
|
|
361
|
+
for (const [key, loop] of sortedLoops) {
|
|
362
|
+
const loopKey = loop.name;
|
|
363
|
+
if (isLoopComplete(db, flowName, loopKey)) {
|
|
364
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerCheckboxes(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop)) {
|
|
368
|
+
markLoopComplete(db, flowName, loopKey);
|
|
369
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
markLoopRunning(db, flowName, loopKey);
|
|
373
|
+
console.log(chalk3.bold(` \u2192 ${loop.name}`));
|
|
374
|
+
await iterationLoop(loop, flowDir, options, void 0, db, flowName);
|
|
375
|
+
if (isLoopComplete(db, flowName, loopKey)) {
|
|
376
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 done`));
|
|
377
|
+
} else {
|
|
378
|
+
console.log(chalk3.yellow(` \u26A0 ${loop.name} \u2014 max iterations, advancing`));
|
|
379
|
+
}
|
|
380
|
+
console.log();
|
|
381
|
+
}
|
|
382
|
+
console.log(chalk3.green(" \u2713 E2E complete"));
|
|
383
|
+
}
|
|
374
384
|
function readPrompt(loop, flowDir, agentName) {
|
|
375
|
-
const promptPath =
|
|
376
|
-
let prompt =
|
|
385
|
+
const promptPath = join3(flowDir, loop.prompt);
|
|
386
|
+
let prompt = readFileSync2(promptPath, "utf-8");
|
|
387
|
+
const appName = basename(flowDir);
|
|
388
|
+
prompt = prompt.replaceAll("{{APP_NAME}}", appName);
|
|
377
389
|
if (agentName && loop.multi_agent !== false) {
|
|
378
390
|
const ma = loop.multi_agent;
|
|
379
391
|
if (ma.agent_placeholder) {
|
|
@@ -384,8 +396,13 @@ function readPrompt(loop, flowDir, agentName) {
|
|
|
384
396
|
}
|
|
385
397
|
|
|
386
398
|
// src/cli/run.ts
|
|
387
|
-
var runCommand = new Command2("run").description("Run a loop").argument("<loop>", "Loop to run (story, tasks, delivery, discovery, research, document)").option("--multi-agent", "Run as a multi-agent instance (auto-assigns agent ID)").option("-m, --model <model>", "Claude model to use").option("-n, --max-iterations <n>", "Maximum iterations", "30").option("-f, --flow <name>", "Which flow to run (auto-detected if only one)").action(async (loop, opts) => {
|
|
399
|
+
var runCommand = new Command2("run").description("Run a loop").argument("<loop>", "Loop to run (story, tasks, delivery, discovery, research, document)").option("--multi-agent", "Run as a multi-agent instance (auto-assigns agent ID)").option("-m, --model <model>", "Claude model to use").option("-n, --max-iterations <n>", "Maximum iterations", "30").option("-f, --flow <name>", "Which flow to run (auto-detected if only one)").option("--ui", "Start web dashboard alongside execution").action(async (loop, opts) => {
|
|
388
400
|
try {
|
|
401
|
+
let dashboardHandle;
|
|
402
|
+
if (opts.ui) {
|
|
403
|
+
const { startDashboard } = await import("./server-O6J52DZT.js");
|
|
404
|
+
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
405
|
+
}
|
|
389
406
|
await runLoop(loop, {
|
|
390
407
|
multiAgent: !!opts.multiAgent,
|
|
391
408
|
model: opts.model,
|
|
@@ -402,137 +419,36 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
|
|
|
402
419
|
}
|
|
403
420
|
});
|
|
404
421
|
|
|
405
|
-
// src/cli/
|
|
422
|
+
// src/cli/e2e.ts
|
|
406
423
|
import { Command as Command3 } from "commander";
|
|
407
|
-
import chalk6 from "chalk";
|
|
408
|
-
|
|
409
|
-
// src/core/status.ts
|
|
410
|
-
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
411
|
-
import { join as join5 } from "path";
|
|
412
424
|
import chalk5 from "chalk";
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
console.log();
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
for (const flow of flows) {
|
|
423
|
-
const flowDir = resolveFlowDir(cwd, flow);
|
|
424
|
-
const config = loadConfig(flowDir);
|
|
425
|
-
console.log();
|
|
426
|
-
console.log(chalk5.bold(` RalphFlow \u2014 ${flow}`));
|
|
427
|
-
console.log();
|
|
428
|
-
const table = new Table({
|
|
429
|
-
chars: {
|
|
430
|
-
top: "",
|
|
431
|
-
"top-mid": "",
|
|
432
|
-
"top-left": "",
|
|
433
|
-
"top-right": "",
|
|
434
|
-
bottom: "",
|
|
435
|
-
"bottom-mid": "",
|
|
436
|
-
"bottom-left": "",
|
|
437
|
-
"bottom-right": "",
|
|
438
|
-
left: " ",
|
|
439
|
-
"left-mid": "",
|
|
440
|
-
mid: "",
|
|
441
|
-
"mid-mid": "",
|
|
442
|
-
right: "",
|
|
443
|
-
"right-mid": "",
|
|
444
|
-
middle: " "
|
|
445
|
-
},
|
|
446
|
-
style: { "padding-left": 0, "padding-right": 1 },
|
|
447
|
-
head: [
|
|
448
|
-
chalk5.dim("Loop"),
|
|
449
|
-
chalk5.dim("Stage"),
|
|
450
|
-
chalk5.dim("Active"),
|
|
451
|
-
chalk5.dim("Progress")
|
|
452
|
-
]
|
|
453
|
-
});
|
|
454
|
-
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
455
|
-
for (const [key, loop] of sortedLoops) {
|
|
456
|
-
const status = parseTracker(loop.tracker, flowDir, loop.name);
|
|
457
|
-
table.push([
|
|
458
|
-
loop.name,
|
|
459
|
-
status.stage,
|
|
460
|
-
status.active,
|
|
461
|
-
`${status.completed}/${status.total}`
|
|
462
|
-
]);
|
|
463
|
-
if (status.agents && status.agents.length > 0) {
|
|
464
|
-
for (const agent of status.agents) {
|
|
465
|
-
table.push([
|
|
466
|
-
chalk5.dim(` ${agent.name}`),
|
|
467
|
-
chalk5.dim(agent.stage),
|
|
468
|
-
chalk5.dim(agent.activeTask),
|
|
469
|
-
chalk5.dim(agent.lastHeartbeat)
|
|
470
|
-
]);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
console.log(table.toString());
|
|
475
|
-
}
|
|
476
|
-
console.log();
|
|
477
|
-
}
|
|
478
|
-
function parseTracker(trackerPath, flowDir, loopName) {
|
|
479
|
-
const fullPath = join5(flowDir, trackerPath);
|
|
480
|
-
const status = {
|
|
481
|
-
loop: loopName,
|
|
482
|
-
stage: "\u2014",
|
|
483
|
-
active: "none",
|
|
484
|
-
completed: 0,
|
|
485
|
-
total: 0
|
|
486
|
-
};
|
|
487
|
-
if (!existsSync5(fullPath)) {
|
|
488
|
-
return status;
|
|
489
|
-
}
|
|
490
|
-
const content = readFileSync4(fullPath, "utf-8");
|
|
491
|
-
const lines = content.split("\n");
|
|
492
|
-
for (const line of lines) {
|
|
493
|
-
const metaMatch = line.match(/^- (\w[\w_]*): (.+)$/);
|
|
494
|
-
if (metaMatch) {
|
|
495
|
-
const [, key, value] = metaMatch;
|
|
496
|
-
if (key === "stage") status.stage = value.trim();
|
|
497
|
-
if (key === "active_story" || key === "active_task") status.active = value.trim();
|
|
498
|
-
if (key === "completed_stories" || key === "completed_tasks") {
|
|
499
|
-
const arrayMatch = value.match(/\[(.+)\]/);
|
|
500
|
-
if (arrayMatch) {
|
|
501
|
-
status.completed = arrayMatch[1].split(",").filter((s) => s.trim()).length;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
507
|
-
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
508
|
-
if (unchecked + checked > 0) {
|
|
509
|
-
status.total = unchecked + checked;
|
|
510
|
-
status.completed = checked;
|
|
511
|
-
}
|
|
512
|
-
const agentTableMatch = content.match(/\| agent \|.*\n\|[-|]+\n((?:\|.*\n)*)/);
|
|
513
|
-
if (agentTableMatch) {
|
|
514
|
-
const agentRows = agentTableMatch[1].trim().split("\n");
|
|
515
|
-
status.agents = [];
|
|
516
|
-
for (const row of agentRows) {
|
|
517
|
-
const cells = row.split("|").map((s) => s.trim()).filter(Boolean);
|
|
518
|
-
if (cells.length >= 4) {
|
|
519
|
-
status.agents.push({
|
|
520
|
-
name: cells[0],
|
|
521
|
-
activeTask: cells[1],
|
|
522
|
-
stage: cells[2],
|
|
523
|
-
lastHeartbeat: cells[3]
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
if (status.agents.length === 0) {
|
|
528
|
-
status.agents = void 0;
|
|
425
|
+
var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with SQLite orchestration (skips completed loops)").option("-m, --model <model>", "Claude model to use").option("-n, --max-iterations <n>", "Maximum iterations per loop", "30").option("-f, --flow <name>", "Which flow to run (auto-detected if only one)").option("--ui", "Start web dashboard alongside execution").action(async (opts) => {
|
|
426
|
+
try {
|
|
427
|
+
let dashboardHandle;
|
|
428
|
+
if (opts.ui) {
|
|
429
|
+
const { startDashboard } = await import("./server-O6J52DZT.js");
|
|
430
|
+
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
529
431
|
}
|
|
432
|
+
await runE2E({
|
|
433
|
+
multiAgent: false,
|
|
434
|
+
model: opts.model,
|
|
435
|
+
maxIterations: parseInt(opts.maxIterations, 10),
|
|
436
|
+
flow: opts.flow,
|
|
437
|
+
cwd: process.cwd()
|
|
438
|
+
});
|
|
439
|
+
} catch (err) {
|
|
440
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
441
|
+
console.error(chalk5.red(`
|
|
442
|
+
${msg}
|
|
443
|
+
`));
|
|
444
|
+
process.exit(1);
|
|
530
445
|
}
|
|
531
|
-
|
|
532
|
-
}
|
|
446
|
+
});
|
|
533
447
|
|
|
534
448
|
// src/cli/status.ts
|
|
535
|
-
|
|
449
|
+
import { Command as Command4 } from "commander";
|
|
450
|
+
import chalk6 from "chalk";
|
|
451
|
+
var statusCommand = new Command4("status").description("Show pipeline status").option("-f, --flow <name>", "Show status for a specific flow").action(async (opts) => {
|
|
536
452
|
try {
|
|
537
453
|
await showStatus(process.cwd(), opts.flow);
|
|
538
454
|
} catch (err) {
|
|
@@ -544,18 +460,170 @@ var statusCommand = new Command3("status").description("Show pipeline status").o
|
|
|
544
460
|
}
|
|
545
461
|
});
|
|
546
462
|
|
|
463
|
+
// src/cli/dashboard.ts
|
|
464
|
+
import { Command as Command5 } from "commander";
|
|
465
|
+
var dashboardCommand = new Command5("dashboard").alias("ui").description("Start the web dashboard").option("-p, --port <port>", "Port number", "4242").action(async (opts) => {
|
|
466
|
+
const { startDashboard } = await import("./server-O6J52DZT.js");
|
|
467
|
+
await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// src/cli/menu.ts
|
|
471
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
472
|
+
import chalk7 from "chalk";
|
|
473
|
+
async function interactiveMenu(cwd) {
|
|
474
|
+
console.log();
|
|
475
|
+
console.log(chalk7.bold(" RalphFlow"));
|
|
476
|
+
console.log();
|
|
477
|
+
try {
|
|
478
|
+
const flows = listFlows(cwd);
|
|
479
|
+
if (flows.length === 0) {
|
|
480
|
+
const action = await select({
|
|
481
|
+
message: "What would you like to do?",
|
|
482
|
+
choices: [
|
|
483
|
+
{ name: "Initialize a new app", value: "init" }
|
|
484
|
+
]
|
|
485
|
+
});
|
|
486
|
+
if (action === "init") {
|
|
487
|
+
await handleInit(cwd);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
const action = await select({
|
|
491
|
+
message: "What would you like to do?",
|
|
492
|
+
choices: [
|
|
493
|
+
{ name: "Run end-to-end", value: "e2e" },
|
|
494
|
+
{ name: "Run a loop", value: "run" },
|
|
495
|
+
{ name: "Run all loops in sequence", value: "run-all" },
|
|
496
|
+
{ name: "Initialize a new app", value: "init" },
|
|
497
|
+
{ name: "Check status", value: "status" },
|
|
498
|
+
{ name: "Open dashboard", value: "dashboard" }
|
|
499
|
+
]
|
|
500
|
+
});
|
|
501
|
+
switch (action) {
|
|
502
|
+
case "e2e":
|
|
503
|
+
await handleE2E(cwd, flows);
|
|
504
|
+
break;
|
|
505
|
+
case "run":
|
|
506
|
+
await handleRunLoop(cwd, flows);
|
|
507
|
+
break;
|
|
508
|
+
case "run-all":
|
|
509
|
+
await handleRunAll(cwd, flows);
|
|
510
|
+
break;
|
|
511
|
+
case "init":
|
|
512
|
+
await handleInit(cwd);
|
|
513
|
+
break;
|
|
514
|
+
case "status":
|
|
515
|
+
await handleStatus(cwd);
|
|
516
|
+
break;
|
|
517
|
+
case "dashboard":
|
|
518
|
+
await handleDashboard(cwd);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
524
|
+
console.log();
|
|
525
|
+
console.log(chalk7.dim(" Cancelled."));
|
|
526
|
+
process.exit(0);
|
|
527
|
+
}
|
|
528
|
+
throw err;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
var TEMPLATES2 = ["code-implementation", "research"];
|
|
532
|
+
async function handleInit(cwd) {
|
|
533
|
+
const template = await select({
|
|
534
|
+
message: "Which template?",
|
|
535
|
+
choices: TEMPLATES2.map((t) => ({ name: t, value: t }))
|
|
536
|
+
});
|
|
537
|
+
const name = await input({
|
|
538
|
+
message: "Flow name?",
|
|
539
|
+
default: template
|
|
540
|
+
});
|
|
541
|
+
await initProject(cwd, { template, name });
|
|
542
|
+
const shouldRun = await confirm({
|
|
543
|
+
message: "Run the first loop now?",
|
|
544
|
+
default: true
|
|
545
|
+
});
|
|
546
|
+
if (shouldRun) {
|
|
547
|
+
const flowDir = resolveFlowDir(cwd, name);
|
|
548
|
+
const config = loadConfig(flowDir);
|
|
549
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
550
|
+
if (sortedLoops.length > 0) {
|
|
551
|
+
const [firstKey] = sortedLoops[0];
|
|
552
|
+
await runLoop(firstKey, {
|
|
553
|
+
multiAgent: false,
|
|
554
|
+
maxIterations: 30,
|
|
555
|
+
cwd,
|
|
556
|
+
flow: name
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function selectFlow(flows) {
|
|
562
|
+
if (flows.length === 1) return flows[0];
|
|
563
|
+
return await select({
|
|
564
|
+
message: "Which flow?",
|
|
565
|
+
choices: flows.map((f) => ({ name: f, value: f }))
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
async function handleRunLoop(cwd, flows) {
|
|
569
|
+
const flow = await selectFlow(flows);
|
|
570
|
+
const flowDir = resolveFlowDir(cwd, flow);
|
|
571
|
+
const config = loadConfig(flowDir);
|
|
572
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
573
|
+
const loopKey = await select({
|
|
574
|
+
message: "Which loop?",
|
|
575
|
+
choices: sortedLoops.map(([key, loop]) => ({
|
|
576
|
+
name: loop.name,
|
|
577
|
+
value: key
|
|
578
|
+
}))
|
|
579
|
+
});
|
|
580
|
+
await runLoop(loopKey, {
|
|
581
|
+
multiAgent: false,
|
|
582
|
+
maxIterations: 30,
|
|
583
|
+
cwd,
|
|
584
|
+
flow
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
async function handleRunAll(cwd, flows) {
|
|
588
|
+
const flow = await selectFlow(flows);
|
|
589
|
+
await runAllLoops({
|
|
590
|
+
multiAgent: false,
|
|
591
|
+
maxIterations: 30,
|
|
592
|
+
cwd,
|
|
593
|
+
flow
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async function handleE2E(cwd, flows) {
|
|
597
|
+
const flow = await selectFlow(flows);
|
|
598
|
+
await runE2E({
|
|
599
|
+
multiAgent: false,
|
|
600
|
+
maxIterations: 30,
|
|
601
|
+
cwd,
|
|
602
|
+
flow
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async function handleStatus(cwd) {
|
|
606
|
+
await showStatus(cwd);
|
|
607
|
+
}
|
|
608
|
+
async function handleDashboard(cwd) {
|
|
609
|
+
const { startDashboard } = await import("./server-O6J52DZT.js");
|
|
610
|
+
await startDashboard({ cwd });
|
|
611
|
+
}
|
|
612
|
+
|
|
547
613
|
// src/cli/index.ts
|
|
548
|
-
var program = new
|
|
614
|
+
var program = new Command6().name("ralphflow").description("Multi-agent AI workflow orchestration for Claude Code").version("0.1.0").addCommand(initCommand).addCommand(runCommand).addCommand(e2eCommand).addCommand(statusCommand).addCommand(dashboardCommand).action(async () => {
|
|
615
|
+
await interactiveMenu(process.cwd());
|
|
616
|
+
});
|
|
549
617
|
process.on("SIGINT", () => {
|
|
550
618
|
console.log();
|
|
551
|
-
console.log(
|
|
619
|
+
console.log(chalk8.dim(" Interrupted."));
|
|
552
620
|
process.exit(130);
|
|
553
621
|
});
|
|
554
622
|
program.configureOutput({
|
|
555
623
|
writeErr: (str) => {
|
|
556
624
|
const clean = str.replace(/^error: /, "");
|
|
557
625
|
if (clean.trim()) {
|
|
558
|
-
console.error(
|
|
626
|
+
console.error(chalk8.red(` ${clean.trim()}`));
|
|
559
627
|
}
|
|
560
628
|
}
|
|
561
629
|
});
|