ralphflow 0.5.0 → 0.5.1
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 +483 -18
- 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 +327 -0
- package/src/dashboard/ui/sidebar.js +97 -0
- package/src/dashboard/ui/state.js +54 -0
- package/src/dashboard/ui/styles.css +2119 -0
- package/src/dashboard/ui/templates.js +1855 -0
- package/src/dashboard/ui/utils.js +115 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +22 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +23 -0
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +21 -0
- package/src/templates/research/loops/00-discovery-loop/prompt.md +22 -0
- package/src/templates/research/loops/01-research-loop/prompt.md +22 -0
- package/src/templates/research/loops/02-story-loop/prompt.md +22 -0
- package/src/templates/research/loops/03-document-loop/prompt.md +22 -0
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Multi-agent AI workflow orchestration for [Claude Code](https://docs.anthropic.c
|
|
|
8
8
|
|
|
9
9
|
Define pipelines as loops, coordinate parallel agents via file-based trackers, and ship structured work — from single-agent interactive sessions to multi-agent autonomous execution.
|
|
10
10
|
|
|
11
|
+
**[Documentation](https://rahulthakur319.github.io/ralph-flow/)** | **[npm](https://www.npmjs.com/package/ralphflow)** | **[GitHub](https://github.com/rahulthakur319/ralph-flow)**
|
|
12
|
+
|
|
11
13
|
## Quick Start
|
|
12
14
|
|
|
13
15
|
```bash
|
|
@@ -138,6 +138,12 @@ function createCustomTemplate(cwd, definition) {
|
|
|
138
138
|
model: loopDef.model || "claude-sonnet-4-6",
|
|
139
139
|
cadence: loopDef.cadence ?? 0
|
|
140
140
|
};
|
|
141
|
+
if (loopDef.claude_args && loopDef.claude_args.length > 0) {
|
|
142
|
+
loopConfig.claude_args = loopDef.claude_args;
|
|
143
|
+
}
|
|
144
|
+
if (loopDef.skip_permissions !== void 0) {
|
|
145
|
+
loopConfig.skip_permissions = loopDef.skip_permissions;
|
|
146
|
+
}
|
|
141
147
|
if (loopDef.data_files && loopDef.data_files.length > 0) {
|
|
142
148
|
loopConfig.data_files = loopDef.data_files.map((f) => `${loopDirName}/${f}`);
|
|
143
149
|
}
|
|
@@ -173,10 +179,11 @@ function createCustomTemplate(cwd, definition) {
|
|
|
173
179
|
const loopDirName = `${dirPrefix}-${loopKey}`;
|
|
174
180
|
const loopDir = join(loopsDir, loopDirName);
|
|
175
181
|
mkdirSync(loopDir, { recursive: true });
|
|
176
|
-
|
|
182
|
+
const promptContent = loopDef.prompt && loopDef.prompt.trim() ? loopDef.prompt : `# ${loopDef.name} \u2014 Prompt
|
|
177
183
|
|
|
178
184
|
<!-- Add your prompt here -->
|
|
179
|
-
|
|
185
|
+
`;
|
|
186
|
+
writeFileSync(join(loopDir, "prompt.md"), promptContent, "utf-8");
|
|
180
187
|
writeFileSync(join(loopDir, "tracker.md"), `# ${loopDef.name} \u2014 Tracker
|
|
181
188
|
|
|
182
189
|
- stage: ${loopDef.stages[0] || "init"}
|
|
@@ -198,6 +205,28 @@ function createCustomTemplate(cwd, definition) {
|
|
|
198
205
|
}
|
|
199
206
|
});
|
|
200
207
|
}
|
|
208
|
+
function cloneBuiltInTemplate(cwd, sourceName, newName) {
|
|
209
|
+
if (!BUILT_IN_TEMPLATES.includes(sourceName)) {
|
|
210
|
+
throw new Error(`"${sourceName}" is not a built-in template. Only built-in templates can be cloned.`);
|
|
211
|
+
}
|
|
212
|
+
const validation = validateTemplateName(newName);
|
|
213
|
+
if (!validation.valid) {
|
|
214
|
+
throw new Error(validation.error);
|
|
215
|
+
}
|
|
216
|
+
const customDir = join(cwd, ".ralph-flow", ".templates", newName);
|
|
217
|
+
if (existsSync(customDir)) {
|
|
218
|
+
throw new Error(`Template "${newName}" already exists`);
|
|
219
|
+
}
|
|
220
|
+
const sourceDir = resolveTemplatePath(sourceName);
|
|
221
|
+
mkdirSync(customDir, { recursive: true });
|
|
222
|
+
cpSync(sourceDir, customDir, { recursive: true });
|
|
223
|
+
const yamlPath = join(customDir, "ralphflow.yaml");
|
|
224
|
+
if (existsSync(yamlPath)) {
|
|
225
|
+
const content = readFileSync(yamlPath, "utf-8");
|
|
226
|
+
const patched = content.replace(/^name:\s*.+$/m, `name: ${newName}`);
|
|
227
|
+
writeFileSync(yamlPath, patched, "utf-8");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
201
230
|
function deleteCustomTemplate(cwd, name) {
|
|
202
231
|
if (BUILT_IN_TEMPLATES.includes(name)) {
|
|
203
232
|
throw new Error("Cannot delete built-in templates");
|
|
@@ -487,6 +516,7 @@ export {
|
|
|
487
516
|
listCustomTemplates,
|
|
488
517
|
getAvailableTemplates,
|
|
489
518
|
createCustomTemplate,
|
|
519
|
+
cloneBuiltInTemplate,
|
|
490
520
|
deleteCustomTemplate,
|
|
491
521
|
listFlows,
|
|
492
522
|
resolveFlowDir,
|
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
|
|
@@ -288,7 +296,9 @@ async function iterationLoop(configKey, loop, flowDir, options, agentName, db, f
|
|
|
288
296
|
env: {
|
|
289
297
|
RALPHFLOW_APP: appName,
|
|
290
298
|
RALPHFLOW_LOOP: configKey
|
|
291
|
-
}
|
|
299
|
+
},
|
|
300
|
+
claudeArgs: loop.claude_args,
|
|
301
|
+
skipPermissions: loop.skip_permissions
|
|
292
302
|
});
|
|
293
303
|
if (db && flowName) incrementIteration(db, flowName, loopKey);
|
|
294
304
|
if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
|
|
@@ -467,7 +477,7 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
|
|
|
467
477
|
try {
|
|
468
478
|
let dashboardHandle;
|
|
469
479
|
if (opts.ui) {
|
|
470
|
-
const { startDashboard } = await import("./server-
|
|
480
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
471
481
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
472
482
|
}
|
|
473
483
|
await runLoop(loop, {
|
|
@@ -493,7 +503,7 @@ var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with
|
|
|
493
503
|
try {
|
|
494
504
|
let dashboardHandle;
|
|
495
505
|
if (opts.ui) {
|
|
496
|
-
const { startDashboard } = await import("./server-
|
|
506
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
497
507
|
dashboardHandle = await startDashboard({ cwd: process.cwd() });
|
|
498
508
|
}
|
|
499
509
|
await runE2E({
|
|
@@ -530,32 +540,487 @@ var statusCommand = new Command4("status").description("Show pipeline status").o
|
|
|
530
540
|
// src/cli/dashboard.ts
|
|
531
541
|
import { Command as Command5 } from "commander";
|
|
532
542
|
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-
|
|
543
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
534
544
|
await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
|
|
535
545
|
});
|
|
536
546
|
|
|
547
|
+
// src/cli/create-template.ts
|
|
548
|
+
import { Command as Command6 } from "commander";
|
|
549
|
+
import chalk7 from "chalk";
|
|
550
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
551
|
+
import { resolve } from "path";
|
|
552
|
+
|
|
553
|
+
// src/core/prompt-generator.ts
|
|
554
|
+
function defaultCapabilities() {
|
|
555
|
+
return {
|
|
556
|
+
webSearch: false,
|
|
557
|
+
mcpServers: false,
|
|
558
|
+
exploreAgents: false,
|
|
559
|
+
fileReadWrite: false,
|
|
560
|
+
codeEditing: false,
|
|
561
|
+
bashCommands: false
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function buildPromptLoopConfig(loopDef) {
|
|
565
|
+
const stages = loopDef.stages;
|
|
566
|
+
const stageConfigs = stages.map((stageName, i) => {
|
|
567
|
+
const caps = defaultCapabilities();
|
|
568
|
+
if (i === 0) {
|
|
569
|
+
caps.fileReadWrite = true;
|
|
570
|
+
caps.exploreAgents = true;
|
|
571
|
+
}
|
|
572
|
+
if (i === stages.length - 1 && stages.length > 1) {
|
|
573
|
+
caps.bashCommands = true;
|
|
574
|
+
caps.fileReadWrite = true;
|
|
575
|
+
}
|
|
576
|
+
if (i > 0 && i < stages.length - 1) {
|
|
577
|
+
caps.codeEditing = true;
|
|
578
|
+
caps.bashCommands = true;
|
|
579
|
+
caps.fileReadWrite = true;
|
|
580
|
+
}
|
|
581
|
+
if (stages.length === 1) {
|
|
582
|
+
caps.codeEditing = true;
|
|
583
|
+
caps.bashCommands = true;
|
|
584
|
+
}
|
|
585
|
+
return { name: stageName, description: "", capabilities: caps };
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
name: loopDef.name,
|
|
589
|
+
stages,
|
|
590
|
+
completion: loopDef.completion,
|
|
591
|
+
multi_agent: !!(loopDef.multi_agent && typeof loopDef.multi_agent === "object" && loopDef.multi_agent.enabled),
|
|
592
|
+
entities: loopDef.entities || [],
|
|
593
|
+
inputFiles: (loopDef.fed_by || []).join(", "),
|
|
594
|
+
outputFiles: (loopDef.feeds || []).join(", "),
|
|
595
|
+
stageConfigs
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function generatePromptFromConfig(loop, loopIndex, allLoops) {
|
|
599
|
+
const loopName = loop.name || "Loop " + (loopIndex + 1);
|
|
600
|
+
const inputFiles = (loop.inputFiles || "").trim();
|
|
601
|
+
const outputFiles = (loop.outputFiles || "").trim();
|
|
602
|
+
const completion = loop.completion || "LOOP COMPLETE";
|
|
603
|
+
const entity = loop.entities && loop.entities.length > 0 ? loop.entities[0] : "";
|
|
604
|
+
const entityUpper = entity ? entity.toUpperCase() : "ITEM";
|
|
605
|
+
const entityLower = entity ? entity.toLowerCase() : "item";
|
|
606
|
+
const entityTitle = entityUpper.charAt(0) + entityLower.slice(1);
|
|
607
|
+
const entityPlural = entityLower.endsWith("s") ? entityLower : entityLower + "s";
|
|
608
|
+
const entityKey = entityUpper + "-{N}";
|
|
609
|
+
const loopKey = (loopName || "loop-" + loopIndex).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
610
|
+
const loopKeyFull = loopKey.endsWith("-loop") ? loopKey : loopKey + "-loop";
|
|
611
|
+
const dirPrefix = String(loopIndex).padStart(2, "0");
|
|
612
|
+
const loopDir = dirPrefix + "-" + loopKeyFull;
|
|
613
|
+
const pipelineIn = inputFiles || "input.md";
|
|
614
|
+
const pipelineOut = outputFiles || "output.md";
|
|
615
|
+
const primaryInputFile = inputFiles ? inputFiles.split(",")[0].trim() : "";
|
|
616
|
+
let p = "";
|
|
617
|
+
p += `# ${loopName} Loop
|
|
618
|
+
|
|
619
|
+
`;
|
|
620
|
+
p += `**App:** \`{{APP_NAME}}\` \u2014 all flow files live under \`.ralph-flow/{{APP_NAME}}/\`.
|
|
621
|
+
|
|
622
|
+
`;
|
|
623
|
+
if (loop.multi_agent) {
|
|
624
|
+
p += `**You are agent \`{{AGENT_NAME}}\`.** Multiple agents may work in parallel.
|
|
625
|
+
`;
|
|
626
|
+
p += `Coordinate via \`tracker.md\` \u2014 the single source of truth.
|
|
627
|
+
`;
|
|
628
|
+
p += `*(If you see the literal text \`{{AGENT_NAME}}\` above \u2014 i.e., it was not substituted \u2014 treat your name as \`agent-1\`.)*
|
|
629
|
+
|
|
630
|
+
`;
|
|
631
|
+
}
|
|
632
|
+
p += `Read \`.ralph-flow/{{APP_NAME}}/${loopDir}/tracker.md\` FIRST to determine where you are.
|
|
633
|
+
|
|
634
|
+
`;
|
|
635
|
+
p += `> **PROJECT CONTEXT.** Read \`CLAUDE.md\` for architecture, stack, conventions, commands, and URLs.
|
|
636
|
+
|
|
637
|
+
`;
|
|
638
|
+
p += `**Pipeline:** \`${pipelineIn} \u2192 YOU \u2192 ${pipelineOut}\`
|
|
639
|
+
|
|
640
|
+
`;
|
|
641
|
+
p += `---
|
|
642
|
+
|
|
643
|
+
`;
|
|
644
|
+
if (loop.multi_agent) {
|
|
645
|
+
p += `## Tracker Lock Protocol
|
|
646
|
+
|
|
647
|
+
`;
|
|
648
|
+
p += `Before ANY write to \`tracker.md\`, you MUST acquire the lock:
|
|
649
|
+
|
|
650
|
+
`;
|
|
651
|
+
p += `**Lock file:** \`.ralph-flow/{{APP_NAME}}/${loopDir}/.tracker-lock\`
|
|
652
|
+
|
|
653
|
+
`;
|
|
654
|
+
p += `### Acquire Lock
|
|
655
|
+
`;
|
|
656
|
+
p += `1. Check if \`.tracker-lock\` exists
|
|
657
|
+
`;
|
|
658
|
+
p += ` - Exists AND file is < 60 seconds old \u2192 sleep 2s, retry (up to 5 retries)
|
|
659
|
+
`;
|
|
660
|
+
p += ` - Exists AND file is \u2265 60 seconds old \u2192 stale lock, delete it (agent crashed mid-write)
|
|
661
|
+
`;
|
|
662
|
+
p += ` - Does not exist \u2192 continue
|
|
663
|
+
`;
|
|
664
|
+
p += `2. Write lock: \`echo "{{AGENT_NAME}} $(date -u +%Y-%m-%dT%H:%M:%SZ)" > .ralph-flow/{{APP_NAME}}/${loopDir}/.tracker-lock\`
|
|
665
|
+
`;
|
|
666
|
+
p += `3. Sleep 500ms (\`sleep 0.5\`)
|
|
667
|
+
`;
|
|
668
|
+
p += `4. Re-read \`.tracker-lock\` \u2014 verify YOUR agent name (\`{{AGENT_NAME}}\`) is in it
|
|
669
|
+
`;
|
|
670
|
+
p += ` - Your name \u2192 you own the lock, proceed to write \`tracker.md\`
|
|
671
|
+
`;
|
|
672
|
+
p += ` - Other name \u2192 you lost the race, retry from step 1
|
|
673
|
+
`;
|
|
674
|
+
p += `5. Write your changes to \`tracker.md\`
|
|
675
|
+
`;
|
|
676
|
+
p += `6. Delete \`.tracker-lock\` immediately: \`rm .ralph-flow/{{APP_NAME}}/${loopDir}/.tracker-lock\`
|
|
677
|
+
`;
|
|
678
|
+
p += `7. Never leave a lock held \u2014 if your write fails, delete the lock in your error handler
|
|
679
|
+
|
|
680
|
+
`;
|
|
681
|
+
p += `### When to Lock
|
|
682
|
+
`;
|
|
683
|
+
p += `- Claiming a ${entityLower} (pending \u2192 in_progress)
|
|
684
|
+
`;
|
|
685
|
+
p += `- Completing a ${entityLower} (in_progress \u2192 completed, unblocking dependents)
|
|
686
|
+
`;
|
|
687
|
+
p += `- Updating stage transitions
|
|
688
|
+
`;
|
|
689
|
+
p += `- Heartbeat updates (bundled with other writes, not standalone)
|
|
690
|
+
|
|
691
|
+
`;
|
|
692
|
+
p += `### When NOT to Lock
|
|
693
|
+
`;
|
|
694
|
+
p += `- Reading \`tracker.md\` \u2014 read-only access needs no lock
|
|
695
|
+
`;
|
|
696
|
+
if (primaryInputFile) {
|
|
697
|
+
p += `- Reading \`${primaryInputFile}\` \u2014 always read-only
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
p += `
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
`;
|
|
704
|
+
p += `## ${entityTitle} Selection Algorithm
|
|
705
|
+
|
|
706
|
+
`;
|
|
707
|
+
p += `1. **Parse tracker** \u2014 read \`completed_${entityPlural}\`, \`## Dependencies\`, ${entityTitle}s Queue metadata \`{agent, status}\`, Agent Status table
|
|
708
|
+
`;
|
|
709
|
+
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\`
|
|
710
|
+
`;
|
|
711
|
+
p += `3. **Resume own work** \u2014 if any ${entityLower} has \`{agent: {{AGENT_NAME}}, status: in_progress}\`, resume it (skip to the current stage)
|
|
712
|
+
`;
|
|
713
|
+
p += `4. **Find claimable** \u2014 filter ${entityPlural} where \`status: pending\` AND \`agent: -\`
|
|
714
|
+
`;
|
|
715
|
+
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
|
|
716
|
+
`;
|
|
717
|
+
p += `6. **Nothing available:**
|
|
718
|
+
`;
|
|
719
|
+
p += ` - All ${entityPlural} completed \u2192 emit \`<promise>${completion}</promise>\`
|
|
720
|
+
`;
|
|
721
|
+
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\`
|
|
722
|
+
|
|
723
|
+
`;
|
|
724
|
+
p += `### New ${entityTitle} Discovery
|
|
725
|
+
|
|
726
|
+
`;
|
|
727
|
+
p += `If you find a ${entityLower} in the Queue without \`{agent, status}\` metadata:
|
|
728
|
+
`;
|
|
729
|
+
p += `1. Read its \`**Depends on:**\` field
|
|
730
|
+
`;
|
|
731
|
+
p += `2. Add the dependency to \`## Dependencies\` section if not already there (skip if \`Depends on: None\`)
|
|
732
|
+
`;
|
|
733
|
+
p += `3. Set status to \`pending\` (all deps in \`completed_${entityPlural}\`) or \`blocked\` (deps incomplete)
|
|
734
|
+
`;
|
|
735
|
+
p += `4. Set agent to \`-\`
|
|
736
|
+
|
|
737
|
+
`;
|
|
738
|
+
p += `---
|
|
739
|
+
|
|
740
|
+
`;
|
|
741
|
+
p += `## Anti-Hijacking Rules
|
|
742
|
+
|
|
743
|
+
`;
|
|
744
|
+
p += `1. **Never touch another agent's \`in_progress\` ${entityLower}** \u2014 do not modify, complete, or reassign it
|
|
745
|
+
`;
|
|
746
|
+
p += `2. **Respect ownership** \u2014 if another agent is active in a group, leave remaining group ${entityPlural} for them
|
|
747
|
+
`;
|
|
748
|
+
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
|
|
749
|
+
|
|
750
|
+
`;
|
|
751
|
+
p += `---
|
|
752
|
+
|
|
753
|
+
`;
|
|
754
|
+
p += `## Heartbeat Protocol
|
|
755
|
+
|
|
756
|
+
`;
|
|
757
|
+
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.
|
|
758
|
+
|
|
759
|
+
`;
|
|
760
|
+
p += `---
|
|
761
|
+
|
|
762
|
+
`;
|
|
763
|
+
p += `## Crash Recovery (Self)
|
|
764
|
+
|
|
765
|
+
`;
|
|
766
|
+
p += `On fresh start, if your agent name has an \`in_progress\` ${entityLower} but you have no memory of it:
|
|
767
|
+
`;
|
|
768
|
+
const lastStage = loop.stages.length > 1 ? loop.stages[loop.stages.length - 1] : "last";
|
|
769
|
+
const firstStage = loop.stages[0] || "first";
|
|
770
|
+
p += `- Work committed for that ${entityLower} \u2192 resume at ${lastStage.toUpperCase()} stage
|
|
771
|
+
`;
|
|
772
|
+
p += `- No work found \u2192 restart from ${firstStage.toUpperCase()} stage
|
|
773
|
+
|
|
774
|
+
`;
|
|
775
|
+
p += `---
|
|
776
|
+
|
|
777
|
+
`;
|
|
778
|
+
}
|
|
779
|
+
const stageCount = loop.stages.length;
|
|
780
|
+
p += `## State Machine (${stageCount} stage${stageCount !== 1 ? "s" : ""} per ${entityLower})
|
|
781
|
+
|
|
782
|
+
\`\`\`
|
|
783
|
+
`;
|
|
784
|
+
loop.stages.forEach((stage, i) => {
|
|
785
|
+
const sc = loop.stageConfigs[i];
|
|
786
|
+
const desc = sc && sc.description ? sc.description.split("\n")[0].substring(0, 55) : "Complete this stage";
|
|
787
|
+
const next = i < stageCount - 1 ? `\u2192 stage: ${loop.stages[i + 1]}` : `\u2192 next ${entityLower}`;
|
|
788
|
+
p += `${stage.toUpperCase()} \u2192 ${desc} ${next}
|
|
789
|
+
`;
|
|
790
|
+
});
|
|
791
|
+
p += `\`\`\`
|
|
792
|
+
|
|
793
|
+
`;
|
|
794
|
+
p += `When ALL done: \`<promise>${completion}</promise>\`
|
|
795
|
+
|
|
796
|
+
`;
|
|
797
|
+
p += `After completing ANY stage, exit: \`kill -INT $PPID\`
|
|
798
|
+
|
|
799
|
+
`;
|
|
800
|
+
p += `---
|
|
801
|
+
|
|
802
|
+
`;
|
|
803
|
+
if (loop.multi_agent) {
|
|
804
|
+
p += `## First-Run Handling
|
|
805
|
+
|
|
806
|
+
`;
|
|
807
|
+
const scanFile = primaryInputFile || "input.md";
|
|
808
|
+
p += `If ${entityTitle}s Queue in tracker is empty: read \`${scanFile}\`, scan \`## ${entityKey}:\` headers, populate queue with \`{agent: -, status: pending|blocked}\` metadata (compute from Dependencies), then start.
|
|
809
|
+
|
|
810
|
+
`;
|
|
811
|
+
p += `---
|
|
812
|
+
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
loop.stages.forEach((stage, i) => {
|
|
816
|
+
const sc = loop.stageConfigs[i];
|
|
817
|
+
p += `## STAGE ${i + 1}: ${stage.toUpperCase()}
|
|
818
|
+
|
|
819
|
+
`;
|
|
820
|
+
if (sc && sc.description) {
|
|
821
|
+
p += `${sc.description}
|
|
822
|
+
|
|
823
|
+
`;
|
|
824
|
+
}
|
|
825
|
+
let step = 1;
|
|
826
|
+
if (loop.multi_agent && i === 0) {
|
|
827
|
+
p += `${step++}. Read tracker \u2192 **run ${entityLower} selection algorithm** (see above)
|
|
828
|
+
`;
|
|
829
|
+
} else {
|
|
830
|
+
p += `${step++}. Read tracker \u2192 determine current state
|
|
831
|
+
`;
|
|
832
|
+
}
|
|
833
|
+
if (sc && sc.capabilities) {
|
|
834
|
+
if (sc.capabilities.fileReadWrite) {
|
|
835
|
+
const files = primaryInputFile ? ` (\`${primaryInputFile}\`)` : "";
|
|
836
|
+
p += `${step++}. Read relevant input files${files} and explore affected areas
|
|
837
|
+
`;
|
|
838
|
+
}
|
|
839
|
+
if (sc.capabilities.exploreAgents) {
|
|
840
|
+
p += `${step++}. Use the Agent tool to explore the codebase \u2014 read **40+ files** across affected areas, dependencies, patterns
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
if (sc.capabilities.webSearch) {
|
|
844
|
+
p += `${step++}. Use WebSearch for 5-10 queries to gather external information
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
if (sc.capabilities.mcpServers) {
|
|
848
|
+
p += `${step++}. Use MCP tools for specialized operations
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
if (sc.capabilities.codeEditing) {
|
|
852
|
+
p += `${step++}. Implement changes, matching existing patterns per \`CLAUDE.md\`
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
if (sc.capabilities.bashCommands) {
|
|
856
|
+
p += `${step++}. Run build/deploy commands to verify changes
|
|
857
|
+
`;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (loop.multi_agent) {
|
|
861
|
+
p += `${step++}. Acquire lock \u2192 update tracker: stage, \`last_heartbeat\`, log entry \u2192 release lock
|
|
862
|
+
`;
|
|
863
|
+
} else {
|
|
864
|
+
p += `${step++}. Update tracker with progress
|
|
865
|
+
`;
|
|
866
|
+
}
|
|
867
|
+
p += `${step++}. Exit: \`kill -INT $PPID\`
|
|
868
|
+
|
|
869
|
+
`;
|
|
870
|
+
if (i < stageCount - 1) p += `---
|
|
871
|
+
|
|
872
|
+
`;
|
|
873
|
+
});
|
|
874
|
+
if (outputFiles && entity) {
|
|
875
|
+
p += `---
|
|
876
|
+
|
|
877
|
+
`;
|
|
878
|
+
p += `## Output Format
|
|
879
|
+
|
|
880
|
+
`;
|
|
881
|
+
p += `Write to \`${outputFiles.split(",")[0].trim()}\` using this format:
|
|
882
|
+
|
|
883
|
+
`;
|
|
884
|
+
p += `\`\`\`markdown
|
|
885
|
+
`;
|
|
886
|
+
p += `## ${entityKey}: {Title}
|
|
887
|
+
|
|
888
|
+
`;
|
|
889
|
+
p += `**Depends on:** {dependency or "None"}
|
|
890
|
+
|
|
891
|
+
`;
|
|
892
|
+
p += `### Description
|
|
893
|
+
`;
|
|
894
|
+
p += `{Content for this ${entityLower}}
|
|
895
|
+
|
|
896
|
+
`;
|
|
897
|
+
p += `### Acceptance Criteria
|
|
898
|
+
`;
|
|
899
|
+
p += `- [ ] {Criterion 1}
|
|
900
|
+
`;
|
|
901
|
+
p += `- [ ] {Criterion 2}
|
|
902
|
+
`;
|
|
903
|
+
p += `\`\`\`
|
|
904
|
+
|
|
905
|
+
`;
|
|
906
|
+
}
|
|
907
|
+
p += `---
|
|
908
|
+
|
|
909
|
+
## Rules
|
|
910
|
+
|
|
911
|
+
`;
|
|
912
|
+
p += `- One ${entityLower} at a time${loop.multi_agent ? " per agent" : ""}. One stage per iteration.
|
|
913
|
+
`;
|
|
914
|
+
p += `- Read tracker first, update tracker last.${loop.multi_agent ? " Always use lock protocol for writes." : ""}
|
|
915
|
+
`;
|
|
916
|
+
p += `- Read \`CLAUDE.md\` for all project-specific context.
|
|
917
|
+
`;
|
|
918
|
+
p += `- Thorough exploration before making changes.
|
|
919
|
+
`;
|
|
920
|
+
if (loop.multi_agent) {
|
|
921
|
+
p += `- **Multi-agent: never touch another agent's in_progress ${entityLower}. Coordinate via tracker.md.**
|
|
922
|
+
`;
|
|
923
|
+
}
|
|
924
|
+
p += `
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
`;
|
|
928
|
+
p += `Read \`.ralph-flow/{{APP_NAME}}/${loopDir}/tracker.md\` now and begin.
|
|
929
|
+
`;
|
|
930
|
+
return p;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/cli/create-template.ts
|
|
934
|
+
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) => {
|
|
935
|
+
try {
|
|
936
|
+
const cwd = process.cwd();
|
|
937
|
+
let raw;
|
|
938
|
+
if (opts.config) {
|
|
939
|
+
raw = opts.config;
|
|
940
|
+
} else if (opts.configFile) {
|
|
941
|
+
const filePath = resolve(cwd, opts.configFile);
|
|
942
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
943
|
+
} else {
|
|
944
|
+
console.error(chalk7.red("\n Provide --config <json> or --config-file <path>\n"));
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
let definition;
|
|
948
|
+
try {
|
|
949
|
+
definition = JSON.parse(raw);
|
|
950
|
+
} catch {
|
|
951
|
+
console.error(chalk7.red("\n Invalid JSON. Check syntax and try again.\n"));
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
if (!definition.name) {
|
|
955
|
+
console.error(chalk7.red("\n Template name is required.\n"));
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
958
|
+
if (!definition.loops || definition.loops.length === 0) {
|
|
959
|
+
console.error(chalk7.red("\n At least one loop is required.\n"));
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
for (const loop of definition.loops) {
|
|
963
|
+
if (!loop.name) {
|
|
964
|
+
console.error(chalk7.red("\n Each loop must have a name.\n"));
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
if (!loop.stages || loop.stages.length === 0) {
|
|
968
|
+
console.error(chalk7.red("\n Each loop must have at least one stage.\n"));
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
if (!loop.completion) {
|
|
972
|
+
console.error(chalk7.red("\n Each loop must have a completion string.\n"));
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const allPromptLoopConfigs = definition.loops.map((l) => buildPromptLoopConfig(l));
|
|
977
|
+
for (let i = 0; i < definition.loops.length; i++) {
|
|
978
|
+
const loopDef = definition.loops[i];
|
|
979
|
+
if (!loopDef.prompt || !loopDef.prompt.trim()) {
|
|
980
|
+
loopDef.prompt = generatePromptFromConfig(allPromptLoopConfigs[i], i, allPromptLoopConfigs);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
createCustomTemplate(cwd, definition);
|
|
984
|
+
const templatePath = `.ralph-flow/.templates/${definition.name}/`;
|
|
985
|
+
console.log();
|
|
986
|
+
console.log(chalk7.green(" \u2713 Template created: ") + chalk7.bold(definition.name));
|
|
987
|
+
console.log(chalk7.dim(` ${templatePath}`));
|
|
988
|
+
console.log();
|
|
989
|
+
console.log(chalk7.dim(" Next steps:"));
|
|
990
|
+
console.log(chalk7.dim(` ralphflow init -t ${definition.name} -n my-project`));
|
|
991
|
+
console.log(chalk7.dim(" ralphflow run <loop>"));
|
|
992
|
+
console.log();
|
|
993
|
+
} catch (err) {
|
|
994
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
995
|
+
console.error(chalk7.red(`
|
|
996
|
+
${msg}
|
|
997
|
+
`));
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
537
1002
|
// src/cli/index.ts
|
|
538
|
-
var program = new
|
|
1003
|
+
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
1004
|
const port = 4242;
|
|
540
|
-
const { startDashboard } = await import("./server-
|
|
1005
|
+
const { startDashboard } = await import("./server-EX5MWYW4.js");
|
|
541
1006
|
await startDashboard({ cwd: process.cwd(), port });
|
|
542
1007
|
const url = `http://localhost:${port}`;
|
|
543
1008
|
exec(`open "${url}"`, (err) => {
|
|
544
1009
|
if (err) {
|
|
545
|
-
console.log(
|
|
1010
|
+
console.log(chalk8.dim(` Open ${url} in your browser`));
|
|
546
1011
|
}
|
|
547
1012
|
});
|
|
548
1013
|
});
|
|
549
1014
|
process.on("SIGINT", () => {
|
|
550
1015
|
console.log();
|
|
551
|
-
console.log(
|
|
1016
|
+
console.log(chalk8.dim(" Interrupted."));
|
|
552
1017
|
process.exit(130);
|
|
553
1018
|
});
|
|
554
1019
|
program.configureOutput({
|
|
555
1020
|
writeErr: (str) => {
|
|
556
1021
|
const clean = str.replace(/^error: /, "");
|
|
557
1022
|
if (clean.trim()) {
|
|
558
|
-
console.error(
|
|
1023
|
+
console.error(chalk8.red(` ${clean.trim()}`));
|
|
559
1024
|
}
|
|
560
1025
|
}
|
|
561
1026
|
});
|