fabis-ralph-loop 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.
@@ -0,0 +1,108 @@
1
+ import { consola } from "consola";
2
+ import { execa } from "execa";
3
+
4
+ //#region src/utils/docker.ts
5
+ async function isContainerRunning(containerName) {
6
+ try {
7
+ const { stdout } = await execa("docker", [
8
+ "inspect",
9
+ "-f",
10
+ "{{.State.Running}}",
11
+ containerName
12
+ ]);
13
+ return stdout.trim() === "true";
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/container/lifecycle.ts
21
+ const COMPOSE_FILE = ".ralph-container/docker-compose.yml";
22
+ const READY_TIMEOUT_MS = 300 * 1e3;
23
+ const POLL_INTERVAL_MS = 1e3;
24
+ async function startContainer(config, options = {}) {
25
+ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
26
+ consola.error("CLAUDE_CODE_OAUTH_TOKEN is not set.\nRun: export CLAUDE_CODE_OAUTH_TOKEN=$(claude auth token)");
27
+ process.exit(1);
28
+ }
29
+ if (config.setup.preStartCommand) {
30
+ consola.info(`Running pre-start command: ${config.setup.preStartCommand}`);
31
+ await execa("sh", ["-c", config.setup.preStartCommand], { stdio: "inherit" });
32
+ }
33
+ if (!await isContainerRunning(config.container.name)) {
34
+ consola.info("Starting Ralph container...");
35
+ await execa("docker", [
36
+ "compose",
37
+ "-f",
38
+ COMPOSE_FILE,
39
+ "up",
40
+ "-d",
41
+ "--build"
42
+ ], { stdio: "inherit" });
43
+ consola.info("Waiting for container initialization...");
44
+ await waitForReady(config.container.name);
45
+ } else consola.info("Ralph container already running.");
46
+ if (options.attach) {
47
+ consola.info("Attaching to container...");
48
+ const result = await execa("docker", [
49
+ "exec",
50
+ "-it",
51
+ config.container.name,
52
+ "bash"
53
+ ], {
54
+ stdio: "inherit",
55
+ reject: false
56
+ });
57
+ process.exit(result.exitCode ?? 0);
58
+ }
59
+ }
60
+ async function waitForReady(containerName) {
61
+ const startTime = Date.now();
62
+ while (Date.now() - startTime < READY_TIMEOUT_MS) try {
63
+ await execa("docker", [
64
+ "exec",
65
+ containerName,
66
+ "test",
67
+ "-f",
68
+ "/tmp/entrypoint-ready"
69
+ ]);
70
+ consola.success("Container ready.");
71
+ return;
72
+ } catch {
73
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
74
+ }
75
+ consola.warn("Container did not become ready within 5 minutes. Proceeding anyway.");
76
+ }
77
+ async function stopContainer() {
78
+ consola.info("Stopping Ralph container...");
79
+ await execa("docker", [
80
+ "compose",
81
+ "-f",
82
+ COMPOSE_FILE,
83
+ "down"
84
+ ], { stdio: "inherit" });
85
+ }
86
+ async function showLogs(containerName) {
87
+ await execa("docker", [
88
+ "logs",
89
+ containerName,
90
+ "--follow"
91
+ ], { stdio: "inherit" });
92
+ }
93
+ async function execInContainer(containerName, command) {
94
+ const result = await execa("docker", [
95
+ "exec",
96
+ "-it",
97
+ containerName,
98
+ ...command
99
+ ], {
100
+ stdio: "inherit",
101
+ reject: false
102
+ });
103
+ process.exit(result.exitCode ?? 0);
104
+ }
105
+
106
+ //#endregion
107
+ export { stopContainer as i, showLogs as n, startContainer as r, execInContainer as t };
108
+ //# sourceMappingURL=lifecycle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/utils/docker.ts","../src/container/lifecycle.ts"],"sourcesContent":["import { execa } from 'execa'\n\nexport async function isContainerRunning(containerName: string): Promise<boolean> {\n try {\n const { stdout } = await execa('docker', ['inspect', '-f', '{{.State.Running}}', containerName])\n return stdout.trim() === 'true'\n } catch {\n return false\n }\n}\n","import { execa } from 'execa'\nimport { consola } from 'consola'\nimport { isContainerRunning } from '../utils/docker.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\nconst COMPOSE_FILE = '.ralph-container/docker-compose.yml'\nconst READY_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes\nconst POLL_INTERVAL_MS = 1000\n\nexport async function startContainer(\n config: ResolvedConfig,\n options: { attach?: boolean } = {},\n): Promise<void> {\n // Validate token\n if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {\n consola.error(\n 'CLAUDE_CODE_OAUTH_TOKEN is not set.\\n' +\n 'Run: export CLAUDE_CODE_OAUTH_TOKEN=$(claude auth token)',\n )\n process.exit(1)\n }\n\n // Run pre-start command\n if (config.setup.preStartCommand) {\n consola.info(`Running pre-start command: ${config.setup.preStartCommand}`)\n await execa('sh', ['-c', config.setup.preStartCommand], { stdio: 'inherit' })\n }\n\n const running = await isContainerRunning(config.container.name)\n if (!running) {\n consola.info('Starting Ralph container...')\n await execa('docker', ['compose', '-f', COMPOSE_FILE, 'up', '-d', '--build'], {\n stdio: 'inherit',\n })\n\n consola.info('Waiting for container initialization...')\n await waitForReady(config.container.name)\n } else {\n consola.info('Ralph container already running.')\n }\n\n if (options.attach) {\n consola.info('Attaching to container...')\n const result = await execa('docker', ['exec', '-it', config.container.name, 'bash'], {\n stdio: 'inherit',\n reject: false,\n })\n process.exit(result.exitCode ?? 0)\n }\n}\n\nasync function waitForReady(containerName: string): Promise<void> {\n const startTime = Date.now()\n\n while (Date.now() - startTime < READY_TIMEOUT_MS) {\n try {\n await execa('docker', ['exec', containerName, 'test', '-f', '/tmp/entrypoint-ready'])\n consola.success('Container ready.')\n return\n } catch {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))\n }\n }\n\n consola.warn('Container did not become ready within 5 minutes. Proceeding anyway.')\n}\n\nexport async function stopContainer(): Promise<void> {\n consola.info('Stopping Ralph container...')\n await execa('docker', ['compose', '-f', COMPOSE_FILE, 'down'], { stdio: 'inherit' })\n}\n\nexport async function showLogs(containerName: string): Promise<void> {\n await execa('docker', ['logs', containerName, '--follow'], { stdio: 'inherit' })\n}\n\nexport async function execInContainer(containerName: string, command: string[]): Promise<void> {\n const result = await execa('docker', ['exec', '-it', containerName, ...command], {\n stdio: 'inherit',\n reject: false,\n })\n process.exit(result.exitCode ?? 0)\n}\n"],"mappings":";;;;AAEA,eAAsB,mBAAmB,eAAyC;AAChF,KAAI;EACF,MAAM,EAAE,WAAW,MAAM,MAAM,UAAU;GAAC;GAAW;GAAM;GAAsB;GAAc,CAAC;AAChG,SAAO,OAAO,MAAM,KAAK;SACnB;AACN,SAAO;;;;;;ACFX,MAAM,eAAe;AACrB,MAAM,mBAAmB,MAAS;AAClC,MAAM,mBAAmB;AAEzB,eAAsB,eACpB,QACA,UAAgC,EAAE,EACnB;AAEf,KAAI,CAAC,QAAQ,IAAI,yBAAyB;AACxC,UAAQ,MACN,gGAED;AACD,UAAQ,KAAK,EAAE;;AAIjB,KAAI,OAAO,MAAM,iBAAiB;AAChC,UAAQ,KAAK,8BAA8B,OAAO,MAAM,kBAAkB;AAC1E,QAAM,MAAM,MAAM,CAAC,MAAM,OAAO,MAAM,gBAAgB,EAAE,EAAE,OAAO,WAAW,CAAC;;AAI/E,KAAI,CADY,MAAM,mBAAmB,OAAO,UAAU,KAAK,EACjD;AACZ,UAAQ,KAAK,8BAA8B;AAC3C,QAAM,MAAM,UAAU;GAAC;GAAW;GAAM;GAAc;GAAM;GAAM;GAAU,EAAE,EAC5E,OAAO,WACR,CAAC;AAEF,UAAQ,KAAK,0CAA0C;AACvD,QAAM,aAAa,OAAO,UAAU,KAAK;OAEzC,SAAQ,KAAK,mCAAmC;AAGlD,KAAI,QAAQ,QAAQ;AAClB,UAAQ,KAAK,4BAA4B;EACzC,MAAM,SAAS,MAAM,MAAM,UAAU;GAAC;GAAQ;GAAO,OAAO,UAAU;GAAM;GAAO,EAAE;GACnF,OAAO;GACP,QAAQ;GACT,CAAC;AACF,UAAQ,KAAK,OAAO,YAAY,EAAE;;;AAItC,eAAe,aAAa,eAAsC;CAChE,MAAM,YAAY,KAAK,KAAK;AAE5B,QAAO,KAAK,KAAK,GAAG,YAAY,iBAC9B,KAAI;AACF,QAAM,MAAM,UAAU;GAAC;GAAQ;GAAe;GAAQ;GAAM;GAAwB,CAAC;AACrF,UAAQ,QAAQ,mBAAmB;AACnC;SACM;AACN,QAAM,IAAI,SAAS,YAAY,WAAW,SAAS,iBAAiB,CAAC;;AAIzE,SAAQ,KAAK,sEAAsE;;AAGrF,eAAsB,gBAA+B;AACnD,SAAQ,KAAK,8BAA8B;AAC3C,OAAM,MAAM,UAAU;EAAC;EAAW;EAAM;EAAc;EAAO,EAAE,EAAE,OAAO,WAAW,CAAC;;AAGtF,eAAsB,SAAS,eAAsC;AACnE,OAAM,MAAM,UAAU;EAAC;EAAQ;EAAe;EAAW,EAAE,EAAE,OAAO,WAAW,CAAC;;AAGlF,eAAsB,gBAAgB,eAAuB,SAAkC;CAC7F,MAAM,SAAS,MAAM,MAAM,UAAU;EAAC;EAAQ;EAAO;EAAe,GAAG;EAAQ,EAAE;EAC/E,OAAO;EACP,QAAQ;EACT,CAAC;AACF,SAAQ,KAAK,OAAO,YAAY,EAAE"}
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { loadConfig } from "c12";
3
+
4
+ //#region src/config/schema.ts
5
+ const backpressureCommandSchema = z.object({
6
+ name: z.string().min(1),
7
+ command: z.string().min(1)
8
+ });
9
+ const containerHooksSchema = z.object({
10
+ rootSetup: z.array(z.string()).default([]),
11
+ userSetup: z.array(z.string()).default([]),
12
+ entrypointSetup: z.array(z.string()).default([])
13
+ });
14
+ const containerSchema = z.object({
15
+ name: z.string().min(1),
16
+ baseImage: z.string().min(1).default("node:22-bookworm"),
17
+ user: z.string().min(1).default("sandbox"),
18
+ systemPackages: z.array(z.string()).default([]),
19
+ playwright: z.boolean().default(false),
20
+ networkMode: z.string().default("host"),
21
+ env: z.record(z.string(), z.string()).default({}),
22
+ shmSize: z.string().default("64m"),
23
+ capabilities: z.array(z.string()).default([]),
24
+ volumes: z.array(z.string()).default([]),
25
+ shadowVolumes: z.array(z.string()).default([]),
26
+ persistVolumes: z.record(z.string(), z.string()).default({ "ralph-claude-config": "/home/sandbox/.claude" }),
27
+ hooks: containerHooksSchema.prefault({})
28
+ });
29
+ const setupSchema = z.object({ preStartCommand: z.string().default("") });
30
+ const defaultsSchema = z.object({
31
+ agent: z.literal("claude").default("claude"),
32
+ model: z.string().default("sonnet"),
33
+ verbose: z.boolean().default(false),
34
+ sleepBetweenMs: z.number().int().min(0).default(2e3),
35
+ completionSignal: z.string().default("RALPH_WORK_FULLY_DONE")
36
+ });
37
+ const projectSchema = z.object({
38
+ name: z.string().min(1),
39
+ description: z.string().default(""),
40
+ context: z.string().default(""),
41
+ backpressureCommands: z.array(backpressureCommandSchema).default([]),
42
+ openAppSkill: z.string().default("")
43
+ });
44
+ const outputSchema = z.object({
45
+ mode: z.enum(["direct", "uac"]).default("direct"),
46
+ uacTemplatesDir: z.string().default(".universal-ai-config")
47
+ });
48
+ const ralphLoopConfigSchema = z.object({
49
+ container: containerSchema.prefault({ name: "ralph-container" }),
50
+ setup: setupSchema.prefault({}),
51
+ defaults: defaultsSchema.prefault({}),
52
+ project: projectSchema,
53
+ output: outputSchema.prefault({})
54
+ });
55
+
56
+ //#endregion
57
+ //#region src/config/defaults.ts
58
+ /**
59
+ * Apply Playwright-specific defaults when playwright is enabled.
60
+ * Merges SYS_ADMIN capability and 2gb shm_size if not already set.
61
+ */
62
+ function applyPlaywrightDefaults(config) {
63
+ if (!config.container.playwright) return config;
64
+ const shmSize = config.container.shmSize === "64m" ? "2gb" : config.container.shmSize;
65
+ const capabilities = config.container.capabilities.includes("SYS_ADMIN") ? config.container.capabilities : [...config.container.capabilities, "SYS_ADMIN"];
66
+ return {
67
+ ...config,
68
+ container: {
69
+ ...config.container,
70
+ shmSize,
71
+ capabilities
72
+ }
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/config/loader.ts
78
+ async function loadRalphConfig(cwd) {
79
+ const { config } = await loadConfig({
80
+ name: "fabis-ralph-loop",
81
+ cwd
82
+ });
83
+ if (!config || Object.keys(config).length === 0) throw new Error("No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.");
84
+ const parsed = ralphLoopConfigSchema.safeParse(config);
85
+ if (!parsed.success) {
86
+ const issues = parsed.error.issues.map((issue) => ` ${issue.path.join(".")}: ${issue.message}`).join("\n");
87
+ throw new Error(`Invalid fabis-ralph-loop config:\n${issues}`);
88
+ }
89
+ return applyPlaywrightDefaults(parsed.data);
90
+ }
91
+
92
+ //#endregion
93
+ export { ralphLoopConfigSchema as n, loadRalphConfig as t };
94
+ //# sourceMappingURL=loader.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.mjs","names":[],"sources":["../src/config/schema.ts","../src/config/defaults.ts","../src/config/loader.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst backpressureCommandSchema = z.object({\n name: z.string().min(1),\n command: z.string().min(1),\n})\n\nconst containerHooksSchema = z.object({\n rootSetup: z.array(z.string()).default([]),\n userSetup: z.array(z.string()).default([]),\n entrypointSetup: z.array(z.string()).default([]),\n})\n\nconst containerSchema = z.object({\n name: z.string().min(1),\n baseImage: z.string().min(1).default('node:22-bookworm'),\n user: z.string().min(1).default('sandbox'),\n systemPackages: z.array(z.string()).default([]),\n playwright: z.boolean().default(false),\n networkMode: z.string().default('host'),\n env: z.record(z.string(), z.string()).default({}),\n shmSize: z.string().default('64m'),\n capabilities: z.array(z.string()).default([]),\n volumes: z.array(z.string()).default([]),\n shadowVolumes: z.array(z.string()).default([]),\n persistVolumes: z\n .record(z.string(), z.string())\n .default({ 'ralph-claude-config': '/home/sandbox/.claude' }),\n hooks: containerHooksSchema.prefault({}),\n})\n\nconst setupSchema = z.object({\n preStartCommand: z.string().default(''),\n})\n\nconst defaultsSchema = z.object({\n agent: z.literal('claude').default('claude'),\n model: z.string().default('sonnet'),\n verbose: z.boolean().default(false),\n sleepBetweenMs: z.number().int().min(0).default(2000),\n completionSignal: z.string().default('RALPH_WORK_FULLY_DONE'),\n})\n\nconst projectSchema = z.object({\n name: z.string().min(1),\n description: z.string().default(''),\n context: z.string().default(''),\n backpressureCommands: z.array(backpressureCommandSchema).default([]),\n openAppSkill: z.string().default(''),\n})\n\nconst outputSchema = z.object({\n mode: z.enum(['direct', 'uac']).default('direct'),\n uacTemplatesDir: z.string().default('.universal-ai-config'),\n})\n\nexport const ralphLoopConfigSchema = z.object({\n container: containerSchema.prefault({ name: 'ralph-container' }),\n setup: setupSchema.prefault({}),\n defaults: defaultsSchema.prefault({}),\n project: projectSchema,\n output: outputSchema.prefault({}),\n})\n\nexport type RalphLoopConfig = z.input<typeof ralphLoopConfigSchema>\nexport type ResolvedConfig = z.output<typeof ralphLoopConfigSchema>\nexport type BackpressureCommand = z.infer<typeof backpressureCommandSchema>\n","import type { ResolvedConfig } from './schema.js'\n\n/**\n * Apply Playwright-specific defaults when playwright is enabled.\n * Merges SYS_ADMIN capability and 2gb shm_size if not already set.\n */\nexport function applyPlaywrightDefaults(config: ResolvedConfig): ResolvedConfig {\n if (!config.container.playwright) return config\n\n const shmSize = config.container.shmSize === '64m' ? '2gb' : config.container.shmSize\n\n const capabilities = config.container.capabilities.includes('SYS_ADMIN')\n ? config.container.capabilities\n : [...config.container.capabilities, 'SYS_ADMIN']\n\n return {\n ...config,\n container: {\n ...config.container,\n shmSize,\n capabilities,\n },\n }\n}\n","import { loadConfig } from 'c12'\nimport { ralphLoopConfigSchema } from './schema.js'\nimport { applyPlaywrightDefaults } from './defaults.js'\nimport type { RalphLoopConfig, ResolvedConfig } from './schema.js'\n\nexport async function loadRalphConfig(cwd?: string): Promise<ResolvedConfig> {\n const { config } = await loadConfig<RalphLoopConfig>({\n name: 'fabis-ralph-loop',\n cwd,\n })\n\n if (!config || Object.keys(config).length === 0) {\n throw new Error('No fabis-ralph-loop config found. Run `fabis-ralph-loop init` to create one.')\n }\n\n const parsed = ralphLoopConfigSchema.safeParse(config)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => ` ${issue.path.join('.')}: ${issue.message}`)\n .join('\\n')\n throw new Error(`Invalid fabis-ralph-loop config:\\n${issues}`)\n }\n\n return applyPlaywrightDefaults(parsed.data)\n}\n"],"mappings":"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"}
package/dist/logs.mjs ADDED
@@ -0,0 +1,18 @@
1
+ import { t as loadRalphConfig } from "./loader.mjs";
2
+ import { n as showLogs } from "./lifecycle.mjs";
3
+ import { defineCommand } from "citty";
4
+
5
+ //#region src/commands/logs.ts
6
+ var logs_default = defineCommand({
7
+ meta: {
8
+ name: "logs",
9
+ description: "Follow container logs"
10
+ },
11
+ async run() {
12
+ await showLogs((await loadRalphConfig()).container.name);
13
+ }
14
+ });
15
+
16
+ //#endregion
17
+ export { logs_default as default };
18
+ //# sourceMappingURL=logs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.mjs","names":[],"sources":["../src/commands/logs.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { showLogs } from '../container/lifecycle.js'\n\nexport default defineCommand({\n meta: {\n name: 'logs',\n description: 'Follow container logs',\n },\n async run() {\n const config = await loadRalphConfig()\n await showLogs(config.container.name)\n },\n})\n"],"mappings":";;;;;AAIA,mBAAe,cAAc;CAC3B,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,MAAM;AAEV,QAAM,UADS,MAAM,iBAAiB,EAChB,UAAU,KAAK;;CAExC,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { t as loadRalphConfig } from "./loader.mjs";
2
+ import { i as stopContainer, r as startContainer } from "./lifecycle.mjs";
3
+ import { defineCommand } from "citty";
4
+
5
+ //#region src/commands/restart.ts
6
+ var restart_default = defineCommand({
7
+ meta: {
8
+ name: "restart",
9
+ description: "Stop + start container"
10
+ },
11
+ args: { "no-attach": {
12
+ type: "boolean",
13
+ description: "Start container without attaching a shell",
14
+ default: false
15
+ } },
16
+ async run({ args }) {
17
+ await stopContainer();
18
+ await startContainer(await loadRalphConfig(), { attach: !args["no-attach"] });
19
+ }
20
+ });
21
+
22
+ //#endregion
23
+ export { restart_default as default };
24
+ //# sourceMappingURL=restart.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restart.mjs","names":[],"sources":["../src/commands/restart.ts"],"sourcesContent":["import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { stopContainer, startContainer } from '../container/lifecycle.js'\n\nexport default defineCommand({\n meta: {\n name: 'restart',\n description: 'Stop + start container',\n },\n args: {\n 'no-attach': {\n type: 'boolean',\n description: 'Start container without attaching a shell',\n default: false,\n },\n },\n async run({ args }) {\n await stopContainer()\n const config = await loadRalphConfig()\n await startContainer(config, { attach: !args['no-attach'] })\n },\n})\n"],"mappings":";;;;;AAIA,sBAAe,cAAc;CAC3B,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,EACJ,aAAa;EACX,MAAM;EACN,aAAa;EACb,SAAS;EACV,EACF;CACD,MAAM,IAAI,EAAE,QAAQ;AAClB,QAAM,eAAe;AAErB,QAAM,eADS,MAAM,iBAAiB,EACT,EAAE,QAAQ,CAAC,KAAK,cAAc,CAAC;;CAE/D,CAAC"}
package/dist/run.mjs ADDED
@@ -0,0 +1,331 @@
1
+ import { t as loadRalphConfig } from "./loader.mjs";
2
+ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { consola } from "consola";
5
+ import { existsSync } from "node:fs";
6
+ import { defineCommand } from "citty";
7
+ import { execa } from "execa";
8
+
9
+ //#region src/container/exec.ts
10
+ /**
11
+ * Execute a command directly as a child process.
12
+ * Returns stdout, stderr, and exit code. If onData/onStderr are provided,
13
+ * streams chunks in real-time. Supports AbortSignal for clean cancellation.
14
+ */
15
+ async function execAgent(options) {
16
+ const { command, args, input, onData, onStderr, signal } = options;
17
+ const proc = execa(command, args, {
18
+ input,
19
+ reject: false
20
+ });
21
+ if (onData && proc.stdout) proc.stdout.on("data", onData);
22
+ if (onStderr && proc.stderr) proc.stderr.on("data", onStderr);
23
+ let aborted = false;
24
+ if (signal) {
25
+ const onAbort = () => {
26
+ aborted = true;
27
+ proc.kill("SIGTERM");
28
+ };
29
+ if (signal.aborted) onAbort();
30
+ else signal.addEventListener("abort", onAbort, { once: true });
31
+ }
32
+ const result = await proc;
33
+ return {
34
+ stdout: result.stdout,
35
+ stderr: result.stderr,
36
+ exitCode: result.exitCode ?? 1,
37
+ aborted
38
+ };
39
+ }
40
+
41
+ //#endregion
42
+ //#region src/loop/progress.ts
43
+ /**
44
+ * Streaming progress parser for Claude stream-json output.
45
+ * Processes lines incrementally as they arrive, logging progress to stderr.
46
+ */
47
+ var StreamProgressParser = class {
48
+ turns = 0;
49
+ cost = null;
50
+ resultText = "";
51
+ buffer = "";
52
+ constructor(iteration) {
53
+ this.iteration = iteration;
54
+ }
55
+ /**
56
+ * Feed a raw chunk of data. Internally buffers and processes complete lines.
57
+ */
58
+ processChunk(chunk) {
59
+ this.buffer += chunk.toString();
60
+ const lines = this.buffer.split("\n");
61
+ this.buffer = lines.pop() ?? "";
62
+ for (const line of lines) if (line.trim()) this.processLine(line);
63
+ }
64
+ /**
65
+ * Flush any remaining buffered data. Call after the process exits.
66
+ */
67
+ flush() {
68
+ if (this.buffer.trim()) {
69
+ this.processLine(this.buffer);
70
+ this.buffer = "";
71
+ }
72
+ }
73
+ getResult() {
74
+ return {
75
+ output: this.resultText,
76
+ turns: this.turns,
77
+ cost: this.cost
78
+ };
79
+ }
80
+ processLine(line) {
81
+ let message;
82
+ try {
83
+ message = JSON.parse(line);
84
+ } catch {
85
+ return;
86
+ }
87
+ switch (message.type) {
88
+ case "system":
89
+ case "user": break;
90
+ case "assistant":
91
+ this.turns++;
92
+ consola.info(`--- Iteration ${this.iteration} | Turn ${this.turns} ---`);
93
+ if (message.message?.content) {
94
+ const toolDetails = message.message.content.filter((c) => c.type === "tool_use").map((c) => {
95
+ const name = c.name || "";
96
+ const input = c.input || {};
97
+ if ([
98
+ "Read",
99
+ "Write",
100
+ "Edit"
101
+ ].includes(name)) return `${name} ${(input.file_path || "").split("/").pop()}`;
102
+ if (name === "Glob") return `${name} ${input.pattern || ""}`;
103
+ if (name === "Grep") return `${name} ${input.pattern || ""}`;
104
+ if (name === "Bash") return `${name} ${(input.command || "").slice(0, 80)}`;
105
+ return name;
106
+ });
107
+ if (toolDetails.length > 0) consola.info(` ${toolDetails.join("\n ")}`);
108
+ const text = message.message.content.filter((c) => c.type === "text").map((c) => c.text || "").join("");
109
+ if (text) consola.info(text);
110
+ }
111
+ break;
112
+ case "result":
113
+ this.resultText = message.result || "";
114
+ this.cost = message.total_cost_usd ?? null;
115
+ consola.info("---");
116
+ consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? "?"}`);
117
+ break;
118
+ default:
119
+ if (message.type) consola.debug(`[debug] unrecognized type: ${message.type}`);
120
+ break;
121
+ }
122
+ }
123
+ };
124
+
125
+ //#endregion
126
+ //#region src/loop/archive.ts
127
+ const RALPH_DIR = ".ralph";
128
+ const PRD_FILE = join(RALPH_DIR, "prd.json");
129
+ const PROGRESS_FILE = join(RALPH_DIR, "progress.txt");
130
+ const LAST_BRANCH_FILE = join(RALPH_DIR, ".last-branch");
131
+ const ARCHIVE_DIR = join(RALPH_DIR, "archive");
132
+ async function archiveIfBranchChanged() {
133
+ if (!existsSync(PRD_FILE) || !existsSync(LAST_BRANCH_FILE)) {
134
+ await trackCurrentBranch();
135
+ return;
136
+ }
137
+ let currentBranch;
138
+ try {
139
+ currentBranch = JSON.parse(await readFile(PRD_FILE, "utf8")).branchName || "";
140
+ } catch {
141
+ return;
142
+ }
143
+ let lastBranch;
144
+ try {
145
+ lastBranch = (await readFile(LAST_BRANCH_FILE, "utf8")).trim();
146
+ } catch {
147
+ lastBranch = "";
148
+ }
149
+ if (!currentBranch || !lastBranch || currentBranch === lastBranch) {
150
+ await trackCurrentBranch();
151
+ return;
152
+ }
153
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
154
+ const archiveFolder = join(ARCHIVE_DIR, `${date}-${lastBranch.replace(/^ralph\//, "")}`);
155
+ consola.info(`Archiving previous run: ${lastBranch}`);
156
+ await mkdir(archiveFolder, { recursive: true });
157
+ if (existsSync(PRD_FILE)) await cp(PRD_FILE, join(archiveFolder, "prd.json"));
158
+ if (existsSync(PROGRESS_FILE)) await cp(PROGRESS_FILE, join(archiveFolder, "progress.txt"));
159
+ consola.info(`Archived to: ${archiveFolder}`);
160
+ await writeFile(PROGRESS_FILE, `# Ralph Progress Log\nStarted: ${(/* @__PURE__ */ new Date()).toISOString()}\n---\n`);
161
+ await trackCurrentBranch();
162
+ }
163
+ async function trackCurrentBranch() {
164
+ if (!existsSync(PRD_FILE)) return;
165
+ try {
166
+ const branch = JSON.parse(await readFile(PRD_FILE, "utf8")).branchName;
167
+ if (branch) {
168
+ await mkdir(RALPH_DIR, { recursive: true });
169
+ await writeFile(LAST_BRANCH_FILE, branch + "\n");
170
+ }
171
+ } catch {}
172
+ }
173
+ async function ensureProgressFile() {
174
+ await mkdir(RALPH_DIR, { recursive: true });
175
+ if (!existsSync(PROGRESS_FILE)) await writeFile(PROGRESS_FILE, `# Ralph Progress Log\nStarted: ${(/* @__PURE__ */ new Date()).toISOString()}\n---\n`);
176
+ }
177
+
178
+ //#endregion
179
+ //#region src/loop/runner.ts
180
+ function buildAgentArgs(agent, options) {
181
+ if (agent === "claude") {
182
+ const args = [
183
+ "--dangerously-skip-permissions",
184
+ "--model",
185
+ options.model,
186
+ "--print"
187
+ ];
188
+ if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
189
+ return args;
190
+ }
191
+ return [];
192
+ }
193
+ function sleep(ms, signal) {
194
+ return new Promise((resolve) => {
195
+ if (signal?.aborted) {
196
+ resolve();
197
+ return;
198
+ }
199
+ const timer = setTimeout(resolve, ms);
200
+ signal?.addEventListener("abort", () => {
201
+ clearTimeout(timer);
202
+ resolve();
203
+ }, { once: true });
204
+ });
205
+ }
206
+ async function runLoop(config, options) {
207
+ const model = options.model || config.defaults.model;
208
+ const verbose = options.verbose ?? config.defaults.verbose;
209
+ const { sleepBetweenMs, completionSignal } = config.defaults;
210
+ if (!existsSync("/.dockerenv")) consola.warn("It looks like you are running outside a Docker container. The loop is designed to run inside the Ralph container (use `ralph-loop start` to launch it).");
211
+ await ensureProgressFile();
212
+ const abortController = new AbortController();
213
+ const { signal } = abortController;
214
+ const handleSignal = () => {
215
+ consola.warn("\nInterrupted. Cleaning up...");
216
+ abortController.abort();
217
+ };
218
+ process.once("SIGINT", handleSignal);
219
+ process.once("SIGTERM", handleSignal);
220
+ consola.info([
221
+ `Starting Ralph`,
222
+ `Agent: ${config.defaults.agent}`,
223
+ `Model: ${model}`,
224
+ `Verbose: ${verbose}`,
225
+ `Iterations: ${options.iterations}`
226
+ ].join(" | "));
227
+ try {
228
+ for (let i = 1; i <= options.iterations; i++) {
229
+ if (signal.aborted) break;
230
+ consola.box(`Ralph Iteration ${i} of ${options.iterations} (${config.defaults.agent})`);
231
+ await archiveIfBranchChanged();
232
+ const promptContent = await readFile(".ralph-container/ralph-prompt.md", "utf8");
233
+ const agentArgs = buildAgentArgs(config.defaults.agent, {
234
+ model,
235
+ verbose
236
+ });
237
+ let finalOutput;
238
+ let turnsUsed = "?";
239
+ try {
240
+ if (verbose) {
241
+ const parser = new StreamProgressParser(i);
242
+ const result = await execAgent({
243
+ command: config.defaults.agent,
244
+ args: agentArgs,
245
+ input: promptContent,
246
+ onData: (chunk) => parser.processChunk(chunk),
247
+ onStderr: (chunk) => process.stderr.write(chunk),
248
+ signal
249
+ });
250
+ if (result.aborted) break;
251
+ parser.flush();
252
+ const progress = parser.getResult();
253
+ finalOutput = progress.output;
254
+ turnsUsed = String(progress.turns);
255
+ if (result.exitCode !== 0) consola.warn(`Agent exited with code ${result.exitCode}`);
256
+ } else {
257
+ const result = await execAgent({
258
+ command: config.defaults.agent,
259
+ args: agentArgs,
260
+ input: promptContent,
261
+ onData: (chunk) => process.stderr.write(chunk),
262
+ onStderr: (chunk) => process.stderr.write(chunk),
263
+ signal
264
+ });
265
+ if (result.aborted) break;
266
+ finalOutput = result.stdout;
267
+ }
268
+ } catch (error) {
269
+ consola.error(`Agent execution failed: ${error instanceof Error ? error.message : error}`);
270
+ finalOutput = "";
271
+ }
272
+ if (finalOutput.includes(completionSignal)) {
273
+ consola.success(`All stories complete! Completed at iteration ${i} of ${options.iterations}`);
274
+ return;
275
+ }
276
+ if (i < options.iterations) {
277
+ consola.info(`Iteration ${i} complete (${turnsUsed} turns). Sleeping ${sleepBetweenMs}ms...`);
278
+ await sleep(sleepBetweenMs, signal);
279
+ }
280
+ }
281
+ if (signal.aborted) {
282
+ consola.info("Stopped.");
283
+ process.exitCode = 130;
284
+ return;
285
+ }
286
+ consola.warn(`Reached max iterations (${options.iterations}) without completing all tasks.`);
287
+ consola.warn("Check .ralph/progress.txt for status.");
288
+ process.exitCode = 1;
289
+ } finally {
290
+ process.removeListener("SIGINT", handleSignal);
291
+ process.removeListener("SIGTERM", handleSignal);
292
+ }
293
+ }
294
+
295
+ //#endregion
296
+ //#region src/commands/run.ts
297
+ var run_default = defineCommand({
298
+ meta: {
299
+ name: "run",
300
+ description: "Execute the ralph iteration loop"
301
+ },
302
+ args: {
303
+ iterations: {
304
+ type: "positional",
305
+ description: "Number of iterations to run (required)",
306
+ required: true
307
+ },
308
+ model: {
309
+ type: "string",
310
+ description: "Override default model"
311
+ },
312
+ verbose: {
313
+ type: "boolean",
314
+ description: "Enable verbose stream-json progress output"
315
+ }
316
+ },
317
+ async run({ args }) {
318
+ const config = await loadRalphConfig();
319
+ const iterations = Number.parseInt(args.iterations, 10);
320
+ if (Number.isNaN(iterations) || iterations < 1) throw new Error("iterations must be a positive integer");
321
+ await runLoop(config, {
322
+ iterations,
323
+ model: args.model,
324
+ verbose: args.verbose
325
+ });
326
+ }
327
+ });
328
+
329
+ //#endregion
330
+ export { run_default as default };
331
+ //# sourceMappingURL=run.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.mjs","names":[],"sources":["../src/container/exec.ts","../src/loop/progress.ts","../src/loop/archive.ts","../src/loop/runner.ts","../src/commands/run.ts"],"sourcesContent":["import { execa } from 'execa'\n\ninterface AgentExecOptions {\n command: string\n args: string[]\n input?: string\n onData?: (chunk: Buffer) => void\n onStderr?: (chunk: Buffer) => void\n signal?: AbortSignal\n}\n\ninterface AgentExecResult {\n stdout: string\n stderr: string\n exitCode: number\n aborted: boolean\n}\n\n/**\n * Execute a command directly as a child process.\n * Returns stdout, stderr, and exit code. If onData/onStderr are provided,\n * streams chunks in real-time. Supports AbortSignal for clean cancellation.\n */\nexport async function execAgent(options: AgentExecOptions): Promise<AgentExecResult> {\n const { command, args, input, onData, onStderr, signal } = options\n\n const proc = execa(command, args, { input, reject: false })\n\n if (onData && proc.stdout) {\n proc.stdout.on('data', onData)\n }\n if (onStderr && proc.stderr) {\n proc.stderr.on('data', onStderr)\n }\n\n let aborted = false\n if (signal) {\n const onAbort = () => {\n aborted = true\n proc.kill('SIGTERM')\n }\n if (signal.aborted) {\n onAbort()\n } else {\n signal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n const result = await proc\n return {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 1,\n aborted,\n }\n}\n","import { consola } from 'consola'\n\ninterface StreamMessage {\n type: string\n message?: {\n content?: Array<{\n type: string\n name?: string\n text?: string\n input?: Record<string, unknown>\n }>\n }\n result?: string\n total_cost_usd?: number\n}\n\ninterface ProgressResult {\n output: string\n turns: number\n cost: number | null\n}\n\n/**\n * Streaming progress parser for Claude stream-json output.\n * Processes lines incrementally as they arrive, logging progress to stderr.\n */\nexport class StreamProgressParser {\n private turns = 0\n private cost: number | null = null\n private resultText = ''\n private buffer = ''\n\n constructor(private iteration: number) {}\n\n /**\n * Feed a raw chunk of data. Internally buffers and processes complete lines.\n */\n processChunk(chunk: Buffer | string): void {\n this.buffer += chunk.toString()\n const lines = this.buffer.split('\\n')\n this.buffer = lines.pop() ?? '' // keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.trim()) this.processLine(line)\n }\n }\n\n /**\n * Flush any remaining buffered data. Call after the process exits.\n */\n flush(): void {\n if (this.buffer.trim()) {\n this.processLine(this.buffer)\n this.buffer = ''\n }\n }\n\n getResult(): ProgressResult {\n return { output: this.resultText, turns: this.turns, cost: this.cost }\n }\n\n private processLine(line: string): void {\n let message: StreamMessage\n try {\n message = JSON.parse(line) as StreamMessage\n } catch {\n return\n }\n\n switch (message.type) {\n case 'system':\n case 'user':\n break\n\n case 'assistant': {\n this.turns++\n consola.info(`--- Iteration ${this.iteration} | Turn ${this.turns} ---`)\n\n if (message.message?.content) {\n const toolDetails = message.message.content\n .filter((c) => c.type === 'tool_use')\n .map((c) => {\n const name = c.name || ''\n const input = (c.input || {}) as Record<string, string>\n\n if (['Read', 'Write', 'Edit'].includes(name)) {\n const filePath = input.file_path || ''\n return `${name} ${filePath.split('/').pop()}`\n }\n if (name === 'Glob') return `${name} ${input.pattern || ''}`\n if (name === 'Grep') return `${name} ${input.pattern || ''}`\n if (name === 'Bash') return `${name} ${(input.command || '').slice(0, 80)}`\n return name\n })\n\n if (toolDetails.length > 0) {\n consola.info(` ${toolDetails.join('\\n ')}`)\n }\n\n const text = message.message.content\n .filter((c) => c.type === 'text')\n .map((c) => c.text || '')\n .join('')\n\n if (text) {\n consola.info(text)\n }\n }\n break\n }\n\n case 'result': {\n this.resultText = message.result || ''\n this.cost = message.total_cost_usd ?? null\n consola.info('---')\n consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? '?'}`)\n break\n }\n\n default: {\n if (message.type) {\n consola.debug(`[debug] unrecognized type: ${message.type}`)\n }\n break\n }\n }\n }\n}\n\n/**\n * Parse Claude stream-json output into human-readable progress.\n * Returns the final result text and metadata.\n */\nexport function parseStreamOutput(rawOutput: string, iteration: number): ProgressResult {\n const parser = new StreamProgressParser(iteration)\n parser.processChunk(rawOutput)\n parser.flush()\n return parser.getResult()\n}\n","import { readFile, writeFile, mkdir, cp } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\n\nconst RALPH_DIR = '.ralph'\nconst PRD_FILE = join(RALPH_DIR, 'prd.json')\nconst PROGRESS_FILE = join(RALPH_DIR, 'progress.txt')\nconst LAST_BRANCH_FILE = join(RALPH_DIR, '.last-branch')\nconst ARCHIVE_DIR = join(RALPH_DIR, 'archive')\n\nexport async function archiveIfBranchChanged(): Promise<void> {\n if (!existsSync(PRD_FILE) || !existsSync(LAST_BRANCH_FILE)) {\n await trackCurrentBranch()\n return\n }\n\n let currentBranch: string\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n currentBranch = prd.branchName || ''\n } catch {\n return\n }\n\n let lastBranch: string\n try {\n lastBranch = (await readFile(LAST_BRANCH_FILE, 'utf8')).trim()\n } catch {\n lastBranch = ''\n }\n\n if (!currentBranch || !lastBranch || currentBranch === lastBranch) {\n await trackCurrentBranch()\n return\n }\n\n // Archive the previous run\n const date = new Date().toISOString().split('T')[0]\n const folderName = lastBranch.replace(/^ralph\\//, '')\n const archiveFolder = join(ARCHIVE_DIR, `${date}-${folderName}`)\n\n consola.info(`Archiving previous run: ${lastBranch}`)\n await mkdir(archiveFolder, { recursive: true })\n\n if (existsSync(PRD_FILE)) {\n await cp(PRD_FILE, join(archiveFolder, 'prd.json'))\n }\n if (existsSync(PROGRESS_FILE)) {\n await cp(PROGRESS_FILE, join(archiveFolder, 'progress.txt'))\n }\n consola.info(`Archived to: ${archiveFolder}`)\n\n // Reset progress file\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n\n await trackCurrentBranch()\n}\n\nasync function trackCurrentBranch(): Promise<void> {\n if (!existsSync(PRD_FILE)) return\n\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n const branch = prd.branchName\n if (branch) {\n await mkdir(RALPH_DIR, { recursive: true })\n await writeFile(LAST_BRANCH_FILE, branch + '\\n')\n }\n } catch {\n // PRD doesn't exist or is invalid, skip\n }\n}\n\nexport async function ensureProgressFile(): Promise<void> {\n await mkdir(RALPH_DIR, { recursive: true })\n if (!existsSync(PROGRESS_FILE)) {\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n }\n}\n","import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { consola } from 'consola'\nimport { execAgent } from '../container/exec.js'\nimport { StreamProgressParser } from './progress.js'\nimport { archiveIfBranchChanged, ensureProgressFile } from './archive.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface RunOptions {\n iterations: number\n model?: string\n verbose?: boolean\n}\n\nfunction buildAgentArgs(agent: string, options: { model: string; verbose: boolean }): string[] {\n if (agent === 'claude') {\n const args = ['--dangerously-skip-permissions', '--model', options.model, '--print']\n if (options.verbose) {\n args.push('--verbose', '--output-format', 'stream-json')\n }\n return args\n }\n // Future: support other agents\n return []\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer)\n resolve()\n },\n { once: true },\n )\n })\n}\n\nexport async function runLoop(config: ResolvedConfig, options: RunOptions): Promise<void> {\n const model = options.model || config.defaults.model\n const verbose = options.verbose ?? config.defaults.verbose\n const { sleepBetweenMs, completionSignal } = config.defaults\n\n if (!existsSync('/.dockerenv')) {\n consola.warn(\n 'It looks like you are running outside a Docker container. The loop is designed to run inside the Ralph container (use `ralph-loop start` to launch it).',\n )\n }\n\n await ensureProgressFile()\n\n const abortController = new AbortController()\n const { signal } = abortController\n\n // First Ctrl+C: graceful cleanup (abort agent + container process).\n // Second Ctrl+C: force exit (process.once removes the handler after first call,\n // so the default Node.js SIGINT behavior kicks in).\n const handleSignal = () => {\n consola.warn('\\nInterrupted. Cleaning up...')\n abortController.abort()\n }\n process.once('SIGINT', handleSignal)\n process.once('SIGTERM', handleSignal)\n\n consola.info(\n [\n `Starting Ralph`,\n `Agent: ${config.defaults.agent}`,\n `Model: ${model}`,\n `Verbose: ${verbose}`,\n `Iterations: ${options.iterations}`,\n ].join(' | '),\n )\n\n try {\n for (let i = 1; i <= options.iterations; i++) {\n if (signal.aborted) break\n\n consola.box(`Ralph Iteration ${i} of ${options.iterations} (${config.defaults.agent})`)\n\n await archiveIfBranchChanged()\n\n const promptContent = await readFile('.ralph-container/ralph-prompt.md', 'utf8')\n const agentArgs = buildAgentArgs(config.defaults.agent, { model, verbose })\n\n let finalOutput: string\n let turnsUsed = '?'\n\n try {\n if (verbose) {\n // Verbose: parse stream-json lines in real-time, show progress on stderr\n const parser = new StreamProgressParser(i)\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => parser.processChunk(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n parser.flush()\n const progress = parser.getResult()\n finalOutput = progress.output\n turnsUsed = String(progress.turns)\n\n if (result.exitCode !== 0) {\n consola.warn(`Agent exited with code ${result.exitCode}`)\n }\n } else {\n // Non-verbose: stream both stdout and stderr to stderr in real-time\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => process.stderr.write(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n finalOutput = result.stdout\n }\n } catch (error) {\n consola.error(`Agent execution failed: ${error instanceof Error ? error.message : error}`)\n finalOutput = ''\n }\n\n if (finalOutput.includes(completionSignal)) {\n consola.success(\n `All stories complete! Completed at iteration ${i} of ${options.iterations}`,\n )\n return\n }\n\n if (i < options.iterations) {\n consola.info(\n `Iteration ${i} complete (${turnsUsed} turns). Sleeping ${sleepBetweenMs}ms...`,\n )\n await sleep(sleepBetweenMs, signal)\n }\n }\n\n if (signal.aborted) {\n consola.info('Stopped.')\n process.exitCode = 130\n return\n }\n\n consola.warn(`Reached max iterations (${options.iterations}) without completing all tasks.`)\n consola.warn('Check .ralph/progress.txt for status.')\n process.exitCode = 1\n } finally {\n process.removeListener('SIGINT', handleSignal)\n process.removeListener('SIGTERM', handleSignal)\n }\n}\n","import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { runLoop } from '../loop/runner.js'\n\nexport default defineCommand({\n meta: {\n name: 'run',\n description: 'Execute the ralph iteration loop',\n },\n args: {\n iterations: {\n type: 'positional',\n description: 'Number of iterations to run (required)',\n required: true,\n },\n model: {\n type: 'string',\n description: 'Override default model',\n },\n verbose: {\n type: 'boolean',\n description: 'Enable verbose stream-json progress output',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n const iterations = Number.parseInt(args.iterations as string, 10)\n\n if (Number.isNaN(iterations) || iterations < 1) {\n throw new Error('iterations must be a positive integer')\n }\n\n await runLoop(config, {\n iterations,\n model: args.model,\n verbose: args.verbose,\n })\n },\n})\n"],"mappings":"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"}