ralph-cli-sandboxed 0.2.6 → 0.2.8

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
@@ -83,6 +83,57 @@ After running `ralph init`, you'll have:
83
83
  └── ...
84
84
  ```
85
85
 
86
+ ### Notifications
87
+
88
+ Ralph can send notifications when events occur during automation. Configure a notification command in `.ralph/config.json`:
89
+
90
+ ```json
91
+ {
92
+ "notifyCommand": "ntfy pub mytopic"
93
+ }
94
+ ```
95
+
96
+ The message is appended as the last argument to your command. Supported notification tools include:
97
+
98
+ | Tool | Example Command | Description |
99
+ |------|----------------|-------------|
100
+ | [ntfy](https://ntfy.sh/) | `ntfy pub mytopic` | Push notifications to phone/desktop |
101
+ | notify-send (Linux) | `notify-send Ralph` | Desktop notifications on Linux |
102
+ | terminal-notifier (macOS) | `terminal-notifier -title Ralph -message` | Desktop notifications on macOS |
103
+ | Custom script | `/path/to/notify.sh` | Your own notification script |
104
+
105
+ #### Notification Events
106
+
107
+ Ralph sends notifications for these events:
108
+
109
+ | Event | Message | When |
110
+ |-------|---------|------|
111
+ | PRD Complete | "Ralph: PRD Complete! All tasks finished." | All PRD tasks are marked as passing |
112
+ | Iteration Complete | "Ralph: Iteration complete." | Single `ralph once` iteration finishes |
113
+ | Run Stopped | "Ralph: Run stopped..." | `ralph run` stops due to no progress or max failures |
114
+ | Error | "Ralph: An error occurred." | CLI fails repeatedly |
115
+
116
+ #### Example: ntfy Setup
117
+
118
+ [ntfy](https://ntfy.sh/) is a simple HTTP-based pub-sub notification service:
119
+
120
+ ```bash
121
+ # 1. Install ntfy CLI
122
+ pip install ntfy
123
+
124
+ # 2. Subscribe to your topic on your phone (ntfy app) or browser
125
+ # Visit: https://ntfy.sh/your-unique-topic
126
+
127
+ # 3. Configure ralph
128
+ # In .ralph/config.json:
129
+ {
130
+ "notifyCommand": "ntfy pub your-unique-topic"
131
+ }
132
+
133
+ # 4. Run ralph - you'll get notifications on completion
134
+ ralph docker run
135
+ ```
136
+
86
137
  ### Supported Languages
87
138
 
88
139
  Ralph supports 18 programming languages with pre-configured build/test commands:
@@ -116,8 +167,9 @@ Ralph supports multiple AI CLI tools. Select your provider during `ralph init`:
116
167
  |-----|--------|----------------------|-------|
117
168
  | [Claude Code](https://github.com/anthropics/claude-code) | Working | `ANTHROPIC_API_KEY` | Default provider. Also supports ~/.claude OAuth credentials |
118
169
  | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Working | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | |
119
- | [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) |
170
+ | [OpenCode](https://github.com/anomalyco/opencode) | Working | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` | No autonomous/yolo mode yet. Requires [PR #9073](https://github.com/anomalyco/opencode/pull/9073) |
120
171
  | [Aider](https://github.com/paul-gauthier/aider) | Working | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | |
172
+ | [Goose](https://github.com/block/goose) | Working | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | Block's AI coding agent |
121
173
  | [Codex CLI](https://github.com/openai/codex) | Testers wanted | `OPENAI_API_KEY` | Sponsors welcome |
122
174
  | [AMP](https://ampcode.com/) | Testers wanted | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | Sponsors welcome |
123
175
  | Custom | - | User-defined | Configure your own CLI |
@@ -130,7 +182,7 @@ Ralph can be configured to use different AI CLI tools. By default, it uses Claud
130
182
  {
131
183
  "cli": {
132
184
  "command": "claude",
133
- "args": ["--permission-mode", "acceptEdits"],
185
+ "args": [],
134
186
  "modelArgs": ["--model"],
135
187
  "promptArgs": ["-p"]
136
188
  }
@@ -144,6 +196,102 @@ Ralph can be configured to use different AI CLI tools. By default, it uses Claud
144
196
 
145
197
  The prompt content and `--dangerously-skip-permissions` (in containers) are added automatically at runtime.
146
198
 
199
+ ### Stream-JSON Output
200
+
201
+ Ralph supports stream-json output mode for real-time streaming of AI responses. This feature provides cleaner terminal output and enables recording of raw JSON logs for debugging or replay.
202
+
203
+ #### Enabling Stream-JSON
204
+
205
+ Configure stream-json in `.ralph/config.json`:
206
+
207
+ ```json
208
+ {
209
+ "docker": {
210
+ "asciinema": {
211
+ "enabled": true,
212
+ "autoRecord": true,
213
+ "outputDir": ".recordings",
214
+ "streamJson": {
215
+ "enabled": true,
216
+ "saveRawJson": true
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ Configuration options:
224
+ - `enabled`: Enable stream-json output mode (default: `false`)
225
+ - `saveRawJson`: Save raw JSON output to `.jsonl` files (default: `true` when enabled)
226
+ - `outputDir`: Directory for recordings and logs (default: `.recordings`)
227
+
228
+ #### Provider Compatibility
229
+
230
+ Not all CLI providers support stream-json output. Here's the compatibility matrix:
231
+
232
+ | CLI Provider | Stream-JSON Support | Arguments Used |
233
+ |--------------|---------------------|----------------|
234
+ | Claude Code | ✅ Yes | `--output-format stream-json --verbose --print` |
235
+ | Gemini CLI | ✅ Yes | `--output-format json` |
236
+ | OpenCode | ✅ Yes | `--format json` |
237
+ | Codex CLI | ✅ Yes | `--json` |
238
+ | Goose | ✅ Yes | `--output-format stream-json` |
239
+ | Aider | ❌ No | - |
240
+ | AMP | ❌ No | - |
241
+ | Custom | ❌ No* | *Add `streamJsonArgs` to your custom config |
242
+
243
+ Each provider uses different command-line arguments and output formats. Ralph automatically selects the correct parser based on your configured provider.
244
+
245
+ #### Output Files
246
+
247
+ When stream-json is enabled, Ralph creates the following files in the `.recordings/` directory (configurable via `outputDir`):
248
+
249
+ | File Type | Pattern | Description |
250
+ |-----------|---------|-------------|
251
+ | `.jsonl` | `ralph-run-YYYYMMDD-HHMMSS.jsonl` | Raw JSON Lines log from `ralph run` |
252
+ | `.jsonl` | `ralph-once-YYYYMMDD-HHMMSS.jsonl` | Raw JSON Lines log from `ralph once` |
253
+ | `.cast` | `session-YYYYMMDD-HHMMSS.cast` | Asciinema terminal recording (when asciinema enabled) |
254
+
255
+ The `.jsonl` files contain one JSON object per line with the raw streaming events from the AI provider. These files are useful for:
256
+ - Debugging AI responses
257
+ - Replaying sessions
258
+ - Analyzing tool calls and outputs
259
+ - Building custom post-processing pipelines
260
+
261
+ #### Troubleshooting Stream-JSON
262
+
263
+ **Stream-JSON not working:**
264
+ 1. Verify your CLI provider supports stream-json (see compatibility matrix above)
265
+ 2. Check that `streamJson.enabled` is set to `true` in config
266
+ 3. Ensure your CLI provider is correctly installed and accessible
267
+
268
+ **No output appearing:**
269
+ - Stream-json parsing extracts human-readable text from JSON events
270
+ - Some providers emit different event types; Ralph handles the most common ones
271
+ - Use `--debug` flag with ralph commands to see raw parsing output: `[stream-json]` prefixed lines go to stderr
272
+
273
+ **Missing .jsonl files:**
274
+ - Verify `saveRawJson` is `true` (or not set, as it defaults to `true`)
275
+ - Check that the `outputDir` directory is writable
276
+ - Files are created at command start; check for permission errors
277
+
278
+ **Parser not recognizing events:**
279
+ - Each provider has a specific parser (ClaudeStreamParser, GeminiStreamParser, etc.)
280
+ - Unknown event types are handled by a default parser that extracts common fields
281
+ - If you see raw JSON in output, the parser may not support that event type yet
282
+
283
+ **Custom CLI provider:**
284
+ To add stream-json support for a custom CLI provider, add `streamJsonArgs` to your CLI config:
285
+ ```json
286
+ {
287
+ "cli": {
288
+ "command": "my-cli",
289
+ "promptArgs": ["-p"],
290
+ "streamJsonArgs": ["--json-output"]
291
+ }
292
+ }
293
+ ```
294
+
147
295
  ## PRD Format
148
296
 
149
297
  The PRD (`prd.json`) is an array of requirements:
@@ -181,11 +329,45 @@ File paths are resolved relative to the project root. Absolute paths are also su
181
329
 
182
330
  Ralph includes automatic PRD protection to handle cases where the LLM corrupts the PRD structure:
183
331
 
184
- - **Automatic backup**: Before each run, the PRD is backed up
332
+ - **Automatic backup**: Before each run, the PRD is backed up to `.ralph/backups/`
185
333
  - **Validation**: After each iteration, the PRD structure is validated
186
334
  - **Smart recovery**: If corrupted, ralph attempts to extract `passes: true` flags from the corrupted PRD and merge them into the backup
187
335
  - **Manual recovery**: Use `ralph fix-prd` to validate, auto-fix, or restore from a specific backup
188
336
 
337
+ ### When PRD Corruption Happens
338
+
339
+ LLMs sometimes modify the PRD file incorrectly, such as:
340
+ - Converting the array to an object
341
+ - Adding invalid JSON syntax
342
+ - Changing the structure entirely
343
+
344
+ If you see an error like:
345
+ ```
346
+ Error: prd.json is corrupted - expected an array of items.
347
+ The file may have been modified incorrectly by an LLM.
348
+
349
+ Run ralph fix-prd to diagnose and repair the file.
350
+ ```
351
+
352
+ ### Using fix-prd
353
+
354
+ The `ralph fix-prd` command diagnoses and repairs corrupted PRD files:
355
+
356
+ ```bash
357
+ ralph fix-prd # Auto-diagnose and fix
358
+ ralph fix-prd --verify # Check structure without modifying
359
+ ralph fix-prd backup.json # Restore from a specific backup file
360
+ ```
361
+
362
+ **What fix-prd does:**
363
+ 1. Validates JSON syntax and structure
364
+ 2. Checks that all required fields exist (category, description, steps, passes)
365
+ 3. Attempts to recover `passes: true` flags from corrupted files
366
+ 4. Falls back to the most recent backup if recovery fails
367
+ 5. Creates a fresh template PRD as a last resort
368
+
369
+ **Backups are stored in:** `.ralph/backups/backup.prd.YYYY-MM-DD-HHMMSS.json`
370
+
189
371
  ### Dynamic Iteration Limits
190
372
 
191
373
  To prevent runaway loops, `ralph run` limits iterations to `incomplete_tasks + 3`. This limit adjusts dynamically if new tasks are added during execution.
@@ -113,6 +113,7 @@ RUN ${gitCommands.join(' \\\n && ')}
113
113
  // Build asciinema installation section if enabled
114
114
  let asciinemaInstall = '';
115
115
  let asciinemaDir = '';
116
+ let streamScriptCopy = '';
116
117
  if (dockerConfig?.asciinema?.enabled) {
117
118
  const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
118
119
  asciinemaInstall = `
@@ -123,6 +124,14 @@ RUN apt-get update && apt-get install -y asciinema && rm -rf /var/lib/apt/lists/
123
124
  # Create asciinema recordings directory
124
125
  RUN mkdir -p /workspace/${outputDir} && chown node:node /workspace/${outputDir}
125
126
  `;
127
+ // Add stream script if streamJson is enabled
128
+ if (dockerConfig.asciinema.streamJson?.enabled) {
129
+ streamScriptCopy = `
130
+ # Copy ralph stream wrapper script for clean JSON output
131
+ COPY ralph-stream.sh /usr/local/bin/ralph-stream.sh
132
+ RUN chmod +x /usr/local/bin/ralph-stream.sh
133
+ `;
134
+ }
126
135
  }
127
136
  return `# Ralph CLI Sandbox Environment
128
137
  # Based on Claude Code devcontainer
@@ -218,7 +227,7 @@ ENV EDITOR=nano
218
227
  # Add bash aliases and prompt (fallback if using bash)
219
228
  RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \\
220
229
  echo 'PS1="\\[\\033[43;30m\\][ralph]\\w\\[\\033[0m\\]\\$ "' >> /etc/bash.bashrc
221
- ${rootBuildCommands}${asciinemaInstall}
230
+ ${rootBuildCommands}${asciinemaInstall}${streamScriptCopy}
222
231
  # Switch to non-root user
223
232
  USER node
224
233
  ${gitConfigSection}${nodeBuildCommands}
@@ -364,11 +373,21 @@ function generateDockerCompose(imageName, dockerConfig) {
364
373
  }
365
374
  // Build command section if configured
366
375
  let commandSection = '';
376
+ let streamJsonNote = '';
367
377
  if (dockerConfig?.asciinema?.enabled && dockerConfig?.asciinema?.autoRecord) {
368
378
  // Wrap with asciinema recording
369
379
  const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
370
380
  const innerCommand = dockerConfig.startCommand || 'zsh';
371
381
  commandSection = ` command: bash -c "mkdir -p /workspace/${outputDir} && asciinema rec -c '${innerCommand}' /workspace/${outputDir}/session-$$(date +%Y%m%d-%H%M%S).cast"\n`;
382
+ // Add note about stream-json if enabled
383
+ if (dockerConfig.asciinema.streamJson?.enabled) {
384
+ streamJsonNote = `
385
+ # Stream JSON mode enabled - use ralph-stream.sh for clean Claude output:
386
+ # ralph-stream.sh -p "your prompt here"
387
+ # This formats stream-json output for readable terminal display.
388
+ # Raw JSON is saved to ${outputDir}/session-*.jsonl for later analysis.
389
+ `;
390
+ }
372
391
  }
373
392
  else if (dockerConfig?.startCommand) {
374
393
  commandSection = ` command: ${dockerConfig.startCommand}\n`;
@@ -394,7 +413,7 @@ ${environmentSection} working_dir: /workspace
394
413
  tty: true
395
414
  cap_add:
396
415
  - NET_ADMIN # Required for firewall
397
- ${commandSection}
416
+ ${streamJsonNote}${commandSection}
398
417
  volumes:
399
418
  ${imageName}-history:
400
419
  `;
@@ -405,6 +424,82 @@ dist
405
424
  .git
406
425
  *.log
407
426
  `;
427
+ // Generate stream wrapper script for clean asciinema recordings
428
+ function generateStreamScript(outputDir, saveRawJson) {
429
+ const saveJsonSection = saveRawJson ? `
430
+ # Save raw JSON for later analysis
431
+ JSON_LOG="$OUTPUT_DIR/session-$TIMESTAMP.jsonl"
432
+ TEE_CMD="tee \\"$JSON_LOG\\""` : `
433
+ TEE_CMD="cat"`;
434
+ return `#!/bin/bash
435
+ # Ralph stream wrapper - formats Claude stream-json output for clean terminal display
436
+ # Generated by ralph-cli
437
+
438
+ set -e
439
+
440
+ OUTPUT_DIR="\${RALPH_RECORDING_DIR:-/workspace/${outputDir}}"
441
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
442
+
443
+ # Ensure output directory exists
444
+ mkdir -p "$OUTPUT_DIR"
445
+ ${saveJsonSection}
446
+
447
+ # jq filter to extract and format content from stream-json
448
+ # Handles text, tool calls, tool results, file operations, and commands
449
+ JQ_FILTER='
450
+ if .type == "content_block_delta" then
451
+ (if .delta.type == "text_delta" then .delta.text // empty
452
+ elif .delta.text then .delta.text
453
+ else empty end)
454
+ elif .type == "content_block_start" then
455
+ (if .content_block.type == "tool_use" then "\\n── Tool: " + (.content_block.name // "unknown") + " ──\\n"
456
+ elif .content_block.type == "text" then .content_block.text // empty
457
+ else empty end)
458
+ elif .type == "tool_result" then
459
+ "\\n── Tool Result ──\\n" + ((.content // .output // "") | tostring) + "\\n"
460
+ elif .type == "assistant" then
461
+ ([.message.content[]? | select(.type == "text") | .text] | join(""))
462
+ elif .type == "message_start" then
463
+ "\\n"
464
+ elif .type == "message_delta" then
465
+ (if .delta.stop_reason then "\\n[" + .delta.stop_reason + "]\\n" else empty end)
466
+ elif .type == "file_edit" or .type == "file_write" then
467
+ "\\n── Writing: " + (.path // .file // "unknown") + " ──\\n"
468
+ elif .type == "file_read" then
469
+ "── Reading: " + (.path // .file // "unknown") + " ──\\n"
470
+ elif .type == "bash" or .type == "command" then
471
+ "\\n── Running: " + (.command // .content // "") + " ──\\n"
472
+ elif .type == "bash_output" or .type == "command_output" then
473
+ (.output // .content // "") + "\\n"
474
+ elif .type == "result" then
475
+ (if .result then "\\n── Result ──\\n" + (.result | tostring) + "\\n" else empty end)
476
+ elif .type == "error" then
477
+ "\\n[Error] " + (.error.message // (.error | tostring)) + "\\n"
478
+ elif .type == "system" then
479
+ (if .message then "[System] " + .message + "\\n" else empty end)
480
+ elif .text then
481
+ .text
482
+ elif (.content | type) == "string" then
483
+ .content
484
+ else
485
+ empty
486
+ end
487
+ '
488
+
489
+ # Pass all arguments to claude with stream-json output
490
+ # Filter JSON lines, optionally save raw JSON, and display formatted text
491
+ claude \\
492
+ --output-format stream-json \\
493
+ --verbose \\
494
+ --print \\
495
+ "\$@" 2>&1 \\
496
+ | grep --line-buffered '^{' \\
497
+ | eval $TEE_CMD \\
498
+ | jq --unbuffered -rj "$JQ_FILTER"
499
+
500
+ echo "" # Ensure final newline
501
+ `;
502
+ }
408
503
  // Generate .mcp.json content for Claude Code MCP servers
409
504
  function generateMcpJson(mcpServers) {
410
505
  return JSON.stringify({ mcpServers }, null, 2);
@@ -432,6 +527,12 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
432
527
  { name: "docker-compose.yml", content: generateDockerCompose(imageName, dockerConfig) },
433
528
  { name: ".dockerignore", content: DOCKERIGNORE },
434
529
  ];
530
+ // Add stream script if streamJson is enabled
531
+ if (dockerConfig?.asciinema?.enabled && dockerConfig.asciinema.streamJson?.enabled) {
532
+ const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
533
+ const saveRawJson = dockerConfig.asciinema.streamJson.saveRawJson !== false; // default true
534
+ files.push({ name: "ralph-stream.sh", content: generateStreamScript(outputDir, saveRawJson) });
535
+ }
435
536
  for (const file of files) {
436
537
  const filePath = join(dockerDir, file.name);
437
538
  if (existsSync(filePath) && !force) {
@@ -93,7 +93,7 @@ CLI CONFIGURATION:
93
93
  {
94
94
  "cli": {
95
95
  "command": "claude",
96
- "args": ["--permission-mode", "acceptEdits"],
96
+ "args": [],
97
97
  "yoloArgs": ["--dangerously-skip-permissions"]
98
98
  },
99
99
  "cliProvider": "claude"
@@ -104,6 +104,7 @@ CLI CONFIGURATION:
104
104
  - aider: AI pair programming
105
105
  - codex: OpenAI Codex CLI
106
106
  - gemini: Google Gemini CLI
107
+ - goose: Block's Goose AI coding agent
107
108
  - opencode: Open source AI coding agent
108
109
  - amp: Sourcegraph AMP CLI
109
110
  - custom: Configure your own CLI
@@ -1,7 +1,7 @@
1
1
  import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
2
2
  import { join, basename, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders } from "../templates/prompts.js";
4
+ import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage } from "../templates/prompts.js";
5
5
  import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
6
6
  import { dockerInit } from "./docker.js";
7
7
  // Get package root directory (works for both dev and installed package)
@@ -39,6 +39,7 @@ export async function init(args) {
39
39
  let cliConfig;
40
40
  let selectedKey;
41
41
  let selectedTechnologies = [];
42
+ let selectedSkills = [];
42
43
  let checkCommand;
43
44
  let testCommand;
44
45
  if (useDefaults) {
@@ -115,6 +116,29 @@ export async function init(args) {
115
116
  console.log("\nNo technologies selected.");
116
117
  }
117
118
  }
119
+ // Step 4: Select skills if available for this language
120
+ const availableSkills = getSkillsForLanguage(selectedKey);
121
+ if (availableSkills.length > 0) {
122
+ const skillOptions = availableSkills.map(s => `${s.name} - ${s.description}`);
123
+ const selectedSkillNames = await promptMultiSelectWithArrows("Select AI coding rules/skills to enable (optional):", skillOptions);
124
+ // Convert selected display names to SkillConfig objects
125
+ selectedSkills = selectedSkillNames.map(sel => {
126
+ const idx = skillOptions.indexOf(sel);
127
+ const skill = availableSkills[idx];
128
+ return {
129
+ name: skill.name,
130
+ description: skill.description,
131
+ instructions: skill.instructions,
132
+ userInvocable: skill.userInvocable,
133
+ };
134
+ });
135
+ if (selectedSkills.length > 0) {
136
+ console.log(`\nSelected skills: ${selectedSkills.map(s => s.name).join(", ")}`);
137
+ }
138
+ else {
139
+ console.log("\nNo skills selected.");
140
+ }
141
+ }
118
142
  // Allow custom commands for "none" language
119
143
  checkCommand = config.checkCommand;
120
144
  testCommand = config.testCommand;
@@ -164,6 +188,10 @@ export async function init(args) {
164
188
  enabled: false,
165
189
  autoRecord: false,
166
190
  outputDir: ".recordings",
191
+ streamJson: {
192
+ enabled: false,
193
+ saveRawJson: true,
194
+ },
167
195
  },
168
196
  firewall: {
169
197
  allowedDomains: [],
@@ -172,7 +200,7 @@ export async function init(args) {
172
200
  // Claude-specific configuration (MCP servers and skills)
173
201
  claude: {
174
202
  mcpServers: {},
175
- skills: [],
203
+ skills: selectedSkills,
176
204
  },
177
205
  };
178
206
  const configPath = join(ralphDir, CONFIG_FILE);