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.
@@ -1,6 +1,10 @@
1
1
  import { spawn } from "child_process";
2
+ import { existsSync, appendFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
2
4
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
3
- import { resolvePromptVariables } from "../templates/prompts.js";
5
+ import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
+ import { getStreamJsonParser } from "../utils/stream-json.js";
7
+ import { sendNotification } from "../utils/notification.js";
4
8
  export async function once(args) {
5
9
  // Parse flags
6
10
  let debug = false;
@@ -32,16 +36,62 @@ export async function once(args) {
32
36
  });
33
37
  const paths = getPaths();
34
38
  const cliConfig = getCliConfig(config);
35
- console.log("Starting single ralph iteration...\n");
39
+ // Check if stream-json output is enabled
40
+ const streamJsonConfig = config.docker?.asciinema?.streamJson;
41
+ const streamJsonEnabled = streamJsonConfig?.enabled ?? false;
42
+ const saveRawJson = streamJsonConfig?.saveRawJson !== false; // default true
43
+ const outputDir = config.docker?.asciinema?.outputDir || ".recordings";
44
+ // Get provider-specific streamJsonArgs (empty array if not defined)
45
+ // This allows providers without JSON streaming to still have output displayed
46
+ const providers = getCliProviders();
47
+ const providerConfig = config.cliProvider ? providers[config.cliProvider] : providers["claude"];
48
+ const streamJsonArgs = providerConfig?.streamJsonArgs ?? [];
49
+ console.log("Starting single ralph iteration...");
50
+ if (streamJsonEnabled) {
51
+ console.log("Stream JSON output enabled - displaying formatted Claude output");
52
+ if (saveRawJson) {
53
+ console.log(`Raw JSON logs will be saved to: ${outputDir}/`);
54
+ }
55
+ }
56
+ console.log();
36
57
  // Build CLI arguments: config args + yolo args + model args + prompt args
37
58
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
38
59
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
39
60
  const promptArgs = cliConfig.promptArgs ?? ["-p"];
40
- const promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
41
61
  const cliArgs = [
42
62
  ...(cliConfig.args ?? []),
43
63
  ...yoloArgs,
44
64
  ];
65
+ // Build the prompt value based on whether fileArgs is configured
66
+ // fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
67
+ // Otherwise, use @file syntax embedded in the prompt (Claude Code style)
68
+ let promptValue;
69
+ if (cliConfig.fileArgs && cliConfig.fileArgs.length > 0) {
70
+ // Add files as separate arguments (e.g., --read prd.json --read progress.txt)
71
+ for (const fileArg of cliConfig.fileArgs) {
72
+ cliArgs.push(fileArg, paths.prd);
73
+ cliArgs.push(fileArg, paths.progress);
74
+ }
75
+ promptValue = prompt;
76
+ }
77
+ else {
78
+ // Use @file syntax embedded in the prompt
79
+ promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
80
+ }
81
+ // Add stream-json output format if enabled (using provider-specific args)
82
+ let jsonLogPath;
83
+ if (streamJsonEnabled) {
84
+ cliArgs.push(...streamJsonArgs);
85
+ // Setup JSON log file if saving raw JSON
86
+ if (saveRawJson) {
87
+ const fullOutputDir = join(process.cwd(), outputDir);
88
+ if (!existsSync(fullOutputDir)) {
89
+ mkdirSync(fullOutputDir, { recursive: true });
90
+ }
91
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
92
+ jsonLogPath = join(fullOutputDir, `ralph-once-${timestamp}.jsonl`);
93
+ }
94
+ }
45
95
  // Add model args if model is specified
46
96
  if (model && cliConfig.modelArgs) {
47
97
  cliArgs.push(...cliConfig.modelArgs, model);
@@ -49,19 +99,128 @@ export async function once(args) {
49
99
  cliArgs.push(...promptArgs, promptValue);
50
100
  if (debug) {
51
101
  console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
102
+ if (jsonLogPath) {
103
+ console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
104
+ }
52
105
  }
106
+ // Create provider-specific stream-json parser
107
+ const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
108
+ // Notification options for this run
109
+ const notifyOptions = { command: config.notifyCommand, debug };
53
110
  return new Promise((resolve, reject) => {
54
- const proc = spawn(cliConfig.command, cliArgs, {
55
- stdio: "inherit",
56
- });
57
- proc.on("close", (code) => {
58
- if (code !== 0) {
59
- console.error(`\n${cliConfig.command} exited with code ${code}`);
60
- }
61
- resolve();
62
- });
63
- proc.on("error", (err) => {
64
- reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
65
- });
111
+ let output = ""; // Accumulate output for PRD complete detection
112
+ if (streamJsonEnabled) {
113
+ // Stream JSON mode: capture stdout, parse JSON, display clean text
114
+ let lineBuffer = "";
115
+ const proc = spawn(cliConfig.command, cliArgs, {
116
+ stdio: ["inherit", "pipe", "inherit"],
117
+ });
118
+ proc.stdout.on("data", (data) => {
119
+ const chunk = data.toString();
120
+ lineBuffer += chunk;
121
+ const lines = lineBuffer.split("\n");
122
+ // Keep the last incomplete line in the buffer
123
+ lineBuffer = lines.pop() || "";
124
+ for (const line of lines) {
125
+ const trimmedLine = line.trim();
126
+ if (!trimmedLine)
127
+ continue;
128
+ // Check if this is a JSON line
129
+ if (trimmedLine.startsWith("{")) {
130
+ // Save raw JSON if enabled
131
+ if (jsonLogPath) {
132
+ try {
133
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
134
+ }
135
+ catch {
136
+ // Ignore write errors
137
+ }
138
+ }
139
+ // Parse and display clean text using provider-specific parser
140
+ const text = streamJsonParser.parseStreamJsonLine(trimmedLine);
141
+ if (text) {
142
+ process.stdout.write(text);
143
+ output += text; // Accumulate for completion detection
144
+ }
145
+ }
146
+ else {
147
+ // Non-JSON line - display as-is (might be status messages, errors, etc.)
148
+ process.stdout.write(trimmedLine + "\n");
149
+ output += trimmedLine + "\n";
150
+ }
151
+ }
152
+ });
153
+ proc.on("close", async (code) => {
154
+ // Process any remaining buffered content
155
+ if (lineBuffer.trim()) {
156
+ const trimmedLine = lineBuffer.trim();
157
+ if (trimmedLine.startsWith("{")) {
158
+ if (jsonLogPath) {
159
+ try {
160
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
161
+ }
162
+ catch {
163
+ // Ignore write errors
164
+ }
165
+ }
166
+ const text = streamJsonParser.parseStreamJsonLine(trimmedLine);
167
+ if (text) {
168
+ process.stdout.write(text);
169
+ output += text;
170
+ }
171
+ }
172
+ else {
173
+ // Non-JSON remaining content
174
+ process.stdout.write(trimmedLine + "\n");
175
+ output += trimmedLine + "\n";
176
+ }
177
+ }
178
+ // Ensure final newline
179
+ process.stdout.write("\n");
180
+ // Send notification based on outcome
181
+ if (code !== 0) {
182
+ console.error(`\n${cliConfig.command} exited with code ${code}`);
183
+ await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
184
+ }
185
+ else if (output.includes("<promise>COMPLETE</promise>")) {
186
+ await sendNotification("prd_complete", undefined, notifyOptions);
187
+ }
188
+ else {
189
+ await sendNotification("iteration_complete", undefined, notifyOptions);
190
+ }
191
+ resolve();
192
+ });
193
+ proc.on("error", (err) => {
194
+ reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
195
+ });
196
+ }
197
+ else {
198
+ // Standard mode: capture stdout while passing through
199
+ const proc = spawn(cliConfig.command, cliArgs, {
200
+ stdio: ["inherit", "pipe", "inherit"],
201
+ });
202
+ proc.stdout.on("data", (data) => {
203
+ const chunk = data.toString();
204
+ output += chunk;
205
+ process.stdout.write(chunk);
206
+ });
207
+ proc.on("close", async (code) => {
208
+ // Send notification based on outcome
209
+ if (code !== 0) {
210
+ console.error(`\n${cliConfig.command} exited with code ${code}`);
211
+ await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
212
+ }
213
+ else if (output.includes("<promise>COMPLETE</promise>")) {
214
+ await sendNotification("prd_complete", undefined, notifyOptions);
215
+ }
216
+ else {
217
+ await sendNotification("iteration_complete", undefined, notifyOptions);
218
+ }
219
+ resolve();
220
+ });
221
+ proc.on("error", (err) => {
222
+ reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
223
+ });
224
+ }
66
225
  });
67
226
  }
@@ -1,9 +1,11 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
2
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
5
- import { resolvePromptVariables } from "../templates/prompts.js";
5
+ import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
6
  import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
7
+ import { getStreamJsonParser } from "../utils/stream-json.js";
8
+ import { sendNotification } from "../utils/notification.js";
7
9
  const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
8
10
  /**
9
11
  * Creates a filtered PRD file containing only incomplete items (passes: false).
@@ -13,7 +15,23 @@ const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing"
13
15
  */
14
16
  function createFilteredPrd(prdPath, baseDir, category) {
15
17
  const content = readFileSync(prdPath, "utf-8");
16
- const items = JSON.parse(content);
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(content);
21
+ }
22
+ catch {
23
+ console.error("\x1b[31mError: prd.json contains invalid JSON.\x1b[0m");
24
+ console.error("The file may have been corrupted by an LLM.\n");
25
+ console.error("Run \x1b[36mralph fix-prd\x1b[0m to diagnose and repair the file.");
26
+ process.exit(1);
27
+ }
28
+ if (!Array.isArray(parsed)) {
29
+ console.error("\x1b[31mError: prd.json is corrupted - expected an array of items.\x1b[0m");
30
+ console.error("The file may have been modified incorrectly by an LLM.\n");
31
+ console.error("Run \x1b[36mralph fix-prd\x1b[0m to diagnose and repair the file.");
32
+ process.exit(1);
33
+ }
34
+ const items = parsed;
17
35
  let filteredItems = items.filter(item => item.passes === false);
18
36
  // Apply category filter if specified
19
37
  if (category) {
@@ -42,9 +60,20 @@ function syncPassesFromTasks(tasksPath, prdPath) {
42
60
  }
43
61
  try {
44
62
  const tasksContent = readFileSync(tasksPath, "utf-8");
45
- const tasks = JSON.parse(tasksContent);
63
+ const tasksParsed = JSON.parse(tasksContent);
64
+ if (!Array.isArray(tasksParsed)) {
65
+ console.warn("\x1b[33mWarning: prd-tasks.json is not a valid array - skipping sync.\x1b[0m");
66
+ return 0;
67
+ }
68
+ const tasks = tasksParsed;
46
69
  const prdContent = readFileSync(prdPath, "utf-8");
47
- const prd = JSON.parse(prdContent);
70
+ const prdParsed = JSON.parse(prdContent);
71
+ if (!Array.isArray(prdParsed)) {
72
+ console.warn("\x1b[33mWarning: prd.json is corrupted - skipping sync.\x1b[0m");
73
+ console.warn("Run \x1b[36mralph fix-prd\x1b[0m after this session to repair.\n");
74
+ return 0;
75
+ }
76
+ const prd = prdParsed;
48
77
  let synced = 0;
49
78
  // Find tasks that were marked as passing
50
79
  for (const task of tasks) {
@@ -71,9 +100,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
71
100
  return 0;
72
101
  }
73
102
  }
74
- async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model) {
103
+ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson) {
75
104
  return new Promise((resolve, reject) => {
76
105
  let output = "";
106
+ let jsonLogPath;
107
+ let lineBuffer = ""; // Buffer for incomplete JSON lines
77
108
  // Build CLI arguments: config args + yolo args + model args + prompt args
78
109
  const cliArgs = [
79
110
  ...(cliConfig.args ?? []),
@@ -84,6 +115,19 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
84
115
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
85
116
  cliArgs.push(...yoloArgs);
86
117
  }
118
+ // Add stream-json output format if enabled (using provider-specific args)
119
+ if (streamJson?.enabled) {
120
+ cliArgs.push(...streamJson.args);
121
+ // Setup JSON log file if saving raw JSON
122
+ if (streamJson.saveRawJson) {
123
+ const outputDir = join(process.cwd(), streamJson.outputDir);
124
+ if (!existsSync(outputDir)) {
125
+ mkdirSync(outputDir, { recursive: true });
126
+ }
127
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
128
+ jsonLogPath = join(outputDir, `ralph-run-${timestamp}.jsonl`);
129
+ }
130
+ }
87
131
  // Add model args if model is specified
88
132
  if (model && cliConfig.modelArgs) {
89
133
  cliArgs.push(...cliConfig.modelArgs, model);
@@ -91,20 +135,104 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
91
135
  // Use the filtered PRD (only incomplete items) for the prompt
92
136
  // promptArgs specifies flags to use (e.g., ["-p"] for Claude, [] for positional)
93
137
  const promptArgs = cliConfig.promptArgs ?? ["-p"];
94
- const promptValue = `@${filteredPrdPath} @${paths.progress} ${prompt}`;
138
+ // Build the prompt value based on whether fileArgs is configured
139
+ // fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
140
+ // Otherwise, use @file syntax embedded in the prompt (Claude Code style)
141
+ let promptValue;
142
+ if (cliConfig.fileArgs && cliConfig.fileArgs.length > 0) {
143
+ // Add files as separate arguments (e.g., --read prd-tasks.json --read progress.txt)
144
+ for (const fileArg of cliConfig.fileArgs) {
145
+ cliArgs.push(fileArg, filteredPrdPath);
146
+ cliArgs.push(fileArg, paths.progress);
147
+ }
148
+ promptValue = prompt;
149
+ }
150
+ else {
151
+ // Use @file syntax embedded in the prompt
152
+ promptValue = `@${filteredPrdPath} @${paths.progress} ${prompt}`;
153
+ }
95
154
  cliArgs.push(...promptArgs, promptValue);
96
155
  if (debug) {
97
156
  console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
157
+ if (jsonLogPath) {
158
+ console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
159
+ }
98
160
  }
99
161
  const proc = spawn(cliConfig.command, cliArgs, {
100
162
  stdio: ["inherit", "pipe", "inherit"],
101
163
  });
102
164
  proc.stdout.on("data", (data) => {
103
165
  const chunk = data.toString();
104
- output += chunk;
105
- process.stdout.write(chunk);
166
+ if (streamJson?.enabled) {
167
+ // Process stream-json output: parse JSON and display clean text
168
+ lineBuffer += chunk;
169
+ const lines = lineBuffer.split("\n");
170
+ // Keep the last incomplete line in the buffer
171
+ lineBuffer = lines.pop() || "";
172
+ for (const line of lines) {
173
+ const trimmedLine = line.trim();
174
+ if (!trimmedLine)
175
+ continue;
176
+ // Check if this is a JSON line
177
+ if (trimmedLine.startsWith("{")) {
178
+ // Save raw JSON if enabled
179
+ if (jsonLogPath) {
180
+ try {
181
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
182
+ }
183
+ catch {
184
+ // Ignore write errors
185
+ }
186
+ }
187
+ // Parse and display clean text using provider-specific parser
188
+ const text = streamJson.parser.parseStreamJsonLine(trimmedLine);
189
+ if (text) {
190
+ process.stdout.write(text);
191
+ output += text; // Accumulate parsed text for completion detection
192
+ }
193
+ }
194
+ else {
195
+ // Non-JSON line - display as-is (might be status messages, errors, etc.)
196
+ process.stdout.write(trimmedLine + "\n");
197
+ output += trimmedLine + "\n";
198
+ }
199
+ }
200
+ }
201
+ else {
202
+ // Standard output: pass through as-is
203
+ output += chunk;
204
+ process.stdout.write(chunk);
205
+ }
106
206
  });
107
207
  proc.on("close", (code) => {
208
+ // Process any remaining buffered content
209
+ if (streamJson?.enabled && lineBuffer.trim()) {
210
+ const trimmedLine = lineBuffer.trim();
211
+ if (trimmedLine.startsWith("{")) {
212
+ if (jsonLogPath) {
213
+ try {
214
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
215
+ }
216
+ catch {
217
+ // Ignore write errors
218
+ }
219
+ }
220
+ const text = streamJson.parser.parseStreamJsonLine(trimmedLine);
221
+ if (text) {
222
+ process.stdout.write(text);
223
+ output += text;
224
+ }
225
+ }
226
+ else {
227
+ // Non-JSON remaining content
228
+ process.stdout.write(trimmedLine + "\n");
229
+ output += trimmedLine + "\n";
230
+ }
231
+ }
232
+ // Ensure final newline for clean output
233
+ if (streamJson?.enabled) {
234
+ process.stdout.write("\n");
235
+ }
108
236
  resolve({ exitCode: code ?? 0, output });
109
237
  });
110
238
  proc.on("error", (err) => {
@@ -142,7 +270,23 @@ function formatElapsedTime(startTime, endTime) {
142
270
  */
143
271
  function countPrdItems(prdPath, category) {
144
272
  const content = readFileSync(prdPath, "utf-8");
145
- const items = JSON.parse(content);
273
+ let parsed;
274
+ try {
275
+ parsed = JSON.parse(content);
276
+ }
277
+ catch {
278
+ console.error("\x1b[31mError: prd.json contains invalid JSON.\x1b[0m");
279
+ console.error("The file may have been corrupted by an LLM.\n");
280
+ console.error("Run \x1b[36mralph fix-prd\x1b[0m to diagnose and repair the file.");
281
+ process.exit(1);
282
+ }
283
+ if (!Array.isArray(parsed)) {
284
+ console.error("\x1b[31mError: prd.json is corrupted - expected an array of items.\x1b[0m");
285
+ console.error("The file may have been modified incorrectly by an LLM.\n");
286
+ console.error("Run \x1b[36mralph fix-prd\x1b[0m to diagnose and repair the file.");
287
+ process.exit(1);
288
+ }
289
+ const items = parsed;
146
290
  let filteredItems = items;
147
291
  if (category) {
148
292
  filteredItems = items.filter(item => item.category === category);
@@ -265,6 +409,21 @@ export async function run(args) {
265
409
  });
266
410
  const paths = getPaths();
267
411
  const cliConfig = getCliConfig(config);
412
+ // Check if stream-json output is enabled
413
+ const streamJsonConfig = config.docker?.asciinema?.streamJson;
414
+ // Get provider-specific streamJsonArgs, falling back to Claude's defaults
415
+ const providers = getCliProviders();
416
+ const providerConfig = config.cliProvider ? providers[config.cliProvider] : providers["claude"];
417
+ // Only use provider's streamJsonArgs if defined, otherwise empty array (no special args)
418
+ // This allows providers without JSON streaming to still have output displayed
419
+ const streamJsonArgs = providerConfig?.streamJsonArgs ?? [];
420
+ const streamJson = streamJsonConfig?.enabled ? {
421
+ enabled: true,
422
+ saveRawJson: streamJsonConfig.saveRawJson !== false, // default true
423
+ outputDir: config.docker?.asciinema?.outputDir || ".recordings",
424
+ args: streamJsonArgs,
425
+ parser: getStreamJsonParser(config.cliProvider, debug),
426
+ } : undefined;
268
427
  // Progress tracking: stop only if no tasks complete after N iterations
269
428
  const MAX_ITERATIONS_WITHOUT_PROGRESS = 3;
270
429
  // Get requested iteration count (may be adjusted dynamically)
@@ -285,6 +444,12 @@ export async function run(args) {
285
444
  if (category) {
286
445
  console.log(`Filtering PRD items by category: ${category}`);
287
446
  }
447
+ if (streamJson?.enabled) {
448
+ console.log("Stream JSON output enabled - displaying formatted Claude output");
449
+ if (streamJson.saveRawJson) {
450
+ console.log(`Raw JSON logs will be saved to: ${streamJson.outputDir}/`);
451
+ }
452
+ }
288
453
  console.log();
289
454
  // Track temp file for cleanup
290
455
  let filteredPrdPath = null;
@@ -378,10 +543,15 @@ export async function run(args) {
378
543
  console.log("PRD COMPLETE - All features already implemented!");
379
544
  }
380
545
  console.log("=".repeat(50));
546
+ // Send notification for PRD completion
547
+ await sendNotification("prd_complete", undefined, {
548
+ command: config.notifyCommand,
549
+ debug,
550
+ });
381
551
  break;
382
552
  }
383
553
  }
384
- const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model);
554
+ const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson);
385
555
  // Sync any completed items from prd-tasks.json back to prd.json
386
556
  // This catches cases where the LLM updated prd-tasks.json instead of prd.json
387
557
  syncPassesFromTasks(filteredPrdPath, paths.prd);
@@ -415,6 +585,8 @@ export async function run(args) {
415
585
  console.log(`(No tasks completed and no new tasks added)`);
416
586
  console.log(`Status: ${progressCounts.complete}/${progressCounts.total} complete, ${progressCounts.incomplete} remaining.`);
417
587
  console.log("Check the PRD and task definitions for issues.");
588
+ // Send notification about stopped run
589
+ await sendNotification("run_stopped", `Ralph: Run stopped - no progress after ${MAX_ITERATIONS_WITHOUT_PROGRESS} iterations. ${progressCounts.incomplete} tasks remaining.`, { command: config.notifyCommand, debug });
418
590
  break;
419
591
  }
420
592
  }
@@ -432,6 +604,8 @@ export async function run(args) {
432
604
  console.error(`\nStopping: ${cliConfig.command} failed ${consecutiveFailures} times in a row with exit code ${exitCode}.`);
433
605
  console.error("This usually indicates a configuration error (e.g., missing API key).");
434
606
  console.error("Please check your CLI configuration and try again.");
607
+ // Send notification about error
608
+ await sendNotification("error", `Ralph: CLI failed ${consecutiveFailures} times with exit code ${exitCode}. Check configuration.`, { command: config.notifyCommand, debug });
435
609
  break;
436
610
  }
437
611
  console.log("Continuing to next iteration...");
@@ -472,13 +646,10 @@ export async function run(args) {
472
646
  }
473
647
  console.log("=".repeat(50));
474
648
  // Send notification if configured
475
- if (config.notifyCommand) {
476
- const [cmd, ...cmdArgs] = config.notifyCommand.split(" ");
477
- const notifyProc = spawn(cmd, [...cmdArgs, "Ralph: PRD Complete!"], { stdio: "ignore" });
478
- notifyProc.on("error", () => {
479
- // Notification command not available, ignore
480
- });
481
- }
649
+ await sendNotification("prd_complete", undefined, {
650
+ command: config.notifyCommand,
651
+ debug,
652
+ });
482
653
  break;
483
654
  }
484
655
  }
@@ -4,9 +4,10 @@
4
4
  "name": "Claude Code",
5
5
  "description": "Anthropic's Claude Code CLI",
6
6
  "command": "claude",
7
- "defaultArgs": ["--permission-mode", "acceptEdits"],
7
+ "defaultArgs": [],
8
8
  "yoloArgs": ["--dangerously-skip-permissions"],
9
9
  "promptArgs": ["-p"],
10
+ "streamJsonArgs": ["--output-format", "stream-json", "--verbose", "--print"],
10
11
  "docker": {
11
12
  "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"
12
13
  },
@@ -25,6 +26,7 @@
25
26
  "defaultArgs": ["--yes"],
26
27
  "yoloArgs": ["--yes-always"],
27
28
  "promptArgs": ["--message"],
29
+ "fileArgs": ["--read"],
28
30
  "docker": {
29
31
  "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",
30
32
  "note": "Check 'aider --help' for available flags"
@@ -43,7 +45,8 @@
43
45
  "command": "codex",
44
46
  "defaultArgs": ["--approval-mode", "suggest"],
45
47
  "yoloArgs": ["--approval-mode", "full-auto"],
46
- "promptArgs": [],
48
+ "promptArgs": ["exec"],
49
+ "streamJsonArgs": ["--json"],
47
50
  "docker": {
48
51
  "install": "# Install OpenAI Codex CLI\nRUN npm install -g @openai/codex",
49
52
  "note": "Check 'codex --help' for available flags"
@@ -63,6 +66,7 @@
63
66
  "defaultArgs": [],
64
67
  "yoloArgs": ["-y"],
65
68
  "promptArgs": [],
69
+ "streamJsonArgs": ["--output-format", "json"],
66
70
  "docker": {
67
71
  "install": "# Install Google Gemini CLI\nRUN npm install -g @google/gemini-cli",
68
72
  "note": "Check 'gemini --help' for available flags"
@@ -80,8 +84,9 @@
80
84
  "description": "Open source AI coding agent for the terminal",
81
85
  "command": "opencode",
82
86
  "defaultArgs": [],
83
- "yoloArgs": ["--yolo"],
87
+ "yoloArgs": [],
84
88
  "promptArgs": ["run"],
89
+ "streamJsonArgs": ["--format", "json"],
85
90
  "docker": {
86
91
  "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",
87
92
  "note": "Check 'opencode --help' for available flags"
@@ -111,6 +116,26 @@
111
116
  },
112
117
  "modelArgs": ["--model"]
113
118
  },
119
+ "goose": {
120
+ "name": "Goose CLI",
121
+ "description": "Block's Goose AI coding agent",
122
+ "command": "goose",
123
+ "defaultArgs": [],
124
+ "yoloArgs": [],
125
+ "promptArgs": ["run", "--text"],
126
+ "streamJsonArgs": ["--output-format", "stream-json"],
127
+ "docker": {
128
+ "install": "# Install Goose CLI (requires Python)\nRUN apt-get update && apt-get install -y python3 python3-pip python3-venv && rm -rf /var/lib/apt/lists/* \\\n && pip3 install --break-system-packages --no-cache-dir goose-ai",
129
+ "note": "Check 'goose --help' for available flags"
130
+ },
131
+ "envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
132
+ "credentialMount": null,
133
+ "modelConfig": {
134
+ "envVar": "GOOSE_PROVIDER",
135
+ "note": "Set provider via GOOSE_PROVIDER env var"
136
+ },
137
+ "modelArgs": ["--model"]
138
+ },
114
139
  "custom": {
115
140
  "name": "Custom CLI",
116
141
  "description": "Configure your own AI CLI tool",
@@ -0,0 +1,12 @@
1
+ {
2
+ "skills": {
3
+ "swift": [
4
+ {
5
+ "name": "swift-main-naming",
6
+ "description": "Prevents naming files main.swift when using @main attribute",
7
+ "instructions": "IMPORTANT: In Swift, files containing the @main attribute MUST NOT be named main.swift.\n\nWhen the @main attribute is used (e.g., @main struct App), Swift automatically generates an entry point. If the file is also named main.swift, Swift treats it as having a manual entry point, causing a conflict.\n\nRULES:\n- Never name a file main.swift if it contains @main attribute\n- Use descriptive names like App.swift, MyApp.swift, or the actual type name\n- If you encounter a main.swift with @main, rename it to match the type (e.g., struct MyApp -> MyApp.swift)\n\nEXAMPLES:\n\nBAD:\n```\n// main.swift\n@main\nstruct App {\n static func main() { ... }\n}\n```\n\nGOOD:\n```\n// App.swift\n@main\nstruct App {\n static func main() { ... }\n}\n```",
8
+ "userInvocable": false
9
+ }
10
+ ]
11
+ }
12
+ }
@@ -35,6 +35,8 @@ export interface CliProviderConfig {
35
35
  yoloArgs: string[];
36
36
  promptArgs: string[];
37
37
  modelArgs?: string[];
38
+ fileArgs?: string[];
39
+ streamJsonArgs?: string[];
38
40
  docker: {
39
41
  install: string;
40
42
  };
@@ -48,9 +50,20 @@ export interface CliProviderConfig {
48
50
  interface CliProvidersJson {
49
51
  providers: Record<string, CliProviderConfig>;
50
52
  }
53
+ export interface SkillDefinition {
54
+ name: string;
55
+ description: string;
56
+ instructions: string;
57
+ userInvocable?: boolean;
58
+ }
59
+ interface SkillsJson {
60
+ skills: Record<string, SkillDefinition[]>;
61
+ }
51
62
  export declare function getLanguagesJson(): LanguagesJson;
52
63
  export declare function getCliProvidersJson(): CliProvidersJson;
53
64
  export declare function getCliProviders(): Record<string, CliProviderConfig>;
65
+ export declare function getSkillsJson(): SkillsJson;
66
+ export declare function getSkillsForLanguage(language: string): SkillDefinition[];
54
67
  export declare function getLanguages(): Record<string, LanguageConfig>;
55
68
  export declare const LANGUAGES: Record<string, LanguageConfig>;
56
69
  export declare function generatePromptTemplate(): string;