ralph-cli-sandboxed 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +99 -15
  2. package/dist/commands/action.d.ts +7 -0
  3. package/dist/commands/action.js +276 -0
  4. package/dist/commands/chat.d.ts +8 -0
  5. package/dist/commands/chat.js +701 -0
  6. package/dist/commands/config.d.ts +1 -0
  7. package/dist/commands/config.js +51 -0
  8. package/dist/commands/daemon.d.ts +23 -0
  9. package/dist/commands/daemon.js +422 -0
  10. package/dist/commands/docker.js +82 -4
  11. package/dist/commands/fix-config.d.ts +4 -0
  12. package/dist/commands/fix-config.js +388 -0
  13. package/dist/commands/help.js +80 -0
  14. package/dist/commands/init.js +135 -1
  15. package/dist/commands/listen.d.ts +8 -0
  16. package/dist/commands/listen.js +280 -0
  17. package/dist/commands/notify.d.ts +7 -0
  18. package/dist/commands/notify.js +165 -0
  19. package/dist/commands/once.js +8 -8
  20. package/dist/commands/prd.js +2 -2
  21. package/dist/commands/run.js +25 -12
  22. package/dist/config/languages.json +4 -0
  23. package/dist/index.js +14 -0
  24. package/dist/providers/telegram.d.ts +39 -0
  25. package/dist/providers/telegram.js +256 -0
  26. package/dist/templates/macos-scripts.d.ts +42 -0
  27. package/dist/templates/macos-scripts.js +448 -0
  28. package/dist/tui/ConfigEditor.d.ts +7 -0
  29. package/dist/tui/ConfigEditor.js +313 -0
  30. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  31. package/dist/tui/components/ArrayEditor.js +193 -0
  32. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  33. package/dist/tui/components/BooleanToggle.js +43 -0
  34. package/dist/tui/components/EditorPanel.d.ts +50 -0
  35. package/dist/tui/components/EditorPanel.js +232 -0
  36. package/dist/tui/components/HelpPanel.d.ts +13 -0
  37. package/dist/tui/components/HelpPanel.js +69 -0
  38. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  39. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  40. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  41. package/dist/tui/components/KeyValueEditor.js +261 -0
  42. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  43. package/dist/tui/components/ObjectEditor.js +227 -0
  44. package/dist/tui/components/PresetSelector.d.ts +23 -0
  45. package/dist/tui/components/PresetSelector.js +58 -0
  46. package/dist/tui/components/Preview.d.ts +18 -0
  47. package/dist/tui/components/Preview.js +190 -0
  48. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  49. package/dist/tui/components/ScrollableContainer.js +77 -0
  50. package/dist/tui/components/SectionNav.d.ts +31 -0
  51. package/dist/tui/components/SectionNav.js +130 -0
  52. package/dist/tui/components/StringEditor.d.ts +21 -0
  53. package/dist/tui/components/StringEditor.js +29 -0
  54. package/dist/tui/hooks/useConfig.d.ts +16 -0
  55. package/dist/tui/hooks/useConfig.js +89 -0
  56. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  57. package/dist/tui/hooks/useTerminalSize.js +48 -0
  58. package/dist/tui/utils/presets.d.ts +52 -0
  59. package/dist/tui/utils/presets.js +191 -0
  60. package/dist/tui/utils/validation.d.ts +49 -0
  61. package/dist/tui/utils/validation.js +198 -0
  62. package/dist/utils/chat-client.d.ts +144 -0
  63. package/dist/utils/chat-client.js +102 -0
  64. package/dist/utils/config.d.ts +52 -0
  65. package/dist/utils/daemon-client.d.ts +36 -0
  66. package/dist/utils/daemon-client.js +70 -0
  67. package/dist/utils/message-queue.d.ts +58 -0
  68. package/dist/utils/message-queue.js +133 -0
  69. package/dist/utils/notification.d.ts +28 -1
  70. package/dist/utils/notification.js +146 -20
  71. package/docs/MACOS-DEVELOPMENT.md +435 -0
  72. package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
  73. package/package.json +6 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Listen command - runs in sandbox to process commands from host.
