ralphflow 0.5.1 → 0.5.3

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.
Files changed (64) hide show
  1. package/dist/{chunk-DOC64TD6.js → chunk-CA4XP6KI.js} +1 -1
  2. package/dist/ralphflow.js +237 -28
  3. package/dist/{server-EX5MWYW4.js → server-64NQCIKJ.js} +88 -21
  4. package/package.json +1 -1
  5. package/src/dashboard/ui/app.js +4 -1
  6. package/src/dashboard/ui/archives.js +27 -2
  7. package/src/dashboard/ui/index.html +1 -1
  8. package/src/dashboard/ui/loop-detail.js +1 -1
  9. package/src/dashboard/ui/prompt-builder.js +39 -4
  10. package/src/dashboard/ui/sidebar.js +1 -1
  11. package/src/dashboard/ui/state.js +3 -0
  12. package/src/dashboard/ui/styles.css +77 -0
  13. package/src/dashboard/ui/templates.js +3 -0
  14. package/src/dashboard/ui/utils.js +30 -0
  15. package/src/templates/code-implementation/loops/00-story-loop/prompt.md +51 -11
  16. package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +28 -2
  17. package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +27 -4
  18. package/src/templates/code-review/loops/00-collect-loop/changesets.md +3 -0
  19. package/src/templates/code-review/loops/00-collect-loop/prompt.md +179 -0
  20. package/src/templates/code-review/loops/00-collect-loop/tracker.md +16 -0
  21. package/src/templates/code-review/loops/01-spec-review-loop/prompt.md +238 -0
  22. package/src/templates/code-review/loops/01-spec-review-loop/tracker.md +16 -0
  23. package/src/templates/code-review/loops/02-quality-review-loop/issues.md +3 -0
  24. package/src/templates/code-review/loops/02-quality-review-loop/prompt.md +306 -0
  25. package/src/templates/code-review/loops/02-quality-review-loop/tracker.md +16 -0
  26. package/src/templates/code-review/loops/03-fix-loop/prompt.md +265 -0
  27. package/src/templates/code-review/loops/03-fix-loop/tracker.md +16 -0
  28. package/src/templates/code-review/ralphflow.yaml +98 -0
  29. package/src/templates/design-review/loops/00-explore-loop/ideas.md +3 -0
  30. package/src/templates/design-review/loops/00-explore-loop/prompt.md +207 -0
  31. package/src/templates/design-review/loops/00-explore-loop/tracker.md +16 -0
  32. package/src/templates/design-review/loops/01-design-loop/designs.md +3 -0
  33. package/src/templates/design-review/loops/01-design-loop/prompt.md +201 -0
  34. package/src/templates/design-review/loops/01-design-loop/tracker.md +16 -0
  35. package/src/templates/design-review/loops/02-review-loop/prompt.md +255 -0
  36. package/src/templates/design-review/loops/02-review-loop/tracker.md +16 -0
  37. package/src/templates/design-review/loops/03-plan-loop/plans.md +3 -0
  38. package/src/templates/design-review/loops/03-plan-loop/prompt.md +247 -0
  39. package/src/templates/design-review/loops/03-plan-loop/tracker.md +16 -0
  40. package/src/templates/design-review/ralphflow.yaml +84 -0
  41. package/src/templates/research/loops/00-discovery-loop/prompt.md +36 -5
  42. package/src/templates/research/loops/01-research-loop/prompt.md +22 -2
  43. package/src/templates/research/loops/02-story-loop/prompt.md +20 -1
  44. package/src/templates/research/loops/03-document-loop/prompt.md +20 -1
  45. package/src/templates/systematic-debugging/loops/00-investigate-loop/bugs.md +3 -0
  46. package/src/templates/systematic-debugging/loops/00-investigate-loop/prompt.md +237 -0
  47. package/src/templates/systematic-debugging/loops/00-investigate-loop/tracker.md +16 -0
  48. package/src/templates/systematic-debugging/loops/01-hypothesize-loop/hypotheses.md +3 -0
  49. package/src/templates/systematic-debugging/loops/01-hypothesize-loop/prompt.md +312 -0
  50. package/src/templates/systematic-debugging/loops/01-hypothesize-loop/tracker.md +18 -0
  51. package/src/templates/systematic-debugging/loops/02-fix-loop/fixes.md +3 -0
  52. package/src/templates/systematic-debugging/loops/02-fix-loop/prompt.md +342 -0
  53. package/src/templates/systematic-debugging/loops/02-fix-loop/tracker.md +18 -0
  54. package/src/templates/systematic-debugging/ralphflow.yaml +81 -0
  55. package/src/templates/tdd-implementation/loops/00-spec-loop/prompt.md +208 -0
  56. package/src/templates/tdd-implementation/loops/00-spec-loop/specs.md +3 -0
  57. package/src/templates/tdd-implementation/loops/00-spec-loop/tracker.md +16 -0
  58. package/src/templates/tdd-implementation/loops/01-tdd-loop/prompt.md +323 -0
  59. package/src/templates/tdd-implementation/loops/01-tdd-loop/test-cases.md +3 -0
  60. package/src/templates/tdd-implementation/loops/01-tdd-loop/tracker.md +18 -0
  61. package/src/templates/tdd-implementation/loops/02-verify-loop/prompt.md +226 -0
  62. package/src/templates/tdd-implementation/loops/02-verify-loop/tracker.md +16 -0
  63. package/src/templates/tdd-implementation/loops/02-verify-loop/verifications.md +3 -0
  64. package/src/templates/tdd-implementation/ralphflow.yaml +73 -0
