ralphflow 0.4.0 → 0.5.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 +77 -87
- package/dist/chunk-TCCMQDVT.js +505 -0
- package/dist/ralphflow.js +207 -275
- package/dist/server-DOSLU36L.js +821 -0
- package/package.json +1 -2
- package/src/dashboard/ui/index.html +2760 -350
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +12 -6
- package/src/templates/code-implementation/ralphflow.yaml +3 -0
- package/src/templates/research/ralphflow.yaml +4 -0
- package/dist/chunk-GVOJO5IN.js +0 -274
- package/dist/server-O6J52DZT.js +0 -323
package/dist/ralphflow.js
CHANGED
|
@@ -1,69 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
copyTemplate,
|
|
3
4
|
getDb,
|
|
4
5
|
incrementIteration,
|
|
5
6
|
isLoopComplete,
|
|
6
|
-
listFlows,
|
|
7
7
|
loadConfig,
|
|
8
8
|
markLoopComplete,
|
|
9
9
|
markLoopRunning,
|
|
10
|
+
resetLoopState,
|
|
10
11
|
resolveFlowDir,
|
|
11
12
|
resolveLoop,
|
|
12
13
|
showStatus
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-TCCMQDVT.js";
|
|
14
15
|
|
|
15
16
|
// src/cli/index.ts
|
|
16
17
|
import { Command as Command6 } from "commander";
|
|
17
|
-
import
|
|
18
|
+
import chalk7 from "chalk";
|
|
19
|
+
import { exec } from "child_process";
|
|
18
20
|
|
|
19
21
|
// src/cli/init.ts
|
|
20
22
|
import { Command } from "commander";
|
|
21
23
|
import chalk2 from "chalk";
|
|
22
24
|
|
|
23
25
|
// src/core/init.ts
|
|
24
|
-
import { existsSync
|
|
25
|
-
import { join
|
|
26
|
+
import { existsSync, readdirSync } from "fs";
|
|
27
|
+
import { join } from "path";
|
|
26
28
|
import { createInterface } from "readline";
|
|
27
29
|
import chalk from "chalk";
|
|
28
|
-
|
|
29
|
-
// src/core/template.ts
|
|
30
|
-
import { readFileSync, mkdirSync, cpSync, existsSync } from "fs";
|
|
31
|
-
import { join, dirname } from "path";
|
|
32
|
-
import { fileURLToPath } from "url";
|
|
33
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
-
function resolveTemplatePath(templateName) {
|
|
35
|
-
const candidates = [
|
|
36
|
-
join(__dirname, "..", "templates", templateName),
|
|
37
|
-
// dev: src/core/ -> src/templates/
|
|
38
|
-
join(__dirname, "..", "src", "templates", templateName)
|
|
39
|
-
// bundled: dist/ -> src/templates/
|
|
40
|
-
];
|
|
41
|
-
for (const candidate of candidates) {
|
|
42
|
-
if (existsSync(candidate)) {
|
|
43
|
-
return candidate;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Template "${templateName}" not found. Searched:
|
|
48
|
-
${candidates.join("\n")}`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
function copyTemplate(templateName, targetDir) {
|
|
52
|
-
const templatePath = resolveTemplatePath(templateName);
|
|
53
|
-
const loopsDir = join(templatePath, "loops");
|
|
54
|
-
if (!existsSync(loopsDir)) {
|
|
55
|
-
throw new Error(`Template "${templateName}" has no loops/ directory`);
|
|
56
|
-
}
|
|
57
|
-
mkdirSync(targetDir, { recursive: true });
|
|
58
|
-
cpSync(loopsDir, targetDir, { recursive: true });
|
|
59
|
-
const yamlSrc = join(templatePath, "ralphflow.yaml");
|
|
60
|
-
if (existsSync(yamlSrc)) {
|
|
61
|
-
const yamlDest = join(targetDir, "ralphflow.yaml");
|
|
62
|
-
cpSync(yamlSrc, yamlDest);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/core/init.ts
|
|
67
30
|
function ask(rl, question) {
|
|
68
31
|
return new Promise((resolve) => {
|
|
69
32
|
rl.question(chalk.cyan("? ") + question + " ", (answer) => {
|
|
@@ -73,9 +36,9 @@ function ask(rl, question) {
|
|
|
73
36
|
}
|
|
74
37
|
var TEMPLATES = ["code-implementation", "research"];
|
|
75
38
|
async function initProject(cwd, options = {}) {
|
|
76
|
-
const ralphFlowDir =
|
|
77
|
-
const claudeMdPath =
|
|
78
|
-
if (!
|
|
39
|
+
const ralphFlowDir = join(cwd, ".ralph-flow");
|
|
40
|
+
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
41
|
+
if (!existsSync(claudeMdPath)) {
|
|
79
42
|
console.log();
|
|
80
43
|
console.log(chalk.yellow(" No CLAUDE.md found."));
|
|
81
44
|
console.log(chalk.dim(' Create one with: claude "Initialize CLAUDE.md for this project"'));
|
|
@@ -83,8 +46,8 @@ async function initProject(cwd, options = {}) {
|
|
|
83
46
|
console.log();
|
|
84
47
|
return;
|
|
85
48
|
}
|
|
86
|
-
if (
|
|
87
|
-
const existing =
|
|
49
|
+
if (existsSync(ralphFlowDir)) {
|
|
50
|
+
const existing = listFlows(ralphFlowDir);
|
|
88
51
|
if (existing.length > 0 && !options.template) {
|
|
89
52
|
console.log();
|
|
90
53
|
console.log(chalk.bold(" Existing flows:"));
|
|
@@ -116,8 +79,8 @@ async function initProject(cwd, options = {}) {
|
|
|
116
79
|
}
|
|
117
80
|
rl.close();
|
|
118
81
|
console.log();
|
|
119
|
-
const flowDir =
|
|
120
|
-
if (
|
|
82
|
+
const flowDir = join(ralphFlowDir, flowName);
|
|
83
|
+
if (existsSync(flowDir)) {
|
|
121
84
|
console.log(chalk.yellow(` Flow "${flowName}" already exists at .ralph-flow/${flowName}/`));
|
|
122
85
|
return;
|
|
123
86
|
}
|
|
@@ -127,7 +90,7 @@ async function initProject(cwd, options = {}) {
|
|
|
127
90
|
console.log(chalk.dim(` Next: npx ralphflow run story --flow ${flowName}`));
|
|
128
91
|
console.log();
|
|
129
92
|
}
|
|
130
|
-
function
|
|
93
|
+
function listFlows(ralphFlowDir) {
|
|
131
94
|
try {
|
|
132
95
|
return readdirSync(ralphFlowDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
133
96
|
} catch {
|
|
@@ -153,14 +116,14 @@ import { Command as Command2 } from "commander";
|
|
|
153
116
|
import chalk4 from "chalk";
|
|
154
117
|
|
|
155
118
|
// src/core/runner.ts
|
|
156
|
-
import { readFileSync
|
|
157
|
-
import { join as
|
|
119
|
+
import { readFileSync, existsSync as existsSync2, mkdirSync, writeFileSync, readdirSync as readdirSync2, unlinkSync } from "fs";
|
|
120
|
+
import { join as join2, basename } from "path";
|
|
158
121
|
import chalk3 from "chalk";
|
|
159
122
|
|
|
160
123
|
// src/core/claude.ts
|
|
161
124
|
import { spawn } from "child_process";
|
|
162
125
|
async function spawnClaude(options) {
|
|
163
|
-
const { prompt, model, cwd } = options;
|
|
126
|
+
const { prompt, model, cwd, env: extraEnv } = options;
|
|
164
127
|
const args = ["--dangerously-skip-permissions", prompt];
|
|
165
128
|
if (model) {
|
|
166
129
|
args.unshift("--model", model);
|
|
@@ -169,7 +132,7 @@ async function spawnClaude(options) {
|
|
|
169
132
|
const child = spawn("claude", args, {
|
|
170
133
|
cwd,
|
|
171
134
|
stdio: "inherit",
|
|
172
|
-
env: { ...process.env }
|
|
135
|
+
env: { ...process.env, ...extraEnv }
|
|
173
136
|
});
|
|
174
137
|
child.on("error", (err) => {
|
|
175
138
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
@@ -186,8 +149,8 @@ async function spawnClaude(options) {
|
|
|
186
149
|
|
|
187
150
|
// src/core/runner.ts
|
|
188
151
|
function agentsDir(flowDir, loop) {
|
|
189
|
-
const loopDir =
|
|
190
|
-
return
|
|
152
|
+
const loopDir = join2(flowDir, loop.tracker, "..");
|
|
153
|
+
return join2(loopDir, ".agents");
|
|
191
154
|
}
|
|
192
155
|
function isProcessAlive(pid) {
|
|
193
156
|
try {
|
|
@@ -198,22 +161,22 @@ function isProcessAlive(pid) {
|
|
|
198
161
|
}
|
|
199
162
|
}
|
|
200
163
|
function cleanStaleAgents(dir) {
|
|
201
|
-
if (!
|
|
164
|
+
if (!existsSync2(dir)) return;
|
|
202
165
|
for (const file of readdirSync2(dir)) {
|
|
203
166
|
if (!file.endsWith(".lock")) continue;
|
|
204
|
-
const pidStr =
|
|
167
|
+
const pidStr = readFileSync(join2(dir, file), "utf-8").trim();
|
|
205
168
|
const pid = parseInt(pidStr, 10);
|
|
206
169
|
if (isNaN(pid) || !isProcessAlive(pid)) {
|
|
207
|
-
unlinkSync(
|
|
170
|
+
unlinkSync(join2(dir, file));
|
|
208
171
|
}
|
|
209
172
|
}
|
|
210
173
|
}
|
|
211
174
|
function acquireAgentId(dir, maxAgents) {
|
|
212
|
-
|
|
175
|
+
mkdirSync(dir, { recursive: true });
|
|
213
176
|
cleanStaleAgents(dir);
|
|
214
177
|
for (let n = 1; n <= maxAgents; n++) {
|
|
215
|
-
const lockFile =
|
|
216
|
-
if (!
|
|
178
|
+
const lockFile = join2(dir, `agent-${n}.lock`);
|
|
179
|
+
if (!existsSync2(lockFile)) {
|
|
217
180
|
writeFileSync(lockFile, String(process.pid));
|
|
218
181
|
return `agent-${n}`;
|
|
219
182
|
}
|
|
@@ -221,22 +184,27 @@ function acquireAgentId(dir, maxAgents) {
|
|
|
221
184
|
throw new Error(`All ${maxAgents} agent slots are occupied. Wait for one to finish or increase max_agents.`);
|
|
222
185
|
}
|
|
223
186
|
function releaseAgentId(dir, agentName) {
|
|
224
|
-
const lockFile =
|
|
187
|
+
const lockFile = join2(dir, `${agentName}.lock`);
|
|
225
188
|
try {
|
|
226
189
|
unlinkSync(lockFile);
|
|
227
190
|
} catch {
|
|
228
191
|
}
|
|
229
192
|
}
|
|
193
|
+
function isMultiAgentLoop(loop) {
|
|
194
|
+
return loop.multi_agent !== false && loop.multi_agent.enabled === true;
|
|
195
|
+
}
|
|
230
196
|
function checkTrackerForCompletion(flowDir, loop) {
|
|
231
|
-
const trackerPath =
|
|
232
|
-
if (!
|
|
233
|
-
const content =
|
|
197
|
+
const trackerPath = join2(flowDir, loop.tracker);
|
|
198
|
+
if (!existsSync2(trackerPath)) return false;
|
|
199
|
+
const content = readFileSync(trackerPath, "utf-8");
|
|
234
200
|
return content.includes(`<promise>${loop.completion}</promise>`) || content.includes(loop.completion);
|
|
235
201
|
}
|
|
236
202
|
function checkTrackerMetadataCompletion(flowDir, loop) {
|
|
237
|
-
const trackerPath =
|
|
238
|
-
if (!
|
|
239
|
-
const content =
|
|
203
|
+
const trackerPath = join2(flowDir, loop.tracker);
|
|
204
|
+
if (!existsSync2(trackerPath)) return false;
|
|
205
|
+
const content = readFileSync(trackerPath, "utf-8");
|
|
206
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
207
|
+
if (unchecked > 0) return false;
|
|
240
208
|
const completedMatch = content.match(/^- completed_(?:tasks|stories): \[(.+)\]$/m);
|
|
241
209
|
if (!completedMatch) return false;
|
|
242
210
|
const completedItems = completedMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -247,10 +215,10 @@ function checkTrackerMetadataCompletion(flowDir, loop) {
|
|
|
247
215
|
if (pendingLines && pendingLines.length > 0) return false;
|
|
248
216
|
return true;
|
|
249
217
|
}
|
|
250
|
-
function
|
|
251
|
-
const trackerPath =
|
|
252
|
-
if (!
|
|
253
|
-
const content =
|
|
218
|
+
function checkTrackerAllChecked(flowDir, loop) {
|
|
219
|
+
const trackerPath = join2(flowDir, loop.tracker);
|
|
220
|
+
if (!existsSync2(trackerPath)) return false;
|
|
221
|
+
const content = readFileSync(trackerPath, "utf-8");
|
|
254
222
|
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
255
223
|
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
256
224
|
return checked > 0 && unchecked === 0;
|
|
@@ -261,7 +229,7 @@ async function runLoop(loopName, options) {
|
|
|
261
229
|
const { key, loop } = resolveLoop(config, loopName);
|
|
262
230
|
let agentName;
|
|
263
231
|
let agentDir;
|
|
264
|
-
if (options.multiAgent) {
|
|
232
|
+
if (options.multiAgent || isMultiAgentLoop(loop)) {
|
|
265
233
|
if (loop.multi_agent === false) {
|
|
266
234
|
throw new Error(`Loop "${loop.name}" does not support multi-agent mode.`);
|
|
267
235
|
}
|
|
@@ -289,33 +257,41 @@ async function runLoop(loopName, options) {
|
|
|
289
257
|
process.exit(143);
|
|
290
258
|
});
|
|
291
259
|
try {
|
|
292
|
-
await iterationLoop(loop, flowDir, options, agentName);
|
|
260
|
+
await iterationLoop(key, loop, flowDir, options, agentName);
|
|
293
261
|
} finally {
|
|
294
262
|
cleanup();
|
|
295
263
|
}
|
|
296
264
|
}
|
|
297
|
-
async function iterationLoop(loop, flowDir, options, agentName, db, flowName) {
|
|
265
|
+
async function iterationLoop(configKey, loop, flowDir, options, agentName, db, flowName, forceFirstIteration) {
|
|
298
266
|
const loopKey = loop.name;
|
|
267
|
+
const appName = basename(flowDir);
|
|
299
268
|
for (let i = 1; i <= options.maxIterations; i++) {
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
269
|
+
if (!(forceFirstIteration && i === 1)) {
|
|
270
|
+
if (db && flowName && isLoopComplete(db, flowName, loopKey)) {
|
|
271
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 already complete`));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
|
|
275
|
+
if (db && flowName) markLoopComplete(db, flowName, loopKey);
|
|
276
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete`));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
308
279
|
}
|
|
309
280
|
const label = agentName ? chalk3.dim(` [${agentName}] Iteration ${i}/${options.maxIterations}`) : chalk3.dim(` Iteration ${i}/${options.maxIterations}`);
|
|
310
281
|
console.log(label);
|
|
311
282
|
const prompt = readPrompt(loop, flowDir, agentName);
|
|
283
|
+
const effectiveModel = options.model || loop.model;
|
|
312
284
|
const result = await spawnClaude({
|
|
313
285
|
prompt,
|
|
314
|
-
model:
|
|
315
|
-
cwd: options.cwd
|
|
286
|
+
model: effectiveModel,
|
|
287
|
+
cwd: options.cwd,
|
|
288
|
+
env: {
|
|
289
|
+
RALPHFLOW_APP: appName,
|
|
290
|
+
RALPHFLOW_LOOP: configKey
|
|
291
|
+
}
|
|
316
292
|
});
|
|
317
293
|
if (db && flowName) incrementIteration(db, flowName, loopKey);
|
|
318
|
-
if (checkTrackerForCompletion(flowDir, loop) ||
|
|
294
|
+
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
|
|
319
295
|
if (db && flowName) markLoopComplete(db, flowName, loopKey);
|
|
320
296
|
console.log();
|
|
321
297
|
console.log(chalk3.green(` Loop complete: ${loop.completion}`));
|
|
@@ -336,54 +312,145 @@ async function iterationLoop(loop, flowDir, options, agentName, db, flowName) {
|
|
|
336
312
|
}
|
|
337
313
|
console.log(chalk3.yellow(` Max iterations (${options.maxIterations}) reached.`));
|
|
338
314
|
}
|
|
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
315
|
async function runE2E(options) {
|
|
353
316
|
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
354
317
|
const config = loadConfig(flowDir);
|
|
355
318
|
const flowName = options.flow || basename(flowDir);
|
|
356
319
|
const db = getDb(options.cwd);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
320
|
+
for (const loop of Object.values(config.loops)) {
|
|
321
|
+
resetLoopState(db, flowName, loop.name);
|
|
322
|
+
}
|
|
323
|
+
let cycle = 1;
|
|
324
|
+
while (true) {
|
|
325
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
326
|
+
console.log();
|
|
327
|
+
console.log(chalk3.bold(` RalphFlow \u2014 E2E` + (cycle > 1 ? ` (cycle ${cycle})` : "")));
|
|
328
|
+
console.log();
|
|
329
|
+
let anyLoopRan = false;
|
|
330
|
+
for (let idx = 0; idx < sortedLoops.length; idx++) {
|
|
331
|
+
const [key, loop] = sortedLoops[idx];
|
|
332
|
+
const loopKey = loop.name;
|
|
333
|
+
const isFirstLoop = idx === 0;
|
|
334
|
+
if (!isFirstLoop) {
|
|
335
|
+
if (isLoopComplete(db, flowName, loopKey)) {
|
|
336
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
|
|
340
|
+
markLoopComplete(db, flowName, loopKey);
|
|
341
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
anyLoopRan = true;
|
|
346
|
+
markLoopRunning(db, flowName, loopKey);
|
|
347
|
+
console.log(chalk3.bold(` \u2192 ${loop.name}`));
|
|
348
|
+
let agentName;
|
|
349
|
+
let agentDir;
|
|
350
|
+
if (isMultiAgentLoop(loop)) {
|
|
351
|
+
agentDir = agentsDir(flowDir, loop);
|
|
352
|
+
agentName = acquireAgentId(agentDir, loop.multi_agent.max_agents);
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
await iterationLoop(key, loop, flowDir, options, agentName, db, flowName, isFirstLoop);
|
|
356
|
+
} finally {
|
|
357
|
+
if (agentDir && agentName) releaseAgentId(agentDir, agentName);
|
|
358
|
+
}
|
|
359
|
+
if (isLoopComplete(db, flowName, loopKey)) {
|
|
360
|
+
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 done`));
|
|
361
|
+
} else {
|
|
362
|
+
console.log(chalk3.yellow(` \u26A0 ${loop.name} \u2014 max iterations, advancing`));
|
|
363
|
+
}
|
|
364
|
+
console.log();
|
|
371
365
|
}
|
|
372
|
-
|
|
373
|
-
|
|
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`));
|
|
366
|
+
if (!hasUndeliveredStories(flowDir, config)) {
|
|
367
|
+
break;
|
|
379
368
|
}
|
|
380
|
-
console.log();
|
|
369
|
+
console.log(chalk3.cyan(" \u21BB Undelivered stories found \u2014 starting new cycle"));
|
|
370
|
+
prepareNextCycle(flowDir, config, db, flowName);
|
|
371
|
+
cycle++;
|
|
381
372
|
}
|
|
382
373
|
console.log(chalk3.green(" \u2713 E2E complete"));
|
|
383
374
|
}
|
|
375
|
+
function hasUndeliveredStories(flowDir, config) {
|
|
376
|
+
const storyEntity = config.entities?.STORY;
|
|
377
|
+
if (!storyEntity) return false;
|
|
378
|
+
const storiesPath = join2(flowDir, storyEntity.data_file);
|
|
379
|
+
if (!existsSync2(storiesPath)) return false;
|
|
380
|
+
const storiesContent = readFileSync(storiesPath, "utf-8");
|
|
381
|
+
const storyIds = [...storiesContent.matchAll(/^## (STORY-\d+):/gm)].map((m) => m[1]);
|
|
382
|
+
if (storyIds.length === 0) return false;
|
|
383
|
+
const sortedLoops = Object.values(config.loops).sort((a, b) => a.order - b.order);
|
|
384
|
+
const deliveryLoop = sortedLoops[sortedLoops.length - 1];
|
|
385
|
+
if (!deliveryLoop) return false;
|
|
386
|
+
const deliveryTrackerPath = join2(flowDir, deliveryLoop.tracker);
|
|
387
|
+
if (!existsSync2(deliveryTrackerPath)) return true;
|
|
388
|
+
const deliveryContent = readFileSync(deliveryTrackerPath, "utf-8");
|
|
389
|
+
const deliveredSection = deliveryContent.split("## Delivered")[1] || "";
|
|
390
|
+
const deliveredIds = [...deliveredSection.matchAll(/\[x\]\s*(STORY-\d+)/gi)].map((m) => m[1]);
|
|
391
|
+
return storyIds.some((id) => !deliveredIds.includes(id));
|
|
392
|
+
}
|
|
393
|
+
function prepareNextCycle(flowDir, config, db, flowName) {
|
|
394
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
395
|
+
for (const [, loop] of sortedLoops) {
|
|
396
|
+
resetLoopState(db, flowName, loop.name);
|
|
397
|
+
}
|
|
398
|
+
const storyLoop = sortedLoops.find(([key]) => key.includes("story"));
|
|
399
|
+
if (storyLoop) {
|
|
400
|
+
const [, storyLoopConfig] = storyLoop;
|
|
401
|
+
const trackerPath = join2(flowDir, storyLoopConfig.tracker);
|
|
402
|
+
if (existsSync2(trackerPath)) {
|
|
403
|
+
let content = readFileSync(trackerPath, "utf-8");
|
|
404
|
+
content = content.replace(new RegExp(`<promise>${storyLoopConfig.completion}</promise>`, "g"), "");
|
|
405
|
+
content = content.replace(new RegExp(storyLoopConfig.completion, "g"), "");
|
|
406
|
+
const storyEntity = config.entities?.STORY;
|
|
407
|
+
if (storyEntity) {
|
|
408
|
+
const storiesPath = join2(flowDir, storyEntity.data_file);
|
|
409
|
+
if (existsSync2(storiesPath)) {
|
|
410
|
+
const storiesContent = readFileSync(storiesPath, "utf-8");
|
|
411
|
+
const allStories = [...storiesContent.matchAll(/^## (STORY-\d+): (.+)$/gm)].map((m) => ({ id: m[1], title: m[2] }));
|
|
412
|
+
const existingIds = [...content.matchAll(/\[[ x]\]\s*(STORY-\d+)/gi)].map((m) => m[1]);
|
|
413
|
+
const newStories = allStories.filter((s) => !existingIds.includes(s.id));
|
|
414
|
+
if (newStories.length > 0) {
|
|
415
|
+
const queueSection = "## Stories Queue";
|
|
416
|
+
const queueIdx = content.indexOf(queueSection);
|
|
417
|
+
if (queueIdx !== -1) {
|
|
418
|
+
const afterQueue = content.indexOf("\n##", queueIdx + queueSection.length);
|
|
419
|
+
const insertPos = afterQueue !== -1 ? afterQueue : content.length;
|
|
420
|
+
const newEntries = newStories.map((s) => `- [ ] ${s.id}: ${s.title}`).join("\n");
|
|
421
|
+
content = content.slice(0, insertPos) + "\n" + newEntries + "\n" + content.slice(insertPos);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
content = content.replace(/^- active_story: .+$/m, "- active_story: none");
|
|
427
|
+
content = content.replace(/^- stage: .+$/m, "- stage: analyze");
|
|
428
|
+
content = content.replace(/^- pending_stories: .+$/m, "- pending_stories: []");
|
|
429
|
+
writeFileSync(trackerPath, content);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const deliveryLoop = sortedLoops[sortedLoops.length - 1];
|
|
433
|
+
if (deliveryLoop) {
|
|
434
|
+
const [, deliveryLoopConfig] = deliveryLoop;
|
|
435
|
+
const trackerPath = join2(flowDir, deliveryLoopConfig.tracker);
|
|
436
|
+
if (existsSync2(trackerPath)) {
|
|
437
|
+
let content = readFileSync(trackerPath, "utf-8");
|
|
438
|
+
content = content.replace(new RegExp(`<promise>${deliveryLoopConfig.completion}</promise>`, "g"), "");
|
|
439
|
+
content = content.replace(new RegExp(deliveryLoopConfig.completion, "g"), "");
|
|
440
|
+
content = content.replace(
|
|
441
|
+
/(## Delivery Queue\n)[\s\S]*?(## Delivered)/,
|
|
442
|
+
"$1\n$2"
|
|
443
|
+
);
|
|
444
|
+
content = content.replace(/^- active_story: .+$/m, "- active_story: none");
|
|
445
|
+
content = content.replace(/^- stage: .+$/m, "- stage: idle");
|
|
446
|
+
content = content.replace(/^- feedback: .+$/m, "- feedback: none");
|
|
447
|
+
writeFileSync(trackerPath, content);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
384
451
|
function readPrompt(loop, flowDir, agentName) {
|
|
385
|
-
const promptPath =
|
|
386
|
-
let prompt =
|
|
452
|
+
const promptPath = join2(flowDir, loop.prompt);
|
|
453
|
+
let prompt = readFileSync(promptPath, "utf-8");
|
|
387
454
|
const appName = basename(flowDir);
|
|
388
455
|
prompt = prompt.replaceAll("{{APP_NAME}}", appName);
|
|
389
456
|
if (agentName && loop.multi_agent !== false) {
|
|
@@ -400,7 +467,7 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
|
|
|
400
467
|
try {
|
|
401
468
|
let dashboardHandle;
|
|
402
469
|
if (opts.ui) {
|
|
403
|
-
const { startDashboard } = await import("./server-
|
|
470
|
+
const { startDashboard } = await import("./server-DOSLU36L.js");
|
|
404
471
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
405
472
|
}
|
|
406
473
|
await runLoop(loop, {
|
|
@@ -426,7 +493,7 @@ var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with
|
|
|
426
493
|
try {
|
|
427
494
|
let dashboardHandle;
|
|
428
495
|
if (opts.ui) {
|
|
429
|
-
const { startDashboard } = await import("./server-
|
|
496
|
+
const { startDashboard } = await import("./server-DOSLU36L.js");
|
|
430
497
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
431
498
|
}
|
|
432
499
|
await runE2E({
|
|
@@ -463,167 +530,32 @@ var statusCommand = new Command4("status").description("Show pipeline status").o
|
|
|
463
530
|
// src/cli/dashboard.ts
|
|
464
531
|
import { Command as Command5 } from "commander";
|
|
465
532
|
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-
|
|
533
|
+
const { startDashboard } = await import("./server-DOSLU36L.js");
|
|
467
534
|
await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
|
|
468
535
|
});
|
|
469
536
|
|
|
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
|
-
|
|
613
537
|
// src/cli/index.ts
|
|
614
538
|
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
|
-
|
|
539
|
+
const port = 4242;
|
|
540
|
+
const { startDashboard } = await import("./server-DOSLU36L.js");
|
|
541
|
+
await startDashboard({ cwd: process.cwd(), port });
|
|
542
|
+
const url = `http://localhost:${port}`;
|
|
543
|
+
exec(`open "${url}"`, (err) => {
|
|
544
|
+
if (err) {
|
|
545
|
+
console.log(chalk7.dim(` Open ${url} in your browser`));
|
|
546
|
+
}
|
|
547
|
+
});
|
|
616
548
|
});
|
|
617
549
|
process.on("SIGINT", () => {
|
|
618
550
|
console.log();
|
|
619
|
-
console.log(
|
|
551
|
+
console.log(chalk7.dim(" Interrupted."));
|
|
620
552
|
process.exit(130);
|
|
621
553
|
});
|
|
622
554
|
program.configureOutput({
|
|
623
555
|
writeErr: (str) => {
|
|
624
556
|
const clean = str.replace(/^error: /, "");
|
|
625
557
|
if (clean.trim()) {
|
|
626
|
-
console.error(
|
|
558
|
+
console.error(chalk7.red(` ${clean.trim()}`));
|
|
627
559
|
}
|
|
628
560
|
}
|
|
629
561
|
});
|