ralphflow 0.2.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/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 Command4 } from "commander";
5
- import chalk7 from "chalk";
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 = listFlows(ralphFlowDir);
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 listFlows(ralphFlowDir) {
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,143 +153,30 @@ import { Command as Command2 } from "commander";
141
153
  import chalk4 from "chalk";
142
154
 
143
155
  // src/core/runner.ts
144
- import { readFileSync as readFileSync3 } from "fs";
145
- import { join as join4 } from "path";
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) {
232
- const { prompt, model, printMode = false, agentName, cwd } = options;
233
- const args = [];
234
- if (printMode) {
235
- args.push("-p");
236
- args.push("--dangerously-skip-permissions");
237
- }
163
+ const { prompt, model, cwd } = options;
164
+ const args = ["--dangerously-skip-permissions", prompt];
238
165
  if (model) {
239
- args.push("--model", model);
166
+ args.unshift("--model", model);
240
167
  }
241
168
  return new Promise((resolve, reject) => {
242
169
  const child = spawn("claude", args, {
243
170
  cwd,
244
- stdio: printMode ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "inherit"],
171
+ stdio: "inherit",
245
172
  env: { ...process.env }
246
173
  });
247
- let output = "";
248
- child.stdout?.on("data", (data) => {
249
- const text = data.toString();
250
- output += text;
251
- if (agentName) {
252
- const lines = text.split("\n");
253
- for (const line of lines) {
254
- if (line) {
255
- process.stdout.write(`[${agentName}] ${line}
256
- `);
257
- }
258
- }
259
- } else {
260
- process.stdout.write(text);
261
- }
262
- });
263
- if (printMode && child.stderr) {
264
- child.stderr.on("data", (data) => {
265
- const text = data.toString();
266
- if (agentName) {
267
- process.stderr.write(`[${agentName}] ${text}`);
268
- } else {
269
- process.stderr.write(text);
270
- }
271
- });
272
- }
273
- child.stdin?.write(prompt);
274
- child.stdin?.end();
275
174
  child.on("error", (err) => {
276
175
  reject(new Error(`Failed to spawn claude: ${err.message}`));
277
176
  });
278
177
  child.on("close", (code, signal) => {
279
178
  resolve({
280
- output,
179
+ output: "",
281
180
  exitCode: code,
282
181
  signal
283
182
  });
@@ -286,41 +185,138 @@ async function spawnClaude(options) {
286
185
  }
287
186
 
288
187
  // src/core/runner.ts
289
- var AGENT_COLORS = [
290
- chalk3.cyan,
291
- chalk3.magenta,
292
- chalk3.yellow,
293
- chalk3.green,
294
- chalk3.blue,
295
- chalk3.red
296
- ];
188
+ function agentsDir(flowDir, loop) {
189
+ const loopDir = join3(flowDir, loop.tracker, "..");
190
+ return join3(loopDir, ".agents");
191
+ }
192
+ function isProcessAlive(pid) {
193
+ try {
194
+ process.kill(pid, 0);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ function cleanStaleAgents(dir) {
201
+ if (!existsSync3(dir)) return;
202
+ for (const file of readdirSync2(dir)) {
203
+ if (!file.endsWith(".lock")) continue;
204
+ const pidStr = readFileSync2(join3(dir, file), "utf-8").trim();
205
+ const pid = parseInt(pidStr, 10);
206
+ if (isNaN(pid) || !isProcessAlive(pid)) {
207
+ unlinkSync(join3(dir, file));
208
+ }
209
+ }
210
+ }
211
+ function acquireAgentId(dir, maxAgents) {
212
+ mkdirSync2(dir, { recursive: true });
213
+ cleanStaleAgents(dir);
214
+ for (let n = 1; n <= maxAgents; n++) {
215
+ const lockFile = join3(dir, `agent-${n}.lock`);
216
+ if (!existsSync3(lockFile)) {
217
+ writeFileSync(lockFile, String(process.pid));
218
+ return `agent-${n}`;
219
+ }
220
+ }
221
+ throw new Error(`All ${maxAgents} agent slots are occupied. Wait for one to finish or increase max_agents.`);
222
+ }
223
+ function releaseAgentId(dir, agentName) {
224
+ const lockFile = join3(dir, `${agentName}.lock`);
225
+ try {
226
+ unlinkSync(lockFile);
227
+ } catch {
228
+ }
229
+ }
230
+ function checkTrackerForCompletion(flowDir, loop) {
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;
257
+ }
297
258
  async function runLoop(loopName, options) {
298
259
  const flowDir = resolveFlowDir(options.cwd, options.flow);
299
260
  const config = loadConfig(flowDir);
300
261
  const { key, loop } = resolveLoop(config, loopName);
301
- const isMultiAgent = options.agents > 1 && loop.multi_agent !== false;
262
+ let agentName;
263
+ let agentDir;
264
+ if (options.multiAgent) {
265
+ if (loop.multi_agent === false) {
266
+ throw new Error(`Loop "${loop.name}" does not support multi-agent mode.`);
267
+ }
268
+ const ma = loop.multi_agent;
269
+ agentDir = agentsDir(flowDir, loop);
270
+ agentName = acquireAgentId(agentDir, ma.max_agents);
271
+ }
302
272
  console.log();
303
273
  console.log(
304
- chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (isMultiAgent ? chalk3.dim(` (${options.agents} agents)`) : "")
274
+ chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (agentName ? chalk3.dim(` [${agentName}]`) : "")
305
275
  );
306
276
  console.log();
307
- if (isMultiAgent) {
308
- await runMultiAgent(loop, flowDir, options);
309
- } else {
310
- await runSingleAgent(loop, flowDir, options);
277
+ const cleanup = () => {
278
+ if (agentDir && agentName) {
279
+ releaseAgentId(agentDir, agentName);
280
+ }
281
+ };
282
+ process.on("exit", cleanup);
283
+ process.on("SIGINT", () => {
284
+ cleanup();
285
+ process.exit(130);
286
+ });
287
+ process.on("SIGTERM", () => {
288
+ cleanup();
289
+ process.exit(143);
290
+ });
291
+ try {
292
+ await iterationLoop(loop, flowDir, options, agentName);
293
+ } finally {
294
+ cleanup();
311
295
  }
312
296
  }
313
- async function runSingleAgent(loop, flowDir, options) {
297
+ async function iterationLoop(loop, flowDir, options, agentName, db, flowName) {
298
+ const loopKey = loop.name;
314
299
  for (let i = 1; i <= options.maxIterations; i++) {
315
- console.log(chalk3.dim(` Iteration ${i}/${options.maxIterations}`));
316
- const prompt = readPrompt(loop, flowDir, options.agentName);
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
+ }
309
+ const label = agentName ? chalk3.dim(` [${agentName}] Iteration ${i}/${options.maxIterations}`) : chalk3.dim(` Iteration ${i}/${options.maxIterations}`);
310
+ console.log(label);
311
+ const prompt = readPrompt(loop, flowDir, agentName);
317
312
  const result = await spawnClaude({
318
313
  prompt,
319
314
  model: options.model,
320
- printMode: false,
321
315
  cwd: options.cwd
322
316
  });
323
- if (result.output.includes(`<promise>${loop.completion}</promise>`)) {
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);
324
320
  console.log();
325
321
  console.log(chalk3.green(` Loop complete: ${loop.completion}`));
326
322
  return;
@@ -330,68 +326,66 @@ async function runSingleAgent(loop, flowDir, options) {
330
326
  console.log();
331
327
  continue;
332
328
  }
333
- if (result.exitCode !== 0 && result.exitCode !== null) {
334
- console.log(chalk3.red(` Claude exited with code ${result.exitCode}`));
335
- return;
329
+ if (result.exitCode === 0 || result.exitCode === null) {
330
+ console.log(chalk3.dim(` Iteration ${i} finished, continuing...`));
331
+ console.log();
332
+ continue;
336
333
  }
337
- console.log(chalk3.dim(` Iteration ${i} finished, continuing...`));
338
- console.log();
334
+ console.log(chalk3.red(` Claude exited with code ${result.exitCode}`));
335
+ return;
339
336
  }
340
337
  console.log(chalk3.yellow(` Max iterations (${options.maxIterations}) reached.`));
341
338
  }
342
- async function runMultiAgent(loop, flowDir, options) {
343
- const agentCount = options.agents;
344
- let completed = false;
345
- const agentRunners = Array.from({ length: agentCount }, (_, idx) => {
346
- const agentNum = idx + 1;
347
- const agentName = `agent-${agentNum}`;
348
- const colorFn = AGENT_COLORS[idx % AGENT_COLORS.length];
349
- return runAgentLoop(loop, flowDir, {
350
- ...options,
351
- agentName
352
- }, colorFn, () => completed, () => {
353
- completed = true;
354
- });
355
- });
356
- await Promise.allSettled(agentRunners);
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"));
357
345
  console.log();
358
- console.log(chalk3.green(` All agents finished.`));
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."));
359
351
  }
360
- async function runAgentLoop(loop, flowDir, options, colorFn, isCompleted, setCompleted) {
361
- const agentName = options.agentName;
362
- for (let i = 1; i <= options.maxIterations; i++) {
363
- if (isCompleted()) {
364
- console.log(colorFn(` [${agentName}] Stopping \u2014 completion detected.`));
365
- return;
366
- }
367
- console.log(colorFn(` [${agentName}] Iteration ${i}/${options.maxIterations}`));
368
- const prompt = readPrompt(loop, flowDir, agentName);
369
- const result = await spawnClaude({
370
- prompt,
371
- model: options.model,
372
- printMode: true,
373
- agentName,
374
- cwd: options.cwd
375
- });
376
- if (result.output.includes(`<promise>${loop.completion}</promise>`)) {
377
- console.log(colorFn(` [${agentName}] Loop complete: ${loop.completion}`));
378
- setCompleted();
379
- return;
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;
380
366
  }
381
- if (result.signal === "SIGINT" || result.exitCode === 130) {
382
- console.log(colorFn(` [${agentName}] Iteration ${i} complete, restarting...`));
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`));
383
370
  continue;
384
371
  }
385
- if (result.exitCode !== 0 && result.exitCode !== null) {
386
- console.log(chalk3.red(` [${agentName}] Claude exited with code ${result.exitCode}`));
387
- return;
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`));
388
379
  }
380
+ console.log();
389
381
  }
390
- console.log(chalk3.yellow(` [${agentName}] Max iterations reached.`));
382
+ console.log(chalk3.green(" \u2713 E2E complete"));
391
383
  }
392
384
  function readPrompt(loop, flowDir, agentName) {
393
- const promptPath = join4(flowDir, loop.prompt);
394
- let prompt = readFileSync3(promptPath, "utf-8");
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);
395
389
  if (agentName && loop.multi_agent !== false) {
396
390
  const ma = loop.multi_agent;
397
391
  if (ma.agent_placeholder) {
@@ -402,10 +396,15 @@ function readPrompt(loop, flowDir, agentName) {
402
396
  }
403
397
 
404
398
  // src/cli/run.ts
405
- var runCommand = new Command2("run").description("Run a loop").argument("<loop>", "Loop to run (story, tasks, delivery, discovery, research, document)").option("-a, --agents <n>", "Number of parallel agents", "1").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) => {
406
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
+ }
407
406
  await runLoop(loop, {
408
- agents: parseInt(opts.agents, 10),
407
+ multiAgent: !!opts.multiAgent,
409
408
  model: opts.model,
410
409
  maxIterations: parseInt(opts.maxIterations, 10),
411
410
  flow: opts.flow,
@@ -420,137 +419,36 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
420
419
  }
421
420
  });
422
421
 
423
- // src/cli/status.ts
422
+ // src/cli/e2e.ts
424
423
  import { Command as Command3 } from "commander";
425
- import chalk6 from "chalk";
426
-
427
- // src/core/status.ts
428
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
429
- import { join as join5 } from "path";
430
424
  import chalk5 from "chalk";
431
- import Table from "cli-table3";
432
- async function showStatus(cwd, flowName) {
433
- const flows = flowName ? [flowName] : listFlows2(cwd);
434
- if (flows.length === 0) {
435
- console.log();
436
- console.log(chalk5.yellow(" No flows found. Run `npx ralphflow init` first."));
437
- console.log();
438
- return;
439
- }
440
- for (const flow of flows) {
441
- const flowDir = resolveFlowDir(cwd, flow);
442
- const config = loadConfig(flowDir);
443
- console.log();
444
- console.log(chalk5.bold(` RalphFlow \u2014 ${flow}`));
445
- console.log();
446
- const table = new Table({
447
- chars: {
448
- top: "",
449
- "top-mid": "",
450
- "top-left": "",
451
- "top-right": "",
452
- bottom: "",
453
- "bottom-mid": "",
454
- "bottom-left": "",
455
- "bottom-right": "",
456
- left: " ",
457
- "left-mid": "",
458
- mid: "",
459
- "mid-mid": "",
460
- right: "",
461
- "right-mid": "",
462
- middle: " "
463
- },
464
- style: { "padding-left": 0, "padding-right": 1 },
465
- head: [
466
- chalk5.dim("Loop"),
467
- chalk5.dim("Stage"),
468
- chalk5.dim("Active"),
469
- chalk5.dim("Progress")
470
- ]
471
- });
472
- const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
473
- for (const [key, loop] of sortedLoops) {
474
- const status = parseTracker(loop.tracker, flowDir, loop.name);
475
- table.push([
476
- loop.name,
477
- status.stage,
478
- status.active,
479
- `${status.completed}/${status.total}`
480
- ]);
481
- if (status.agents && status.agents.length > 0) {
482
- for (const agent of status.agents) {
483
- table.push([
484
- chalk5.dim(` ${agent.name}`),
485
- chalk5.dim(agent.stage),
486
- chalk5.dim(agent.activeTask),
487
- chalk5.dim(agent.lastHeartbeat)
488
- ]);
489
- }
490
- }
491
- }
492
- console.log(table.toString());
493
- }
494
- console.log();
495
- }
496
- function parseTracker(trackerPath, flowDir, loopName) {
497
- const fullPath = join5(flowDir, trackerPath);
498
- const status = {
499
- loop: loopName,
500
- stage: "\u2014",
501
- active: "none",
502
- completed: 0,
503
- total: 0
504
- };
505
- if (!existsSync4(fullPath)) {
506
- return status;
507
- }
508
- const content = readFileSync4(fullPath, "utf-8");
509
- const lines = content.split("\n");
510
- for (const line of lines) {
511
- const metaMatch = line.match(/^- (\w[\w_]*): (.+)$/);
512
- if (metaMatch) {
513
- const [, key, value] = metaMatch;
514
- if (key === "stage") status.stage = value.trim();
515
- if (key === "active_story" || key === "active_task") status.active = value.trim();
516
- if (key === "completed_stories" || key === "completed_tasks") {
517
- const arrayMatch = value.match(/\[(.+)\]/);
518
- if (arrayMatch) {
519
- status.completed = arrayMatch[1].split(",").filter((s) => s.trim()).length;
520
- }
521
- }
522
- }
523
- }
524
- const unchecked = (content.match(/- \[ \]/g) || []).length;
525
- const checked = (content.match(/- \[x\]/gi) || []).length;
526
- if (unchecked + checked > 0) {
527
- status.total = unchecked + checked;
528
- status.completed = checked;
529
- }
530
- const agentTableMatch = content.match(/\| agent \|.*\n\|[-|]+\n((?:\|.*\n)*)/);
531
- if (agentTableMatch) {
532
- const agentRows = agentTableMatch[1].trim().split("\n");
533
- status.agents = [];
534
- for (const row of agentRows) {
535
- const cells = row.split("|").map((s) => s.trim()).filter(Boolean);
536
- if (cells.length >= 4) {
537
- status.agents.push({
538
- name: cells[0],
539
- activeTask: cells[1],
540
- stage: cells[2],
541
- lastHeartbeat: cells[3]
542
- });
543
- }
544
- }
545
- if (status.agents.length === 0) {
546
- 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() });
547
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);
548
445
  }
549
- return status;
550
- }
446
+ });
551
447
 
552
448
  // src/cli/status.ts
553
- var statusCommand = new Command3("status").description("Show pipeline status").option("-f, --flow <name>", "Show status for a specific flow").action(async (opts) => {
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) => {
554
452
  try {
555
453
  await showStatus(process.cwd(), opts.flow);
556
454
  } catch (err) {
@@ -562,18 +460,170 @@ var statusCommand = new Command3("status").description("Show pipeline status").o
562
460
  }
563
461
  });
564
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
+
565
613
  // src/cli/index.ts
566
- var program = new Command4().name("ralphflow").description("Multi-agent AI workflow orchestration for Claude Code").version("0.1.0").addCommand(initCommand).addCommand(runCommand).addCommand(statusCommand);
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
+ });
567
617
  process.on("SIGINT", () => {
568
618
  console.log();
569
- console.log(chalk7.dim(" Interrupted."));
619
+ console.log(chalk8.dim(" Interrupted."));
570
620
  process.exit(130);
571
621
  });
572
622
  program.configureOutput({
573
623
  writeErr: (str) => {
574
624
  const clean = str.replace(/^error: /, "");
575
625
  if (clean.trim()) {
576
- console.error(chalk7.red(` ${clean.trim()}`));
626
+ console.error(chalk8.red(` ${clean.trim()}`));
577
627
  }
578
628
  }
579
629
  });