ralphflow 0.5.2 → 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 (55) hide show
  1. package/dist/{chunk-DOC64TD6.js → chunk-CA4XP6KI.js} +1 -1
  2. package/dist/ralphflow.js +132 -18
  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/sidebar.js +1 -1
  10. package/src/dashboard/ui/state.js +3 -0
  11. package/src/dashboard/ui/styles.css +56 -0
  12. package/src/dashboard/ui/utils.js +30 -0
  13. package/src/templates/code-review/loops/00-collect-loop/changesets.md +3 -0
  14. package/src/templates/code-review/loops/00-collect-loop/prompt.md +179 -0
  15. package/src/templates/code-review/loops/00-collect-loop/tracker.md +16 -0
  16. package/src/templates/code-review/loops/01-spec-review-loop/prompt.md +238 -0
  17. package/src/templates/code-review/loops/01-spec-review-loop/tracker.md +16 -0
  18. package/src/templates/code-review/loops/02-quality-review-loop/issues.md +3 -0
  19. package/src/templates/code-review/loops/02-quality-review-loop/prompt.md +306 -0
  20. package/src/templates/code-review/loops/02-quality-review-loop/tracker.md +16 -0
  21. package/src/templates/code-review/loops/03-fix-loop/prompt.md +265 -0
  22. package/src/templates/code-review/loops/03-fix-loop/tracker.md +16 -0
  23. package/src/templates/code-review/ralphflow.yaml +98 -0
  24. package/src/templates/design-review/loops/00-explore-loop/ideas.md +3 -0
  25. package/src/templates/design-review/loops/00-explore-loop/prompt.md +207 -0
  26. package/src/templates/design-review/loops/00-explore-loop/tracker.md +16 -0
  27. package/src/templates/design-review/loops/01-design-loop/designs.md +3 -0
  28. package/src/templates/design-review/loops/01-design-loop/prompt.md +201 -0
  29. package/src/templates/design-review/loops/01-design-loop/tracker.md +16 -0
  30. package/src/templates/design-review/loops/02-review-loop/prompt.md +255 -0
  31. package/src/templates/design-review/loops/02-review-loop/tracker.md +16 -0
  32. package/src/templates/design-review/loops/03-plan-loop/plans.md +3 -0
  33. package/src/templates/design-review/loops/03-plan-loop/prompt.md +247 -0
  34. package/src/templates/design-review/loops/03-plan-loop/tracker.md +16 -0
  35. package/src/templates/design-review/ralphflow.yaml +84 -0
  36. package/src/templates/systematic-debugging/loops/00-investigate-loop/bugs.md +3 -0
  37. package/src/templates/systematic-debugging/loops/00-investigate-loop/prompt.md +237 -0
  38. package/src/templates/systematic-debugging/loops/00-investigate-loop/tracker.md +16 -0
  39. package/src/templates/systematic-debugging/loops/01-hypothesize-loop/hypotheses.md +3 -0
  40. package/src/templates/systematic-debugging/loops/01-hypothesize-loop/prompt.md +312 -0
  41. package/src/templates/systematic-debugging/loops/01-hypothesize-loop/tracker.md +18 -0
  42. package/src/templates/systematic-debugging/loops/02-fix-loop/fixes.md +3 -0
  43. package/src/templates/systematic-debugging/loops/02-fix-loop/prompt.md +342 -0
  44. package/src/templates/systematic-debugging/loops/02-fix-loop/tracker.md +18 -0
  45. package/src/templates/systematic-debugging/ralphflow.yaml +81 -0
  46. package/src/templates/tdd-implementation/loops/00-spec-loop/prompt.md +208 -0
  47. package/src/templates/tdd-implementation/loops/00-spec-loop/specs.md +3 -0
  48. package/src/templates/tdd-implementation/loops/00-spec-loop/tracker.md +16 -0
  49. package/src/templates/tdd-implementation/loops/01-tdd-loop/prompt.md +323 -0
  50. package/src/templates/tdd-implementation/loops/01-tdd-loop/test-cases.md +3 -0
  51. package/src/templates/tdd-implementation/loops/01-tdd-loop/tracker.md +18 -0
  52. package/src/templates/tdd-implementation/loops/02-verify-loop/prompt.md +226 -0
  53. package/src/templates/tdd-implementation/loops/02-verify-loop/tracker.md +16 -0
  54. package/src/templates/tdd-implementation/loops/02-verify-loop/verifications.md +3 -0
  55. 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