@@ -4,7 +4,7 @@ import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { stringify as stringifyYaml } from "yaml";
6
6
  var __dirname = dirname(fileURLToPath(import.meta.url));
7
- var BUILT_IN_TEMPLATES = ["code-implementation", "research"];
7
+ var BUILT_IN_TEMPLATES = ["code-implementation", "research", "tdd-implementation", "systematic-debugging", "design-review", "code-review"];
8
8
  function resolveTemplatePath(templateName) {
9
9
  const candidates = [
10
10
  join(__dirname, "..", "templates", templateName),
package/dist/ralphflow.js CHANGED
@@ -12,11 +12,11 @@ import {
12
12
  resolveFlowDir,
13
13
  resolveLoop,
14
14
  showStatus
15
- } from "./chunk-DOC64TD6.js";
15
+ } from "./chunk-CA4XP6KI.js";
16
16
 
17
17
  // src/cli/index.ts
18
- import { Command as Command7 } from "commander";
19
- import chalk8 from "chalk";
18
+ import { Command as Command8 } from "commander";
19
+ import chalk9 from "chalk";
20
20
  import { exec } from "child_process";
21
21
 
22
22
  // src/cli/init.ts
@@ -29,9 +29,9 @@ import { join } from "path";
29
29
  import { createInterface } from "readline";
30
30
  import chalk from "chalk";
31
31
  function ask(rl, question) {
32
- return new Promise((resolve2) => {
32
+ return new Promise((resolve3) => {
33
33
  rl.question(chalk.cyan("? ") + question + " ", (answer) => {
34
- resolve2(answer.trim());
34
+ resolve3(answer.trim());
35
35
  });
36
36
  });
37
37
  }
@@ -100,7 +100,7 @@ function listFlows(ralphFlowDir) {
100
100
  }
101
101
 
102
102
  // src/cli/init.ts
103
- var initCommand = new Command("init").description("Initialize a new RalphFlow flow").option("-t, --template <name>", "Template to use (code-implementation, research)").option("-n, --name <name>", "Custom name for the flow").action(async (opts) => {
103
+ var initCommand = new Command("init").description("Initialize a new RalphFlow flow").option("-t, --template <name>", "Template to use (code-implementation, research, tdd-implementation, systematic-debugging, design-review, code-review)").option("-n, --name <name>", "Custom name for the flow").action(async (opts) => {
104
104
  try {
105
105
  await initProject(process.cwd(), { template: opts.template, name: opts.name });
106
106
  } catch (err) {
@@ -136,7 +136,7 @@ async function spawnClaude(options) {
136
136
  if (model) {
137
137
  args.unshift("--model", model);
138
138
  }
139
- return new Promise((resolve2, reject) => {
139
+ return new Promise((resolve3, reject) => {
140
140
  const child = spawn("claude", args, {
141
141
  cwd,
142
142
  stdio: "inherit",
@@ -146,7 +146,7 @@ async function spawnClaude(options) {
146
146
  reject(new Error(`Failed to spawn claude: ${err.message}`));
147
147
  });
148
148
  child.on("close", (code, signal) => {
149
- resolve2({
149
+ resolve3({
150
150
  output: "",
151
151
  exitCode: code,
152
152
  signal
@@ -231,6 +231,48 @@ function checkTrackerAllChecked(flowDir, loop) {
231
231
  const unchecked = (content.match(/- \[ \]/g) || []).length;
232
232
  return checked > 0 && unchecked === 0;
233
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
+ }
234
276
  async function runLoop(loopName, options) {
235
277
  const flowDir = resolveFlowDir(options.cwd, options.flow);
236
278
  const config = loadConfig(flowDir);
@@ -264,22 +306,25 @@ async function runLoop(loopName, options) {
264
306
  cleanup();
265
307
  process.exit(143);
266
308
  });
309
+ stripCompletionString(flowDir, loop);
267
310
  try {
268
- await iterationLoop(key, loop, flowDir, options, agentName);
311
+ await iterationLoop(key, loop, flowDir, options, agentName, void 0, void 0, true, config);
269
312
  } finally {
270
313
  cleanup();
271
314
  }
272
315
  }
273
- async function iterationLoop(configKey, loop, flowDir, options, agentName, db, flowName, forceFirstIteration) {
316
+ async function iterationLoop(configKey, loop, flowDir, options, agentName, db, flowName, forceFirstIteration, config) {
274
317
  const loopKey = loop.name;
275
318
  const appName = basename(flowDir);
319
+ if (config) stripStaleCompletion(flowDir, loop, config);
276
320
  for (let i = 1; i <= options.maxIterations; i++) {
277
321
  if (!(forceFirstIteration && i === 1)) {
278
322
  if (db && flowName && isLoopComplete(db, flowName, loopKey)) {
279
323
  console.log(chalk3.green(` \u2713 ${loop.name} \u2014 already complete`));
280
324
  return;
281
325
  }
282
- 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))) {
283
328
  if (db && flowName) markLoopComplete(db, flowName, loopKey);
284
329
  console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete`));
285
330
  return;
@@ -301,7 +346,8 @@ async function iterationLoop(configKey, loop, flowDir, options, agentName, db, f
301
346
  skipPermissions: loop.skip_permissions
302
347
  });
303
348
  if (db && flowName) incrementIteration(db, flowName, loopKey);
304
- 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))) {
305
351
  if (db && flowName) markLoopComplete(db, flowName, loopKey);
306
352
  console.log();
307
353
  console.log(chalk3.green(` Loop complete: ${loop.completion}`));
@@ -346,7 +392,8 @@ async function runE2E(options) {
346
392
  console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
347
393
  continue;
348
394
  }
349
- 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))) {
350
397
  markLoopComplete(db, flowName, loopKey);
351
398
  console.log(chalk3.green(` \u2713 ${loop.name} \u2014 complete, skipping`));
352
399
  continue;
@@ -362,7 +409,7 @@ async function runE2E(options) {
362
409
  agentName = acquireAgentId(agentDir, loop.multi_agent.max_agents);
363
410
  }
364
411
  try {
365
- await iterationLoop(key, loop, flowDir, options, agentName, db, flowName, isFirstLoop);
412
+ await iterationLoop(key, loop, flowDir, options, agentName, db, flowName, isFirstLoop, config);
366
413
  } finally {
367
414
  if (agentDir && agentName) releaseAgentId(agentDir, agentName);
368
415
  }
@@ -477,7 +524,7 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
477
524
  try {
478
525
  let dashboardHandle;
479
526
  if (opts.ui) {
480
- const { startDashboard } = await import("./server-EX5MWYW4.js");
527
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
481
528
  dashboardHandle = await startDashboard({ cwd: process.cwd() });
482
529
  }
483
530
  await runLoop(loop, {
@@ -503,7 +550,7 @@ var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with
503
550
  try {
504
551
  let dashboardHandle;
505
552
  if (opts.ui) {
506
- const { startDashboard } = await import("./server-EX5MWYW4.js");
553
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
507
554
  dashboardHandle = await startDashboard({ cwd: process.cwd() });
508
555
  }
509
556
  await runE2E({
@@ -540,7 +587,7 @@ var statusCommand = new Command4("status").description("Show pipeline status").o
540
587
  // src/cli/dashboard.ts
541
588
  import { Command as Command5 } from "commander";
542
589
  var dashboardCommand = new Command5("dashboard").alias("ui").description("Start the web dashboard").option("-p, --port <port>", "Port number", "4242").action(async (opts) => {
543
- const { startDashboard } = await import("./server-EX5MWYW4.js");
590
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
544
591
  await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
545
592
  });
546
593
 
@@ -595,6 +642,39 @@ function buildPromptLoopConfig(loopDef) {
595
642
  stageConfigs
596
643
  };
597
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
+ }
598
678
  function generatePromptFromConfig(loop, loopIndex, allLoops) {
599
679
  const loopName = loop.name || "Loop " + (loopIndex + 1);
600
680
  const inputFiles = (loop.inputFiles || "").trim();
@@ -641,6 +721,7 @@ function generatePromptFromConfig(loop, loopIndex, allLoops) {
641
721
  p += `---
642
722
 
643
723
  `;
724
+ p += generateVisualProtocol();
644
725
  if (loop.multi_agent) {
645
726
  p += `## Tracker Lock Protocol
646
727
 
@@ -800,14 +881,20 @@ function generatePromptFromConfig(loop, loopIndex, allLoops) {
800
881
  p += `---
801
882
 
802
883
  `;
803
- if (loop.multi_agent) {
804
- p += `## First-Run Handling
884
+ {
885
+ const scanFile = primaryInputFile || "input.md";
886
+ p += `## First-Run / New ${entityTitle} Detection
805
887
 
806
888
  `;
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.
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.
809
891
 
810
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
+ }
811
898
  p += `---
812
899
 
813
900
  `;
@@ -857,6 +944,14 @@ function generatePromptFromConfig(loop, loopIndex, allLoops) {
857
944
  `;
858
945
  }
859
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
+ }
860
955
  if (loop.multi_agent) {
861
956
  p += `${step++}. Acquire lock \u2192 update tracker: stage, \`last_heartbeat\`, log entry \u2192 release lock
862
957
  `;
@@ -999,28 +1094,142 @@ var createTemplateCommand = new Command6("create-template").description("Create
999
1094
  }
1000
1095
  });
1001
1096
 
1097
+ // src/cli/summarize.ts
1098
+ import { Command as Command7 } from "commander";
1099
+ import chalk8 from "chalk";
1100
+ import { existsSync as existsSync3 } from "fs";
1101
+ import { join as join3, resolve as resolve2 } from "path";
1102
+ var SUMMARIZE_PROMPT = `You are summarizing an archived RalphFlow workflow run.
1103
+
1104
+ ## What to read
1105
+
1106
+ This directory is an archived app snapshot. It contains:
1107
+ - Loop subdirectories (e.g. \`00-story-loop/\`, \`01-tasks-loop/\`, \`02-delivery-loop/\`) \u2014 each has:
1108
+ - \`stories.md\` or \`tasks.md\` \u2014 the work items with \`## STORY-N:\` or \`## TASK-N:\` headers
1109
+ - \`tracker.md\` \u2014 completion state with \`- [x]\` (done) and \`- [ ]\` (incomplete) checkboxes, \`completed_tasks\`/\`completed_stories\` lists, and agent activity logs
1110
+ - \`ralphflow.yaml\` \u2014 pipeline configuration
1111
+
1112
+ Read ALL \`.md\` files across all subdirectories. Parse stories, task groups (\`# TASK-GROUP-N:\` headers in tasks.md), and individual tasks. Determine completion from tracker checkboxes: \`[x]\` = completed, \`[ ]\` = incomplete.
1113
+
1114
+ ## What to write
1115
+
1116
+ Write a file called \`summary.md\` in the current directory with this exact structure:
1117
+
1118
+ \`\`\`
1119
+ # Archive Summary
1120
+
1121
+ **N stories \xB7 N task groups \xB7 N/N tasks completed \xB7 N agents**
1122
+
1123
+ ## Pipeline
1124
+
1125
+ \\\`\\\`\\\`
1126
+ STORY-1: Title
1127
+ \u251C\u2500\u2500 TASK-GROUP-1: Title
1128
+ \u2502 \u251C\u2500\u2500 \u2713 TASK-1: Title
1129
+ \u2502 \u251C\u2500\u2500 \u2713 TASK-2: Title
1130
+ \u2502 \u2514\u2500\u2500 \u25CB TASK-3: Title
1131
+ \u2514\u2500\u2500 TASK-GROUP-2: Title
1132
+ \u2514\u2500\u2500 \u2713 TASK-4: Title
1133
+
1134
+ STORY-2: Title
1135
+ \u2514\u2500\u2500 TASK-GROUP-3: Title
1136
+ \u251C\u2500\u2500 \u2713 TASK-5: Title
1137
+ \u2514\u2500\u2500 \u2713 TASK-6: Title
1138
+ \\\`\\\`\\\`
1139
+
1140
+ ## What was built
1141
+
1142
+ **STORY-1: Title** \u2014 1-2 sentence narrative of what was accomplished and why.
1143
+
1144
+ **STORY-2: Title** \u2014 1-2 sentence narrative.
1145
+
1146
+ ## Key decisions
1147
+
1148
+ - Decision or trade-off noted in tracker logs or task descriptions
1149
+ - Another significant decision
1150
+ \`\`\`
1151
+
1152
+ Rules:
1153
+ - Use \`\u2713\` for completed tasks, \`\u25CB\` for incomplete
1154
+ - The ASCII tree uses box-drawing characters (\`\u251C\u2500\u2500 \u2514\u2500\u2500 \u2502\`)
1155
+ - Stats line counts: stories (from stories.md headers), task groups, tasks completed vs total, unique agents (from tracker agent columns)
1156
+ - Story narratives should capture *what was built and why* \u2014 not just list tasks
1157
+ - Key decisions come from tracker logs, task descriptions, or trade-offs visible in the work
1158
+ - Keep the whole file under 80 lines \u2014 it should fit on one screen
1159
+ - If no tasks.md exists (e.g. a research pipeline), adapt: use whatever entity headers exist (topics, stories, etc.)
1160
+
1161
+ After writing summary.md, you are done. Do not modify any other files.`;
1162
+ var summarizeCommand = new Command7("summarize").description("Generate a summary of an archived workflow run").argument("<app>", "App name (e.g., code-implementation)").argument("<archive-date>", "Archive timestamp (e.g., 2026-03-14_15-30)").option("-m, --model <model>", "Claude model to use").action(async (app, archiveDate, opts) => {
1163
+ if (app.includes("..") || app.includes("/") || app.includes("\\")) {
1164
+ console.error(chalk8.red("\n Invalid app name.\n"));
1165
+ process.exit(1);
1166
+ }
1167
+ if (archiveDate.includes("..") || archiveDate.includes("/") || archiveDate.includes("\\")) {
1168
+ console.error(chalk8.red("\n Invalid archive date.\n"));
1169
+ process.exit(1);
1170
+ }
1171
+ const archiveDir = resolve2(process.cwd(), ".ralph-flow", ".archives", app, archiveDate);
1172
+ if (!existsSync3(archiveDir)) {
1173
+ const appArchivesDir = join3(process.cwd(), ".ralph-flow", ".archives", app);
1174
+ if (!existsSync3(appArchivesDir)) {
1175
+ console.error(chalk8.red(`
1176
+ No archives found for app "${app}".`));
1177
+ console.error(chalk8.dim(` Archive an app first via the dashboard.
1178
+ `));
1179
+ } else {
1180
+ const { readdirSync: readdirSync3 } = await import("fs");
1181
+ const available = readdirSync3(appArchivesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
1182
+ console.error(chalk8.red(`
1183
+ Archive "${archiveDate}" not found for app "${app}".`));
1184
+ if (available.length > 0) {
1185
+ console.error(chalk8.dim(` Available archives:`));
1186
+ for (const a of available) {
1187
+ console.error(chalk8.dim(` ${a}`));
1188
+ }
1189
+ }
1190
+ console.error("");
1191
+ }
1192
+ process.exit(1);
1193
+ }
1194
+ console.log(chalk8.dim(`
1195
+ Summarizing archive: ${app}/${archiveDate}
1196
+ `));
1197
+ try {
1198
+ await spawnClaude({
1199
+ prompt: SUMMARIZE_PROMPT,
1200
+ model: opts.model,
1201
+ cwd: archiveDir
1202
+ });
1203
+ } catch (err) {
1204
+ const msg = err instanceof Error ? err.message : String(err);
1205
+ console.error(chalk8.red(`
1206
+ ${msg}
1207
+ `));
1208
+ process.exit(1);
1209
+ }
1210
+ });
1211
+
1002
1212
  // src/cli/index.ts
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 () => {
1004
- const port = 4242;
1005
- const { startDashboard } = await import("./server-EX5MWYW4.js");
1006
- await startDashboard({ cwd: process.cwd(), port });
1213
+ var program = new Command8().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).addCommand(summarizeCommand).action(async () => {
1214
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
1215
+ const { port } = await startDashboard({ cwd: process.cwd() });
1007
1216
  const url = `http://localhost:${port}`;
1008
1217
  exec(`open "${url}"`, (err) => {
1009
1218
  if (err) {
1010
- console.log(chalk8.dim(` Open ${url} in your browser`));
1219
+ console.log(chalk9.dim(` Open ${url} in your browser`));
1011
1220
  }
1012
1221
  });
1013
1222
  });
1014
1223
  process.on("SIGINT", () => {
1015
1224
  console.log();
1016
- console.log(chalk8.dim(" Interrupted."));
1225
+ console.log(chalk9.dim(" Interrupted."));
1017
1226
  process.exit(130);
1018
1227
  });
1019
1228
  program.configureOutput({
1020
1229
  writeErr: (str) => {
1021
1230
  const clean = str.replace(/^error: /, "");
1022
1231
  if (clean.trim()) {
1023
- console.error(chalk8.red(` ${clean.trim()}`));
1232
+ console.error(chalk9.red(` ${clean.trim()}`));
1024
1233
  }
1025
1234
  }
1026
1235
  });
@@ -15,12 +15,12 @@ import {
15
15
  resolveFlowDir,
16
16
  resolveTemplatePathWithCustom,
17
17
  validateTemplateName
18
- } from "./chunk-DOC64TD6.js";
18
+ } from "./chunk-CA4XP6KI.js";
19
19
 
20
20
  // src/dashboard/server.ts
21
21
  import { Hono as Hono2 } from "hono";
22
22
  import { cors } from "hono/cors";
23
- import { serve } from "@hono/node-server";
23
+ import { createAdaptorServer } from "@hono/node-server";
24
24
  import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
25
25
  import { join as join4, dirname, extname } from "path";
26
26
  import { fileURLToPath } from "url";
@@ -45,13 +45,13 @@ function broadcastWs(wss, event) {
45
45
  }
46
46
  }
47
47
  }
48
- function createApiRoutes(cwd, port = 4242, wss) {
48
+ function createApiRoutes(cwd, portRef, wss) {
49
49
  const api = new Hono();
50
50
  api.get("/api/context", (c) => {
51
51
  return c.json({
52
52
  cwd,
53
53
  projectName: basename(cwd),
54
- port
54
+ port: portRef.value
55
55
  });
56
56
  });
57
57
  api.get("/api/apps", (c) => {
@@ -175,24 +175,51 @@ function createApiRoutes(cwd, port = 4242, wss) {
175
175
  mkdirSync(archiveDir, { recursive: true });
176
176
  cpSync(flowDir, archiveDir, { recursive: true });
177
177
  let templateDir;
178
+ let usedFallbackReset = false;
178
179
  try {
179
180
  templateDir = resolveTemplatePathWithCustom(config.name, cwd);
180
181
  } catch {
182
+ usedFallbackReset = true;
183
+ console.warn(`[archive] Template "${config.name}" not found \u2014 using fallback reset`);
181
184
  }
182
- for (const loop of Object.values(config.loops)) {
185
+ for (const [loopKey, loop] of Object.entries(config.loops)) {
186
+ const appTracker = join(flowDir, loop.tracker);
183
187
  if (templateDir) {
184
188
  const templateTracker = join(templateDir, "loops", loop.tracker);
185
- const appTracker = join(flowDir, loop.tracker);
186
189
  if (existsSync(templateTracker)) {
187
190
  writeFileSync(appTracker, readFileSync(templateTracker, "utf-8"));
188
191
  }
192
+ } else if (existsSync(appTracker)) {
193
+ const title = loop.name || loopKey;
194
+ writeFileSync(appTracker, `# ${title} \u2014 Tracker
195
+
196
+ - stage: idle
197
+ `);
189
198
  }
190
- if (loop.data_files && templateDir) {
199
+ if (loop.data_files) {
191
200
  for (const dataFile of loop.data_files) {
192
- const templateData = join(templateDir, "loops", dataFile);
193
201
  const appData = join(flowDir, dataFile);
194
- if (existsSync(templateData)) {
195
- writeFileSync(appData, readFileSync(templateData, "utf-8"));
202
+ if (templateDir) {
203
+ const templateData = join(templateDir, "loops", dataFile);
204
+ if (existsSync(templateData)) {
205
+ writeFileSync(appData, readFileSync(templateData, "utf-8"));
206
+ }
207
+ } else if (existsSync(appData)) {
208
+ const fileName = basename(dataFile, ".md");
209
+ const header = fileName.charAt(0).toUpperCase() + fileName.slice(1);
210
+ writeFileSync(appData, `# ${header}
211
+ `);
212
+ }
213
+ }
214
+ }
215
+ if (loop.directories) {
216
+ for (const dir of loop.directories) {
217
+ const dirPath = join(flowDir, dir);
218
+ if (existsSync(dirPath)) {
219
+ for (const entry of readdirSync(dirPath)) {
220
+ const entryPath = join(dirPath, entry);
221
+ rmSync(entryPath, { recursive: true, force: true });
222
+ }
196
223
  }
197
224
  }
198
225
  }
@@ -224,7 +251,11 @@ function createApiRoutes(cwd, port = 4242, wss) {
224
251
  }
225
252
  }
226
253
  const archivePath = `.ralph-flow/.archives/${appName}/${archiveTimestamp}`;
227
- return c.json({ ok: true, archivePath, timestamp: archiveTimestamp });
254
+ const result = { ok: true, archivePath, timestamp: archiveTimestamp };
255
+ if (usedFallbackReset) {
256
+ result.warning = `Template "${config.name}" not found \u2014 reset used fallback (minimal empty content)`;
257
+ }
258
+ return c.json(result);
228
259
  } catch (err) {
229
260
  const msg = err instanceof Error ? err.message : String(err);
230
261
  return c.json({ error: `Archive failed: ${msg}` }, 500);
@@ -913,6 +944,7 @@ function removeNotificationHook(cwd) {
913
944
 
914
945
  // src/dashboard/server.ts
915
946
  var __dirname = dirname(fileURLToPath(import.meta.url));
947
+ var MAX_PORT_ATTEMPTS = 10;
916
948
  var CONTENT_TYPES = {
917
949
  ".css": "text/css",
918
950
  ".js": "text/javascript",
@@ -933,15 +965,33 @@ function resolveUiDir() {
933
965
  ${candidates.map((c) => join4(c, "index.html")).join("\n")}`
934
966
  );
935
967
  }
968
+ function tryListen(server, hostname, port) {
969
+ return new Promise((resolve2, reject) => {
970
+ const onError = (err) => {
971
+ server.removeListener("listening", onListening);
972
+ reject(err);
973
+ };
974
+ const onListening = () => {
975
+ server.removeListener("error", onError);
976
+ const addr = server.address();
977
+ const boundPort = addr && typeof addr === "object" ? addr.port : port;
978
+ resolve2(boundPort);
979
+ };
980
+ server.once("error", onError);
981
+ server.once("listening", onListening);
982
+ server.listen(port, hostname);
983
+ });
984
+ }
936
985
  async function startDashboard(options) {
937
- const { cwd, port = 4242 } = options;
986
+ const { cwd, port: requestedPort = 4242 } = options;
938
987
  const app = new Hono2();
939
988
  const wss = new WebSocketServer3({ noServer: true });
989
+ const portRef = { value: requestedPort };
940
990
  app.use("*", cors({
941
991
  origin: (origin) => origin || "*",
942
992
  allowMethods: ["GET", "PUT", "POST", "DELETE"]
943
993
  }));
944
- const apiRoutes = createApiRoutes(cwd, port, wss);
994
+ const apiRoutes = createApiRoutes(cwd, portRef, wss);
945
995
  app.route("/", apiRoutes);
946
996
  const uiDir = resolveUiDir();
947
997
  app.get("/", (c) => {
@@ -958,11 +1008,25 @@ async function startDashboard(options) {
958
1008
  const contentType = CONTENT_TYPES[extname(filePath)] || "text/plain";
959
1009
  return c.text(content, 200, { "Content-Type": contentType });
960
1010
  });
961
- const server = serve({
962
- fetch: app.fetch,
963
- port,
964
- hostname: "127.0.0.1"
965
- });
1011
+ let server = createAdaptorServer({ fetch: app.fetch });
1012
+ let actualPort = requestedPort;
1013
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
1014
+ const candidatePort = requestedPort + attempt;
1015
+ try {
1016
+ actualPort = await tryListen(server, "127.0.0.1", candidatePort);
1017
+ break;
1018
+ } catch (err) {
1019
+ const nodeErr = err;
1020
+ if (nodeErr.code !== "EADDRINUSE") throw err;
1021
+ if (attempt >= MAX_PORT_ATTEMPTS - 1) {
1022
+ throw new Error(
1023
+ `Could not find an available port (tried ${requestedPort}\u2013${requestedPort + MAX_PORT_ATTEMPTS - 1})`
1024
+ );
1025
+ }
1026
+ server = createAdaptorServer({ fetch: app.fetch });
1027
+ }
1028
+ }
1029
+ portRef.value = actualPort;
966
1030
  server.on("upgrade", (request, socket, head) => {
967
1031
  const url = new URL(request.url || "/", `http://${request.headers.host}`);
968
1032
  if (url.pathname === "/ws") {
@@ -975,14 +1039,17 @@ async function startDashboard(options) {
975
1039
  });
976
1040
  const watcherHandle = setupWatcher(cwd, wss);
977
1041
  try {
978
- installNotificationHook(cwd, port);
1042
+ installNotificationHook(cwd, actualPort);
979
1043
  console.log(chalk.dim(` Configured Claude hook \u2192 .claude/settings.local.json`));
980
1044
  } catch (err) {
981
1045
  const msg = err instanceof Error ? err.message : String(err);
982
1046
  console.log(chalk.yellow(` \u26A0 Could not configure Claude hook: ${msg}`));
983
1047
  }
984
1048
  console.log();
985
- console.log(chalk.bold(` Dashboard ${chalk.dim("\u2192")} http://localhost:${port}`));
1049
+ if (actualPort !== requestedPort) {
1050
+ console.log(chalk.yellow(` Port ${requestedPort} was busy, using ${actualPort} instead`));
1051
+ }
1052
+ console.log(chalk.bold(` Dashboard ${chalk.dim("\u2192")} http://localhost:${actualPort}`));
986
1053
  console.log(chalk.dim(` Watching ${cwd}/.ralph-flow/`));
987
1054
  console.log();
988
1055
  let closed = false;
@@ -1014,7 +1081,7 @@ async function startDashboard(options) {
1014
1081
  }
1015
1082
  }
1016
1083
  });
1017
- return { close };
1084
+ return { close, port: actualPort };
1018
1085
  }
1019
1086
  export {
1020
1087
  startDashboard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphflow",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Multi-agent AI workflow orchestration framework for Claude Code. Define pipelines as loops, coordinate parallel agents, and ship structured work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,6 +56,9 @@ fetch('/api/context')
56
56
  .then(r => r.json())
57
57
  .then(ctx => {
58
58
  dom.hostDisplay.textContent = ctx.projectName + ' :' + ctx.port;
59
+ dom.pageTitle.textContent = ctx.projectName;
60
+ document.title = ctx.projectName + ' \u00b7 RalphFlow Dashboard';
61
+ state.projectName = ctx.projectName;
59
62
  })
60
63
  .catch(() => { /* keep location.host as fallback */ });
61
64
 
@@ -183,7 +186,7 @@ document.getElementById('templatesNav').addEventListener('click', () => {
183
186
  state.showTemplateWizard = false;
184
187
  state.wizardStep = 0;
185
188
  state.wizardData = null;
186
- document.title = 'Templates - RalphFlow Dashboard';
189
+ document.title = 'Templates \u00b7 ' + (state.projectName || 'RalphFlow Dashboard');
187
190
  renderSidebar();
188
191
  renderContent();
189
192
  });