ralphflow 0.1.0
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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/ralphflow.js +575 -0
- package/package.json +68 -0
- package/src/templates/claude-md.template.md +33 -0
- package/src/templates/code-implementation/loops/00-story-loop/loop.md +21 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +135 -0
- package/src/templates/code-implementation/loops/00-story-loop/stories.md +3 -0
- package/src/templates/code-implementation/loops/00-story-loop/tracker.md +16 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/loop.md +60 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/phases/.gitkeep +0 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +162 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/tasks.md +3 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/testing/.gitkeep +0 -0
- package/src/templates/code-implementation/loops/01-tasks-loop/tracker.md +18 -0
- package/src/templates/code-implementation/loops/02-delivery-loop/loop.md +21 -0
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +105 -0
- package/src/templates/code-implementation/loops/02-delivery-loop/tracker.md +13 -0
- package/src/templates/code-implementation/ralphflow.yaml +71 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command4 } from "commander";
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/cli/init.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import chalk2 from "chalk";
|
|
10
|
+
|
|
11
|
+
// src/core/init.ts
|
|
12
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
13
|
+
import { join as join2 } from "path";
|
|
14
|
+
import { createInterface } from "readline";
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
|
|
17
|
+
// src/core/template.ts
|
|
18
|
+
import { readFileSync, mkdirSync, cpSync, existsSync } from "fs";
|
|
19
|
+
import { join, dirname } from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
function resolveTemplatePath(templateName) {
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(__dirname, "..", "templates", templateName),
|
|
25
|
+
// dev: src/core/ -> src/templates/
|
|
26
|
+
join(__dirname, "..", "src", "templates", templateName)
|
|
27
|
+
// bundled: dist/ -> src/templates/
|
|
28
|
+
];
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (existsSync(candidate)) {
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Template "${templateName}" not found. Searched:
|
|
36
|
+
${candidates.join("\n")}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function copyTemplate(templateName, targetDir) {
|
|
40
|
+
const templatePath = resolveTemplatePath(templateName);
|
|
41
|
+
const loopsDir = join(templatePath, "loops");
|
|
42
|
+
if (!existsSync(loopsDir)) {
|
|
43
|
+
throw new Error(`Template "${templateName}" has no loops/ directory`);
|
|
44
|
+
}
|
|
45
|
+
mkdirSync(targetDir, { recursive: true });
|
|
46
|
+
cpSync(loopsDir, targetDir, { recursive: true });
|
|
47
|
+
const yamlSrc = join(templatePath, "ralphflow.yaml");
|
|
48
|
+
if (existsSync(yamlSrc)) {
|
|
49
|
+
const yamlDest = join(targetDir, "ralphflow.yaml");
|
|
50
|
+
cpSync(yamlSrc, yamlDest);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/core/init.ts
|
|
55
|
+
function ask(rl, question) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
rl.question(chalk.cyan("? ") + question + " ", (answer) => {
|
|
58
|
+
resolve(answer.trim());
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
var TEMPLATES = ["code-implementation", "research"];
|
|
63
|
+
async function initProject(cwd, options = {}) {
|
|
64
|
+
const ralphFlowDir = join2(cwd, ".ralph-flow");
|
|
65
|
+
const claudeMdPath = join2(cwd, "CLAUDE.md");
|
|
66
|
+
if (!existsSync2(claudeMdPath)) {
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.yellow(" No CLAUDE.md found."));
|
|
69
|
+
console.log(chalk.dim(' Create one with: claude "Initialize CLAUDE.md for this project"'));
|
|
70
|
+
console.log(chalk.dim(" Or create it manually with your project description, stack, and commands."));
|
|
71
|
+
console.log();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (existsSync2(ralphFlowDir)) {
|
|
75
|
+
const existing = listFlows(ralphFlowDir);
|
|
76
|
+
if (existing.length > 0 && !options.template) {
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(chalk.bold(" Existing flows:"));
|
|
79
|
+
for (const flow of existing) {
|
|
80
|
+
console.log(chalk.dim(` - ${flow}`));
|
|
81
|
+
}
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(chalk.dim(" To add another: npx ralphflow init --template code-implementation --name my-feature"));
|
|
84
|
+
console.log(chalk.dim(" To check status: npx ralphflow status"));
|
|
85
|
+
console.log();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
+
console.log();
|
|
91
|
+
let template = options.template;
|
|
92
|
+
if (!template) {
|
|
93
|
+
console.log(chalk.dim(` Templates: ${TEMPLATES.join(", ")}`));
|
|
94
|
+
template = await ask(rl, "Which template?") || "code-implementation";
|
|
95
|
+
}
|
|
96
|
+
if (!TEMPLATES.includes(template)) {
|
|
97
|
+
console.log(chalk.red(` Unknown template "${template}". Available: ${TEMPLATES.join(", ")}`));
|
|
98
|
+
rl.close();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let flowName = options.name;
|
|
102
|
+
if (!flowName) {
|
|
103
|
+
flowName = await ask(rl, "Flow name? (Enter for default)") || template;
|
|
104
|
+
}
|
|
105
|
+
rl.close();
|
|
106
|
+
console.log();
|
|
107
|
+
const flowDir = join2(ralphFlowDir, flowName);
|
|
108
|
+
if (existsSync2(flowDir)) {
|
|
109
|
+
console.log(chalk.yellow(` Flow "${flowName}" already exists at .ralph-flow/${flowName}/`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
copyTemplate(template, flowDir);
|
|
113
|
+
console.log(chalk.green(` Created .ralph-flow/${flowName}/`) + " (loops, trackers, prompts)");
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(chalk.dim(` Next: npx ralphflow run story --flow ${flowName}`));
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
function listFlows(ralphFlowDir) {
|
|
119
|
+
try {
|
|
120
|
+
return readdirSync(ralphFlowDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/cli/init.ts
|
|
127
|
+
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) => {
|
|
128
|
+
try {
|
|
129
|
+
await initProject(process.cwd(), { template: opts.template, name: opts.name });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
132
|
+
console.error(chalk2.red(`
|
|
133
|
+
${msg}
|
|
134
|
+
`));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// src/cli/run.ts
|
|
140
|
+
import { Command as Command2 } from "commander";
|
|
141
|
+
import chalk4 from "chalk";
|
|
142
|
+
|
|
143
|
+
// src/core/runner.ts
|
|
144
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
145
|
+
import { join as join4 } from "path";
|
|
146
|
+
import chalk3 from "chalk";
|
|
147
|
+
|
|
148
|
+
// src/core/config.ts
|
|
149
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
|
|
150
|
+
import { join as join3 } from "path";
|
|
151
|
+
import { parse as parseYaml } from "yaml";
|
|
152
|
+
var LOOP_ALIASES = {
|
|
153
|
+
story: "story-loop",
|
|
154
|
+
stories: "story-loop",
|
|
155
|
+
tasks: "tasks-loop",
|
|
156
|
+
task: "tasks-loop",
|
|
157
|
+
delivery: "delivery-loop",
|
|
158
|
+
deliver: "delivery-loop"
|
|
159
|
+
};
|
|
160
|
+
function listFlows2(cwd) {
|
|
161
|
+
const baseDir = join3(cwd, ".ralph-flow");
|
|
162
|
+
if (!existsSync3(baseDir)) return [];
|
|
163
|
+
return readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).filter((d) => existsSync3(join3(baseDir, d.name, "ralphflow.yaml"))).map((d) => d.name);
|
|
164
|
+
}
|
|
165
|
+
function resolveFlowDir(cwd, flowName) {
|
|
166
|
+
const baseDir = join3(cwd, ".ralph-flow");
|
|
167
|
+
if (!existsSync3(baseDir)) {
|
|
168
|
+
throw new Error("No .ralph-flow/ found. Run `npx ralphflow init` first.");
|
|
169
|
+
}
|
|
170
|
+
const flows = listFlows2(cwd);
|
|
171
|
+
if (flows.length === 0) {
|
|
172
|
+
throw new Error("No flows found in .ralph-flow/. Run `npx ralphflow init` first.");
|
|
173
|
+
}
|
|
174
|
+
if (flowName) {
|
|
175
|
+
if (!flows.includes(flowName)) {
|
|
176
|
+
throw new Error(`Flow "${flowName}" not found. Available: ${flows.join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
return join3(baseDir, flowName);
|
|
179
|
+
}
|
|
180
|
+
if (flows.length === 1) {
|
|
181
|
+
return join3(baseDir, flows[0]);
|
|
182
|
+
}
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Multiple flows found: ${flows.join(", ")}. Use --flow <name> to specify which one.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
function loadConfig(flowDir) {
|
|
188
|
+
const configPath = join3(flowDir, "ralphflow.yaml");
|
|
189
|
+
if (!existsSync3(configPath)) {
|
|
190
|
+
throw new Error(`No ralphflow.yaml found in ${flowDir}`);
|
|
191
|
+
}
|
|
192
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
193
|
+
const config = parseYaml(raw);
|
|
194
|
+
if (!config.name) {
|
|
195
|
+
throw new Error('ralphflow.yaml: missing required field "name"');
|
|
196
|
+
}
|
|
197
|
+
if (!config.loops || Object.keys(config.loops).length === 0) {
|
|
198
|
+
throw new Error('ralphflow.yaml: missing required field "loops"');
|
|
199
|
+
}
|
|
200
|
+
if (!config.dir) {
|
|
201
|
+
config.dir = ".ralph-flow";
|
|
202
|
+
}
|
|
203
|
+
return config;
|
|
204
|
+
}
|
|
205
|
+
function resolveLoop(config, name) {
|
|
206
|
+
if (config.loops[name]) {
|
|
207
|
+
return { key: name, loop: config.loops[name] };
|
|
208
|
+
}
|
|
209
|
+
const aliased = LOOP_ALIASES[name.toLowerCase()];
|
|
210
|
+
if (aliased && config.loops[aliased]) {
|
|
211
|
+
return { key: aliased, loop: config.loops[aliased] };
|
|
212
|
+
}
|
|
213
|
+
for (const [key, loop] of Object.entries(config.loops)) {
|
|
214
|
+
if (key.startsWith(name) || loop.name.toLowerCase().includes(name.toLowerCase())) {
|
|
215
|
+
return { key, loop };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const available = Object.keys(config.loops).join(", ");
|
|
219
|
+
throw new Error(`Unknown loop "${name}". Available: ${available}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/core/claude.ts
|
|
223
|
+
import { spawn } from "child_process";
|
|
224
|
+
async function spawnClaude(options) {
|
|
225
|
+
const { prompt, model, printMode = false, agentName, cwd } = options;
|
|
226
|
+
const args = [];
|
|
227
|
+
if (printMode) {
|
|
228
|
+
args.push("-p");
|
|
229
|
+
args.push("--dangerously-skip-permissions");
|
|
230
|
+
}
|
|
231
|
+
if (model) {
|
|
232
|
+
args.push("--model", model);
|
|
233
|
+
}
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const child = spawn("claude", args, {
|
|
236
|
+
cwd,
|
|
237
|
+
stdio: printMode ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "inherit"],
|
|
238
|
+
env: { ...process.env }
|
|
239
|
+
});
|
|
240
|
+
let output = "";
|
|
241
|
+
child.stdout?.on("data", (data) => {
|
|
242
|
+
const text = data.toString();
|
|
243
|
+
output += text;
|
|
244
|
+
if (agentName) {
|
|
245
|
+
const lines = text.split("\n");
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (line) {
|
|
248
|
+
process.stdout.write(`[${agentName}] ${line}
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
process.stdout.write(text);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
if (printMode && child.stderr) {
|
|
257
|
+
child.stderr.on("data", (data) => {
|
|
258
|
+
const text = data.toString();
|
|
259
|
+
if (agentName) {
|
|
260
|
+
process.stderr.write(`[${agentName}] ${text}`);
|
|
261
|
+
} else {
|
|
262
|
+
process.stderr.write(text);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
child.stdin?.write(prompt);
|
|
267
|
+
child.stdin?.end();
|
|
268
|
+
child.on("error", (err) => {
|
|
269
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
270
|
+
});
|
|
271
|
+
child.on("close", (code, signal) => {
|
|
272
|
+
resolve({
|
|
273
|
+
output,
|
|
274
|
+
exitCode: code,
|
|
275
|
+
signal
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/core/runner.ts
|
|
282
|
+
var AGENT_COLORS = [
|
|
283
|
+
chalk3.cyan,
|
|
284
|
+
chalk3.magenta,
|
|
285
|
+
chalk3.yellow,
|
|
286
|
+
chalk3.green,
|
|
287
|
+
chalk3.blue,
|
|
288
|
+
chalk3.red
|
|
289
|
+
];
|
|
290
|
+
async function runLoop(loopName, options) {
|
|
291
|
+
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
292
|
+
const config = loadConfig(flowDir);
|
|
293
|
+
const { key, loop } = resolveLoop(config, loopName);
|
|
294
|
+
const isMultiAgent = options.agents > 1 && loop.multi_agent !== false;
|
|
295
|
+
console.log();
|
|
296
|
+
console.log(
|
|
297
|
+
chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (isMultiAgent ? chalk3.dim(` (${options.agents} agents)`) : "")
|
|
298
|
+
);
|
|
299
|
+
console.log();
|
|
300
|
+
if (isMultiAgent) {
|
|
301
|
+
await runMultiAgent(loop, flowDir, options);
|
|
302
|
+
} else {
|
|
303
|
+
await runSingleAgent(loop, flowDir, options);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function runSingleAgent(loop, flowDir, options) {
|
|
307
|
+
for (let i = 1; i <= options.maxIterations; i++) {
|
|
308
|
+
console.log(chalk3.dim(` Iteration ${i}/${options.maxIterations}`));
|
|
309
|
+
const prompt = readPrompt(loop, flowDir, options.agentName);
|
|
310
|
+
const result = await spawnClaude({
|
|
311
|
+
prompt,
|
|
312
|
+
model: options.model,
|
|
313
|
+
printMode: false,
|
|
314
|
+
cwd: options.cwd
|
|
315
|
+
});
|
|
316
|
+
if (result.output.includes(`<promise>${loop.completion}</promise>`)) {
|
|
317
|
+
console.log();
|
|
318
|
+
console.log(chalk3.green(` Loop complete: ${loop.completion}`));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (result.signal === "SIGINT" || result.exitCode === 130) {
|
|
322
|
+
console.log(chalk3.dim(` Iteration ${i} complete, restarting...`));
|
|
323
|
+
console.log();
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
327
|
+
console.log(chalk3.red(` Claude exited with code ${result.exitCode}`));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.log(chalk3.dim(` Iteration ${i} finished, continuing...`));
|
|
331
|
+
console.log();
|
|
332
|
+
}
|
|
333
|
+
console.log(chalk3.yellow(` Max iterations (${options.maxIterations}) reached.`));
|
|
334
|
+
}
|
|
335
|
+
async function runMultiAgent(loop, flowDir, options) {
|
|
336
|
+
const agentCount = options.agents;
|
|
337
|
+
let completed = false;
|
|
338
|
+
const agentRunners = Array.from({ length: agentCount }, (_, idx) => {
|
|
339
|
+
const agentNum = idx + 1;
|
|
340
|
+
const agentName = `agent-${agentNum}`;
|
|
341
|
+
const colorFn = AGENT_COLORS[idx % AGENT_COLORS.length];
|
|
342
|
+
return runAgentLoop(loop, flowDir, {
|
|
343
|
+
...options,
|
|
344
|
+
agentName
|
|
345
|
+
}, colorFn, () => completed, () => {
|
|
346
|
+
completed = true;
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
await Promise.allSettled(agentRunners);
|
|
350
|
+
console.log();
|
|
351
|
+
console.log(chalk3.green(` All agents finished.`));
|
|
352
|
+
}
|
|
353
|
+
async function runAgentLoop(loop, flowDir, options, colorFn, isCompleted, setCompleted) {
|
|
354
|
+
const agentName = options.agentName;
|
|
355
|
+
for (let i = 1; i <= options.maxIterations; i++) {
|
|
356
|
+
if (isCompleted()) {
|
|
357
|
+
console.log(colorFn(` [${agentName}] Stopping \u2014 completion detected.`));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(colorFn(` [${agentName}] Iteration ${i}/${options.maxIterations}`));
|
|
361
|
+
const prompt = readPrompt(loop, flowDir, agentName);
|
|
362
|
+
const result = await spawnClaude({
|
|
363
|
+
prompt,
|
|
364
|
+
model: options.model,
|
|
365
|
+
printMode: true,
|
|
366
|
+
agentName,
|
|
367
|
+
cwd: options.cwd
|
|
368
|
+
});
|
|
369
|
+
if (result.output.includes(`<promise>${loop.completion}</promise>`)) {
|
|
370
|
+
console.log(colorFn(` [${agentName}] Loop complete: ${loop.completion}`));
|
|
371
|
+
setCompleted();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (result.signal === "SIGINT" || result.exitCode === 130) {
|
|
375
|
+
console.log(colorFn(` [${agentName}] Iteration ${i} complete, restarting...`));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
379
|
+
console.log(chalk3.red(` [${agentName}] Claude exited with code ${result.exitCode}`));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
console.log(chalk3.yellow(` [${agentName}] Max iterations reached.`));
|
|
384
|
+
}
|
|
385
|
+
function readPrompt(loop, flowDir, agentName) {
|
|
386
|
+
const promptPath = join4(flowDir, loop.prompt);
|
|
387
|
+
let prompt = readFileSync3(promptPath, "utf-8");
|
|
388
|
+
if (agentName && loop.multi_agent !== false) {
|
|
389
|
+
const ma = loop.multi_agent;
|
|
390
|
+
if (ma.agent_placeholder) {
|
|
391
|
+
prompt = prompt.replaceAll(ma.agent_placeholder, agentName);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return prompt;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/cli/run.ts
|
|
398
|
+
var runCommand = new Command2("run").description("Run a loop").argument("<loop>", "Loop to run (story, tasks, delivery)").option("-a, --agents <n>", "Number of parallel agents", "1").option("-m, --model <model>", "Claude model to use").option("-n, --max-iterations <n>", "Maximum iterations", "30").option("-f, --flow <name>", "Which flow to run (auto-detected if only one)").action(async (loop, opts) => {
|
|
399
|
+
try {
|
|
400
|
+
await runLoop(loop, {
|
|
401
|
+
agents: parseInt(opts.agents, 10),
|
|
402
|
+
model: opts.model,
|
|
403
|
+
maxIterations: parseInt(opts.maxIterations, 10),
|
|
404
|
+
flow: opts.flow,
|
|
405
|
+
cwd: process.cwd()
|
|
406
|
+
});
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
409
|
+
console.error(chalk4.red(`
|
|
410
|
+
${msg}
|
|
411
|
+
`));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// src/cli/status.ts
|
|
417
|
+
import { Command as Command3 } from "commander";
|
|
418
|
+
import chalk6 from "chalk";
|
|
419
|
+
|
|
420
|
+
// src/core/status.ts
|
|
421
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
422
|
+
import { join as join5 } from "path";
|
|
423
|
+
import chalk5 from "chalk";
|
|
424
|
+
import Table from "cli-table3";
|
|
425
|
+
async function showStatus(cwd, flowName) {
|
|
426
|
+
const flows = flowName ? [flowName] : listFlows2(cwd);
|
|
427
|
+
if (flows.length === 0) {
|
|
428
|
+
console.log();
|
|
429
|
+
console.log(chalk5.yellow(" No flows found. Run `npx ralphflow init` first."));
|
|
430
|
+
console.log();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
for (const flow of flows) {
|
|
434
|
+
const flowDir = resolveFlowDir(cwd, flow);
|
|
435
|
+
const config = loadConfig(flowDir);
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(chalk5.bold(` RalphFlow \u2014 ${flow}`));
|
|
438
|
+
console.log();
|
|
439
|
+
const table = new Table({
|
|
440
|
+
chars: {
|
|
441
|
+
top: "",
|
|
442
|
+
"top-mid": "",
|
|
443
|
+
"top-left": "",
|
|
444
|
+
"top-right": "",
|
|
445
|
+
bottom: "",
|
|
446
|
+
"bottom-mid": "",
|
|
447
|
+
"bottom-left": "",
|
|
448
|
+
"bottom-right": "",
|
|
449
|
+
left: " ",
|
|
450
|
+
"left-mid": "",
|
|
451
|
+
mid: "",
|
|
452
|
+
"mid-mid": "",
|
|
453
|
+
right: "",
|
|
454
|
+
"right-mid": "",
|
|
455
|
+
middle: " "
|
|
456
|
+
},
|
|
457
|
+
style: { "padding-left": 0, "padding-right": 1 },
|
|
458
|
+
head: [
|
|
459
|
+
chalk5.dim("Loop"),
|
|
460
|
+
chalk5.dim("Stage"),
|
|
461
|
+
chalk5.dim("Active"),
|
|
462
|
+
chalk5.dim("Progress")
|
|
463
|
+
]
|
|
464
|
+
});
|
|
465
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
466
|
+
for (const [key, loop] of sortedLoops) {
|
|
467
|
+
const status = parseTracker(loop.tracker, flowDir, loop.name);
|
|
468
|
+
table.push([
|
|
469
|
+
loop.name,
|
|
470
|
+
status.stage,
|
|
471
|
+
status.active,
|
|
472
|
+
`${status.completed}/${status.total}`
|
|
473
|
+
]);
|
|
474
|
+
if (status.agents && status.agents.length > 0) {
|
|
475
|
+
for (const agent of status.agents) {
|
|
476
|
+
table.push([
|
|
477
|
+
chalk5.dim(` ${agent.name}`),
|
|
478
|
+
chalk5.dim(agent.stage),
|
|
479
|
+
chalk5.dim(agent.activeTask),
|
|
480
|
+
chalk5.dim(agent.lastHeartbeat)
|
|
481
|
+
]);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
console.log(table.toString());
|
|
486
|
+
}
|
|
487
|
+
console.log();
|
|
488
|
+
}
|
|
489
|
+
function parseTracker(trackerPath, flowDir, loopName) {
|
|
490
|
+
const fullPath = join5(flowDir, trackerPath);
|
|
491
|
+
const status = {
|
|
492
|
+
loop: loopName,
|
|
493
|
+
stage: "\u2014",
|
|
494
|
+
active: "none",
|
|
495
|
+
completed: 0,
|
|
496
|
+
total: 0
|
|
497
|
+
};
|
|
498
|
+
if (!existsSync4(fullPath)) {
|
|
499
|
+
return status;
|
|
500
|
+
}
|
|
501
|
+
const content = readFileSync4(fullPath, "utf-8");
|
|
502
|
+
const lines = content.split("\n");
|
|
503
|
+
for (const line of lines) {
|
|
504
|
+
const metaMatch = line.match(/^- (\w[\w_]*): (.+)$/);
|
|
505
|
+
if (metaMatch) {
|
|
506
|
+
const [, key, value] = metaMatch;
|
|
507
|
+
if (key === "stage") status.stage = value.trim();
|
|
508
|
+
if (key === "active_story" || key === "active_task") status.active = value.trim();
|
|
509
|
+
if (key === "completed_stories" || key === "completed_tasks") {
|
|
510
|
+
const arrayMatch = value.match(/\[(.+)\]/);
|
|
511
|
+
if (arrayMatch) {
|
|
512
|
+
status.completed = arrayMatch[1].split(",").filter((s) => s.trim()).length;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
518
|
+
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
519
|
+
if (unchecked + checked > 0) {
|
|
520
|
+
status.total = unchecked + checked;
|
|
521
|
+
status.completed = checked;
|
|
522
|
+
}
|
|
523
|
+
const agentTableMatch = content.match(/\| agent \|.*\n\|[-|]+\n((?:\|.*\n)*)/);
|
|
524
|
+
if (agentTableMatch) {
|
|
525
|
+
const agentRows = agentTableMatch[1].trim().split("\n");
|
|
526
|
+
status.agents = [];
|
|
527
|
+
for (const row of agentRows) {
|
|
528
|
+
const cells = row.split("|").map((s) => s.trim()).filter(Boolean);
|
|
529
|
+
if (cells.length >= 4) {
|
|
530
|
+
status.agents.push({
|
|
531
|
+
name: cells[0],
|
|
532
|
+
activeTask: cells[1],
|
|
533
|
+
stage: cells[2],
|
|
534
|
+
lastHeartbeat: cells[3]
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (status.agents.length === 0) {
|
|
539
|
+
status.agents = void 0;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return status;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/cli/status.ts
|
|
546
|
+
var statusCommand = new Command3("status").description("Show pipeline status").option("-f, --flow <name>", "Show status for a specific flow").action(async (opts) => {
|
|
547
|
+
try {
|
|
548
|
+
await showStatus(process.cwd(), opts.flow);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
551
|
+
console.error(chalk6.red(`
|
|
552
|
+
${msg}
|
|
553
|
+
`));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// src/cli/index.ts
|
|
559
|
+
var program = new Command4().name("ralphflow").description("Multi-agent AI workflow orchestration for Claude Code").version("0.1.0").addCommand(initCommand).addCommand(runCommand).addCommand(statusCommand);
|
|
560
|
+
process.on("SIGINT", () => {
|
|
561
|
+
console.log();
|
|
562
|
+
console.log(chalk7.dim(" Interrupted."));
|
|
563
|
+
process.exit(130);
|
|
564
|
+
});
|
|
565
|
+
program.configureOutput({
|
|
566
|
+
writeErr: (str) => {
|
|
567
|
+
const clean = str.replace(/^error: /, "");
|
|
568
|
+
if (clean.trim()) {
|
|
569
|
+
console.error(chalk7.red(` ${clean.trim()}`));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// src/bin/ralphflow.ts
|
|
575
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ralphflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-agent AI workflow orchestration framework for Claude Code. Define pipelines as loops, coordinate parallel agents, and ship structured work.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ralphflow": "./dist/ralphflow.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/core/index.js",
|
|
10
|
+
"types": "./dist/core/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/core/index.js",
|
|
14
|
+
"types": "./dist/core/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./cli": {
|
|
17
|
+
"import": "./dist/cli/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"lint": "eslint src/",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"claude",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"multi-agent",
|
|
32
|
+
"workflow",
|
|
33
|
+
"orchestration",
|
|
34
|
+
"pipeline",
|
|
35
|
+
"state-machine",
|
|
36
|
+
"automation",
|
|
37
|
+
"developer-tools"
|
|
38
|
+
],
|
|
39
|
+
"author": "",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/user/ralph-flow"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist/",
|
|
50
|
+
"src/templates/"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"chalk": "^5.3.0",
|
|
54
|
+
"chokidar": "^4.0.0",
|
|
55
|
+
"cli-table3": "^0.6.5",
|
|
56
|
+
"commander": "^12.1.0",
|
|
57
|
+
"hono": "^4.6.0",
|
|
58
|
+
"simple-git": "^3.27.0",
|
|
59
|
+
"ws": "^8.18.0",
|
|
60
|
+
"yaml": "^2.6.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^22.0.0",
|
|
64
|
+
"@types/ws": "^8.5.0",
|
|
65
|
+
"tsup": "^8.3.0",
|
|
66
|
+
"typescript": "^5.6.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
{{PROJECT_DESCRIPTION}}
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
**Stack:** {{TECH_STACK}}
|
|
8
|
+
|
|
9
|
+
<!-- Describe: folder structure, key modules, data flow, external services -->
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### Development
|
|
14
|
+
{{DEV_COMMANDS}}
|
|
15
|
+
|
|
16
|
+
### Testing
|
|
17
|
+
<!-- npm test, pytest, etc. -->
|
|
18
|
+
|
|
19
|
+
### Build & Deploy
|
|
20
|
+
<!-- docker compose up, npm run build, etc. -->
|
|
21
|
+
|
|
22
|
+
### Health Checks
|
|
23
|
+
<!-- curl localhost:3000/health, etc. -->
|
|
24
|
+
|
|
25
|
+
## Conventions
|
|
26
|
+
|
|
27
|
+
- Follow existing patterns in the codebase
|
|
28
|
+
- Write tests for new functionality
|
|
29
|
+
- Use descriptive commit messages
|
|
30
|
+
|
|
31
|
+
## URLs & Endpoints
|
|
32
|
+
|
|
33
|
+
<!-- API base URL, frontend URL, admin panel, etc. -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Story Loop — Execution
|
|
2
|
+
|
|
3
|
+
## Using RalphFlow CLI
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx ralphflow run story
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Manual (without CLI)
|
|
10
|
+
|
|
11
|
+
### Option 1: ralph-loop slash command
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/ralph-loop "$(cat .ralph-flow/00-story-loop/prompt.md)" --max-iterations 30 --completion-promise "ALL STORIES PROCESSED"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Option 2: while loop
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
while :; do cat .ralph-flow/00-story-loop/prompt.md | claude --dangerously-skip-permissions --model claude-opus-4-6; done
|
|
21
|
+
```
|