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/README.md +67 -27
- package/dist/chunk-GVOJO5IN.js +274 -0
- package/dist/ralphflow.js +372 -322
- 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,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
|
|
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) {
|
|
232
|
-
const { prompt, model,
|
|
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.
|
|
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:
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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}`) + (
|
|
274
|
+
chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (agentName ? chalk3.dim(` [${agentName}]`) : "")
|
|
305
275
|
);
|
|
306
276
|
console.log();
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
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 (
|
|
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
|
|
334
|
-
console.log(chalk3.
|
|
335
|
-
|
|
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.
|
|
338
|
-
|
|
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
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 (
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
382
|
+
console.log(chalk3.green(" \u2713 E2E complete"));
|
|
391
383
|
}
|
|
392
384
|
function readPrompt(loop, flowDir, agentName) {
|
|
393
|
-
const promptPath =
|
|
394
|
-
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);
|
|
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("-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
550
|
-
}
|
|
446
|
+
});
|
|
551
447
|
|
|
552
448
|
// src/cli/status.ts
|
|
553
|
-
|
|
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
|
|
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(
|
|
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(
|
|
626
|
+
console.error(chalk8.red(` ${clean.trim()}`));
|
|
577
627
|
}
|
|
578
628
|
}
|
|
579
629
|
});
|