ralphflow 0.5.0 → 0.5.2
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 +2 -0
- package/dist/{chunk-TCCMQDVT.js → chunk-DOC64TD6.js} +32 -2
- package/dist/ralphflow.js +584 -24
- package/dist/{server-DOSLU36L.js → server-EX5MWYW4.js} +210 -10
- package/package.json +6 -2
- package/src/dashboard/ui/app.js +203 -0
- package/src/dashboard/ui/archives.js +167 -0
- package/src/dashboard/ui/index.html +2 -3210
- package/src/dashboard/ui/loop-detail.js +880 -0
- package/src/dashboard/ui/notifications.js +151 -0
- package/src/dashboard/ui/prompt-builder.js +362 -0
- package/src/dashboard/ui/sidebar.js +97 -0
- package/src/dashboard/ui/state.js +54 -0
- package/src/dashboard/ui/styles.css +2140 -0
- package/src/dashboard/ui/templates.js +1858 -0
- package/src/dashboard/ui/utils.js +115 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +73 -11
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +51 -2
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +48 -4
- package/src/templates/research/loops/00-discovery-loop/prompt.md +58 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +44 -2
- package/src/templates/research/loops/02-story-loop/prompt.md +42 -1
- package/src/templates/research/loops/03-document-loop/prompt.md +42 -1
package/dist/ralphflow.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
copyTemplate,
|
|
4
|
+
createCustomTemplate,
|
|
4
5
|
getDb,
|
|
5
6
|
incrementIteration,
|
|
6
7
|
isLoopComplete,
|
|
@@ -11,11 +12,11 @@ import {
|
|
|
11
12
|
resolveFlowDir,
|
|
12
13
|
resolveLoop,
|
|
13
14
|
showStatus
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-DOC64TD6.js";
|
|
15
16
|
|
|
16
17
|
// src/cli/index.ts
|
|
17
|
-
import { Command as
|
|
18
|
-
import
|
|
18
|
+
import { Command as Command7 } from "commander";
|
|
19
|
+
import chalk8 from "chalk";
|
|
19
20
|
import { exec } from "child_process";
|
|
20
21
|
|
|
21
22
|
// src/cli/init.ts
|
|
@@ -28,9 +29,9 @@ import { join } from "path";
|
|
|
28
29
|
import { createInterface } from "readline";
|
|
29
30
|
import chalk from "chalk";
|
|
30
31
|
function ask(rl, question) {
|
|
31
|
-
return new Promise((
|
|
32
|
+
return new Promise((resolve2) => {
|
|
32
33
|
rl.question(chalk.cyan("? ") + question + " ", (answer) => {
|
|
33
|
-
|
|
34
|
+
resolve2(answer.trim());
|
|
34
35
|
});
|
|
35
36
|
});
|
|
36
37
|
}
|
|
@@ -123,12 +124,19 @@ import chalk3 from "chalk";
|
|
|
123
124
|
// src/core/claude.ts
|
|
124
125
|
import { spawn } from "child_process";
|
|
125
126
|
async function spawnClaude(options) {
|
|
126
|
-
const { prompt, model, cwd, env: extraEnv } = options;
|
|
127
|
-
const args = [
|
|
127
|
+
const { prompt, model, cwd, env: extraEnv, claudeArgs, skipPermissions } = options;
|
|
128
|
+
const args = [];
|
|
129
|
+
if (skipPermissions !== false) {
|
|
130
|
+
args.push("--dangerously-skip-permissions");
|
|
131
|
+
}
|
|
132
|
+
if (claudeArgs && claudeArgs.length > 0) {
|
|
133
|
+
args.push(...claudeArgs);
|
|
134
|
+
}
|
|
135
|
+
args.push(prompt);
|
|
128
136
|
if (model) {
|
|
129
137
|
args.unshift("--model", model);
|
|
130
138
|
}
|
|
131
|
-
return new Promise((
|
|
139
|
+
return new Promise((resolve2, reject) => {
|
|
132
140
|
const child = spawn("claude", args, {
|
|
133
141
|
cwd,
|
|
134
142
|
stdio: "inherit",
|
|
@@ -138,7 +146,7 @@ async function spawnClaude(options) {
|
|
|
138
146
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
139
147
|
});
|
|
140
148
|
child.on("close", (code, signal) => {
|
|
141
|
-
|
|
149
|
+
resolve2({
|
|
142
150
|
output: "",
|
|
143
151
|
exitCode: code,
|
|
144
152
|
signal
|
|
@@ -223,6 +231,48 @@ function checkTrackerAllChecked(flowDir, loop) {
|
|
|
223
231
|
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
224
232
|
return checked > 0 && unchecked === 0;
|
|
225
233
|
}
|
|
234
|
+
function hasNewItemsInDataFile(flowDir, loop, config) {
|
|
235
|
+
if (!loop.entities || loop.entities.length === 0) return false;
|
|
236
|
+
const entityName = loop.entities[0];
|
|
237
|
+
const entity = config.entities?.[entityName];
|
|
238
|
+
if (!entity) return false;
|
|
239
|
+
const dataFilePath = join2(flowDir, entity.data_file);
|
|
240
|
+
if (!existsSync2(dataFilePath)) return false;
|
|
241
|
+
const trackerPath = join2(flowDir, loop.tracker);
|
|
242
|
+
if (!existsSync2(trackerPath)) return false;
|
|
243
|
+
const dataContent = readFileSync(dataFilePath, "utf-8");
|
|
244
|
+
const trackerContent = readFileSync(trackerPath, "utf-8");
|
|
245
|
+
const prefix = entity.prefix;
|
|
246
|
+
const itemRegex = new RegExp(`^## (${prefix}-\\d+):`, "gm");
|
|
247
|
+
const dataFileIds = [...dataContent.matchAll(itemRegex)].map((m) => m[1]);
|
|
248
|
+
if (dataFileIds.length === 0) return false;
|
|
249
|
+
const trackerIdRegex = new RegExp(`\\[[ x]\\]\\s*(${prefix}-\\d+)`, "gi");
|
|
250
|
+
const trackerIds = [...trackerContent.matchAll(trackerIdRegex)].map((m) => m[1]);
|
|
251
|
+
return dataFileIds.some((id) => !trackerIds.includes(id));
|
|
252
|
+
}
|
|
253
|
+
function stripStaleCompletion(flowDir, loop, config) {
|
|
254
|
+
if (!hasNewItemsInDataFile(flowDir, loop, config)) return false;
|
|
255
|
+
const trackerPath = join2(flowDir, loop.tracker);
|
|
256
|
+
if (!existsSync2(trackerPath)) return false;
|
|
257
|
+
let content = readFileSync(trackerPath, "utf-8");
|
|
258
|
+
const hadCompletion = content.includes(`<promise>${loop.completion}</promise>`) || content.includes(loop.completion);
|
|
259
|
+
if (!hadCompletion) return false;
|
|
260
|
+
content = content.replace(new RegExp(`<promise>${loop.completion}</promise>`, "g"), "");
|
|
261
|
+
content = content.replace(new RegExp(loop.completion, "g"), "");
|
|
262
|
+
writeFileSync(trackerPath, content);
|
|
263
|
+
console.log(chalk3.cyan(` \u21BB New items detected in data file \u2014 cleared stale completion for ${loop.name}`));
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
function stripCompletionString(flowDir, loop) {
|
|
267
|
+
const trackerPath = join2(flowDir, loop.tracker);
|
|
268
|
+
if (!existsSync2(trackerPath)) return;
|
|
269
|
+
let content = readFileSync(trackerPath, "utf-8");
|
|
270
|
+
const had = content.includes(`<promise>${loop.completion}</promise>`) || content.includes(loop.completion);
|
|
271
|
+
if (!had) return;
|
|
272
|
+
content = content.replace(new RegExp(`<promise>${loop.completion}</promise>`, "g"), "");
|
|
273
|
+
content = content.replace(new RegExp(loop.completion, "g"), "");
|
|
274
|
+
writeFileSync(trackerPath, content);
|
|
275
|
+
}
|
|
226
276
|
async function runLoop(loopName, options) {
|
|
227
277
|
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
228
278
|
const config = loadConfig(flowDir);
|
|
@@ -256,22 +306,25 @@ async function runLoop(loopName, options) {
|
|
|
256
306
|
cleanup();
|
|
257
307
|
process.exit(143);
|
|
258
308
|
});
|
|
309
|
+
stripCompletionString(flowDir, loop);
|
|
259
310
|
try {
|
|
260
|
-
await iterationLoop(key, loop, flowDir, options, agentName);
|
|
311
|
+
await iterationLoop(key, loop, flowDir, options, agentName, void 0, void 0, true, config);
|
|
261
312
|
} finally {
|
|
262
313
|
cleanup();
|
|
263
314
|
}
|
|
264
315
|
}
|
|
265
|
-
async function iterationLoop(configKey, loop, flowDir, options, agentName, db, flowName, forceFirstIteration) {
|
|
316
|
+
async function iterationLoop(configKey, loop, flowDir, options, agentName, db, flowName, forceFirstIteration, config) {
|
|
266
317
|
const loopKey = loop.name;
|
|
267
318
|
const appName = basename(flowDir);
|
|
319
|
+
if (config) stripStaleCompletion(flowDir, loop, config);
|
|
268
320
|
for (let i = 1; i <= options.maxIterations; i++) {
|
|
269
321
|
if (!(forceFirstIteration && i === 1)) {
|
|
270
322
|
if (db && flowName && isLoopComplete(db, flowName, loopKey)) {
|
|
271
323
|
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 already complete`));
|
|
272
324
|
return;
|
|
273
325
|
}
|
|
274
|
-
|
|
326
|
+
const newItemsExist = config ? hasNewItemsInDataFile(flowDir, loop, config) : false;
|
|
327
|
+
if (!newItemsExist && (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop))) {
|
|
275
328
|
if (db && flowName) markLoopComplete(db, flowName, loopKey);
|
|
276
329
|
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete`));
|
|
277
330
|
return;
|
|
@@ -288,10 +341,13 @@ async function iterationLoop(configKey, loop, flowDir, options, agentName, db, f
|
|
|
288
341
|
env: {
|
|
289
342
|
RALPHFLOW_APP: appName,
|
|
290
343
|
RALPHFLOW_LOOP: configKey
|
|
291
|
-
}
|
|
344
|
+
},
|
|
345
|
+
claudeArgs: loop.claude_args,
|
|
346
|
+
skipPermissions: loop.skip_permissions
|
|
292
347
|
});
|
|
293
348
|
if (db && flowName) incrementIteration(db, flowName, loopKey);
|
|
294
|
-
|
|
349
|
+
const newItemsAfter = config ? hasNewItemsInDataFile(flowDir, loop, config) : false;
|
|
350
|
+
if (!newItemsAfter && (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop))) {
|
|
295
351
|
if (db && flowName) markLoopComplete(db, flowName, loopKey);
|
|
296
352
|
console.log();
|
|
297
353
|
console.log(chalk3.green(` Loop complete: ${loop.completion}`));
|
|
@@ -336,7 +392,8 @@ async function runE2E(options) {
|
|
|
336
392
|
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
|
|
337
393
|
continue;
|
|
338
394
|
}
|
|
339
|
-
|
|
395
|
+
const loopHasNewItems = hasNewItemsInDataFile(flowDir, loop, config);
|
|
396
|
+
if (!loopHasNewItems && (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop))) {
|
|
340
397
|
markLoopComplete(db, flowName, loopKey);
|
|
341
398
|
console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
|
|
342
399
|
continue;
|
|
@@ -352,7 +409,7 @@ async function runE2E(options) {
|
|
|
352
409
|
agentName = acquireAgentId(agentDir, loop.multi_agent.max_agents);
|
|
353
410
|
}
|
|
354
411
|
try {
|
|
355
|
-
await iterationLoop(key, loop, flowDir, options, agentName, db, flowName, isFirstLoop);
|
|
412
|
+
await iterationLoop(key, loop, flowDir, options, agentName, db, flowName, isFirstLoop, config);
|
|
356
413
|
} finally {
|
|
357
414
|
if (agentDir && agentName) releaseAgentId(agentDir, agentName);
|
|
358
415
|
}
|
|
@@ -467,7 +524,7 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
|
|
|
467
524
|
try {
|
|
468
525
|
let dashboardHandle;
|
|
469
526
|
if (opts.ui) {
|
|
470
|
-
const { startDashboard } = await import("./server-
|
|
527
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
471
528
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
472
529
|
}
|
|
473
530
|
await runLoop(loop, {
|
|
@@ -493,7 +550,7 @@ var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with
|
|
|
493
550
|
try {
|
|
494
551
|
let dashboardHandle;
|
|
495
552
|
if (opts.ui) {
|
|
496
|
-
const { startDashboard } = await import("./server-
|
|
553
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
497
554
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
498
555
|
}
|
|
499
556
|
await runE2E({
|
|
@@ -530,32 +587,535 @@ var statusCommand = new Command4("status").description("Show pipeline status").o
|
|
|
530
587
|
// src/cli/dashboard.ts
|
|
531
588
|
import { Command as Command5 } from "commander";
|
|
532
589
|
var dashboardCommand = new Command5("dashboard").alias("ui").description("Start the web dashboard").option("-p, --port <port>", "Port number", "4242").action(async (opts) => {
|
|
533
|
-
const { startDashboard } = await import("./server-
|
|
590
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
534
591
|
await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
|
|
535
592
|
});
|
|
536
593
|
|
|
594
|
+
// src/cli/create-template.ts
|
|
595
|
+
import { Command as Command6 } from "commander";
|
|
596
|
+
import chalk7 from "chalk";
|
|
597
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
598
|
+
import { resolve } from "path";
|
|
599
|
+
|
|
600
|
+
// src/core/prompt-generator.ts
|
|
601
|
+
function defaultCapabilities() {
|
|
602
|
+
return {
|
|
603
|
+
webSearch: false,
|
|
604
|
+
mcpServers: false,
|
|
605
|
+
exploreAgents: false,
|
|
606
|
+
fileReadWrite: false,
|
|
607
|
+
codeEditing: false,
|
|
608
|
+
bashCommands: false
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function buildPromptLoopConfig(loopDef) {
|
|
612
|
+
const stages = loopDef.stages;
|
|
613
|
+
const stageConfigs = stages.map((stageName, i) => {
|
|
614
|
+
const caps = defaultCapabilities();
|
|
615
|
+
if (i === 0) {
|
|
616
|
+
caps.fileReadWrite = true;
|
|
617
|
+
caps.exploreAgents = true;
|
|
618
|
+
}
|
|
619
|
+
if (i === stages.length - 1 && stages.length > 1) {
|
|
620
|
+
caps.bashCommands = true;
|
|
621
|
+
caps.fileReadWrite = true;
|
|
622
|
+
}
|
|
623
|
+
if (i > 0 && i < stages.length - 1) {
|
|
624
|
+
caps.codeEditing = true;
|
|
625
|
+
caps.bashCommands = true;
|
|
626
|
+
caps.fileReadWrite = true;
|
|
627
|
+
}
|
|
628
|
+
if (stages.length === 1) {
|
|
629
|
+
caps.codeEditing = true;
|
|
630
|
+
caps.bashCommands = true;
|
|
631
|
+
}
|
|
632
|
+
return { name: stageName, description: "", capabilities: caps };
|
|
633
|
+
});
|
|
634
|
+
return {
|
|
635
|
+
name: loopDef.name,
|
|
636
|
+
stages,
|
|
637
|
+
completion: loopDef.completion,
|
|
638
|
+
multi_agent: !!(loopDef.multi_agent && typeof loopDef.multi_agent === "object" && loopDef.multi_agent.enabled),
|
|
639
|
+
entities: loopDef.entities || [],
|
|
640
|
+
inputFiles: (loopDef.fed_by || []).join(", "),
|
|
641
|
+
outputFiles: (loopDef.feeds || []).join(", "),
|
|
642
|
+
stageConfigs
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function generateVisualProtocol() {
|
|
646
|
+
let s = "";
|
|
647
|
+
s += `## Visual Communication Protocol
|
|
648
|
+
|
|
649
|
+
`;
|
|
650
|
+
s += `When communicating scope, structure, relationships, or status, render **ASCII diagrams** using Unicode box-drawing characters. These help the user see the full picture at the terminal without scrolling through prose.
|
|
651
|
+
|
|
652
|
+
`;
|
|
653
|
+
s += `**Character set:** \`\u250C \u2500 \u2510 \u2502 \u2514 \u2518 \u251C \u2524 \u252C \u2534 \u253C \u2550 \u25CF \u25CB \u25BC \u25B6\`
|
|
654
|
+
|
|
655
|
+
`;
|
|
656
|
+
s += `**Diagram types to use:**
|
|
657
|
+
|
|
658
|
+
`;
|
|
659
|
+
s += `- **Scope/Architecture Map** \u2014 components and their relationships in a bordered grid
|
|
660
|
+
`;
|
|
661
|
+
s += `- **Decomposition Tree** \u2014 hierarchical breakdown with \`\u251C\u2500\u2500\` and \`\u2514\u2500\u2500\` branches
|
|
662
|
+
`;
|
|
663
|
+
s += `- **Data Flow** \u2014 arrows (\`\u2500\u2500\u2192\`) showing how information moves between components
|
|
664
|
+
`;
|
|
665
|
+
s += `- **Comparison Table** \u2014 bordered table for trade-offs and design options
|
|
666
|
+
`;
|
|
667
|
+
s += `- **Status Summary** \u2014 bordered box with completion indicators (\`\u2713\` done, \`\u25CC\` pending)
|
|
668
|
+
|
|
669
|
+
`;
|
|
670
|
+
s += `**Rules:** Keep diagrams under 20 lines and under 70 characters wide. Populate with real data from current context. Render inside fenced code blocks. Use diagrams to supplement, not replace, prose.
|
|
671
|
+
|
|
672
|
+
`;
|
|
673
|
+
s += `---
|
|
674
|
+
|
|
675
|
+
`;
|
|
676
|
+
return s;
|
|
677
|
+
}
|
|
678
|
+
function generatePromptFromConfig(loop, loopIndex, allLoops) {
|
|
679
|
+
const loopName = loop.name || "Loop " + (loopIndex + 1);
|
|
680
|
+
const inputFiles = (loop.inputFiles || "").trim();
|
|
681
|
+
const outputFiles = (loop.outputFiles || "").trim();
|
|
682
|
+
const completion = loop.completion || "LOOP COMPLETE";
|
|
683
|
+
const entity = loop.entities && loop.entities.length > 0 ? loop.entities[0] : "";
|
|
684
|
+
const entityUpper = entity ? entity.toUpperCase() : "ITEM";
|
|
685
|
+
const entityLower = entity ? entity.toLowerCase() : "item";
|
|
686
|
+
const entityTitle = entityUpper.charAt(0) + entityLower.slice(1);
|
|
687
|
+
const entityPlural = entityLower.endsWith("s") ? entityLower : entityLower + "s";
|
|
688
|
+
const entityKey = entityUpper + "-{N}";
|
|
689
|
+
const loopKey = (loopName || "loop-" + loopIndex).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
690
|
+
const loopKeyFull = loopKey.endsWith("-loop") ? loopKey : loopKey + "-loop";
|
|
691
|
+
const dirPrefix = String(loopIndex).padStart(2, "0");
|
|
692
|
+
const loopDir = dirPrefix + "-" + loopKeyFull;
|
|
693
|
+
const pipelineIn = inputFiles || "input.md";
|
|
694
|
+
const pipelineOut = outputFiles || "output.md";
|
|
695
|
+
const primaryInputFile = inputFiles ? inputFiles.split(",")[0].trim() : "";
|
|
696
|
+
let p = "";
|
|
697
|
+
p += `# ${loopName} Loop
|
|
698
|
+
|
|
699
|
+
`;
|
|
700
|
+
p += `**App:** \`{{APP_NAME}}\` \u2014 all flow files live under \`.ralph-flow/{{APP_NAME}}/\`.
|
|
701
|
+
|
|
702
|
+
`;
|
|
703
|
+
if (loop.multi_agent) {
|
|
704
|
+
p += `**You are agent \`{{AGENT_NAME}}\`.** Multiple agents may work in parallel.
|
|
705
|
+
`;
|
|
706
|
+
p += `Coordinate via \`tracker.md\` \u2014 the single source of truth.
|
|
707
|
+
`;
|
|
708
|
+
p += `*(If you see the literal text \`{{AGENT_NAME}}\` above \u2014 i.e., it was not substituted \u2014 treat your name as \`agent-1\`.)*
|
|
709
|
+
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
p += `Read \`.ralph-flow/{{APP_NAME}}/${loopDir}/tracker.md\` FIRST to determine where you are.
|
|
713
|
+
|
|
714
|
+
`;
|
|
715
|
+
p += `> **PROJECT CONTEXT.** Read \`CLAUDE.md\` for architecture, stack, conventions, commands, and URLs.
|
|
716
|
+
|
|
717
|
+
`;
|
|
718
|
+
p += `**Pipeline:** \`${pipelineIn} \u2192 YOU \u2192 ${pipelineOut}\`
|
|
719
|
+
|
|
720
|
+
`;
|
|
721
|
+
p += `---
|
|
722
|
+
|
|
723
|
+
`;
|
|
724
|
+
p += generateVisualProtocol();
|
|
725
|
+
if (loop.multi_agent) {
|
|
726
|
+
p += `## Tracker Lock Protocol
|
|
727
|
+
|
|
728
|
+
`;
|
|
729
|
+
p += `Before ANY write to \`tracker.md\`, you MUST acquire the lock:
|
|
730
|
+
|
|
731
|
+
`;
|
|
732
|
+
p += `**Lock file:** \`.ralph-flow/{{APP_NAME}}/${loopDir}/.tracker-lock\`
|
|
733
|
+
|
|
734
|
+
`;
|
|
735
|
+
p += `### Acquire Lock
|
|
736
|
+
`;
|
|
737
|
+
p += `1. Check if \`.tracker-lock\` exists
|
|
738
|
+
`;
|
|
739
|
+
p += ` - Exists AND file is < 60 seconds old \u2192 sleep 2s, retry (up to 5 retries)
|
|
740
|
+
`;
|
|
741
|
+
p += ` - Exists AND file is \u2265 60 seconds old \u2192 stale lock, delete it (agent crashed mid-write)
|
|
742
|
+
`;
|
|
743
|
+
p += ` - Does not exist \u2192 continue
|
|
744
|
+
`;
|
|
745
|
+
p += `2. Write lock: \`echo "{{AGENT_NAME}} $(date -u +%Y-%m-%dT%H:%M:%SZ)" > .ralph-flow/{{APP_NAME}}/${loopDir}/.tracker-lock\`
|
|
746
|
+
`;
|
|
747
|
+
p += `3. Sleep 500ms (\`sleep 0.5\`)
|
|
748
|
+
`;
|
|
749
|
+
p += `4. Re-read \`.tracker-lock\` \u2014 verify YOUR agent name (\`{{AGENT_NAME}}\`) is in it
|
|
750
|
+
`;
|
|
751
|
+
p += ` - Your name \u2192 you own the lock, proceed to write \`tracker.md\`
|
|
752
|
+
`;
|
|
753
|
+
p += ` - Other name \u2192 you lost the race, retry from step 1
|
|
754
|
+
`;
|
|
755
|
+
p += `5. Write your changes to \`tracker.md\`
|
|
756
|
+
`;
|
|
757
|
+
p += `6. Delete \`.tracker-lock\` immediately: \`rm .ralph-flow/{{APP_NAME}}/${loopDir}/.tracker-lock\`
|
|
758
|
+
`;
|
|
759
|
+
p += `7. Never leave a lock held \u2014 if your write fails, delete the lock in your error handler
|
|
760
|
+
|
|
761
|
+
`;
|
|
762
|
+
p += `### When to Lock
|
|
763
|
+
`;
|
|
764
|
+
p += `- Claiming a ${entityLower} (pending \u2192 in_progress)
|
|
765
|
+
`;
|
|
766
|
+
p += `- Completing a ${entityLower} (in_progress \u2192 completed, unblocking dependents)
|
|
767
|
+
`;
|
|
768
|
+
p += `- Updating stage transitions
|
|
769
|
+
`;
|
|
770
|
+
p += `- Heartbeat updates (bundled with other writes, not standalone)
|
|
771
|
+
|
|
772
|
+
`;
|
|
773
|
+
p += `### When NOT to Lock
|
|
774
|
+
`;
|
|
775
|
+
p += `- Reading \`tracker.md\` \u2014 read-only access needs no lock
|
|
776
|
+
`;
|
|
777
|
+
if (primaryInputFile) {
|
|
778
|
+
p += `- Reading \`${primaryInputFile}\` \u2014 always read-only
|
|
779
|
+
`;
|
|
780
|
+
}
|
|
781
|
+
p += `
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
`;
|
|
785
|
+
p += `## ${entityTitle} Selection Algorithm
|
|
786
|
+
|
|
787
|
+
`;
|
|
788
|
+
p += `1. **Parse tracker** \u2014 read \`completed_${entityPlural}\`, \`## Dependencies\`, ${entityTitle}s Queue metadata \`{agent, status}\`, Agent Status table
|
|
789
|
+
`;
|
|
790
|
+
p += `2. **Update blocked\u2192pending** \u2014 for each ${entityLower} with \`status: blocked\`, check if ALL its dependencies (from \`## Dependencies\`) are in \`completed_${entityPlural}\`. If yes, acquire lock and update to \`status: pending\`
|
|
791
|
+
`;
|
|
792
|
+
p += `3. **Resume own work** \u2014 if any ${entityLower} has \`{agent: {{AGENT_NAME}}, status: in_progress}\`, resume it (skip to the current stage)
|
|
793
|
+
`;
|
|
794
|
+
p += `4. **Find claimable** \u2014 filter ${entityPlural} where \`status: pending\` AND \`agent: -\`
|
|
795
|
+
`;
|
|
796
|
+
p += `5. **Claim** \u2014 acquire lock, set \`{agent: {{AGENT_NAME}}, status: in_progress}\`, update your Agent Status row, update \`last_heartbeat\`, release lock, log the claim
|
|
797
|
+
`;
|
|
798
|
+
p += `6. **Nothing available:**
|
|
799
|
+
`;
|
|
800
|
+
p += ` - All ${entityPlural} completed \u2192 emit \`<promise>${completion}</promise>\`
|
|
801
|
+
`;
|
|
802
|
+
p += ` - All remaining ${entityPlural} are blocked or claimed by others \u2192 log "{{AGENT_NAME}}: waiting \u2014 all ${entityPlural} blocked or claimed", exit: \`kill -INT $PPID\`
|
|
803
|
+
|
|
804
|
+
`;
|
|
805
|
+
p += `### New ${entityTitle} Discovery
|
|
806
|
+
|
|
807
|
+
`;
|
|
808
|
+
p += `If you find a ${entityLower} in the Queue without \`{agent, status}\` metadata:
|
|
809
|
+
`;
|
|
810
|
+
p += `1. Read its \`**Depends on:**\` field
|
|
811
|
+
`;
|
|
812
|
+
p += `2. Add the dependency to \`## Dependencies\` section if not already there (skip if \`Depends on: None\`)
|
|
813
|
+
`;
|
|
814
|
+
p += `3. Set status to \`pending\` (all deps in \`completed_${entityPlural}\`) or \`blocked\` (deps incomplete)
|
|
815
|
+
`;
|
|
816
|
+
p += `4. Set agent to \`-\`
|
|
817
|
+
|
|
818
|
+
`;
|
|
819
|
+
p += `---
|
|
820
|
+
|
|
821
|
+
`;
|
|
822
|
+
p += `## Anti-Hijacking Rules
|
|
823
|
+
|
|
824
|
+
`;
|
|
825
|
+
p += `1. **Never touch another agent's \`in_progress\` ${entityLower}** \u2014 do not modify, complete, or reassign it
|
|
826
|
+
`;
|
|
827
|
+
p += `2. **Respect ownership** \u2014 if another agent is active in a group, leave remaining group ${entityPlural} for them
|
|
828
|
+
`;
|
|
829
|
+
p += `3. **Note file overlap conflicts** \u2014 if your ${entityLower} modifies files that another agent's active ${entityLower} also modifies, log a WARNING in the tracker
|
|
830
|
+
|
|
831
|
+
`;
|
|
832
|
+
p += `---
|
|
833
|
+
|
|
834
|
+
`;
|
|
835
|
+
p += `## Heartbeat Protocol
|
|
836
|
+
|
|
837
|
+
`;
|
|
838
|
+
p += `Every tracker write includes updating your \`last_heartbeat\` to current ISO 8601 timestamp in the Agent Status table. If another agent's heartbeat is **30+ minutes stale**, log a WARNING in the tracker log but do NOT auto-reclaim their ${entityLower} \u2014 user must manually reset.
|
|
839
|
+
|
|
840
|
+
`;
|
|
841
|
+
p += `---
|
|
842
|
+
|
|
843
|
+
`;
|
|
844
|
+
p += `## Crash Recovery (Self)
|
|
845
|
+
|
|
846
|
+
`;
|
|
847
|
+
p += `On fresh start, if your agent name has an \`in_progress\` ${entityLower} but you have no memory of it:
|
|
848
|
+
`;
|
|
849
|
+
const lastStage = loop.stages.length > 1 ? loop.stages[loop.stages.length - 1] : "last";
|
|
850
|
+
const firstStage = loop.stages[0] || "first";
|
|
851
|
+
p += `- Work committed for that ${entityLower} \u2192 resume at ${lastStage.toUpperCase()} stage
|
|
852
|
+
`;
|
|
853
|
+
p += `- No work found \u2192 restart from ${firstStage.toUpperCase()} stage
|
|
854
|
+
|
|
855
|
+
`;
|
|
856
|
+
p += `---
|
|
857
|
+
|
|
858
|
+
`;
|
|
859
|
+
}
|
|
860
|
+
const stageCount = loop.stages.length;
|
|
861
|
+
p += `## State Machine (${stageCount} stage${stageCount !== 1 ? "s" : ""} per ${entityLower})
|
|
862
|
+
|
|
863
|
+
\`\`\`
|
|
864
|
+
`;
|
|
865
|
+
loop.stages.forEach((stage, i) => {
|
|
866
|
+
const sc = loop.stageConfigs[i];
|
|
867
|
+
const desc = sc && sc.description ? sc.description.split("\n")[0].substring(0, 55) : "Complete this stage";
|
|
868
|
+
const next = i < stageCount - 1 ? `\u2192 stage: ${loop.stages[i + 1]}` : `\u2192 next ${entityLower}`;
|
|
869
|
+
p += `${stage.toUpperCase()} \u2192 ${desc} ${next}
|
|
870
|
+
`;
|
|
871
|
+
});
|
|
872
|
+
p += `\`\`\`
|
|
873
|
+
|
|
874
|
+
`;
|
|
875
|
+
p += `When ALL done: \`<promise>${completion}</promise>\`
|
|
876
|
+
|
|
877
|
+
`;
|
|
878
|
+
p += `After completing ANY stage, exit: \`kill -INT $PPID\`
|
|
879
|
+
|
|
880
|
+
`;
|
|
881
|
+
p += `---
|
|
882
|
+
|
|
883
|
+
`;
|
|
884
|
+
{
|
|
885
|
+
const scanFile = primaryInputFile || "input.md";
|
|
886
|
+
p += `## First-Run / New ${entityTitle} Detection
|
|
887
|
+
|
|
888
|
+
`;
|
|
889
|
+
if (loop.multi_agent) {
|
|
890
|
+
p += `If ${entityTitle}s Queue in tracker is empty OR all entries are \`[x]\`: read \`${scanFile}\`, scan \`## ${entityKey}:\` headers + \`**Depends on:**\` tags. For any ${entityLower} NOT already in the queue, add as \`- [ ] ${entityKey}: {title}\` with \`{agent: -, status: pending|blocked}\` metadata (compute from Dependencies), then start.
|
|
891
|
+
|
|
892
|
+
`;
|
|
893
|
+
} else {
|
|
894
|
+
p += `If ${entityTitle}s Queue in tracker is empty OR all entries are \`[x]\`: read \`${scanFile}\`, scan \`## ${entityKey}:\` headers + \`**Depends on:**\` tags. For any ${entityLower} NOT already in the queue, add as \`- [ ] ${entityKey}: {title}\` and update Dependencies. If new ${entityPlural} were added, proceed to process them.
|
|
895
|
+
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
p += `---
|
|
899
|
+
|
|
900
|
+
`;
|
|
901
|
+
}
|
|
902
|
+
loop.stages.forEach((stage, i) => {
|
|
903
|
+
const sc = loop.stageConfigs[i];
|
|
904
|
+
p += `## STAGE ${i + 1}: ${stage.toUpperCase()}
|
|
905
|
+
|
|
906
|
+
`;
|
|
907
|
+
if (sc && sc.description) {
|
|
908
|
+
p += `${sc.description}
|
|
909
|
+
|
|
910
|
+
`;
|
|
911
|
+
}
|
|
912
|
+
let step = 1;
|
|
913
|
+
if (loop.multi_agent && i === 0) {
|
|
914
|
+
p += `${step++}. Read tracker \u2192 **run ${entityLower} selection algorithm** (see above)
|
|
915
|
+
`;
|
|
916
|
+
} else {
|
|
917
|
+
p += `${step++}. Read tracker \u2192 determine current state
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
if (sc && sc.capabilities) {
|
|
921
|
+
if (sc.capabilities.fileReadWrite) {
|
|
922
|
+
const files = primaryInputFile ? ` (\`${primaryInputFile}\`)` : "";
|
|
923
|
+
p += `${step++}. Read relevant input files${files} and explore affected areas
|
|
924
|
+
`;
|
|
925
|
+
}
|
|
926
|
+
if (sc.capabilities.exploreAgents) {
|
|
927
|
+
p += `${step++}. Use the Agent tool to explore the codebase \u2014 read **40+ files** across affected areas, dependencies, patterns
|
|
928
|
+
`;
|
|
929
|
+
}
|
|
930
|
+
if (sc.capabilities.webSearch) {
|
|
931
|
+
p += `${step++}. Use WebSearch for 5-10 queries to gather external information
|
|
932
|
+
`;
|
|
933
|
+
}
|
|
934
|
+
if (sc.capabilities.mcpServers) {
|
|
935
|
+
p += `${step++}. Use MCP tools for specialized operations
|
|
936
|
+
`;
|
|
937
|
+
}
|
|
938
|
+
if (sc.capabilities.codeEditing) {
|
|
939
|
+
p += `${step++}. Implement changes, matching existing patterns per \`CLAUDE.md\`
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
if (sc.capabilities.bashCommands) {
|
|
943
|
+
p += `${step++}. Run build/deploy commands to verify changes
|
|
944
|
+
`;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (i === 0) {
|
|
948
|
+
p += `${step++}. **Render a Scope Diagram** \u2014 output an ASCII architecture/scope map showing the areas this ${entityLower} touches, dependencies, and what needs to change
|
|
949
|
+
`;
|
|
950
|
+
}
|
|
951
|
+
if (i === stageCount - 1 && stageCount > 1) {
|
|
952
|
+
p += `${step++}. **Render a Completion Summary** \u2014 output an ASCII status diagram showing what was built/changed, verification results, and how this ${entityLower} fits in overall progress
|
|
953
|
+
`;
|
|
954
|
+
}
|
|
955
|
+
if (loop.multi_agent) {
|
|
956
|
+
p += `${step++}. Acquire lock \u2192 update tracker: stage, \`last_heartbeat\`, log entry \u2192 release lock
|
|
957
|
+
`;
|
|
958
|
+
} else {
|
|
959
|
+
p += `${step++}. Update tracker with progress
|
|
960
|
+
`;
|
|
961
|
+
}
|
|
962
|
+
p += `${step++}. Exit: \`kill -INT $PPID\`
|
|
963
|
+
|
|
964
|
+
`;
|
|
965
|
+
if (i < stageCount - 1) p += `---
|
|
966
|
+
|
|
967
|
+
`;
|
|
968
|
+
});
|
|
969
|
+
if (outputFiles && entity) {
|
|
970
|
+
p += `---
|
|
971
|
+
|
|
972
|
+
`;
|
|
973
|
+
p += `## Output Format
|
|
974
|
+
|
|
975
|
+
`;
|
|
976
|
+
p += `Write to \`${outputFiles.split(",")[0].trim()}\` using this format:
|
|
977
|
+
|
|
978
|
+
`;
|
|
979
|
+
p += `\`\`\`markdown
|
|
980
|
+
`;
|
|
981
|
+
p += `## ${entityKey}: {Title}
|
|
982
|
+
|
|
983
|
+
`;
|
|
984
|
+
p += `**Depends on:** {dependency or "None"}
|
|
985
|
+
|
|
986
|
+
`;
|
|
987
|
+
p += `### Description
|
|
988
|
+
`;
|
|
989
|
+
p += `{Content for this ${entityLower}}
|
|
990
|
+
|
|
991
|
+
`;
|
|
992
|
+
p += `### Acceptance Criteria
|
|
993
|
+
`;
|
|
994
|
+
p += `- [ ] {Criterion 1}
|
|
995
|
+
`;
|
|
996
|
+
p += `- [ ] {Criterion 2}
|
|
997
|
+
`;
|
|
998
|
+
p += `\`\`\`
|
|
999
|
+
|
|
1000
|
+
`;
|
|
1001
|
+
}
|
|
1002
|
+
p += `---
|
|
1003
|
+
|
|
1004
|
+
## Rules
|
|
1005
|
+
|
|
1006
|
+
`;
|
|
1007
|
+
p += `- One ${entityLower} at a time${loop.multi_agent ? " per agent" : ""}. One stage per iteration.
|
|
1008
|
+
`;
|
|
1009
|
+
p += `- Read tracker first, update tracker last.${loop.multi_agent ? " Always use lock protocol for writes." : ""}
|
|
1010
|
+
`;
|
|
1011
|
+
p += `- Read \`CLAUDE.md\` for all project-specific context.
|
|
1012
|
+
`;
|
|
1013
|
+
p += `- Thorough exploration before making changes.
|
|
1014
|
+
`;
|
|
1015
|
+
if (loop.multi_agent) {
|
|
1016
|
+
p += `- **Multi-agent: never touch another agent's in_progress ${entityLower}. Coordinate via tracker.md.**
|
|
1017
|
+
`;
|
|
1018
|
+
}
|
|
1019
|
+
p += `
|
|
1020
|
+
---
|
|
1021
|
+
|
|
1022
|
+
`;
|
|
1023
|
+
p += `Read \`.ralph-flow/{{APP_NAME}}/${loopDir}/tracker.md\` now and begin.
|
|
1024
|
+
`;
|
|
1025
|
+
return p;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/cli/create-template.ts
|
|
1029
|
+
var createTemplateCommand = new Command6("create-template").description("Create a custom template from a JSON definition").option("--config <json>", "Template definition as inline JSON").option("--config-file <path>", "Path to a JSON file with the template definition").action(async (opts) => {
|
|
1030
|
+
try {
|
|
1031
|
+
const cwd = process.cwd();
|
|
1032
|
+
let raw;
|
|
1033
|
+
if (opts.config) {
|
|
1034
|
+
raw = opts.config;
|
|
1035
|
+
} else if (opts.configFile) {
|
|
1036
|
+
const filePath = resolve(cwd, opts.configFile);
|
|
1037
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
1038
|
+
} else {
|
|
1039
|
+
console.error(chalk7.red("\n Provide --config <json> or --config-file <path>\n"));
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
let definition;
|
|
1043
|
+
try {
|
|
1044
|
+
definition = JSON.parse(raw);
|
|
1045
|
+
} catch {
|
|
1046
|
+
console.error(chalk7.red("\n Invalid JSON. Check syntax and try again.\n"));
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
if (!definition.name) {
|
|
1050
|
+
console.error(chalk7.red("\n Template name is required.\n"));
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
if (!definition.loops || definition.loops.length === 0) {
|
|
1054
|
+
console.error(chalk7.red("\n At least one loop is required.\n"));
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
for (const loop of definition.loops) {
|
|
1058
|
+
if (!loop.name) {
|
|
1059
|
+
console.error(chalk7.red("\n Each loop must have a name.\n"));
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
if (!loop.stages || loop.stages.length === 0) {
|
|
1063
|
+
console.error(chalk7.red("\n Each loop must have at least one stage.\n"));
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
if (!loop.completion) {
|
|
1067
|
+
console.error(chalk7.red("\n Each loop must have a completion string.\n"));
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const allPromptLoopConfigs = definition.loops.map((l) => buildPromptLoopConfig(l));
|
|
1072
|
+
for (let i = 0; i < definition.loops.length; i++) {
|
|
1073
|
+
const loopDef = definition.loops[i];
|
|
1074
|
+
if (!loopDef.prompt || !loopDef.prompt.trim()) {
|
|
1075
|
+
loopDef.prompt = generatePromptFromConfig(allPromptLoopConfigs[i], i, allPromptLoopConfigs);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
createCustomTemplate(cwd, definition);
|
|
1079
|
+
const templatePath = `.ralph-flow/.templates/${definition.name}/`;
|
|
1080
|
+
console.log();
|
|
1081
|
+
console.log(chalk7.green(" \u2713 Template created: ") + chalk7.bold(definition.name));
|
|
1082
|
+
console.log(chalk7.dim(` ${templatePath}`));
|
|
1083
|
+
console.log();
|
|
1084
|
+
console.log(chalk7.dim(" Next steps:"));
|
|
1085
|
+
console.log(chalk7.dim(` ralphflow init -t ${definition.name} -n my-project`));
|
|
1086
|
+
console.log(chalk7.dim(" ralphflow run <loop>"));
|
|
1087
|
+
console.log();
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
+
console.error(chalk7.red(`
|
|
1091
|
+
${msg}
|
|
1092
|
+
`));
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
537
1097
|
// src/cli/index.ts
|
|
538
|
-
var program = new
|
|
1098
|
+
var program = new Command7().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).addCommand(createTemplateCommand).action(async () => {
|
|
539
1099
|
const port = 4242;
|
|
540
|
-
const { startDashboard } = await import("./server-
|
|
1100
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
541
1101
|
await startDashboard({ cwd: process.cwd(), port });
|
|
542
1102
|
const url = `http://localhost:${port}`;
|
|
543
1103
|
exec(`open "${url}"`, (err) => {
|
|
544
1104
|
if (err) {
|
|
545
|
-
console.log(
|
|
1105
|
+
console.log(chalk8.dim(` Open ${url} in your browser`));
|
|
546
1106
|
}
|
|
547
1107
|
});
|
|
548
1108
|
});
|
|
549
1109
|
process.on("SIGINT", () => {
|
|
550
1110
|
console.log();
|
|
551
|
-
console.log(
|
|
1111
|
+
console.log(chalk8.dim(" Interrupted."));
|
|
552
1112
|
process.exit(130);
|
|
553
1113
|
});
|
|
554
1114
|
program.configureOutput({
|
|
555
1115
|
writeErr: (str) => {
|
|
556
1116
|
const clean = str.replace(/^error: /, "");
|
|
557
1117
|
if (clean.trim()) {
|
|
558
|
-
console.error(
|
|
1118
|
+
console.error(chalk8.red(` ${clean.trim()}`));
|
|
559
1119
|
}
|
|
560
1120
|
}
|
|
561
1121
|
});
|