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.
- package/dist/{chunk-DOC64TD6.js → chunk-CA4XP6KI.js} +1 -1
- package/dist/ralphflow.js +237 -28
- package/dist/{server-EX5MWYW4.js → server-64NQCIKJ.js} +88 -21
- package/package.json +1 -1
- package/src/dashboard/ui/app.js +4 -1
- package/src/dashboard/ui/archives.js +27 -2
- package/src/dashboard/ui/index.html +1 -1
- package/src/dashboard/ui/loop-detail.js +1 -1
- package/src/dashboard/ui/prompt-builder.js +39 -4
- package/src/dashboard/ui/sidebar.js +1 -1
- package/src/dashboard/ui/state.js +3 -0
- package/src/dashboard/ui/styles.css +77 -0
- package/src/dashboard/ui/templates.js +3 -0
- package/src/dashboard/ui/utils.js +30 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +51 -11
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +28 -2
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +27 -4
- package/src/templates/code-review/loops/00-collect-loop/changesets.md +3 -0
- package/src/templates/code-review/loops/00-collect-loop/prompt.md +179 -0
- package/src/templates/code-review/loops/00-collect-loop/tracker.md +16 -0
- package/src/templates/code-review/loops/01-spec-review-loop/prompt.md +238 -0
- package/src/templates/code-review/loops/01-spec-review-loop/tracker.md +16 -0
- package/src/templates/code-review/loops/02-quality-review-loop/issues.md +3 -0
- package/src/templates/code-review/loops/02-quality-review-loop/prompt.md +306 -0
- package/src/templates/code-review/loops/02-quality-review-loop/tracker.md +16 -0
- package/src/templates/code-review/loops/03-fix-loop/prompt.md +265 -0
- package/src/templates/code-review/loops/03-fix-loop/tracker.md +16 -0
- package/src/templates/code-review/ralphflow.yaml +98 -0
- package/src/templates/design-review/loops/00-explore-loop/ideas.md +3 -0
- package/src/templates/design-review/loops/00-explore-loop/prompt.md +207 -0
- package/src/templates/design-review/loops/00-explore-loop/tracker.md +16 -0
- package/src/templates/design-review/loops/01-design-loop/designs.md +3 -0
- package/src/templates/design-review/loops/01-design-loop/prompt.md +201 -0
- package/src/templates/design-review/loops/01-design-loop/tracker.md +16 -0
- package/src/templates/design-review/loops/02-review-loop/prompt.md +255 -0
- package/src/templates/design-review/loops/02-review-loop/tracker.md +16 -0
- package/src/templates/design-review/loops/03-plan-loop/plans.md +3 -0
- package/src/templates/design-review/loops/03-plan-loop/prompt.md +247 -0
- package/src/templates/design-review/loops/03-plan-loop/tracker.md +16 -0
- package/src/templates/design-review/ralphflow.yaml +84 -0
- package/src/templates/research/loops/00-discovery-loop/prompt.md +36 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +22 -2
- package/src/templates/research/loops/02-story-loop/prompt.md +20 -1
- package/src/templates/research/loops/03-document-loop/prompt.md +20 -1
- package/src/templates/systematic-debugging/loops/00-investigate-loop/bugs.md +3 -0
- package/src/templates/systematic-debugging/loops/00-investigate-loop/prompt.md +237 -0
- package/src/templates/systematic-debugging/loops/00-investigate-loop/tracker.md +16 -0
- package/src/templates/systematic-debugging/loops/01-hypothesize-loop/hypotheses.md +3 -0
- package/src/templates/systematic-debugging/loops/01-hypothesize-loop/prompt.md +312 -0
- package/src/templates/systematic-debugging/loops/01-hypothesize-loop/tracker.md +18 -0
- package/src/templates/systematic-debugging/loops/02-fix-loop/fixes.md +3 -0
- package/src/templates/systematic-debugging/loops/02-fix-loop/prompt.md +342 -0
- package/src/templates/systematic-debugging/loops/02-fix-loop/tracker.md +18 -0
- package/src/templates/systematic-debugging/ralphflow.yaml +81 -0
- package/src/templates/tdd-implementation/loops/00-spec-loop/prompt.md +208 -0
- package/src/templates/tdd-implementation/loops/00-spec-loop/specs.md +3 -0
- package/src/templates/tdd-implementation/loops/00-spec-loop/tracker.md +16 -0
- package/src/templates/tdd-implementation/loops/01-tdd-loop/prompt.md +323 -0
- package/src/templates/tdd-implementation/loops/01-tdd-loop/test-cases.md +3 -0
- package/src/templates/tdd-implementation/loops/01-tdd-loop/tracker.md +18 -0
- package/src/templates/tdd-implementation/loops/02-verify-loop/prompt.md +226 -0
- package/src/templates/tdd-implementation/loops/02-verify-loop/tracker.md +16 -0
- package/src/templates/tdd-implementation/loops/02-verify-loop/verifications.md +3 -0
- 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-
|
|
15
|
+
} from "./chunk-CA4XP6KI.js";
|
|
16
16
|
|
|
17
17
|
// src/cli/index.ts
|
|
18
|
-
import { Command as
|
|
19
|
-
import
|
|
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((
|
|
32
|
+
return new Promise((resolve3) => {
|
|
33
33
|
rl.question(chalk.cyan("? ") + question + " ", (answer) => {
|
|
34
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
804
|
-
|
|
884
|
+
{
|
|
885
|
+
const scanFile = primaryInputFile || "input.md";
|
|
886
|
+
p += `## First-Run / New ${entityTitle} Detection
|
|
805
887
|
|
|
806
888
|
`;
|
|
807
|
-
|
|
808
|
-
|
|
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
|
|
1004
|
-
const
|
|
1005
|
-
const {
|
|
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(
|
|
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(
|
|
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(
|
|
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-
|
|
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 {
|
|
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,
|
|
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.
|
|
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
|
|
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 (
|
|
195
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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,
|
|
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
|
-
|
|
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
package/src/dashboard/ui/app.js
CHANGED
|
@@ -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
|
|
189
|
+
document.title = 'Templates \u00b7 ' + (state.projectName || 'RalphFlow Dashboard');
|
|
187
190
|
renderSidebar();
|
|
188
191
|
renderContent();
|
|
189
192
|
});
|