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.
Files changed (2) hide show
  1. package/dist/ralphflow.js +98 -116
  2. 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, printMode = false, agentName, cwd } = options;
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.push("--model", model);
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: printMode ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "inherit"],
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
- var AGENT_COLORS = [
290
- chalk3.cyan,
291
- chalk3.magenta,
292
- chalk3.yellow,
293
- chalk3.green,
294
- chalk3.blue,
295
- chalk3.red
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
- const isMultiAgent = options.agents > 1 && loop.multi_agent !== false;
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}`) + (isMultiAgent ? chalk3.dim(` (${options.agents} agents)`) : "")
321
+ chalk3.bold(` RalphFlow \u2014 ${loop.name}`) + (agentName ? chalk3.dim(` [${agentName}]`) : "")
305
322
  );
306
323
  console.log();
307
- if (isMultiAgent) {
308
- await runMultiAgent(loop, flowDir, options);
309
- } else {
310
- await runSingleAgent(loop, flowDir, options);
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 runSingleAgent(loop, flowDir, options) {
344
+ async function iterationLoop(loop, flowDir, options, agentName) {
314
345
  for (let i = 1; i <= options.maxIterations; i++) {
315
- console.log(chalk3.dim(` Iteration ${i}/${options.maxIterations}`));
316
- const prompt = readPrompt(loop, flowDir, options.agentName);
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 (result.output.includes(`<promise>${loop.completion}</promise>`)) {
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 !== 0 && result.exitCode !== null) {
334
- console.log(chalk3.red(` Claude exited with code ${result.exitCode}`));
335
- return;
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
- if (result.exitCode !== 0 && result.exitCode !== null) {
386
- console.log(chalk3.red(` [${agentName}] Claude exited with code ${result.exitCode}`));
387
- return;
388
- }
369
+ console.log(chalk3.red(` Claude exited with code ${result.exitCode}`));
370
+ return;
389
371
  }
390
- console.log(chalk3.yellow(` [${agentName}] Max iterations reached.`));
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("-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) => {
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
- agents: parseInt(opts.agents, 10),
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 existsSync4 } from "fs";
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 (!existsSync4(fullPath)) {
487
+ if (!existsSync5(fullPath)) {
506
488
  return status;
507
489
  }
508
490
  const content = readFileSync4(fullPath, "utf-8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphflow",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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": {