3
+ * This enables Telegram/chat commands to execute inside the container.
4
+ */
5
+ import { spawn } from "child_process";
6
+ import { existsSync, watch } from "fs";
7
+ import { isRunningInContainer } from "../utils/config.js";
8
+ import { getMessagesPath, getPendingMessages, respondToMessage, cleanupOldMessages, } from "../utils/message-queue.js";
9
+ /**
10
+ * Execute a shell command and return the result.
11
+ */
12
+ async function executeCommand(command, timeout = 60000) {
13
+ return new Promise((resolve) => {
14
+ const proc = spawn("sh", ["-c", command], {
15
+ stdio: ["ignore", "pipe", "pipe"],
16
+ cwd: "/workspace",
17
+ });
18
+ let stdout = "";
19
+ let stderr = "";
20
+ let killed = false;
21
+ const timer = setTimeout(() => {
22
+ killed = true;
23
+ proc.kill();
24
+ resolve({
25
+ success: false,
26
+ output: stdout,
27
+ error: "Command timed out after 60 seconds",
28
+ });
29
+ }, timeout);
30
+ proc.stdout.on("data", (data) => {
31
+ stdout += data.toString();
32
+ });
33
+ proc.stderr.on("data", (data) => {
34
+ stderr += data.toString();
35
+ });
36
+ proc.on("close", (code) => {
37
+ if (killed)
38
+ return;
39
+ clearTimeout(timer);
40
+ if (code === 0) {
41
+ resolve({ success: true, output: stdout.trim() || "(no output)" });
42
+ }
43
+ else {
44
+ resolve({
45
+ success: false,
46
+ output: stdout.trim(),
47
+ error: stderr.trim() || `Exit code: ${code}`,
48
+ });
49
+ }
50
+ });
51
+ proc.on("error", (err) => {
52
+ clearTimeout(timer);
53
+ resolve({ success: false, output: "", error: err.message });
54
+ });
55
+ });
56
+ }
57
+ /**
58
+ * Process a message from the host.
59
+ */
60
+ async function processMessage(message, messagesPath, debug) {
61
+ const { action, args } = message;
62
+ if (debug) {
63
+ console.log(`[listen] Processing: ${action} ${args?.join(" ") || ""}`);
64
+ }
65
+ switch (action) {
66
+ case "exec": {
67
+ const command = args?.join(" ") || "";
68
+ if (!command) {
69
+ respondToMessage(messagesPath, message.id, {
70
+ success: false,
71
+ error: "No command provided",
72
+ });
73
+ return;
74
+ }
75
+ console.log(`[listen] Executing: ${command}`);
76
+ const result = await executeCommand(command);
77
+ // Truncate long output
78
+ let output = result.output;
79
+ if (output.length > 4000) {
80
+ output = output.substring(0, 4000) + "\n...(truncated)";
81
+ }
82
+ respondToMessage(messagesPath, message.id, {
83
+ success: result.success,
84
+ output,
85
+ error: result.error,
86
+ });
87
+ if (debug) {
88
+ console.log(`[listen] Result: ${result.success ? "OK" : "FAILED"}`);
89
+ }
90
+ break;
91
+ }
92
+ case "run": {
93
+ // Start ralph run in background
94
+ // Support optional category filter: run [category]
95
+ const runArgs = ["run"];
96
+ if (message.args && message.args.length > 0) {
97
+ runArgs.push("--category", message.args[0]);
98
+ console.log(`[listen] Starting ralph run with category: ${message.args[0]}...`);
99
+ }
100
+ else {
101
+ console.log("[listen] Starting ralph run...");
102
+ }
103
+ const proc = spawn("ralph", runArgs, {
104
+ stdio: "inherit",
105
+ cwd: "/workspace",
106
+ detached: true,
107
+ });
108
+ proc.unref();
109
+ respondToMessage(messagesPath, message.id, {
110
+ success: true,
111
+ output: message.args?.length ? `Ralph run started (category: ${message.args[0]})` : "Ralph run started",
112
+ });
113
+ break;
114
+ }
115
+ case "status": {
116
+ // Get PRD status
117
+ const result = await executeCommand("ralph status");
118
+ respondToMessage(messagesPath, message.id, {
119
+ success: result.success,
120
+ output: result.output,
121
+ error: result.error,
122
+ });
123
+ break;
124
+ }
125
+ case "ping": {
126
+ respondToMessage(messagesPath, message.id, {
127
+ success: true,
128
+ output: "pong from sandbox",
129
+ });
130
+ break;
131
+ }
132
+ case "claude": {
133
+ // Run Claude Code with the provided prompt in YOLO mode
134
+ const prompt = args?.join(" ") || "";
135
+ if (!prompt) {
136
+ respondToMessage(messagesPath, message.id, {
137
+ success: false,
138
+ error: "No prompt provided",
139
+ });
140
+ return;
141
+ }
142
+ console.log(`[listen] Running Claude Code with prompt: ${prompt.substring(0, 50)}...`);
143
+ // Build the command: claude -p "prompt" --dangerously-skip-permissions
144
+ // Using --print to get non-interactive output
145
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
146
+ const command = `claude -p '${escapedPrompt}' --dangerously-skip-permissions --print`;
147
+ // Run with 5 minute timeout
148
+ const result = await executeCommand(command, 300000);
149
+ // Truncate long output
150
+ let output = result.output;
151
+ if (output.length > 4000) {
152
+ output = output.substring(0, 4000) + "\n...(truncated)";
153
+ }
154
+ respondToMessage(messagesPath, message.id, {
155
+ success: result.success,
156
+ output,
157
+ error: result.error,
158
+ });
159
+ if (debug) {
160
+ console.log(`[listen] Claude Code result: ${result.success ? "OK" : "FAILED"}`);
161
+ }
162
+ break;
163
+ }
164
+ default:
165
+ respondToMessage(messagesPath, message.id, {
166
+ success: false,
167
+ error: `Unknown action: ${action}. Supported: exec, run, status, ping, claude`,
168
+ });
169
+ }
170
+ }
171
+ /**
172
+ * Start listening for messages from host.
173
+ */
174
+ async function startListening(debug) {
175
+ const messagesPath = getMessagesPath(true); // true = in container
176
+ const ralphDir = "/workspace/.ralph";
177
+ console.log("Ralph Sandbox Listener");
178
+ console.log("-".repeat(40));
179
+ console.log(`Messages file: ${messagesPath}`);
180
+ console.log("");
181
+ console.log("Listening for commands from host...");
182
+ console.log("Supported actions: exec, run, status, ping, claude");
183
+ console.log("");
184
+ console.log("Press Ctrl+C to stop.");
185
+ // Process any pending messages on startup
186
+ const pending = getPendingMessages(messagesPath, "host");
187
+ for (const msg of pending) {
188
+ await processMessage(msg, messagesPath, debug);
189
+ }
190
+ // Watch for file changes
191
+ let processing = false;
192
+ let watcher = null;
193
+ const checkMessages = async () => {
194
+ if (processing)
195
+ return;
196
+ processing = true;
197
+ try {
198
+ const pending = getPendingMessages(messagesPath, "host");
199
+ for (const msg of pending) {
200
+ await processMessage(msg, messagesPath, debug);
201
+ }
202
+ // Cleanup old messages periodically
203
+ cleanupOldMessages(messagesPath, 300000); // 5 minutes
204
+ }
205
+ catch (err) {
206
+ if (debug) {
207
+ console.error(`[listen] Error: ${err}`);
208
+ }
209
+ }
210
+ processing = false;
211
+ };
212
+ // Watch the .ralph directory for changes
213
+ if (existsSync(ralphDir)) {
214
+ watcher = watch(ralphDir, { persistent: true }, (eventType, filename) => {
215
+ if (filename === "messages.json") {
216
+ checkMessages();
217
+ }
218
+ });
219
+ }
220
+ // Also poll periodically as backup
221
+ const pollInterval = setInterval(checkMessages, 1000);
222
+ // Handle shutdown
223
+ const shutdown = () => {
224
+ console.log("\nStopping listener...");
225
+ if (watcher) {
226
+ watcher.close();
227
+ }
228
+ clearInterval(pollInterval);
229
+ process.exit(0);
230
+ };
231
+ process.on("SIGINT", shutdown);
232
+ process.on("SIGTERM", shutdown);
233
+ }
234
+ /**
235
+ * Main listen command handler.
236
+ */
237
+ export async function listen(args) {
238
+ const debug = args.includes("--debug") || args.includes("-d");
239
+ if (args.includes("--help") || args.includes("-h")) {
240
+ console.log(`
241
+ ralph listen - Listen for commands from host (run inside sandbox)
242
+
243
+ USAGE:
244
+ ralph listen [--debug] Start listening for host commands
245
+
246
+ DESCRIPTION:
247
+ This command runs inside the sandbox container and listens for
248
+ commands sent from the host via the message queue. It enables
249
+ remote control of the sandbox via Telegram or other chat clients.
250
+
251
+ The host sends commands to .ralph/messages.json, and this listener
252
+ processes them and writes responses back.
253
+
254
+ SUPPORTED ACTIONS:
255
+ exec [cmd] Execute a shell command in the sandbox
256
+ run Start ralph run
257
+ status Get PRD status
258
+ ping Health check
259
+ claude [prompt] Run Claude Code with prompt (YOLO mode)
260
+
261
+ SETUP:
262
+ 1. Start the daemon on the host: ralph daemon start
263
+ 2. Start the chat client: ralph chat start
264
+ 3. Inside the container, start the listener: ralph listen
265
+ 4. Send commands via Telegram: /exec npm test
266
+
267
+ EXAMPLE:
268
+ # Inside the container
269
+ ralph listen --debug
270
+ `);
271
+ return;
272
+ }
273
+ // Warn if not in container (but allow for testing)
274
+ if (!isRunningInContainer()) {
275
+ console.warn("Warning: ralph listen is designed to run inside a container.");
276
+ console.warn("Running on host for testing purposes...");
277
+ console.warn("");
278
+ }
279
+ await startListening(debug);
280
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Send a notification - works both inside and outside containers.
3
+ *
4
+ * Inside container: Uses file-based message queue to communicate with host daemon
5
+ * Outside container: Uses notifyCommand directly (for testing)
6
+ */
7
+ export declare function notify(args: string[]): Promise<void>;
@@ -0,0 +1,165 @@
1
+ import { isRunningInContainer } from "../utils/config.js";
2
+ import { sendNotification } from "../utils/notification.js";
3
+ import { loadConfig } from "../utils/config.js";
4
+ import { getMessagesPath, sendMessage, waitForResponse, } from "../utils/message-queue.js";
5
+ import { existsSync } from "fs";
6
+ /**
7
+ * Send a notification - works both inside and outside containers.
8
+ *
9
+ * Inside container: Uses file-based message queue to communicate with host daemon
10
+ * Outside container: Uses notifyCommand directly (for testing)
11
+ */
12
+ export async function notify(args) {
13
+ // Parse arguments
14
+ let action = "notify";
15
+ let message;
16
+ let debug = false;
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ if (arg === "--action" || arg === "-a") {
20
+ action = args[++i] || "notify";
21
+ }
22
+ else if (arg === "--debug" || arg === "-d") {
23
+ debug = true;
24
+ }
25
+ else if (arg === "--help" || arg === "-h") {
26
+ showHelp();
27
+ return;
28
+ }
29
+ else if (!arg.startsWith("-")) {
30
+ // Collect remaining args as message
31
+ message = args.slice(i).join(" ");
32
+ break;
33
+ }
34
+ }
35
+ // Default message based on action
36
+ if (!message) {
37
+ if (action === "notify") {
38
+ message = "Ralph notification";
39
+ }
40
+ }
41
+ const inContainer = isRunningInContainer();
42
+ const messagesPath = getMessagesPath(inContainer);
43
+ if (debug) {
44
+ console.log(`[notify] Action: ${action}`);
45
+ console.log(`[notify] Message/Args: ${message || "(none)"}`);
46
+ console.log(`[notify] In container: ${inContainer}`);
47
+ console.log(`[notify] Messages file: ${messagesPath}`);
48
+ }
49
+ if (inContainer) {
50
+ // Inside container - use file-based message queue
51
+ if (!existsSync(messagesPath)) {
52
+ // Check if .ralph directory exists (mounted from host)
53
+ const ralphDir = "/workspace/.ralph";
54
+ if (!existsSync(ralphDir)) {
55
+ console.error("Error: .ralph directory not mounted in container.");
56
+ console.error("Make sure the container is started with 'ralph docker run'.");
57
+ process.exit(1);
58
+ }
59
+ }
60
+ // Send message via file queue
61
+ const messageId = sendMessage(messagesPath, "sandbox", action, message ? [message] : undefined);
62
+ if (debug) {
63
+ console.log(`[notify] Sent message: ${messageId}`);
64
+ }
65
+ console.log("Message sent. Waiting for daemon response...");
66
+ // Wait for response
67
+ const response = await waitForResponse(messagesPath, messageId, 10000);
68
+ if (!response) {
69
+ console.error("No response from daemon (timeout).");
70
+ console.error("");
71
+ console.error("Make sure the daemon is running on the host:");
72
+ console.error(" ralph daemon start");
73
+ process.exit(1);
74
+ }
75
+ if (debug) {
76
+ console.log(`[notify] Response: ${JSON.stringify(response)}`);
77
+ }
78
+ if (response.success) {
79
+ if (action === "ping") {
80
+ console.log("Daemon is responsive: pong");
81
+ }
82
+ else {
83
+ console.log("Notification sent successfully.");
84
+ if (response.output && debug) {
85
+ console.log(`Output: ${response.output}`);
86
+ }
87
+ }
88
+ }
89
+ else {
90
+ console.error(`Failed: ${response.error}`);
91
+ process.exit(1);
92
+ }
93
+ }
94
+ else {
95
+ // Outside container - use direct notification or message queue
96
+ try {
97
+ const config = loadConfig();
98
+ if (config.notifyCommand) {
99
+ await sendNotification("iteration_complete", message, {
100
+ command: config.notifyCommand,
101
+ debug,
102
+ });
103
+ console.log("Notification sent directly.");
104
+ }
105
+ else {
106
+ console.error("No notifyCommand configured.");
107
+ console.error("Configure notifyCommand in .ralph/config.json");
108
+ process.exit(1);
109
+ }
110
+ }
111
+ catch {
112
+ console.error("Failed to load config. Run 'ralph init' first.");
113
+ process.exit(1);
114
+ }
115
+ }
116
+ }
117
+ function showHelp() {
118
+ console.log(`
119
+ ralph notify - Send notification to host from sandbox
120
+
121
+ USAGE:
122
+ ralph notify [message] Send a notification message
123
+ ralph notify --action <action> [args...] Execute a daemon action
124
+ ralph notify --help Show this help
125
+
126
+ OPTIONS:
127
+ -a, --action <name> Execute a specific daemon action (default: notify)
128
+ -d, --debug Show debug output
129
+ -h, --help Show this help message
130
+
131
+ DESCRIPTION:
132
+ This command sends notifications or executes actions through the ralph
133
+ daemon. Communication happens via a shared file (.ralph/messages.json)
134
+ that is mounted into the container.
135
+
136
+ EXAMPLES:
137
+ # Send a notification
138
+ ralph notify "Build complete!"
139
+ ralph notify "PRD task finished"
140
+
141
+ # Check daemon connectivity
142
+ ralph notify --action ping
143
+
144
+ # Execute custom action (if configured)
145
+ ralph notify --action custom-action arg1 arg2
146
+
147
+ SETUP:
148
+ 1. Configure notification command in .ralph/config.json:
149
+ { "notifyCommand": "ntfy pub mytopic" }
150
+
151
+ 2. Start the daemon on the host:
152
+ ralph daemon start
153
+
154
+ 3. Run the container:
155
+ ralph docker run
156
+
157
+ 4. Send notifications from inside the container:
158
+ ralph notify "Hello from sandbox!"
159
+
160
+ NOTES:
161
+ - The daemon must be running on the host to process messages
162
+ - Communication uses .ralph/messages.json (works on all platforms)
163
+ - Other tools can also read/write to this file for integration
164
+ `);
165
+ }
@@ -4,7 +4,7 @@ import { join } from "path";
4
4
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
5
5
  import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
