ralph-cli-sandboxed 0.4.1 → 0.4.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.
Files changed (69) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +9 -9
  3. package/dist/commands/chat.js +13 -12
  4. package/dist/commands/config.js +2 -1
  5. package/dist/commands/daemon.js +4 -3
  6. package/dist/commands/docker.js +102 -66
  7. package/dist/commands/fix-config.js +2 -1
  8. package/dist/commands/fix-prd.js +2 -2
  9. package/dist/commands/init.js +78 -17
  10. package/dist/commands/listen.js +3 -1
  11. package/dist/commands/notify.js +1 -1
  12. package/dist/commands/once.js +17 -9
  13. package/dist/commands/prd.js +4 -1
  14. package/dist/commands/run.js +40 -25
  15. package/dist/commands/slack.js +2 -2
  16. package/dist/config/responder-presets.json +69 -0
  17. package/dist/index.js +1 -1
  18. package/dist/providers/discord.d.ts +28 -0
  19. package/dist/providers/discord.js +227 -14
  20. package/dist/providers/slack.d.ts +41 -1
  21. package/dist/providers/slack.js +389 -8
  22. package/dist/providers/telegram.d.ts +30 -0
  23. package/dist/providers/telegram.js +185 -5
  24. package/dist/responders/claude-code-responder.d.ts +48 -0
  25. package/dist/responders/claude-code-responder.js +203 -0
  26. package/dist/responders/cli-responder.d.ts +62 -0
  27. package/dist/responders/cli-responder.js +298 -0
  28. package/dist/responders/llm-responder.d.ts +135 -0
  29. package/dist/responders/llm-responder.js +582 -0
  30. package/dist/templates/macos-scripts.js +2 -4
  31. package/dist/templates/prompts.js +4 -2
  32. package/dist/tui/ConfigEditor.js +19 -5
  33. package/dist/tui/components/ArrayEditor.js +1 -1
  34. package/dist/tui/components/EditorPanel.js +10 -6
  35. package/dist/tui/components/HelpPanel.d.ts +1 -1
  36. package/dist/tui/components/HelpPanel.js +1 -1
  37. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  38. package/dist/tui/components/KeyValueEditor.js +54 -9
  39. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  40. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  41. package/dist/tui/components/ObjectEditor.js +1 -1
  42. package/dist/tui/components/Preview.js +1 -1
  43. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  44. package/dist/tui/components/RespondersEditor.js +437 -0
  45. package/dist/tui/components/SectionNav.js +27 -3
  46. package/dist/utils/chat-client.d.ts +4 -0
  47. package/dist/utils/chat-client.js +12 -5
  48. package/dist/utils/config.d.ts +84 -0
  49. package/dist/utils/config.js +78 -1
  50. package/dist/utils/daemon-client.d.ts +21 -0
  51. package/dist/utils/daemon-client.js +28 -1
  52. package/dist/utils/llm-client.d.ts +82 -0
  53. package/dist/utils/llm-client.js +185 -0
  54. package/dist/utils/message-queue.js +6 -6
  55. package/dist/utils/notification.d.ts +6 -1
  56. package/dist/utils/notification.js +103 -2
  57. package/dist/utils/prd-validator.js +60 -19
  58. package/dist/utils/prompt.js +22 -12
  59. package/dist/utils/responder-logger.d.ts +47 -0
  60. package/dist/utils/responder-logger.js +129 -0
  61. package/dist/utils/responder-presets.d.ts +92 -0
  62. package/dist/utils/responder-presets.js +156 -0
  63. package/dist/utils/responder.d.ts +88 -0
  64. package/dist/utils/responder.js +207 -0
  65. package/dist/utils/stream-json.js +6 -6
  66. package/docs/CHAT-RESPONDERS.md +785 -0
  67. package/docs/DEVELOPMENT.md +25 -0
  68. package/docs/chat-architecture.md +251 -0
  69. package/package.json +11 -1
@@ -1,10 +1,11 @@
1
1
  import { existsSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } 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, getSkillsForLanguage } from "../templates/prompts.js";