@@ -524,7 +524,7 @@ var runCommand = new Command2("run").description("Run a loop").argument("<loop>"
524
524
  try {
525
525
  let dashboardHandle;
526
526
  if (opts.ui) {
527
- const { startDashboard } = await import("./server-EX5MWYW4.js");
527
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
528
528
  dashboardHandle = await startDashboard({ cwd: process.cwd() });
529
529
  }
530
530
  await runLoop(loop, {
@@ -550,7 +550,7 @@ var e2eCommand = new Command3("e2e").description("Run all loops end-to-end with
550
550
  try {
551
551
  let dashboardHandle;
552
552
  if (opts.ui) {
553
- const { startDashboard } = await import("./server-EX5MWYW4.js");
553
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
554
554
  dashboardHandle = await startDashboard({ cwd: process.cwd() });
555
555
  }
556
556
  await runE2E({
@@ -587,7 +587,7 @@ var statusCommand = new Command4("status").description("Show pipeline status").o
587
587
  // src/cli/dashboard.ts
588
588
  import { Command as Command5 } from "commander";
589
589
  var dashboardCommand = new Command5("dashboard").alias("ui").description("Start the web dashboard").option("-p, --port <port>", "Port number", "4242").action(async (opts) => {
590
- const { startDashboard } = await import("./server-EX5MWYW4.js");
590
+ const { startDashboard } = await import("./server-64NQCIKJ.js");
591
591
  await startDashboard({ cwd: process.cwd(), port: parseInt(opts.port, 10) });
592
592
  });
593
593
 
@@ -1094,28 +1094,142 @@ var createTemplateCommand = new Command6("create-template").description("Create
1094
1094
  }
1095
1095
  });
1096
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
+
1097
1212
  // src/cli/index.ts
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 () => {
1099
- const port = 4242;
1100
- const { startDashboard } = await import("./server-EX5MWYW4.js");
1101
- 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() });
1102
1216
  const url = `http://localhost:${port}`;
1103
1217
  exec(`open "${url}"`, (err) => {
1104
1218
  if (err) {
1105
- console.log(chalk8.dim(` Open ${url} in your browser`));
1219
+ console.log(chalk9.dim(` Open ${url} in your browser`));
1106
1220
  }
1107
1221
  });
1108
1222
  });
1109
1223
  process.on("SIGINT", () => {
1110
1224
  console.log();
1111
- console.log(chalk8.dim(" Interrupted."));
1225
+ console.log(chalk9.dim(" Interrupted."));
1112
1226
  process.exit(130);
1113
1227
  });
1114
1228
  program.configureOutput({
1115
1229
  writeErr: (str) => {
1116
1230
  const clean = str.replace(/^error: /, "");
1117
1231
  if (clean.trim()) {
1118
- console.error(chalk8.red(` ${clean.trim()}`));
1232
+ console.error(chalk9.red(` ${clean.trim()}`));
1119
1233
  }
1120
1234
  }
1121
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.2",
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
  });
@@ -1,13 +1,14 @@
1
1
  // Archive browsing: listing, file tree, file viewer.
2
2
 
3
3
  import { state, actions } from './state.js';
4
- import { fetchJson, esc } from './utils.js';
4
+ import { fetchJson, esc, renderMarkdown } from './utils.js';
5
5
 
6
6
  export function switchAppTab(tab) {
7
7
  if (tab === state.activeAppTab) return;
8
8
  state.activeAppTab = tab;
9
9
  state.expandedArchive = null;
10
10
  state.archiveFilesCache = {};
11
+ state.archiveSummaryCache = {};
11
12
  state.viewingArchiveFile = null;
12
13
  actions.renderContent();
13
14
  }
@@ -52,8 +53,21 @@ function renderArchivesView(container, appName) {
52
53
  if (isExpanded) {
53
54
  const files = state.archiveFilesCache[archive.timestamp];
54
55
  if (files) {
56
+ // Show summary.md prominently at top if available, otherwise show CLI hint
57
+ const summaryContent = state.archiveSummaryCache[archive.timestamp];
58
+ if (summaryContent) {
59
+ html += `<div class="archive-summary">${renderMarkdown(summaryContent)}</div>`;
60
+ } else {
61
+ html += `<div class="archive-summary-hint">
62
+ <span class="archive-summary-hint-text">No summary yet. Generate one with:</span>
63
+ <code class="archive-summary-hint-cmd">npx ralphflow summarize ${esc(appName)} ${esc(archive.timestamp)}</code>
64
+ </div>`;
65
+ }
66
+
67
+ // Filter summary.md from regular file list
68
+ const displayFiles = files.filter(f => f.path !== 'summary.md');
55
69
  html += '<div class="archive-files">';
56
- for (const file of files) {
70
+ for (const file of displayFiles) {
57
71
  const isActive = state.viewingArchiveFile === file.path;
58
72
  html += `<div class="archive-file-item${isActive ? ' active' : ''}" data-archive-file="${esc(file.path)}" data-archive-ts="${esc(archive.timestamp)}">
59
73
  <span class="archive-file-icon">&#128196;</span>
@@ -129,6 +143,17 @@ async function toggleArchiveCard(appName, timestamp) {
129
143
  try {
130
144
  const files = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files`);
131
145
  state.archiveFilesCache[timestamp] = files;
146
+
147
+ // Auto-fetch summary.md if present
148
+ const hasSummary = files.some(f => f.path === 'summary.md');
149
+ if (hasSummary) {
150
+ try {
151
+ const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files/summary.md`);
152
+ state.archiveSummaryCache[timestamp] = data.content || '';
153
+ } catch {
154
+ // summary fetch failed — skip display
155
+ }
156
+ }
132
157
  } catch {
133
158
  state.archiveFilesCache[timestamp] = [];
134
159
  }
@@ -9,7 +9,7 @@
9
9
  <body>
10
10
 
11
11
  <div class="header">
12
- <h1>RalphFlow Dashboard</h1>
12
+ <h1 id="pageTitle">RalphFlow Dashboard</h1>
13
13
  <span class="host" id="hostDisplay"></span>
14
14
  </div>
15
15
 
@@ -604,7 +604,7 @@ async function submitDeleteApp(overlay, appName) {
604
604
  if (state.selectedApp && state.selectedApp.appName === appName) {
605
605
  state.selectedApp = null;
606
606
  state.selectedLoop = null;
607
- document.title = 'RalphFlow Dashboard';
607
+ document.title = state.projectName ? state.projectName + ' \u00b7 RalphFlow Dashboard' : 'RalphFlow Dashboard';
608
608
  }
609
609
  actions.fetchApps();
610
610
  } catch (err) {
@@ -79,7 +79,7 @@ export function selectApp(app) {
79
79
  state.viewingTemplateName = null;
80
80
  state.viewingTemplateConfig = null;
81
81
  state.viewingTemplatePrompts = {};
82
- document.title = app.appName + ' - RalphFlow Dashboard';
82
+ document.title = app.appName + ' \u00b7 ' + (state.projectName || 'RalphFlow Dashboard');
83
83
  renderSidebar();
84
84
  actions.renderContent();
85
85
  actions.fetchAppStatus(app.appName);
@@ -4,6 +4,7 @@
4
4
  export const $ = (sel) => document.querySelector(sel);
5
5
 
6
6
  export const dom = {
7
+ pageTitle: $('#pageTitle'),
7
8
  hostDisplay: $('#hostDisplay'),
8
9
  sidebarApps: $('#sidebarApps'),
9
10
  content: $('#content'),
@@ -34,7 +35,9 @@ export const state = {
34
35
  archivesData: [],
35
36
  expandedArchive: null,
36
37
  archiveFilesCache: {},
38
+ archiveSummaryCache: {},
37
39
  viewingArchiveFile: null,
40
+ projectName: null,
38
41
  currentPage: 'app',
39
42
  templatesList: [],
40
43
  showTemplateBuilder: false,
@@ -1195,6 +1195,62 @@ body {
1195
1195
  transform: rotate(90deg);
1196
1196
  }
1197
1197
 
1198
+ /* Archive summary display */
1199
+ .archive-summary {
1200
+ border-top: 1px solid var(--border);
1201
+ padding: 16px 20px;
1202
+ background: var(--bg);
1203
+ font-family: var(--mono);
1204
+ font-size: 13px;
1205
+ line-height: 1.6;
1206
+ color: var(--text);
1207
+ overflow-x: auto;
1208
+ }
1209
+ .archive-summary h1,
1210
+ .archive-summary h2,
1211
+ .archive-summary h3 {
1212
+ color: var(--text);
1213
+ margin: 12px 0 6px;
1214
+ font-size: 14px;
1215
+ }
1216
+ .archive-summary h1 { font-size: 15px; margin-top: 0; }
1217
+ .archive-summary .md-code-block {
1218
+ background: var(--bg-surface);
1219
+ border: 1px solid var(--border);
1220
+ border-radius: var(--radius);
1221
+ padding: 12px 16px;
1222
+ margin: 8px 0;
1223
+ overflow-x: auto;
1224
+ white-space: pre;
1225
+ font-size: 12px;
1226
+ line-height: 1.5;
1227
+ }
1228
+ .archive-summary .md-code-block code {
1229
+ font-family: var(--mono);
1230
+ }
1231
+ .archive-summary-hint {
1232
+ border-top: 1px solid var(--border);
1233
+ padding: 12px 20px;
1234
+ display: flex;
1235
+ align-items: center;
1236
+ gap: 10px;
1237
+ flex-wrap: wrap;
1238
+ }
1239
+ .archive-summary-hint-text {
1240
+ font-size: 12px;
1241
+ color: var(--text-dim);
1242
+ }
1243
+ .archive-summary-hint-cmd {
1244
+ font-family: var(--mono);
1245
+ font-size: 11px;
1246
+ color: var(--text-dim);
1247
+ background: var(--bg);
1248
+ border: 1px solid var(--border);
1249
+ border-radius: var(--radius);
1250
+ padding: 3px 8px;
1251
+ user-select: all;
1252
+ }
1253
+
1198
1254
  /* Archive file browser */
1199
1255
  .archive-files {
1200
1256
  border-top: 1px solid var(--border);