ralphflow 0.2.0 → 0.3.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/dist/ralphflow.js +98 -116
- package/package.json +1 -1
package/dist/ralphflow.js
CHANGED
|
@@ -141,7 +141,7 @@ import { Command as Command2 } from "commander";
|
|
|
141
141
|
import chalk4 from "chalk";
|
|
142
142
|
|
|
143
143
|
// src/core/runner.ts
|
|
144
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
144
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync, readdirSync as readdirSync3, unlinkSync } from "fs";
|
|
145
145
|
import { join as join4 } from "path";
|
|
146
146
|
import chalk3 from "chalk";
|
|
147
147
|
|
|
@@ -229,55 +229,23 @@ function resolveLoop(config, name) {
|
|
|
229
229
|
// src/core/claude.ts
|
|
230
230
|
import { spawn } from "child_process";
|
|
231
231
|
async function spawnClaude(options) {
|
|
232
|
-
const { prompt, model,
|
|
233
|
-
const args = [];
|
|
234
|
-
if (printMode) {
|
|
235
|
-
args.push("-p");
|
|
236
|
-
args.push("--dangerously-skip-permissions");
|
|
237
|
-
}
|
|
232
|
+
const { prompt, model, cwd } = options;
|
|
233
|
+
const args = ["--dangerously-skip-permissions", prompt];
|
|
238
234
|
if (model) {
|
|
239
|
-
args.
|
|
235
|
+
args.unshift("--model", model);
|
|
240
236
|
}
|
|
241
237
|
return new Promise((resolve, reject) => {
|
|
242
238
|
const child = spawn("claude", args, {
|
|
243
239
|
cwd,
|
|
244
|
-
stdio:
|
|
240
|
+
stdio: "inherit",
|
|
245
241
|
env: { ...process.env }
|
|
246
242
|
});
|
|
247
|
-
let output = "";
|
|
248
|
-
child.stdout?.on("data", (data) => {
|
|
249
|
-
const text = data.toString();
|
|
250
|
-
output += text;
|
|
251
|
-
if (agentName) {
|
|
252
|
-
const lines = text.split("\n");
|
|
253
|
-
for (const line of lines) {
|
|
254
|
-
if (line) {
|
|
255
|
-
process.stdout.write(`[${agentName}] ${line}
|
|
256
|
-
`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
process.stdout.write(text);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
if (printMode && child.stderr) {
|
|
264
|
-
child.stderr.on("data", (data) => {
|
|
265
|
-
const text = data.toString();
|
|
266
|
-
if (agentName) {
|
|
267
|
-
process.stderr.write(`[${agentName}] ${text}`);
|
|
268
|
-
} else {
|
|
269
|
-
process.stderr.write(text);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
child.stdin?.write(prompt);
|
|
274
|
-
child.stdin?.end();
|
|
275
243
|
child.on("error", (err) => {
|
|
276
244
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
277
245
|
});
|
|
278
246
|
child.on("close", (code, signal) => {
|
|
279
247
|
resolve({
|
|
280
|
-
output,
|
|
248
|
+
output: "",
|
|
281
249
|
exitCode: code,
|
|
282
250
|
signal
|
|
283
251
|
});
|
|
@@ -286,41 +254,104 @@ async function spawnClaude(options) {
|
|
|
286
254
|
}
|
|
287
255
|
|
|
288
256
|
// src/core/runner.ts
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
257
|
+
function agentsDir(flowDir, loop) {
|
|
258
|
+
const loopDir = join4(flowDir, loop.tracker, "..");
|
|
259
|
+
return join4(loopDir, ".agents");
|
|
260
|
+
}
|
|
261
|
+
function isProcessAlive(pid) {
|
|
262
|
+
try {
|
|
263
|
+
process.kill(pid, 0);
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function cleanStaleAgents(dir) {
|
|
270
|
+
if (!existsSync4(dir)) return;
|
|
271
|
+
for (const file of readdirSync3(dir)) {
|
|
272
|
+
if (!file.endsWith(".lock")) continue;
|
|
273
|
+
const pidStr = readFileSync3(join4(dir, file), "utf-8").trim();
|
|
274
|
+
const pid = parseInt(pidStr, 10);
|
|
275
|
+
if (isNaN(pid) || !isProcessAlive(pid)) {
|
|
276
|
+
unlinkSync(join4(dir, file));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function acquireAgentId(dir, maxAgents) {
|
|
281
|
+
mkdirSync2(dir, { recursive: true });
|
|
282
|
+
cleanStaleAgents(dir);
|
|
283
|
+
for (let n = 1; n <= maxAgents; n++) {
|
|
284
|
+
const lockFile = join4(dir, `agent-${n}.lock`);
|
|
285
|
+
if (!existsSync4(lockFile)) {
|
|
286
|
+
writeFileSync(lockFile, String(process.pid));
|
|
287
|
+
return `agent-${n}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
throw new Error(`All ${maxAgents} agent slots are occupied. Wait for one to finish or increase max_agents.`);
|
|
291
|
+
}
|
|
292
|
+
function releaseAgentId(dir, agentName) {
|
|
293
|
+
const lockFile = join4(dir, `${agentName}.lock`);
|
|
294
|
+
try {
|
|
295
|
+
unlinkSync(lockFile);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function checkTrackerForCompletion(flowDir, loop) {
|
|
300
|
+
const trackerPath = join4(flowDir, loop.tracker);
|
|
301
|
+
if (!existsSync4(trackerPath)) return false;
|
|
302
|
+
const content = readFileSync3(trackerPath, "utf-8");
|
|
303
|
+
return content.includes(`<promise>${loop.completion}</promise>`);
|
|
304
|
+
}
|
|
297
305
|
async function runLoop(loopName, options) {
|
|
298
306
|
const flowDir = resolveFlowDir(options.cwd, options.flow);
|
|
299
307
|
const config = loadConfig(flowDir);
|
|
300
308
|
const { key, loop } = resolveLoop(config, loopName);
|
|
301
|
-
|
|
309
|
+
let agentName;
|
|
310
|
+
let agentDir;
|
|
311
|
+
if (options.multiAgent) {
|
|
312
|
+
if (loop.multi_agent === false) {
|
|
313
|
+
throw new Error(`Loop "${loop.name}" does not support multi-agent mode.`);
|
|
314
|
+
}
|
|
315
|
+
const ma = loop.multi_agent;
|
|
316
|
+
agentDir = agentsDir(flowDir, loop);
|
|
317
|
+
agentName = acquireAgentId(agentDir, ma.max_agents);
|
|
318
|
+
}
|
|
302
319
|
console.log();
|
|
303
320
|
console.log(
|
|
304
|
-
chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (
|
|
321
|
+
chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (agentName ? chalk3.dim(` [${agentName}]`) : "")
|
|
305
322
|
);
|
|
306
323
|
console.log();
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
const cleanup = () => {
|
|
325
|
+
if (agentDir && agentName) {
|
|
326
|
+
releaseAgentId(agentDir, agentName);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
process.on("exit", cleanup);
|
|
330
|
+
process.on("SIGINT", () => {
|
|
331
|
+
cleanup();
|
|
332
|
+
process.exit(130);
|
|
333
|
+
});
|
|
334
|
+
process.on("SIGTERM", () => {
|
|
335
|
+
cleanup();
|
|
336
|
+
process.exit(143);
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
await iterationLoop(loop, flowDir, options, agentName);
|
|
340
|
+
} finally {
|
|
341
|
+
cleanup();
|
|
311
342
|
}
|
|
312
343
|
}
|
|
313
|
-
async function
|
|
344
|
+
async function iterationLoop(loop, flowDir, options, agentName) {
|
|
314
345
|
for (let i = 1; i <= options.maxIterations; i++) {
|
|
315
|
-
|
|
316
|
-
|
|
346
|
+
const label = agentName ? chalk3.dim(` [${agentName}] Iteration ${i}/${options.maxIterations}`) : chalk3.dim(` Iteration ${i}/${options.maxIterations}`);
|
|
347
|
+
console.log(label);
|
|
348
|
+
const prompt = readPrompt(loop, flowDir, agentName);
|
|
317
349
|
const result = await spawnClaude({
|
|
318
350
|
prompt,
|
|
319
351
|
model: options.model,
|
|
320
|
-
printMode: false,
|
|
321
352
|
cwd: options.cwd
|
|
322
353
|
});
|
|
323
|
-
if (
|
|
354
|
+
if (checkTrackerForCompletion(flowDir, loop)) {
|
|
324
355
|
console.log();
|
|
325
356
|
console.log(chalk3.green(` Loop complete: ${loop.completion}`));
|
|
326
357
|
return;
|
|
@@ -330,64 +361,15 @@ async function runSingleAgent(loop, flowDir, options) {
|
|
|
330
361
|
console.log();
|
|
331
362
|
continue;
|
|
332
363
|
}
|
|
333
|
-
if (result.exitCode
|
|
334
|
-
console.log(chalk3.
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
console.log(chalk3.dim(` Iteration ${i} finished, continuing...`));
|
|
338
|
-
console.log();
|
|
339
|
-
}
|
|
340
|
-
console.log(chalk3.yellow(` Max iterations (${options.maxIterations}) reached.`));
|
|
341
|
-
}
|
|
342
|
-
async function runMultiAgent(loop, flowDir, options) {
|
|
343
|
-
const agentCount = options.agents;
|
|
344
|
-
let completed = false;
|
|
345
|
-
const agentRunners = Array.from({ length: agentCount }, (_, idx) => {
|
|
346
|
-
const agentNum = idx + 1;
|
|
347
|
-
const agentName = `agent-${agentNum}`;
|
|
348
|
-
const colorFn = AGENT_COLORS[idx % AGENT_COLORS.length];
|
|
349
|
-
return runAgentLoop(loop, flowDir, {
|
|
350
|
-
...options,
|
|
351
|
-
agentName
|
|
352
|
-
}, colorFn, () => completed, () => {
|
|
353
|
-
completed = true;
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
await Promise.allSettled(agentRunners);
|
|
357
|
-
console.log();
|
|
358
|
-
console.log(chalk3.green(` All agents finished.`));
|
|
359
|
-
}
|
|
360
|
-
async function runAgentLoop(loop, flowDir, options, colorFn, isCompleted, setCompleted) {
|
|
361
|
-
const agentName = options.agentName;
|
|
362
|
-
for (let i = 1; i <= options.maxIterations; i++) {
|
|
363
|
-
if (isCompleted()) {
|
|
364
|
-
console.log(colorFn(` [${agentName}] Stopping \u2014 completion detected.`));
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
console.log(colorFn(` [${agentName}] Iteration ${i}/${options.maxIterations}`));
|
|
368
|
-
const prompt = readPrompt(loop, flowDir, agentName);
|
|
369
|
-
const result = await spawnClaude({
|
|
370
|
-
prompt,
|
|
371
|
-
model: options.model,
|
|
372
|
-
printMode: true,
|
|
373
|
-
agentName,
|
|
374
|
-
cwd: options.cwd
|
|
375
|
-
});
|
|
376
|
-
if (result.output.includes(`<promise>${loop.completion}</promise>`)) {
|
|
377
|
-
console.log(colorFn(` [${agentName}] Loop complete: ${loop.completion}`));
|
|
378
|
-
setCompleted();
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (result.signal === "SIGINT" || result.exitCode === 130) {
|
|
382
|
-
console.log(colorFn(` [${agentName}] Iteration ${i} complete, restarting...`));
|
|
364
|
+
if (result.exitCode === 0 || result.exitCode === null) {
|
|
365
|
+
console.log(chalk3.dim(` Iteration ${i} finished, continuing...`));
|
|
366
|
+
console.log();
|
|
383
367
|
continue;
|
|
384
368
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
369
|
+
console.log(chalk3.red(` Claude exited with code ${result.exitCode}`));
|
|
370
|
+
return;
|
|
389
371
|
}
|
|
390
|
-
console.log(chalk3.yellow(`
|
|
372
|
+
console.log(chalk3.yellow(` Max iterations (${options.maxIterations}) reached.`));
|
|
391
373
|
}
|
|
392
374
|
function readPrompt(loop, flowDir, agentName) {
|
|
393
375
|
const promptPath = join4(flowDir, loop.prompt);
|
|
@@ -402,10 +384,10 @@ function readPrompt(loop, flowDir, agentName) {
|
|
|
402
384
|
}
|
|
403
385
|
|
|
404
386
|
// src/cli/run.ts
|
|
405
|
-
var runCommand = new Command2("run").description("Run a loop").argument("<loop>", "Loop to run (story, tasks, delivery, discovery, research, document)").option("-
|
|
387
|
+
var runCommand = new Command2("run").description("Run a loop").argument("<loop>", "Loop to run (story, tasks, delivery, discovery, research, document)").option("--multi-agent", "Run as a multi-agent instance (auto-assigns agent ID)").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) => {
|
|
406
388
|
try {
|
|
407
389
|
await runLoop(loop, {
|
|
408
|
-
|
|
390
|
+
multiAgent: !!opts.multiAgent,
|
|
409
391
|
model: opts.model,
|
|
410
392
|
maxIterations: parseInt(opts.maxIterations, 10),
|
|
411
393
|
flow: opts.flow,
|
|
@@ -425,7 +407,7 @@ import { Command as Command3 } from "commander";
|
|
|
425
407
|
import chalk6 from "chalk";
|
|
426
408
|
|
|
427
409
|
// src/core/status.ts
|
|
428
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
410
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
429
411
|
import { join as join5 } from "path";
|
|
430
412
|
import chalk5 from "chalk";
|
|
431
413
|
import Table from "cli-table3";
|
|
@@ -502,7 +484,7 @@ function parseTracker(trackerPath, flowDir, loopName) {
|
|
|
502
484
|
completed: 0,
|
|
503
485
|
total: 0
|
|
504
486
|
};
|
|
505
|
-
if (!
|
|
487
|
+
if (!existsSync5(fullPath)) {
|
|
506
488
|
return status;
|
|
507
489
|
}
|
|
508
490
|
const content = readFileSync4(fullPath, "utf-8");
|
package/package.json
CHANGED