5
- import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection } from "../templates/macos-scripts.js";
6
- import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
4
+ import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage, } from "../templates/prompts.js";
5
+ import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection, } from "../templates/macos-scripts.js";
6
+ import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows, } from "../utils/prompt.js";
7
7
  import { dockerInit } from "./docker.js";
8
+ import { getBundleDisplayOptions, displayOptionToBundleId, bundleToRespondersConfig, getPresetDisplayOptions, displayOptionToPresetId, presetsToRespondersConfig, } from "../utils/responder-presets.js";
8
9
  // Get package root directory (works for both dev and installed package)
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -41,6 +42,7 @@ export async function init(args) {
41
42
  let selectedKey;
42
43
  let selectedTechnologies = [];
43
44
  let selectedSkills = [];
45
+ let selectedResponders = {};
44
46
  let checkCommand;
45
47
  let testCommand;
46
48
  if (useDefaults) {
@@ -62,7 +64,7 @@ export async function init(args) {
62
64
  else {
63
65
  // Step 1: Select CLI provider (first)
64
66
  const providerKeys = Object.keys(CLI_PROVIDERS);
65
- const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
67
+ const providerNames = providerKeys.map((k) => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
66
68
  const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
67
69
  const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
68
70
  selectedCliProviderKey = providerKeys[selectedProviderIndex];
@@ -73,9 +75,13 @@ export async function init(args) {
73
75
  const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
74
76
  const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
75
77
  const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
76
- const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
78
+ const customYoloArgs = customYoloArgsInput.trim()
79
+ ? customYoloArgsInput.trim().split(/\s+/)
80
+ : [];
77
81
  const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
78
- const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
82
+ const customPromptArgs = customPromptArgsInput.trim()
83
+ ? customPromptArgsInput.trim().split(/\s+/)
84
+ : [];
79
85
  cliConfig = {
80
86
  command: customCommand || "claude",
81
87
  args: customArgs,
@@ -94,7 +100,7 @@ export async function init(args) {
94
100
  console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
95
101
  // Step 2: Select language (second)
96
102
  const languageKeys = Object.keys(LANGUAGES);
97
- const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
103
+ const languageNames = languageKeys.map((k) => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
98
104
  const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
99
105
  const selectedIndex = languageNames.indexOf(selectedName);
100
106
  selectedKey = languageKeys[selectedIndex];
@@ -102,11 +108,11 @@ export async function init(args) {
102
108
  console.log(`\nSelected language: ${config.name}`);
103
109
  // Step 3: Select technology stack if available (third)
104
110
  if (config.technologies && config.technologies.length > 0) {
105
- const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
106
- const techNames = config.technologies.map(t => t.name);
111
+ const techOptions = config.technologies.map((t) => `${t.name} - ${t.description}`);
112
+ const techNames = config.technologies.map((t) => t.name);
107
113
  selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
108
114
  // Convert display names back to just technology names for predefined options
109
- selectedTechnologies = selectedTechnologies.map(sel => {
115
+ selectedTechnologies = selectedTechnologies.map((sel) => {
110
116
  const idx = techOptions.indexOf(sel);
111
117
  return idx >= 0 ? techNames[idx] : sel;
112
118
  });
@@ -120,10 +126,10 @@ export async function init(args) {
120
126
  // Step 4: Select skills if available for this language
121
127
  const availableSkills = getSkillsForLanguage(selectedKey);
122
128
  if (availableSkills.length > 0) {
123
- const skillOptions = availableSkills.map(s => `${s.name} - ${s.description}`);
129
+ const skillOptions = availableSkills.map((s) => `${s.name} - ${s.description}`);
124
130
  const selectedSkillNames = await promptMultiSelectWithArrows("Select AI coding rules/skills to enable (optional):", skillOptions);
125
131
  // Convert selected display names to SkillConfig objects
126
- selectedSkills = selectedSkillNames.map(sel => {
132
+ selectedSkills = selectedSkillNames.map((sel) => {
127
133
  const idx = skillOptions.indexOf(sel);
128
134
  const skill = availableSkills[idx];
129
135
  return {
@@ -134,18 +140,52 @@ export async function init(args) {
134
140
  };
135
141
  });
136
142
  if (selectedSkills.length > 0) {
137
- console.log(`\nSelected skills: ${selectedSkills.map(s => s.name).join(", ")}`);
143
+ console.log(`\nSelected skills: ${selectedSkills.map((s) => s.name).join(", ")}`);
138
144
  }
139
145
  else {
140
146
  console.log("\nNo skills selected.");
141
147
  }
142
148
  }
149
+ // Step 5: Select chat responder presets (optional)
150
+ const setupResponders = await promptConfirm("\nWould you like to set up chat responders?", false);
151
+ if (setupResponders) {
152
+ // First, ask if they want a bundle or individual presets
153
+ const selectionType = await promptSelectWithArrows("How would you like to configure responders?", [
154
+ "Use a preset bundle (recommended)",
155
+ "Select individual presets",
156
+ "Skip - configure later",
157
+ ]);
158
+ if (selectionType === "Use a preset bundle (recommended)") {
159
+ const bundleOptions = getBundleDisplayOptions();
160
+ const selectedBundle = await promptSelectWithArrows("Select a responder bundle:", bundleOptions);
161
+ const bundleId = displayOptionToBundleId(selectedBundle);
162
+ if (bundleId) {
163
+ selectedResponders = bundleToRespondersConfig(bundleId);
164
+ console.log(`\nConfigured responders from bundle: ${Object.keys(selectedResponders).join(", ")}`);
165
+ }
166
+ }
167
+ else if (selectionType === "Select individual presets") {
168
+ const presetOptions = getPresetDisplayOptions();
169
+ const selectedPresets = await promptMultiSelectWithArrows("Select responder presets to enable:", presetOptions);
170
+ // Convert display options back to preset IDs
171
+ const presetIds = selectedPresets
172
+ .map(displayOptionToPresetId)
173
+ .filter((id) => id !== undefined);
174
+ if (presetIds.length > 0) {
175
+ selectedResponders = presetsToRespondersConfig(presetIds);
176
+ console.log(`\nConfigured responders: ${Object.keys(selectedResponders).join(", ")}`);
177
+ }
178
+ }
179
+ else {
180
+ console.log("\nSkipping responders - you can configure them later in config.json");
181
+ }
182
+ }
143
183
  // Allow custom commands for "none" language
144
184
  checkCommand = config.checkCommand;
145
185
  testCommand = config.testCommand;
146
186
  if (selectedKey === "none") {
147
- checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
148
- testCommand = await promptInput("Enter your test command: ") || testCommand;
187
+ checkCommand = (await promptInput("\nEnter your type/build check command: ")) || checkCommand;
188
+ testCommand = (await promptInput("Enter your test command: ")) || testCommand;
149
189
  }
150
190
  }
151
191
  const finalConfig = {
@@ -154,7 +194,9 @@ export async function init(args) {
154
194
  testCommand,
155
195
  };
156
196
  // Generate image name from directory name
157
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
197
+ const projectName = basename(cwd)
198
+ .toLowerCase()
199
+ .replace(/[^a-z0-9-]/g, "-");
158
200
  const imageName = `ralph-${projectName}`;
159
201
  // Generate macOS development actions for Swift + SwiftUI projects
160
202
  const macOsActions = {};
@@ -197,6 +239,14 @@ export async function init(args) {
197
239
  // CLI configuration
198
240
  cli: cliConfig,
199
241
  cliProvider: selectedCliProviderKey,
242
+ // LLM providers for chat responders (empty by default, uses defaults from env vars)
243
+ // Example:
244
+ // llmProviders: {
245
+ // "claude": { type: "anthropic", model: "claude-sonnet-4-20250514" },
246
+ // "gpt4": { type: "openai", model: "gpt-4o" },
247
+ // "local": { type: "ollama", model: "llama3", baseUrl: "http://localhost:11434" }
248
+ // }
249
+ llmProviders: {},
200
250
  // Optional fields with defaults/empty values for discoverability
201
251
  notifyCommand: "",
202
252
  technologies: selectedTechnologies.length > 0 ? selectedTechnologies : [],
@@ -244,6 +294,17 @@ export async function init(args) {
244
294
  botToken: "",
245
295
  allowedChatIds: [],
246
296
  },
297
+ // Chat responders - handle incoming messages based on triggers
298
+ // Special "default" responder handles messages that don't match any trigger
299
+ // Trigger patterns: "@name" for mentions, "keyword" for prefix matching
300
+ // Use "ralph init" to select from preset bundles, or configure manually:
301
+ // responders: {
302
+ // "default": { type: "llm", provider: "anthropic", systemPrompt: "You are a helpful assistant for {{project}}." },
303
+ // "qa": { type: "llm", trigger: "@qa", provider: "anthropic", systemPrompt: "Answer questions about the codebase." },
304
+ // "code": { type: "claude-code", trigger: "@code" },
305
+ // "lint": { type: "cli", trigger: "!lint", command: "npm run lint" }
306
+ // }
307
+ responders: selectedResponders,
247
308
  },
248
309
  // Daemon configuration for sandbox-to-host communication
249
310
  daemon: {
@@ -332,7 +393,7 @@ docker/.config-hash
332
393
  const swiftProjectName = basename(cwd)
333
394
  .replace(/[^a-zA-Z0-9]+/g, " ")
334
395
  .split(" ")
335
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
396
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
336
397
  .join("") || "App";
337
398
  if (!existsSync(genXcodePath)) {
338
399
  writeFileSync(genXcodePath, generateGenXcodeScript(swiftProjectName));
@@ -190,7 +190,9 @@ async function processMessage(message, messagesPath, debug) {
190
190
  proc.unref();
191
191
  respondToMessage(messagesPath, message.id, {
192
192
  success: true,
193
- output: message.args?.length ? `Ralph run started (category: ${message.args[0]})` : "Ralph run started",
193
+ output: message.args?.length
194
+ ? `Ralph run started (category: ${message.args[0]})`
195
+ : "Ralph run started",
194
196
  });
195
197
  break;
196
198
  }
@@ -1,7 +1,7 @@
1
1
  import { isRunningInContainer } from "../utils/config.js";
2
2
  import { sendNotification } from "../utils/notification.js";
3
3
  import { loadConfig } from "../utils/config.js";
4
- import { getMessagesPath, sendMessage, waitForResponse, } from "../utils/message-queue.js";
4
+ import { getMessagesPath, sendMessage, waitForResponse } from "../utils/message-queue.js";
5
5
  import { existsSync } from "fs";
6
6
  /**
7
7
  * Send a notification - works both inside and outside containers.
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import { existsSync, appendFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
- import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
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
7
  import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
@@ -58,10 +58,7 @@ export async function once(args) {
58
58
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
59
59
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
60
60
  const promptArgs = cliConfig.promptArgs ?? ["-p"];
61
- const cliArgs = [
62
- ...(cliConfig.args ?? []),
63
- ...yoloArgs,
64
- ];
61
+ const cliArgs = [...(cliConfig.args ?? []), ...yoloArgs];
65
62
  // Build the prompt value based on whether fileArgs is configured
66
63
  // fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
67
64
  // Otherwise, use @file syntax embedded in the prompt (Claude Code style)
@@ -98,7 +95,7 @@ export async function once(args) {
98
95
  }
99
96
  cliArgs.push(...promptArgs, promptValue);
100
97
  if (debug) {
101
- console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
98
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}\n`);
102
99
  if (jsonLogPath) {
103
100
  console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
104
101
  }
@@ -106,7 +103,12 @@ export async function once(args) {
106
103
  // Create provider-specific stream-json parser
107
104
  const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
108
105
  // Notification options for this run
109
- const notifyOptions = { command: config.notifyCommand, debug, daemonConfig: config.daemon };
106
+ const notifyOptions = {
107
+ command: config.notifyCommand,
108
+ debug,
109
+ daemonConfig: config.daemon,
110
+ chatConfig: config.chat,
111
+ };
110
112
  return new Promise((resolve, reject) => {
111
113
  let output = ""; // Accumulate output for PRD complete detection
112
114
  if (streamJsonEnabled) {
@@ -181,7 +183,10 @@ export async function once(args) {
181
183
  if (code !== 0) {
182
184
  console.error(`\n${cliConfig.command} exited with code ${code}`);
183
185
  const errorMessage = `Iteration failed with exit code ${code}`;
184
- await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, { ...notifyOptions, errorMessage });
186
+ await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
187
+ ...notifyOptions,
188
+ errorMessage,
189
+ });
185
190
  }
186
191
  else if (output.includes("<promise>COMPLETE</promise>")) {
187
192
  await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
@@ -210,7 +215,10 @@ export async function once(args) {
210
215
  if (code !== 0) {
211
216
  console.error(`\n${cliConfig.command} exited with code ${code}`);
212
217
  const errorMessage = `Iteration failed with exit code ${code}`;
213
- await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, { ...notifyOptions, errorMessage });
218
+ await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
219
+ ...notifyOptions,
220
+ errorMessage,
221
+ });
214
222
  }
215
223
  else if (output.includes("<promise>COMPLETE</promise>")) {
216
224
  await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
@@ -234,7 +234,10 @@ export function parseListArgs(args) {
234
234
  else if (args[i] === "--passes" || args[i] === "--passed") {
235
235
  passesFilter = true;
236
236
  }
237
- else if (args[i] === "--no-passes" || args[i] === "--no-passed" || args[i] === "--not-passed" || args[i] === "--not-passes") {
237
+ else if (args[i] === "--no-passes" ||
238
+ args[i] === "--no-passed" ||
239
+ args[i] === "--not-passed" ||
240
+ args[i] === "--not-passes") {
238
241
  passesFilter = false;
239
242
  }
240
243
  }
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "child_process";
2
2
  import { existsSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
- import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
4
+ import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer, } from "../utils/config.js";
5
5
  import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
- import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
6
+ import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences, } from "../utils/prd-validator.js";
7
7
  import { getStreamJsonParser } from "../utils/stream-json.js";
8
8
  import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
9
9
  const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
@@ -32,10 +32,10 @@ function createFilteredPrd(prdPath, baseDir, category) {
32
32
  process.exit(1);
33
33
  }
34
34
  const items = parsed;
35
- let filteredItems = items.filter(item => item.passes === false);
35
+ let filteredItems = items.filter((item) => item.passes === false);
36
36
  // Apply category filter if specified
37
37
  if (category) {
38
- filteredItems = filteredItems.filter(item => item.category === category);
38
+ filteredItems = filteredItems.filter((item) => item.category === category);
39
39
  }
40
40
  // Expand @{filepath} references in description and steps
41
41
  const expandedItems = expandPrdFileReferences(filteredItems, baseDir);
@@ -44,7 +44,7 @@ function createFilteredPrd(prdPath, baseDir, category) {
44
44
  writeFileSync(tempPath, JSON.stringify(expandedItems, null, 2));
45
45
  return {
46
46
  tempPath,
47
- hasIncomplete: filteredItems.length > 0
47
+ hasIncomplete: filteredItems.length > 0,
48
48
  };
49
49
  }
50
50
  /**
@@ -88,7 +88,7 @@ function syncPassesFromTasks(tasksPath, prdPath) {
88
88
  for (const task of tasks) {
89
89
  if (task.passes === true) {
90
90
  // Find matching item in prd by description
91
- const match = prd.find(item => item.description === task.description ||
91
+ const match = prd.find((item) => item.description === task.description ||
92
92
  item.description.includes(task.description) ||
93
93
  task.description.includes(item.description));
94
94
  if (match && !match.passes) {
@@ -116,9 +116,7 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
116
116
  let jsonLogPath;
117
117
  let lineBuffer = ""; // Buffer for incomplete JSON lines
118
118
  // Build CLI arguments: config args + yolo args + model args + prompt args
119
- const cliArgs = [
120
- ...(cliConfig.args ?? []),
121
- ];
119
+ const cliArgs = [...(cliConfig.args ?? [])];
122
120
  // Only add yolo args when running in a container
123
121
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
124
122
  if (sandboxed) {
@@ -163,7 +161,7 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
163
161
  }
164
162
  cliArgs.push(...promptArgs, promptValue);
165
163
  if (debug) {
166
- console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
164
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}\n`);
167
165
  if (jsonLogPath) {
168
166
  console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
169
167
  }
@@ -254,7 +252,7 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
254
252
  * Sleep for the specified number of milliseconds.
255
253
  */
256
254
  function sleep(ms) {
257
- return new Promise(resolve => setTimeout(resolve, ms));
255
+ return new Promise((resolve) => setTimeout(resolve, ms));
258
256
  }
259
257
  /**
260
258
  * Formats elapsed time in a human-readable format.
@@ -299,14 +297,14 @@ function countPrdItems(prdPath, category) {
299
297
  const items = parsed;
300
298
  let filteredItems = items;
301
299
  if (category) {
302
- filteredItems = items.filter(item => item.category === category);
300
+ filteredItems = items.filter((item) => item.category === category);
303
301
  }
304
- const complete = filteredItems.filter(item => item.passes === true).length;
305
- const incomplete = filteredItems.filter(item => item.passes === false).length;
302
+ const complete = filteredItems.filter((item) => item.passes === true).length;
303
+ const incomplete = filteredItems.filter((item) => item.passes === false).length;
306
304
  return {
307
305
  total: filteredItems.length,
308
306
  complete,
309
- incomplete
307
+ incomplete,
310
308
  };
311
309
  }
312
310
  /**
@@ -340,7 +338,7 @@ function validateAndRecoverPrd(prdPath, validPrd) {
340
338
  console.log("\x1b[32mRecovered: restored valid PRD structure.\x1b[0m");
341
339
  }
342
340
  if (mergeResult.warnings.length > 0) {
343
- mergeResult.warnings.forEach(w => console.log(` \x1b[33m${w}\x1b[0m`));
341
+ mergeResult.warnings.forEach((w) => console.log(` \x1b[33m${w}\x1b[0m`));
344
342
  }
345
343
  return { recovered: true, itemsUpdated: mergeResult.itemsUpdated };
346
344
  }
@@ -434,13 +432,15 @@ export async function run(args) {
434
432
  // Only use provider's streamJsonArgs if defined, otherwise empty array (no special args)
435
433
  // This allows providers without JSON streaming to still have output displayed
436
434
  const streamJsonArgs = providerConfig?.streamJsonArgs ?? [];
437
- const streamJson = streamJsonConfig?.enabled ? {
438
- enabled: true,
439
- saveRawJson: streamJsonConfig.saveRawJson !== false, // default true
440
- outputDir: config.docker?.asciinema?.outputDir || ".recordings",
441
- args: streamJsonArgs,
442
- parser: getStreamJsonParser(config.cliProvider, debug),
443
- } : undefined;
435
+ const streamJson = streamJsonConfig?.enabled
436
+ ? {
437
+ enabled: true,
438
+ saveRawJson: streamJsonConfig.saveRawJson !== false, // default true
439
+ outputDir: config.docker?.asciinema?.outputDir || ".recordings",
440
+ args: streamJsonArgs,
441
+ parser: getStreamJsonParser(config.cliProvider, debug),
442
+ }
443
+ : undefined;
444
444
  // Progress tracking: stop only if no tasks complete after N iterations
445
445
  const MAX_ITERATIONS_WITHOUT_PROGRESS = 3;
446
446
  // Get requested iteration count (may be adjusted dynamically)
@@ -615,6 +615,7 @@ export async function run(args) {
615
615
  command: config.notifyCommand,
616
616
  debug,
617
617
  daemonConfig: config.daemon,
618
+ chatConfig: config.chat,
618
619
  });
619
620
  break;
620
621
  }
@@ -629,6 +630,7 @@ export async function run(args) {
629
630
  command: config.notifyCommand,
630
631
  debug,
631
632
  daemonConfig: config.daemon,
633
+ chatConfig: config.chat,
632
634
  taskName,
633
635
  });
634
636
  }
@@ -664,7 +666,13 @@ export async function run(args) {
664
666
  console.log("Check the PRD and task definitions for issues.");
665
667
  // Send notification about stopped run
666
668
  const stoppedMessage = `No progress after ${MAX_ITERATIONS_WITHOUT_PROGRESS} iterations. ${progressCounts.incomplete} tasks remaining.`;
667
- await sendNotificationWithDaemonEvents("run_stopped", `Ralph: Run stopped - ${stoppedMessage}`, { command: config.notifyCommand, debug, daemonConfig: config.daemon, errorMessage: stoppedMessage });
669
+ await sendNotificationWithDaemonEvents("run_stopped", `Ralph: Run stopped - ${stoppedMessage}`, {
670
+ command: config.notifyCommand,
671
+ debug,
672
+ daemonConfig: config.daemon,
673
+ chatConfig: config.chat,
674
+ errorMessage: stoppedMessage,
675
+ });
668
676
  break;
669
677
  }
670
678
  }
@@ -684,7 +692,13 @@ export async function run(args) {
684
692
  console.error("Please check your CLI configuration and try again.");
685
693
  // Send notification about error
686
694
  const errorMessage = `CLI failed ${consecutiveFailures} times with exit code ${exitCode}. Check configuration.`;
687
- await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, { command: config.notifyCommand, debug, daemonConfig: config.daemon, errorMessage });
695
+ await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
696
+ command: config.notifyCommand,
697
+ debug,
698
+ daemonConfig: config.daemon,
699
+ chatConfig: config.chat,
700
+ errorMessage,
701
+ });
688
702
  break;
689
703
  }
690
704
  console.log("Continuing to next iteration...");
@@ -729,6 +743,7 @@ export async function run(args) {
729
743
  command: config.notifyCommand,
730
744
  debug,
731
745
  daemonConfig: config.daemon,
746
+ chatConfig: config.chat,
732
747
  });
733
748
  break;
734
749
  }
@@ -106,12 +106,12 @@ async function createSlackApp(configToken, appName) {
106
106
  const response = await fetch("https://slack.com/api/apps.manifest.create", {
107
107
  method: "POST",
108
108
  headers: {
109
- "Authorization": `Bearer ${configToken}`,
109
+ Authorization: `Bearer ${configToken}`,
110
110
  "Content-Type": "application/json",
111
111
  },
112
112
  body: JSON.stringify({ manifest: JSON.stringify(manifest) }),
113
113
  });
114
- const data = await response.json();
114
+ const data = (await response.json());
115
115
  if (!data.ok) {
116
116
  console.error(`\nError creating Slack app: ${data.error}`);
117
117
  if (data.errors) {
@@ -0,0 +1,69 @@
1
+ {
2
+ "presets": {
3
+ "qa": {
4
+ "name": "Q&A Assistant",
5
+ "description": "LLM responder optimized for answering questions about the codebase",
6
+ "type": "llm",
7
+ "trigger": "@qa",
8
+ "provider": "anthropic",
9
+ "systemPrompt": "You are a knowledgeable Q&A assistant for the {{project}} project. Your role is to:\n\n1. Answer questions accurately and concisely about the codebase, its architecture, and functionality\n2. Explain how specific features, components, or modules work\n3. Help developers understand code patterns, conventions, and best practices used in the project\n4. Clarify dependencies, APIs, and integration points\n5. Provide context about why certain design decisions were made\n\nKeep responses focused and practical. If you're unsure about something, say so rather than guessing. Reference specific files, functions, or code sections when helpful.",
10
+ "timeout": 60000,
11
+ "maxLength": 2000
12
+ },
13
+ "reviewer": {
14
+ "name": "Code Reviewer",
15
+ "description": "LLM responder for code review feedback and suggestions",
16
+ "type": "llm",
17
+ "trigger": "@review",
18
+ "provider": "anthropic",
19
+ "systemPrompt": "You are an expert code reviewer for the {{project}} project. Your role is to:\n\n1. Review code changes, diffs, or code snippets shared with you\n2. Identify potential bugs, security issues, and edge cases\n3. Suggest improvements for readability, maintainability, and performance\n4. Check for adherence to project conventions and best practices\n5. Highlight what's done well alongside areas for improvement\n\nProvide constructive, actionable feedback. Prioritize critical issues over minor style preferences. When suggesting changes, explain the reasoning. Be specific about file locations and line numbers when possible.",
20
+ "timeout": 90000,
21
+ "maxLength": 2500
22
+ },
23
+ "architect": {
24
+ "name": "Architecture Advisor",
25
+ "description": "LLM responder for architecture and design discussions",
26
+ "type": "llm",
27
+ "trigger": "@arch",
28
+ "provider": "anthropic",
29
+ "systemPrompt": "You are a software architecture advisor for the {{project}} project. Your role is to:\n\n1. Discuss high-level system design and architectural decisions\n2. Evaluate trade-offs between different approaches (scalability, maintainability, performance)\n3. Recommend design patterns and architectural styles appropriate for the use case\n4. Help plan major refactors or new feature implementations\n5. Identify potential technical debt and suggest mitigation strategies\n\nConsider the existing codebase constraints when making recommendations. Provide clear reasoning for architectural choices. Focus on practical, incremental improvements rather than complete rewrites. Draw diagrams in ASCII when helpful.",
30
+ "timeout": 120000,
31
+ "maxLength": 3000
32
+ },
33
+ "explain": {
34
+ "name": "Code Explainer",
35
+ "description": "LLM responder focused on explaining code in detail",
36
+ "type": "llm",
37
+ "trigger": "@explain",
38
+ "provider": "anthropic",
39
+ "systemPrompt": "You are a code explanation specialist for the {{project}} project. Your role is to:\n\n1. Break down complex code into understandable parts\n2. Explain the purpose and behavior of functions, classes, and modules\n3. Trace data flow and control flow through the codebase\n4. Describe how different components interact with each other\n5. Provide context about the 'why' behind implementation choices\n\nTailor explanations to the apparent experience level of the questioner. Use analogies and examples when helpful. Walk through code step-by-step when explaining algorithms or complex logic. Be thorough but avoid unnecessary jargon.",
40
+ "timeout": 60000,
41
+ "maxLength": 2500
42
+ },
43
+ "code": {
44
+ "name": "Code Editor",
45
+ "description": "Claude Code responder for making file modifications",
46
+ "type": "claude-code",
47
+ "trigger": "@code",
48
+ "timeout": 300000,
49
+ "maxLength": 2000
50
+ }
51
+ },
52
+ "bundles": {
53
+ "standard": {
54
+ "name": "Standard Bundle",
55
+ "description": "Common responders for development workflow (qa, reviewer, code)",
56
+ "presets": ["qa", "reviewer", "code"]
57
+ },
58
+ "full": {
59
+ "name": "Full Bundle",
60
+ "description": "All available responders",
61
+ "presets": ["qa", "reviewer", "architect", "explain", "code"]
62
+ },
63
+ "minimal": {
64
+ "name": "Minimal Bundle",
65
+ "description": "Just the essentials (qa, code)",
66
+ "presets": ["qa", "code"]
67
+ }
68
+ }
69
+ }
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { help } from "./commands/help.js";
6
6
  import { init } from "./commands/init.js";
7
7
  import { once } from "./commands/once.js";
8
8
  import { run } from "./commands/run.js";
9
- import { prd, prdAdd, prdList, prdStatus, prdToggle, prdClean, parseListArgs } from "./commands/prd.js";
9
+ import { prd, prdAdd, prdList, prdStatus, prdToggle, prdClean, parseListArgs, } from "./commands/prd.js";
10
10
  import { docker } from "./commands/docker.js";
11
11
  import { prompt } from "./commands/prompt.js";
12
12
  import { fixPrd } from "./commands/fix-prd.js";
@@ -14,7 +14,35 @@ export declare class DiscordChatClient implements ChatClient {
14
14
  private client;
15
15
  private onCommand;
16
16
  private onMessage;
17
+ private responderMatcher;
18
+ private respondersConfig;
19
+ private botUserId;
17
20
  constructor(settings: DiscordSettings, debug?: boolean);
21
+ /**
22
+ * Initialize responder matching from config.
23
+ */
24
+ private initializeResponders;
25
+ /**
26
+ * Execute a responder and return the result.
27
+ */
28
+ private executeResponder;
29
+ /**
30
+ * Handle a message that might match a responder.
31
+ * Returns true if a responder was matched and executed.
32
+ */
33
+ private handleResponderMessage;
34
+ /**
35
+ * Check if the bot is mentioned in a message.
36
+ */
37
+ private isBotMentioned;
38
+ /**
39
+ * Remove bot mention from message text.
40
+ */
41
+ private removeBotMention;
42
+ /**
43
+ * Check if this is a DM (direct message) channel.
44
+ */
45
+ private isDMChannel;
18
46
  /**
19
47
  * Check if a guild ID is allowed.
20
48
  */