6
  import { getStreamJsonParser } from "../utils/stream-json.js";
7
- import { sendNotification } from "../utils/notification.js";
7
+ import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
8
8
  export async function once(args) {
9
9
  // Parse flags
10
10
  let debug = false;
@@ -106,7 +106,7 @@ export async function once(args) {
106
106
  // Create provider-specific stream-json parser
107
107
  const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
108
108
  // Notification options for this run
109
- const notifyOptions = { command: config.notifyCommand, debug };
109
+ const notifyOptions = { command: config.notifyCommand, debug, daemonConfig: config.daemon };
110
110
  return new Promise((resolve, reject) => {
111
111
  let output = ""; // Accumulate output for PRD complete detection
112
112
  if (streamJsonEnabled) {
@@ -180,13 +180,13 @@ export async function once(args) {
180
180
  // Send notification based on outcome
181
181
  if (code !== 0) {
182
182
  console.error(`\n${cliConfig.command} exited with code ${code}`);
183
- await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
183
+ await sendNotificationWithDaemonEvents("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
184
184
  }
185
185
  else if (output.includes("<promise>COMPLETE</promise>")) {
186
- await sendNotification("prd_complete", undefined, notifyOptions);
186
+ await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
187
187
  }
188
188
  else {
189
- await sendNotification("iteration_complete", undefined, notifyOptions);
189
+ await sendNotificationWithDaemonEvents("iteration_complete", undefined, notifyOptions);
190
190
  }
191
191
  resolve();
192
192
  });
@@ -208,13 +208,13 @@ export async function once(args) {
208
208
  // Send notification based on outcome
209
209
  if (code !== 0) {
210
210
  console.error(`\n${cliConfig.command} exited with code ${code}`);
211
- await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
211
+ await sendNotificationWithDaemonEvents("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
212
212
  }
213
213
  else if (output.includes("<promise>COMPLETE</promise>")) {
214
- await sendNotification("prd_complete", undefined, notifyOptions);
214
+ await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
215
215
  }
216
216
  else {
217
- await sendNotification("iteration_complete", undefined, notifyOptions);
217
+ await sendNotificationWithDaemonEvents("iteration_complete", undefined, notifyOptions);
218
218
  }
219
219
  resolve();
220
220
  });
@@ -91,8 +91,8 @@ export function prdList(category, passesFilter) {
91
91
  console.log("\nPRD Entries:\n");
92
92
  }
93
93
  filteredPrd.forEach(({ entry, originalIndex }) => {
94
- const status = entry.passes ? "\x1b[32m[PASS]\x1b[0m" : "\x1b[33m[ ]\x1b[0m";
95
- console.log(` ${originalIndex + 1}. ${status} [${entry.category}] ${entry.description}`);
94
+ const statusEmoji = entry.passes ? "" : "";
95
+ console.log(` ${originalIndex + 1}. ${statusEmoji} [${entry.category}] ${entry.description}`);
96
96
  entry.steps.forEach((step, j) => {
97
97
  console.log(` ${j + 1}. ${step}`);
98
98
  });
@@ -5,7 +5,7 @@ import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requir
5
5
  import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
6
  import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
7
7
  import { getStreamJsonParser } from "../utils/stream-json.js";
8
- import { sendNotification } from "../utils/notification.js";
8
+ import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
9
9
  const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
10
10
  /**
11
11
  * Creates a filtered PRD file containing only incomplete items (passes: false).
@@ -51,19 +51,19 @@ function createFilteredPrd(prdPath, baseDir, category) {
51
51
  * Syncs passes flags from prd-tasks.json back to prd.json.
52
52
  * If the LLM marked any item as passes: true in prd-tasks.json,
53
53
  * find the matching item in prd.json and update it.
54
- * Returns the number of items synced.
54
+ * Returns the number of items synced and their names.
55
55
  */
56
56
  function syncPassesFromTasks(tasksPath, prdPath) {
57
57
  // Check if tasks file exists
58
58
  if (!existsSync(tasksPath)) {
59
- return 0;
59
+ return { count: 0, taskNames: [] };
60
60
  }
61
61
  try {
62
62
  const tasksContent = readFileSync(tasksPath, "utf-8");
63
63
  const tasksParsed = JSON.parse(tasksContent);
64
64
  if (!Array.isArray(tasksParsed)) {
65
65
  console.warn("\x1b[33mWarning: prd-tasks.json is not a valid array - skipping sync.\x1b[0m");
66
- return 0;
66
+ return { count: 0, taskNames: [] };
67
67
  }
68
68
  const tasks = tasksParsed;
69
69
  const prdContent = readFileSync(prdPath, "utf-8");
@@ -71,10 +71,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
71
71
  if (!Array.isArray(prdParsed)) {
72
72
  console.warn("\x1b[33mWarning: prd.json is corrupted - skipping sync.\x1b[0m");
73
73
  console.warn("Run \x1b[36mralph fix-prd\x1b[0m after this session to repair.\n");
74
- return 0;
74
+ return { count: 0, taskNames: [] };
75
75
  }
76
76
  const prd = prdParsed;
77
77
  let synced = 0;
78
+ const syncedTaskNames = [];
78
79
  // Find tasks that were marked as passing
79
80
  for (const task of tasks) {
80
81
  if (task.passes === true) {
@@ -85,6 +86,7 @@ function syncPassesFromTasks(tasksPath, prdPath) {
85
86
  if (match && !match.passes) {
86
87
  match.passes = true;
87
88
  synced++;
89
+ syncedTaskNames.push(task.description);
88
90
  }
89
91
  }
90
92
  }
@@ -93,11 +95,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
93
95
  writeFileSync(prdPath, JSON.stringify(prd, null, 2) + "\n");
94
96
  console.log(`\x1b[32mSynced ${synced} completed item(s) from prd-tasks.json to prd.json\x1b[0m`);
95
97
  }
96
- return synced;
98
+ return { count: synced, taskNames: syncedTaskNames };
97
99
  }
98
100
  catch {
99
101
  // Ignore errors - the validation step will handle any issues
100
- return 0;
102
+ return { count: 0, taskNames: [] };
101
103
  }
102
104
  }
103
105
  async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson) {
@@ -544,9 +546,10 @@ export async function run(args) {
544
546
  }
545
547
  console.log("=".repeat(50));
546
548
  // Send notification for PRD completion
547
- await sendNotification("prd_complete", undefined, {
549
+ await sendNotificationWithDaemonEvents("prd_complete", undefined, {
548
550
  command: config.notifyCommand,
549
551
  debug,
552
+ daemonConfig: config.daemon,
550
553
  });
551
554
  break;
552
555
  }
@@ -554,7 +557,16 @@ export async function run(args) {
554
557
  const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson);
555
558
  // Sync any completed items from prd-tasks.json back to prd.json
556
559
  // This catches cases where the LLM updated prd-tasks.json instead of prd.json
557
- syncPassesFromTasks(filteredPrdPath, paths.prd);
560
+ const syncResult = syncPassesFromTasks(filteredPrdPath, paths.prd);
561
+ // Send task_complete notification for each completed task
562
+ for (const taskName of syncResult.taskNames) {
563
+ await sendNotificationWithDaemonEvents("task_complete", `Ralph: Task complete - ${taskName}`, {
564
+ command: config.notifyCommand,
565
+ debug,
566
+ daemonConfig: config.daemon,
567
+ taskName,
568
+ });
569
+ }
558
570
  // Clean up temp file after each iteration
559
571
  try {
560
572
  unlinkSync(filteredPrdPath);
@@ -586,7 +598,7 @@ export async function run(args) {
586
598
  console.log(`Status: ${progressCounts.complete}/${progressCounts.total} complete, ${progressCounts.incomplete} remaining.`);
587
599
  console.log("Check the PRD and task definitions for issues.");
588
600
  // 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 });
601
+ await sendNotificationWithDaemonEvents("run_stopped", `Ralph: Run stopped - no progress after ${MAX_ITERATIONS_WITHOUT_PROGRESS} iterations. ${progressCounts.incomplete} tasks remaining.`, { command: config.notifyCommand, debug, daemonConfig: config.daemon });
590
602
  break;
591
603
  }
592
604
  }
@@ -605,7 +617,7 @@ export async function run(args) {
605
617
  console.error("This usually indicates a configuration error (e.g., missing API key).");
606
618
  console.error("Please check your CLI configuration and try again.");
607
619
  // Send notification about error
608
- await sendNotification("error", `Ralph: CLI failed ${consecutiveFailures} times with exit code ${exitCode}. Check configuration.`, { command: config.notifyCommand, debug });
620
+ await sendNotificationWithDaemonEvents("error", `Ralph: CLI failed ${consecutiveFailures} times with exit code ${exitCode}. Check configuration.`, { command: config.notifyCommand, debug, daemonConfig: config.daemon });
609
621
  break;
610
622
  }
611
623
  console.log("Continuing to next iteration...");
@@ -646,9 +658,10 @@ export async function run(args) {
646
658
  }
647
659
  console.log("=".repeat(50));
648
660
  // Send notification if configured
649
- await sendNotification("prd_complete", undefined, {
661
+ await sendNotificationWithDaemonEvents("prd_complete", undefined, {
650
662
  command: config.notifyCommand,
651
663
  debug,
664
+ daemonConfig: config.daemon,
652
665
  });
653
666
  break;
654
667
  }
@@ -266,6 +266,10 @@
266
266
  { "name": "Hummingbird", "description": "Lightweight Swift web framework" },
267
267
  { "name": "Fluent ORM", "description": "Swift ORM for Vapor" },
268
268
  { "name": "SwiftNIO", "description": "Event-driven network framework" },
269
+ { "name": "SwiftUI", "description": "Apple declarative UI framework" },
270
+ { "name": "Fastlane", "description": "iOS/macOS deployment automation" },
271
+ { "name": "Combine", "description": "Reactive programming framework" },
272
+ { "name": "Swift Testing", "description": "Modern testing framework for Swift" },
269
273
  { "name": "XCTest", "description": "Swift testing framework" },
270
274
  { "name": "PostgreSQL", "description": "Advanced SQL database" },
271
275
  { "name": "SQLite", "description": "Embedded SQL database" },