ralph-cli-sandboxed 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -71,6 +71,20 @@ After running `ralph init`, you'll have:
71
71
  - **Rust** - `cargo check`, `cargo test`
72
72
  - **Custom** - Define your own commands
73
73
 
74
+ ### Supported CLI Providers
75
+
76
+ Ralph supports multiple AI CLI tools. Select your provider during `ralph init`:
77
+
78
+ | CLI | Status | Environment Variables | Notes |
79
+ |-----|--------|----------------------|-------|
80
+ | [Claude Code](https://github.com/anthropics/claude-code) | Working | `ANTHROPIC_API_KEY` | Default provider. Also supports ~/.claude OAuth credentials |
81
+ | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Working | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | |
82
+ | [OpenCode](https://github.com/anomalyco/opencode) | Working | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` | Requires [PR #9073](https://github.com/anomalyco/opencode/pull/9073) |
83
+ | [Aider](https://github.com/paul-gauthier/aider) | Untested | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | |
84
+ | [Codex CLI](https://github.com/openai/codex) | Untested | `OPENAI_API_KEY` | |
85
+ | [AMP](https://ampcode.com/) | Untested | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | |
86
+ | Custom | - | User-defined | Configure your own CLI |
87
+
74
88
  ### CLI Configuration
75
89
 
76
90
  Ralph can be configured to use different AI CLI tools. By default, it uses Claude Code. Configure in `.ralph/config.json`:
@@ -221,8 +235,8 @@ podman run -v $(pwd):/workspace -v /workspace/node_modules your-image
221
235
  ## Requirements
222
236
 
223
237
  - Node.js 18+
224
- - [Claude Code CLI](https://github.com/anthropics/claude-code) installed
225
- - Claude Pro/Max subscription or Anthropic API key
238
+ - A supported AI CLI tool installed (see [Supported CLI Providers](#supported-cli-providers))
239
+ - API key or subscription for your chosen provider
226
240
 
227
241
  ## License
228
242
 
@@ -29,7 +29,7 @@ function getCliProviderSnippet(cliProvider) {
29
29
  const provider = cliProvidersJson.providers[providerKey];
30
30
  if (!provider || !provider.docker) {
31
31
  // Default to Claude Code CLI if provider not found
32
- return "# Install Claude Code CLI\nRUN curl -fsSL https://claude.ai/install.sh | bash";
32
+ return "# Install Claude Code CLI (as node user so it installs to /home/node/.local/bin)\nRUN su - node -c 'curl -fsSL https://claude.ai/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> /home/node/.zshrc";
33
33
  }
34
34
  return provider.docker.install;
35
35
  }
@@ -291,9 +291,13 @@ async function buildImage(ralphDir) {
291
291
  process.exit(1);
292
292
  }
293
293
  console.log("Building Docker image...\n");
294
+ // Get image name for compose project name
295
+ const config = loadConfig();
296
+ const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
294
297
  return new Promise((resolve, reject) => {
295
298
  // Use --no-cache and --pull to ensure we always get the latest CLI versions
296
- const proc = spawn("docker", ["compose", "build", "--no-cache", "--pull"], {
299
+ // Use -p to set unique project name per ralph project
300
+ const proc = spawn("docker", ["compose", "-p", imageName, "build", "--no-cache", "--pull"], {
297
301
  cwd: dockerDir,
298
302
  stdio: "inherit",
299
303
  });
@@ -329,6 +333,27 @@ async function imageExists(imageName) {
329
333
  });
330
334
  });
331
335
  }
336
+ // Get CLI provider configuration
337
+ function getCliProviderConfig(cliProvider) {
338
+ const cliProvidersJson = getCliProvidersJson();
339
+ const providerKey = cliProvider || "claude";
340
+ const provider = cliProvidersJson.providers[providerKey];
341
+ if (!provider) {
342
+ // Default to Claude Code CLI if provider not found
343
+ return {
344
+ name: "Claude Code",
345
+ command: "claude",
346
+ yoloArgs: ["--dangerously-skip-permissions"],
347
+ envVars: ["ANTHROPIC_API_KEY"],
348
+ };
349
+ }
350
+ return {
351
+ name: provider.name,
352
+ command: provider.command,
353
+ yoloArgs: provider.yoloArgs || [],
354
+ envVars: provider.envVars || [],
355
+ };
356
+ }
332
357
  async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider) {
333
358
  const dockerDir = join(ralphDir, DOCKER_DIR);
334
359
  const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
@@ -346,9 +371,37 @@ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvi
346
371
  console.log("");
347
372
  }
348
373
  }
374
+ // Get CLI provider info for the startup note
375
+ const cliConfig = getCliProviderConfig(cliProvider);
376
+ const yoloCommand = cliConfig.yoloArgs.length > 0
377
+ ? `${cliConfig.command} ${cliConfig.yoloArgs.join(" ")}`
378
+ : cliConfig.command;
349
379
  console.log("Starting Docker container...\n");
380
+ // Show note about yolo mode and credentials
381
+ console.log("IMPORTANT: Getting Started");
382
+ console.log("-".repeat(40));
383
+ console.log("");
384
+ console.log("To run ralph automation, you might need to activate YOLO mode");
385
+ console.log("which allows the AI to execute commands without prompts.");
386
+ console.log("");
387
+ console.log(`CLI Provider: ${cliConfig.name}`);
388
+ console.log(`Yolo command: ${yoloCommand}`);
389
+ console.log("");
390
+ console.log("Before running 'ralph run' or 'ralph once', ensure your");
391
+ console.log("credentials are configured:");
392
+ console.log("");
393
+ if (cliConfig.envVars.length > 0) {
394
+ console.log("Required environment variables:");
395
+ for (const envVar of cliConfig.envVars) {
396
+ console.log(` - ${envVar}`);
397
+ }
398
+ }
399
+ console.log("");
400
+ console.log("Set them in docker-compose.yml or export before running.");
401
+ console.log("");
350
402
  return new Promise((resolve, reject) => {
351
- const proc = spawn("docker", ["compose", "run", "--rm", "ralph"], {
403
+ // Use -p to set unique project name per ralph project
404
+ const proc = spawn("docker", ["compose", "-p", imageName, "run", "--rm", "ralph"], {
352
405
  cwd: dockerDir,
353
406
  stdio: "inherit",
354
407
  });
@@ -371,8 +424,9 @@ async function cleanImage(imageName, ralphDir) {
371
424
  // First, stop any running containers via docker compose
372
425
  if (existsSync(join(dockerDir, "docker-compose.yml"))) {
373
426
  // Stop running containers first
427
+ // Use -p to target only this project's resources
374
428
  await new Promise((resolve) => {
375
- const proc = spawn("docker", ["compose", "stop", "--timeout", "5"], {
429
+ const proc = spawn("docker", ["compose", "-p", imageName, "stop", "--timeout", "5"], {
376
430
  cwd: dockerDir,
377
431
  stdio: "inherit",
378
432
  });
@@ -384,8 +438,9 @@ async function cleanImage(imageName, ralphDir) {
384
438
  });
385
439
  });
386
440
  // Remove containers, volumes, networks, and local images
441
+ // Use -p to target only this project's resources
387
442
  await new Promise((resolve) => {
388
- const proc = spawn("docker", ["compose", "down", "--rmi", "local", "-v", "--remove-orphans", "--timeout", "5"], {
443
+ const proc = spawn("docker", ["compose", "-p", imageName, "down", "--rmi", "local", "-v", "--remove-orphans", "--timeout", "5"], {
389
444
  cwd: dockerDir,
390
445
  stdio: "inherit",
391
446
  });
@@ -398,9 +453,10 @@ async function cleanImage(imageName, ralphDir) {
398
453
  });
399
454
  });
400
455
  }
401
- // Find and forcibly remove any containers using volumes with our image name pattern
456
+ // Find and forcibly remove any containers using volumes with our project name pattern
402
457
  // This handles orphaned containers from previous runs or pods
403
- const volumePattern = `docker_${imageName}`;
458
+ // Project name is now imageName (via -p flag), so volumes are named ${imageName}_*
459
+ const volumePattern = imageName;
404
460
  await new Promise((resolve) => {
405
461
  // List all containers (including stopped) and filter by volume name pattern
406
462
  const proc = spawn("docker", ["ps", "-aq", "--filter", `volume=${volumePattern}`], {
@@ -510,8 +566,8 @@ async function cleanImage(imageName, ralphDir) {
510
566
  resolve();
511
567
  });
512
568
  });
513
- // Clean up project-specific network (named after imageName, not generic docker_default)
514
- const networkName = `docker_${imageName}_default`;
569
+ // Clean up project-specific network (project name is imageName via -p flag)
570
+ const networkName = `${imageName}_default`;
515
571
  await new Promise((resolve) => {
516
572
  const proc = spawn("docker", ["network", "rm", networkName], {
517
573
  stdio: ["ignore", "ignore", "ignore"], // Suppress output - network may not exist
@@ -45,10 +45,13 @@ export async function init(_args) {
45
45
  const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
46
46
  const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
47
47
  const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
48
+ const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
49
+ const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
48
50
  cliConfig = {
49
51
  command: customCommand || "claude",
50
52
  args: customArgs,
51
53
  yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
54
+ promptArgs: customPromptArgs,
52
55
  };
53
56
  }
54
57
  else {
@@ -56,6 +59,7 @@ export async function init(_args) {
56
59
  command: selectedProvider.command,
57
60
  args: selectedProvider.defaultArgs,
58
61
  yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
62
+ promptArgs: selectedProvider.promptArgs ?? [],
59
63
  };
60
64
  }
61
65
  console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
@@ -1 +1 @@
1
- export declare function once(_args: string[]): Promise<void>;
1
+ export declare function once(args: string[]): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "child_process";
2
2
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
3
3
  import { resolvePromptVariables } from "../templates/prompts.js";
4
- export async function once(_args) {
4
+ export async function once(args) {
5
+ const debug = args.includes("--debug") || args.includes("-d");
5
6
  requireContainer("once");
6
7
  checkFilesExist();
7
8
  const config = loadConfig();
@@ -18,16 +19,20 @@ export async function once(_args) {
18
19
  // Build CLI arguments: config args + yolo args + prompt args
19
20
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
20
21
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
22
+ const promptArgs = cliConfig.promptArgs ?? ["-p"];
23
+ const promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
21
24
  const cliArgs = [
22
25
  ...(cliConfig.args ?? []),
23
26
  ...yoloArgs,
24
- "-p",
25
- `@${paths.prd} @${paths.progress} ${prompt}`,
27
+ ...promptArgs,
28
+ promptValue,
26
29
  ];
30
+ if (debug) {
31
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
32
+ }
27
33
  return new Promise((resolve, reject) => {
28
34
  const proc = spawn(cliConfig.command, cliArgs, {
29
35
  stdio: "inherit",
30
- shell: true,
31
36
  });
32
37
  proc.on("close", (code) => {
33
38
  if (code !== 0) {
@@ -216,10 +216,10 @@ export function parseListArgs(args) {
216
216
  process.exit(1);
217
217
  }
218
218
  }
219
- else if (args[i] === "--passes") {
219
+ else if (args[i] === "--passes" || args[i] === "--passed") {
220
220
  passesFilter = true;
221
221
  }
222
- else if (args[i] === "--no-passes") {
222
+ else if (args[i] === "--no-passes" || args[i] === "--no-passed" || args[i] === "--not-passed" || args[i] === "--not-passes") {
223
223
  passesFilter = false;
224
224
  }
225
225
  }
@@ -25,7 +25,7 @@ function createFilteredPrd(prdPath, category) {
25
25
  hasIncomplete: filteredItems.length > 0
26
26
  };
27
27
  }
28
- async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig) {
28
+ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug) {
29
29
  return new Promise((resolve, reject) => {
30
30
  let output = "";
31
31
  // Build CLI arguments: config args + yolo args + prompt args
@@ -39,7 +39,13 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
39
39
  cliArgs.push(...yoloArgs);
40
40
  }
41
41
  // Use the filtered PRD (only incomplete items) for the prompt
42
- cliArgs.push("-p", `@${filteredPrdPath} @${paths.progress} ${prompt}`);
42
+ // promptArgs specifies flags to use (e.g., ["-p"] for Claude, [] for positional)
43
+ const promptArgs = cliConfig.promptArgs ?? ["-p"];
44
+ const promptValue = `@${filteredPrdPath} @${paths.progress} ${prompt}`;
45
+ cliArgs.push(...promptArgs, promptValue);
46
+ if (debug) {
47
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
48
+ }
43
49
  const proc = spawn(cliConfig.command, cliArgs, {
44
50
  stdio: ["inherit", "pipe", "inherit"],
45
51
  });
@@ -104,6 +110,7 @@ export async function run(args) {
104
110
  let category;
105
111
  let loopMode = false;
106
112
  let allModeExplicit = false;
113
+ let debug = false;
107
114
  const filteredArgs = [];
108
115
  for (let i = 0; i < args.length; i++) {
109
116
  if (args[i] === "--category" || args[i] === "-c") {
@@ -123,6 +130,9 @@ export async function run(args) {
123
130
  else if (args[i] === "--all" || args[i] === "-a") {
124
131
  allModeExplicit = true;
125
132
  }
133
+ else if (args[i] === "--debug" || args[i] === "-d") {
134
+ debug = true;
135
+ }
126
136
  else {
127
137
  filteredArgs.push(args[i]);
128
138
  }
@@ -173,7 +183,10 @@ export async function run(args) {
173
183
  // Track temp file for cleanup
174
184
  let filteredPrdPath = null;
175
185
  const POLL_INTERVAL_MS = 30000; // 30 seconds between checks when waiting for new items
186
+ const MAX_CONSECUTIVE_FAILURES = 3; // Stop after this many consecutive failures
176
187
  const startTime = Date.now();
188
+ let consecutiveFailures = 0;
189
+ let lastExitCode = 0;
177
190
  try {
178
191
  for (let i = 1; i <= iterations; i++) {
179
192
  console.log(`\n${"=".repeat(50)}`);
@@ -246,7 +259,7 @@ export async function run(args) {
246
259
  break;
247
260
  }
248
261
  }
249
- const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig);
262
+ const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug);
250
263
  // Clean up temp file after each iteration
251
264
  try {
252
265
  unlinkSync(filteredPrdPath);
@@ -257,8 +270,27 @@ export async function run(args) {
257
270
  filteredPrdPath = null;
258
271
  if (exitCode !== 0) {
259
272
  console.error(`\n${cliConfig.command} exited with code ${exitCode}`);
273
+ // Track consecutive failures to detect persistent errors (e.g., missing API key)
274
+ if (exitCode === lastExitCode) {
275
+ consecutiveFailures++;
276
+ }
277
+ else {
278
+ consecutiveFailures = 1;
279
+ lastExitCode = exitCode;
280
+ }
281
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
282
+ console.error(`\nStopping: ${cliConfig.command} failed ${consecutiveFailures} times in a row with exit code ${exitCode}.`);
283
+ console.error("This usually indicates a configuration error (e.g., missing API key).");
284
+ console.error("Please check your CLI configuration and try again.");
285
+ break;
286
+ }
260
287
  console.log("Continuing to next iteration...");
261
288
  }
289
+ else {
290
+ // Reset failure tracking on success
291
+ consecutiveFailures = 0;
292
+ lastExitCode = 0;
293
+ }
262
294
  // Check for completion signal
263
295
  if (output.includes("<promise>COMPLETE</promise>")) {
264
296
  if (loopMode) {
@@ -6,8 +6,9 @@
6
6
  "command": "claude",
7
7
  "defaultArgs": ["--permission-mode", "acceptEdits"],
8
8
  "yoloArgs": ["--dangerously-skip-permissions"],
9
+ "promptArgs": ["-p"],
9
10
  "docker": {
10
- "install": "# Install Claude Code CLI\nRUN curl -fsSL https://claude.ai/install.sh | bash"
11
+ "install": "# Install Claude Code CLI (as node user so it installs to /home/node/.local/bin)\nRUN su - node -c 'curl -fsSL https://claude.ai/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> /home/node/.zshrc"
11
12
  },
12
13
  "envVars": ["ANTHROPIC_API_KEY"],
13
14
  "credentialMount": "~/.claude:/home/node/.claude"
@@ -18,6 +19,7 @@
18
19
  "command": "aider",
19
20
  "defaultArgs": ["--yes"],
20
21
  "yoloArgs": ["--yes-always"],
22
+ "promptArgs": ["--message"],
21
23
  "docker": {
22
24
  "install": "# Install Aider (requires Python)\nRUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* \\\n && pip3 install --break-system-packages --no-cache-dir aider-chat",
23
25
  "note": "Check 'aider --help' for available flags"
@@ -31,6 +33,7 @@
31
33
  "command": "codex",
32
34
  "defaultArgs": ["--approval-mode", "suggest"],
33
35
  "yoloArgs": ["--approval-mode", "full-auto"],
36
+ "promptArgs": [],
34
37
  "docker": {
35
38
  "install": "# Install OpenAI Codex CLI\nRUN npm install -g @openai/codex",
36
39
  "note": "Check 'codex --help' for available flags"
@@ -44,11 +47,12 @@
44
47
  "command": "gemini",
45
48
  "defaultArgs": [],
46
49
  "yoloArgs": ["-y"],
50
+ "promptArgs": [],
47
51
  "docker": {
48
52
  "install": "# Install Google Gemini CLI\nRUN npm install -g @google/gemini-cli",
49
53
  "note": "Check 'gemini --help' for available flags"
50
54
  },
51
- "envVars": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
55
+ "envVars": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
52
56
  "credentialMount": "~/.gemini:/home/node/.gemini"
53
57
  },
54
58
  "opencode": {
@@ -57,11 +61,12 @@
57
61
  "command": "opencode",
58
62
  "defaultArgs": [],
59
63
  "yoloArgs": ["--yolo"],
64
+ "promptArgs": ["run"],
60
65
  "docker": {
61
- "install": "# Install OpenCode\nRUN curl -fsSL https://opencode.ai/install | bash \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
66
+ "install": "# Install OpenCode (as node user)\nRUN su - node -c 'curl -fsSL https://opencode.ai/install | bash' \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
62
67
  "note": "Check 'opencode --help' for available flags"
63
68
  },
64
- "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY"],
69
+ "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
65
70
  "credentialMount": null
66
71
  },
67
72
  "amp": {
@@ -70,8 +75,9 @@
70
75
  "command": "amp",
71
76
  "defaultArgs": [],
72
77
  "yoloArgs": ["--yolo"],
78
+ "promptArgs": [],
73
79
  "docker": {
74
- "install": "# Install AMP CLI\nRUN curl -fsSL https://ampcode.com/install.sh | bash \\\n && echo 'export PATH=\"$HOME/.amp/bin:$PATH\"' >> /home/node/.zshrc",
80
+ "install": "# Install AMP CLI (as node user)\nRUN su - node -c 'curl -fsSL https://ampcode.com/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.amp/bin:$PATH\"' >> /home/node/.zshrc",
75
81
  "note": "Check 'amp --help' for available flags"
76
82
  },
77
83
  "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
@@ -83,6 +89,7 @@
83
89
  "command": "",
84
90
  "defaultArgs": [],
85
91
  "yoloArgs": [],
92
+ "promptArgs": [],
86
93
  "docker": {
87
94
  "install": "# Custom CLI - add your installation commands here"
88
95
  },
@@ -33,6 +33,7 @@ export interface CliProviderConfig {
33
33
  command: string;
34
34
  defaultArgs: string[];
35
35
  yoloArgs: string[];
36
+ promptArgs: string[];
36
37
  docker: {
37
38
  install: string;
38
39
  };
@@ -2,6 +2,7 @@ export interface CliConfig {
2
2
  command: string;
3
3
  args?: string[];
4
4
  yoloArgs?: string[];
5
+ promptArgs?: string[];
5
6
  }
6
7
  export interface RalphConfig {
7
8
  language: string;
@@ -3,9 +3,30 @@ import { join } from "path";
3
3
  export const DEFAULT_CLI_CONFIG = {
4
4
  command: "claude",
5
5
  args: ["--permission-mode", "acceptEdits"],
6
+ promptArgs: ["-p"],
6
7
  };
8
+ // Lazy import to avoid circular dependency
9
+ let _getCliProviders = null;
7
10
  export function getCliConfig(config) {
8
- return config.cli ?? DEFAULT_CLI_CONFIG;
11
+ const cliConfig = config.cli ?? DEFAULT_CLI_CONFIG;
12
+ // If promptArgs is already set, use it
13
+ if (cliConfig.promptArgs !== undefined) {
14
+ return cliConfig;
15
+ }
16
+ // Look up promptArgs from cliProvider if available
17
+ if (config.cliProvider) {
18
+ if (!_getCliProviders) {
19
+ // Dynamic import to avoid circular dependency
20
+ _getCliProviders = require("../templates/prompts.js").getCliProviders;
21
+ }
22
+ const providers = _getCliProviders();
23
+ const provider = providers[config.cliProvider];
24
+ if (provider?.promptArgs !== undefined) {
25
+ return { ...cliConfig, promptArgs: provider.promptArgs };
26
+ }
27
+ }
28
+ // Default to -p for backwards compatibility
29
+ return { ...cliConfig, promptArgs: ["-p"] };
9
30
  }
10
31
  const RALPH_DIR = ".ralph";
11
32
  const CONFIG_FILE = "config.json";
@@ -68,6 +68,7 @@ export async function promptSelectWithArrows(message, options) {
68
68
  if (process.stdin.isTTY) {
69
69
  process.stdin.setRawMode(false);
70
70
  }
71
+ process.stdin.pause();
71
72
  process.stdout.write("\x1B[?25h"); // Show cursor
72
73
  };
73
74
  process.stdin.on("data", onKeypress);
@@ -230,6 +231,7 @@ export async function promptMultiSelectWithArrows(message, options) {
230
231
  if (process.stdin.isTTY) {
231
232
  process.stdin.setRawMode(false);
232
233
  }
234
+ process.stdin.pause();
233
235
  process.stdout.write("\x1B[?25h"); // Show cursor
234
236
  };
235
237
  process.stdin.on("data", onKeypress);
@@ -0,0 +1,68 @@
1
+ # Run Command State Machine
2
+
3
+ ```mermaid
4
+ stateDiagram-v2
5
+ [*] --> ParseFlags : run(args)
6
+
7
+ ParseFlags --> DetermineMode
8
+
9
+ state DetermineMode {
10
+ [*] --> CheckLoopFlag
11
+ CheckLoopFlag --> LoopMode : --loop
12
+ CheckLoopFlag --> CheckIterationArg : no --loop
13
+ CheckIterationArg --> CountMode : number provided
14
+ CheckIterationArg --> AllMode : no number (default)
15
+ }
16
+
17
+ DetermineMode --> CheckItems
18
+
19
+ state "Check Items" as CheckItems {
20
+ [*] --> CreateFilteredPRD
21
+ CreateFilteredPRD --> HasIncomplete
22
+ HasIncomplete --> StartIteration : yes
23
+ HasIncomplete --> HandleComplete : no
24
+ }
25
+
26
+ state HandleComplete {
27
+ [*] --> CheckMode
28
+ CheckMode --> WaitForNewItems : LoopMode
29
+ CheckMode --> PrintComplete : AllMode/CountMode
30
+ WaitForNewItems --> PollLoop
31
+ PollLoop --> CheckNewItems : every 30s
32
+ CheckNewItems --> StartIteration : found
33
+ CheckNewItems --> PollLoop : not found
34
+ PrintComplete --> [*]
35
+ }
36
+
37
+ state "Start Iteration" as StartIteration
38
+
39
+ StartIteration --> RunCLI
40
+
41
+ state "Run CLI" as RunCLI {
42
+ [*] --> SpawnProcess
43
+ SpawnProcess --> WaitForExit
44
+ WaitForExit --> ProcessOutput
45
+ }
46
+
47
+ RunCLI --> CheckResult
48
+
49
+ state "Check Result" as CheckResult {
50
+ [*] --> CheckExitCode
51
+ CheckExitCode --> TrackFailure : non-zero
52
+ CheckExitCode --> ResetFailures : zero
53
+ TrackFailure --> CheckConsecutive
54
+ CheckConsecutive --> StopRun : >= 3 consecutive
55
+ CheckConsecutive --> CheckCompletionSignal : < 3
56
+ ResetFailures --> CheckCompletionSignal
57
+ CheckCompletionSignal --> HandleLoopComplete : COMPLETE signal
58
+ CheckCompletionSignal --> NextIteration : no signal
59
+ HandleLoopComplete --> WaitForNewItems : LoopMode
60
+ HandleLoopComplete --> PrintFinalStatus : AllMode/CountMode
61
+ StopRun --> [*]
62
+ PrintFinalStatus --> [*]
63
+ }
64
+
65
+ NextIteration --> CheckIterationLimit
66
+ CheckIterationLimit --> CheckItems : more iterations
67
+ CheckIterationLimit --> [*] : limit reached
68
+ ```
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ralph-cli-sandboxed",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "AI-driven development automation CLI for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ralph": "./dist/index.js"
7
+ "ralph": "dist/index.js"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "files": [
@@ -17,7 +17,8 @@
17
17
  "copy-config": "mkdir -p dist/config && cp src/config/*.json dist/config/",
18
18
  "dev": "npx tsx src/index.ts",
19
19
  "typecheck": "tsc --noEmit",
20
- "prepublishOnly": "npm run build"
20
+ "prepublishOnly": "npm run build",
21
+ "prepare": "npm run build"
21
22
  },
22
23
  "dependencies": {
23
24
  "readline": "^1.3.0"