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/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-TCCMQDVT.js";
15
+ } from "./chunk-DOC64TD6.js";
15
16
 
16
17
  // src/cli/index.ts
17
- import { Command as Command6 } from "commander";
18
- import chalk7 from "chalk";
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((resolve) => {
32
+ return new Promise((resolve2) => {
32
33
  rl.question(chalk.cyan("? ") + question + " ", (answer) => {
33
- resolve(answer.trim());
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 = ["--dangerously-skip-permissions", prompt];
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((resolve, reject) => {
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
- resolve({
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
- if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
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
- if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
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
- if (checkTrackerForCompletion(flowDir, loop) || checkTrackerMetadataCompletion(flowDir, loop) || checkTrackerAllChecked(flowDir, loop)) {
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-DOSLU36L.js");
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-DOSLU36L.js");
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-DOSLU36L.js");
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 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 () => {
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-DOSLU36L.js");
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(chalk7.dim(` Open ${url} in your browser`));
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(chalk7.dim(" Interrupted."));
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(chalk7.red(` ${clean.trim()}`));
1118
+ console.error(chalk8.red(` ${clean.trim()}`));
559
1119
  }
560
1120
  }
561
1121
  });