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 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
- writeFileSync(join(loopDir, "prompt.md"), `# ${loopDef.name} \u2014 Prompt
182
+ const promptContent = loopDef.prompt && loopDef.prompt.trim() ? loopDef.prompt : `# ${loopDef.name} \u2014 Prompt
177
183
 
178
184
  <!-- Add your prompt here -->
179
- `, "utf-8");
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-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
@@ -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-DOSLU36L.js");
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-DOSLU36L.js");
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-DOSLU36L.js");
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 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 () => {
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-DOSLU36L.js");
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(chalk7.dim(` Open ${url} in your browser`));
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(chalk7.dim(" Interrupted."));
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(chalk7.red(` ${clean.trim()}`));
1023
+ console.error(chalk8.red(` ${clean.trim()}`));
559
1024
  }
560
1025
  }
561
1026
  });