ralph-cli-sandboxed 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/chat.js +320 -2
- package/dist/commands/docker.js +23 -18
- package/dist/commands/run.js +20 -11
- package/dist/providers/slack.js +1 -1
- package/dist/utils/chat-client.js +1 -0
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, watch } from "fs";
|
|
6
6
|
import { join, basename, extname } from "path";
|
|
7
|
-
import { spawn } from "child_process";
|
|
7
|
+
import { execSync, spawn } from "child_process";
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
-
import { loadConfig, getRalphDir, isRunningInContainer, getPrdFiles } from "../utils/config.js";
|
|
9
|
+
import { loadConfig, getRalphDir, isRunningInContainer, getPrdFiles, loadBranchState, getProjectName as getConfigProjectName } from "../utils/config.js";
|
|
10
10
|
import { createTelegramClient } from "../providers/telegram.js";
|
|
11
11
|
import { createSlackClient } from "../providers/slack.js";
|
|
12
12
|
import { createDiscordClient } from "../providers/discord.js";
|
|
@@ -163,6 +163,290 @@ function addPrdTask(description) {
|
|
|
163
163
|
return false;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Get the current git branch in the project directory.
|
|
168
|
+
*/
|
|
169
|
+
function getBaseBranch() {
|
|
170
|
+
try {
|
|
171
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
172
|
+
encoding: "utf-8",
|
|
173
|
+
cwd: process.cwd(),
|
|
174
|
+
}).trim();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return "main";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if a git branch exists.
|
|
182
|
+
*/
|
|
183
|
+
function branchExists(branch) {
|
|
184
|
+
try {
|
|
185
|
+
execSync(`git rev-parse --verify "${branch}"`, {
|
|
186
|
+
stdio: "pipe",
|
|
187
|
+
cwd: process.cwd(),
|
|
188
|
+
});
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Convert branch name to worktree directory name.
|
|
197
|
+
*/
|
|
198
|
+
function branchToWorktreeName(branch) {
|
|
199
|
+
const projectName = getConfigProjectName();
|
|
200
|
+
return `${projectName}_${branch.replace(/\//g, "-")}`;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Handle /branch list — show branches from PRD grouped by branch field.
|
|
204
|
+
*/
|
|
205
|
+
async function handleBranchList(chatId, client, state) {
|
|
206
|
+
const prdFiles = getPrdFiles();
|
|
207
|
+
if (prdFiles.none || !prdFiles.primary) {
|
|
208
|
+
await client.sendMessage(chatId, `${state.projectName}: No PRD file found.`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const content = readFileSync(prdFiles.primary, "utf-8");
|
|
212
|
+
const items = parsePrdContent(prdFiles.primary, content);
|
|
213
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
214
|
+
await client.sendMessage(chatId, `${state.projectName}: No PRD items found.`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const activeBranch = loadBranchState();
|
|
218
|
+
// Group items by branch
|
|
219
|
+
const branchGroups = new Map();
|
|
220
|
+
const noBranchItems = [];
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
if (item.branch) {
|
|
223
|
+
const group = branchGroups.get(item.branch) || [];
|
|
224
|
+
group.push(item);
|
|
225
|
+
branchGroups.set(item.branch, group);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
noBranchItems.push(item);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (branchGroups.size === 0 && noBranchItems.length === 0) {
|
|
232
|
+
await client.sendMessage(chatId, `${state.projectName}: No PRD items found.`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const lines = [`${state.projectName}: Branches\n`];
|
|
236
|
+
const sortedBranches = [...branchGroups.keys()].sort();
|
|
237
|
+
for (const branchName of sortedBranches) {
|
|
238
|
+
const branchItems = branchGroups.get(branchName);
|
|
239
|
+
const passing = branchItems.filter((e) => e.passes === true).length;
|
|
240
|
+
const total = branchItems.length;
|
|
241
|
+
const allPassing = passing === total;
|
|
242
|
+
const isActive = activeBranch?.currentBranch === branchName;
|
|
243
|
+
const icon = allPassing ? "[OK]" : "[ ]";
|
|
244
|
+
const active = isActive ? " << active" : "";
|
|
245
|
+
lines.push(` ${icon} ${branchName} ${passing}/${total}${active}`);
|
|
246
|
+
}
|
|
247
|
+
if (noBranchItems.length > 0) {
|
|
248
|
+
const passing = noBranchItems.filter((e) => e.passes === true).length;
|
|
249
|
+
const total = noBranchItems.length;
|
|
250
|
+
const icon = passing === total ? "[OK]" : "[ ]";
|
|
251
|
+
lines.push(` ${icon} (no branch) ${passing}/${total}`);
|
|
252
|
+
}
|
|
253
|
+
await client.sendMessage(chatId, lines.join("\n"));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Handle /branch pr <name> — add a PRD item to create a pull request.
|
|
257
|
+
*/
|
|
258
|
+
async function handleBranchPr(args, chatId, client, state) {
|
|
259
|
+
const branchName = args[0];
|
|
260
|
+
if (!branchName) {
|
|
261
|
+
const usage = client.provider === "slack"
|
|
262
|
+
? "/ralph branch pr <branch-name>"
|
|
263
|
+
: "/branch pr <branch-name>";
|
|
264
|
+
await client.sendMessage(chatId, `${state.projectName}: Usage: ${usage}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!branchExists(branchName)) {
|
|
268
|
+
await client.sendMessage(chatId, `${state.projectName}: Branch "${branchName}" does not exist.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const baseBranch = getBaseBranch();
|
|
272
|
+
const prdFiles = getPrdFiles();
|
|
273
|
+
if (prdFiles.none || !prdFiles.primary) {
|
|
274
|
+
await client.sendMessage(chatId, `${state.projectName}: No PRD file found.`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const content = readFileSync(prdFiles.primary, "utf-8");
|
|
278
|
+
const items = parsePrdContent(prdFiles.primary, content);
|
|
279
|
+
if (!Array.isArray(items)) {
|
|
280
|
+
await client.sendMessage(chatId, `${state.projectName}: Failed to parse PRD file.`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
items.push({
|
|
284
|
+
category: "feature",
|
|
285
|
+
description: `Create a pull request from \`${branchName}\` into \`${baseBranch}\``,
|
|
286
|
+
steps: [
|
|
287
|
+
`Ensure all changes on \`${branchName}\` are committed`,
|
|
288
|
+
`Push \`${branchName}\` to the remote if not already pushed`,
|
|
289
|
+
`Create a pull request from \`${branchName}\` into \`${baseBranch}\` using the appropriate tool (e.g. gh pr create)`,
|
|
290
|
+
"Include a descriptive title and summary of the changes in the PR",
|
|
291
|
+
],
|
|
292
|
+
passes: false,
|
|
293
|
+
branch: branchName,
|
|
294
|
+
});
|
|
295
|
+
const ext = extname(prdFiles.primary).toLowerCase();
|
|
296
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
297
|
+
writeFileSync(prdFiles.primary, YAML.stringify(items));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
writeFileSync(prdFiles.primary, JSON.stringify(items, null, 2) + "\n");
|
|
301
|
+
}
|
|
302
|
+
await client.sendMessage(chatId, `${state.projectName}: Added PRD entry: Create PR for ${branchName} -> ${baseBranch}`);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Handle /branch merge <name> — merge branch into base branch.
|
|
306
|
+
* Skips confirmation (user explicitly typed the command in chat).
|
|
307
|
+
*/
|
|
308
|
+
async function handleBranchMerge(args, chatId, client, state) {
|
|
309
|
+
const branchName = args[0];
|
|
310
|
+
if (!branchName) {
|
|
311
|
+
const usage = client.provider === "slack"
|
|
312
|
+
? "/ralph branch merge <branch-name>"
|
|
313
|
+
: "/branch merge <branch-name>";
|
|
314
|
+
await client.sendMessage(chatId, `${state.projectName}: Usage: ${usage}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (!branchExists(branchName)) {
|
|
318
|
+
await client.sendMessage(chatId, `${state.projectName}: Branch "${branchName}" does not exist.`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const baseBranch = getBaseBranch();
|
|
322
|
+
const cwd = process.cwd();
|
|
323
|
+
try {
|
|
324
|
+
execSync(`git merge "${branchName}" --no-edit`, { stdio: "pipe", cwd });
|
|
325
|
+
await client.sendMessage(chatId, `${state.projectName}: Merged "${branchName}" into "${baseBranch}".`);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Check for merge conflicts
|
|
329
|
+
let conflictingFiles = [];
|
|
330
|
+
try {
|
|
331
|
+
const status = execSync("git status --porcelain", { encoding: "utf-8", cwd });
|
|
332
|
+
conflictingFiles = status
|
|
333
|
+
.split("\n")
|
|
334
|
+
.filter((line) => line.startsWith("UU") || line.startsWith("AA") || line.startsWith("DD") ||
|
|
335
|
+
line.startsWith("AU") || line.startsWith("UA") || line.startsWith("DU") ||
|
|
336
|
+
line.startsWith("UD"))
|
|
337
|
+
.map((line) => line.substring(3).trim());
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Ignore status errors
|
|
341
|
+
}
|
|
342
|
+
if (conflictingFiles.length > 0) {
|
|
343
|
+
// Abort the merge
|
|
344
|
+
try {
|
|
345
|
+
execSync("git merge --abort", { stdio: "pipe", cwd });
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Ignore abort errors
|
|
349
|
+
}
|
|
350
|
+
await client.sendMessage(chatId, `${state.projectName}: Merge conflict! Conflicting files:\n${conflictingFiles.join("\n")}\nMerge aborted.`);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
try {
|
|
354
|
+
execSync("git merge --abort", { stdio: "pipe", cwd });
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Ignore
|
|
358
|
+
}
|
|
359
|
+
await client.sendMessage(chatId, `${state.projectName}: Merge of "${branchName}" failed. Merge aborted.`);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Clean up worktree if it exists
|
|
364
|
+
const dirName = branchToWorktreeName(branchName);
|
|
365
|
+
const config = loadConfig();
|
|
366
|
+
const worktreesPath = config.docker?.worktreesPath;
|
|
367
|
+
if (worktreesPath) {
|
|
368
|
+
const worktreePath = join(worktreesPath, dirName);
|
|
369
|
+
if (existsSync(worktreePath)) {
|
|
370
|
+
try {
|
|
371
|
+
execSync(`git worktree remove "${worktreePath}"`, { stdio: "pipe", cwd });
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// Non-critical, ignore
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Handle /branch delete <name> — delete branch, worktree, and untag PRD items.
|
|
381
|
+
* Skips confirmation (user explicitly typed the command in chat).
|
|
382
|
+
*/
|
|
383
|
+
async function handleBranchDelete(args, chatId, client, state) {
|
|
384
|
+
const branchName = args[0];
|
|
385
|
+
if (!branchName) {
|
|
386
|
+
const usage = client.provider === "slack"
|
|
387
|
+
? "/ralph branch delete <branch-name>"
|
|
388
|
+
: "/branch delete <branch-name>";
|
|
389
|
+
await client.sendMessage(chatId, `${state.projectName}: Usage: ${usage}`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!branchExists(branchName)) {
|
|
393
|
+
await client.sendMessage(chatId, `${state.projectName}: Branch "${branchName}" does not exist.`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const cwd = process.cwd();
|
|
397
|
+
const results = [];
|
|
398
|
+
// Step 1: Remove worktree if it exists
|
|
399
|
+
const dirName = branchToWorktreeName(branchName);
|
|
400
|
+
const config = loadConfig();
|
|
401
|
+
const worktreesPath = config.docker?.worktreesPath;
|
|
402
|
+
if (worktreesPath) {
|
|
403
|
+
const worktreePath = join(worktreesPath, dirName);
|
|
404
|
+
if (existsSync(worktreePath)) {
|
|
405
|
+
try {
|
|
406
|
+
execSync(`git worktree remove "${worktreePath}" --force`, { stdio: "pipe", cwd });
|
|
407
|
+
results.push("Worktree removed.");
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
results.push("Warning: Could not remove worktree.");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Step 2: Delete the git branch
|
|
415
|
+
try {
|
|
416
|
+
execSync(`git branch -D "${branchName}"`, { stdio: "pipe", cwd });
|
|
417
|
+
results.push("Branch deleted.");
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
results.push("Warning: Could not delete git branch.");
|
|
421
|
+
}
|
|
422
|
+
// Step 3: Remove branch tag from PRD items
|
|
423
|
+
const prdFiles = getPrdFiles();
|
|
424
|
+
if (!prdFiles.none && prdFiles.primary) {
|
|
425
|
+
const content = readFileSync(prdFiles.primary, "utf-8");
|
|
426
|
+
const items = parsePrdContent(prdFiles.primary, content);
|
|
427
|
+
if (Array.isArray(items)) {
|
|
428
|
+
const taggedCount = items.filter((e) => e.branch === branchName).length;
|
|
429
|
+
if (taggedCount > 0) {
|
|
430
|
+
const updatedItems = items.map((item) => {
|
|
431
|
+
if (item.branch === branchName) {
|
|
432
|
+
const { branch: _, ...rest } = item;
|
|
433
|
+
return rest;
|
|
434
|
+
}
|
|
435
|
+
return item;
|
|
436
|
+
});
|
|
437
|
+
const ext = extname(prdFiles.primary).toLowerCase();
|
|
438
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
439
|
+
writeFileSync(prdFiles.primary, YAML.stringify(updatedItems));
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
writeFileSync(prdFiles.primary, JSON.stringify(updatedItems, null, 2) + "\n");
|
|
443
|
+
}
|
|
444
|
+
results.push(`${taggedCount} PRD item(s) untagged.`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
await client.sendMessage(chatId, `${state.projectName}: Deleted "${branchName}". ${results.join(" ")}`);
|
|
449
|
+
}
|
|
166
450
|
/**
|
|
167
451
|
* Execute a shell command and return the output.
|
|
168
452
|
*/
|
|
@@ -428,6 +712,37 @@ async function handleCommand(command, client, config, state, debug) {
|
|
|
428
712
|
}
|
|
429
713
|
break;
|
|
430
714
|
}
|
|
715
|
+
case "branch": {
|
|
716
|
+
const subCmd = args[0]?.toLowerCase();
|
|
717
|
+
const branchArgs = args.slice(1);
|
|
718
|
+
switch (subCmd) {
|
|
719
|
+
case "list":
|
|
720
|
+
await handleBranchList(chatId, client, state);
|
|
721
|
+
break;
|
|
722
|
+
case "pr":
|
|
723
|
+
await handleBranchPr(branchArgs, chatId, client, state);
|
|
724
|
+
break;
|
|
725
|
+
case "merge":
|
|
726
|
+
await handleBranchMerge(branchArgs, chatId, client, state);
|
|
727
|
+
break;
|
|
728
|
+
case "delete":
|
|
729
|
+
await handleBranchDelete(branchArgs, chatId, client, state);
|
|
730
|
+
break;
|
|
731
|
+
default: {
|
|
732
|
+
const usage = client.provider === "slack"
|
|
733
|
+
? `/ralph branch list - List branches
|
|
734
|
+
/ralph branch pr <name> - Add PRD item to create PR
|
|
735
|
+
/ralph branch merge <name> - Merge branch into base
|
|
736
|
+
/ralph branch delete <name> - Delete branch and worktree`
|
|
737
|
+
: `/branch list - List branches
|
|
738
|
+
/branch pr <name> - Add PRD item to create PR
|
|
739
|
+
/branch merge <name> - Merge branch into base
|
|
740
|
+
/branch delete <name> - Delete branch and worktree`;
|
|
741
|
+
await client.sendMessage(chatId, `${state.projectName}: ${usage}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
431
746
|
case "help": {
|
|
432
747
|
const isSlack = client.provider === "slack";
|
|
433
748
|
const helpText = isSlack
|
|
@@ -438,6 +753,7 @@ async function handleCommand(command, client, config, state, debug) {
|
|
|
438
753
|
/ralph add [desc] - Add task
|
|
439
754
|
/ralph exec [cmd] - Shell command
|
|
440
755
|
/ralph action [name] - Run action
|
|
756
|
+
/ralph branch ... - Manage branches
|
|
441
757
|
/ralph <prompt> - Run Claude Code`
|
|
442
758
|
: `/help - This help
|
|
443
759
|
/status - PRD progress
|
|
@@ -446,6 +762,7 @@ async function handleCommand(command, client, config, state, debug) {
|
|
|
446
762
|
/add [desc] - Add task
|
|
447
763
|
/exec [cmd] - Shell command
|
|
448
764
|
/action [name] - Run action
|
|
765
|
+
/branch ... - Manage branches
|
|
449
766
|
/claude [prompt] - Run Claude Code`;
|
|
450
767
|
await client.sendMessage(chatId, helpText);
|
|
451
768
|
break;
|
|
@@ -640,6 +957,7 @@ async function startChat(config, debug) {
|
|
|
640
957
|
console.log(" /ralph add ... - Add new task to PRD");
|
|
641
958
|
console.log(" /ralph exec ... - Execute shell command");
|
|
642
959
|
console.log(" /ralph action ... - Run daemon action");
|
|
960
|
+
console.log(" /ralph branch ... - Manage branches");
|
|
643
961
|
console.log(" /ralph <prompt> - Run Claude Code with prompt");
|
|
644
962
|
}
|
|
645
963
|
else {
|
package/dist/commands/docker.js
CHANGED
|
@@ -187,31 +187,36 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
|
|
|
187
187
|
-a "export HISTFILE=/commandhistory/.zsh_history" \\
|
|
188
188
|
-a 'alias ll="ls -la"'
|
|
189
189
|
|
|
190
|
-
#
|
|
190
|
+
# Copy oh-my-zsh config to node user (after oh-my-zsh to avoid override)
|
|
191
191
|
RUN cp -r /root/.oh-my-zsh /home/node/.oh-my-zsh && chown -R node:node /home/node/.oh-my-zsh && \\
|
|
192
192
|
cp /root/.zshrc /home/node/.zshrc && chown node:node /home/node/.zshrc && \\
|
|
193
|
-
sed -i 's|/root/.oh-my-zsh|/home/node/.oh-my-zsh|g' /home/node/.zshrc
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
193
|
+
sed -i 's|/root/.oh-my-zsh|/home/node/.oh-my-zsh|g' /home/node/.zshrc
|
|
194
|
+
|
|
195
|
+
# Set custom prompt and Ralph ASCII art banner
|
|
196
|
+
RUN cat >> /home/node/.zshrc <<'RALPH_BANNER'
|
|
197
|
+
PROMPT="%K{yellow}%F{black}[ralph]%f%k%K{yellow}%F{black}%d%f%k\\$ "
|
|
198
|
+
|
|
199
|
+
# Ralph ASCII art banner
|
|
200
|
+
if [ -z "$RALPH_BANNER_SHOWN" ]; then
|
|
201
|
+
export RALPH_BANNER_SHOWN=1
|
|
202
|
+
echo ""
|
|
203
|
+
echo "\\033[38;2;255;245;157m██████╗ █████╗ ██╗ ██████╗ ██╗ ██╗ ██████╗██╗ ██╗\\033[0m"
|
|
204
|
+
echo "\\033[38;2;255;238;88m██╔══██╗██╔══██╗██║ ██╔══██╗██║ ██║ ██╔════╝██║ ██║\\033[0m"
|
|
205
|
+
echo "\\033[38;2;255;235;59m██████╔╝███████║██║ ██████╔╝███████║ ██║ ██║ ██║ sandboxed\\033[0m"
|
|
206
|
+
echo "\\033[38;2;253;216;53m██╔══██╗██╔══██║██║ ██╔═══╝ ██╔══██║ ██║ ██║ ██║\\033[0m"
|
|
207
|
+
echo "\\033[38;2;251;192;45m██║ ██║██║ ██║███████╗██║ ██║ ██║ ╚██████╗███████╗██║\\033[0m"
|
|
208
|
+
echo "\\033[38;2;249;168;37m╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝\\033[0m"
|
|
209
|
+
RALPH_VERSION=$(ralph --version 2>/dev/null | head -1 || echo "unknown")
|
|
210
|
+
echo "\\033[38;5;248mv$RALPH_VERSION\\033[0m"
|
|
211
|
+
echo ""
|
|
212
|
+
fi
|
|
213
|
+
RALPH_BANNER
|
|
210
214
|
|
|
211
215
|
${cliSnippet}
|
|
212
216
|
|
|
213
217
|
# Install ralph-cli-sandboxed from npm registry
|
|
214
218
|
RUN npm install -g ralph-cli-sandboxed
|
|
219
|
+
RUN ralph logo
|
|
215
220
|
${languageSnippet}
|
|
216
221
|
# Setup sudo only for firewall script (no general sudo for security)
|
|
217
222
|
RUN echo "node ALL=(ALL) NOPASSWD: /usr/local/bin/init-firewall.sh" >> /etc/sudoers.d/node-firewall
|
package/dist/commands/run.js
CHANGED
|
@@ -483,11 +483,11 @@ function validateAndRecoverPrd(prdPath, validPrd) {
|
|
|
483
483
|
}
|
|
484
484
|
// If we can't even parse the JSON, restore from valid copy (with new items if we found any)
|
|
485
485
|
if (!parsed) {
|
|
486
|
-
console.log("\
|
|
486
|
+
console.log("\nNote: PRD corrupted (invalid JSON) - restored from memory.");
|
|
487
487
|
const mergedPrd = [...validPrd, ...newItems];
|
|
488
488
|
writePrd(prdPath, mergedPrd);
|
|
489
489
|
if (newItems.length > 0) {
|
|
490
|
-
console.log(
|
|
490
|
+
console.log(`Preserved ${newItems.length} newly-added item(s).`);
|
|
491
491
|
}
|
|
492
492
|
return { recovered: true, itemsUpdated: 0, newItemsPreserved: newItems.length };
|
|
493
493
|
}
|
|
@@ -498,23 +498,23 @@ function validateAndRecoverPrd(prdPath, validPrd) {
|
|
|
498
498
|
return { recovered: false, itemsUpdated: 0, newItemsPreserved: 0 };
|
|
499
499
|
}
|
|
500
500
|
// PRD is corrupted - use smart merge to extract passes flags
|
|
501
|
-
console.log("\
|
|
501
|
+
console.log("\nNote: PRD format corrupted by LLM - recovering...");
|
|
502
502
|
const mergeResult = smartMerge(validPrd, parsed.content);
|
|
503
503
|
// Add any newly-added items
|
|
504
504
|
const mergedPrd = [...mergeResult.merged, ...newItems];
|
|
505
505
|
// Write the valid structure back (with new items)
|
|
506
506
|
writePrd(prdPath, mergedPrd);
|
|
507
507
|
if (mergeResult.itemsUpdated > 0) {
|
|
508
|
-
console.log(
|
|
508
|
+
console.log(`Recovered: merged ${mergeResult.itemsUpdated} passes flag(s) into valid PRD structure.`);
|
|
509
509
|
}
|
|
510
510
|
else {
|
|
511
|
-
console.log("
|
|
511
|
+
console.log("Recovered: restored valid PRD structure.");
|
|
512
512
|
}
|
|
513
513
|
if (newItems.length > 0) {
|
|
514
|
-
console.log(
|
|
514
|
+
console.log(`Preserved ${newItems.length} newly-added item(s).`);
|
|
515
515
|
}
|
|
516
516
|
if (mergeResult.warnings.length > 0) {
|
|
517
|
-
mergeResult.warnings.forEach((w) => console.log(`
|
|
517
|
+
mergeResult.warnings.forEach((w) => console.log(` ${w}`));
|
|
518
518
|
}
|
|
519
519
|
return { recovered: true, itemsUpdated: mergeResult.itemsUpdated, newItemsPreserved: newItems.length };
|
|
520
520
|
}
|
|
@@ -1034,9 +1034,19 @@ export async function run(args) {
|
|
|
1034
1034
|
consecutiveFailures = 0;
|
|
1035
1035
|
lastExitCode = 0;
|
|
1036
1036
|
}
|
|
1037
|
-
// Check for completion signal
|
|
1037
|
+
// Check for completion signal from the LLM.
|
|
1038
|
+
// The LLM only sees a subset of items (branch group or no-branch group),
|
|
1039
|
+
// so its COMPLETE signal means "this group is done", not "all PRD items are done".
|
|
1040
|
+
// We must verify the full PRD before treating this as a global completion.
|
|
1038
1041
|
if (iterOutput.includes("<promise>COMPLETE</promise>")) {
|
|
1039
|
-
|
|
1042
|
+
const fullCounts = countPrdItems(paths.prd, category);
|
|
1043
|
+
if (fullCounts.incomplete > 0) {
|
|
1044
|
+
// There are still incomplete items in other groups — continue the loop
|
|
1045
|
+
if (debug) {
|
|
1046
|
+
console.log(`\n\x1b[90m[ralph] LLM signalled COMPLETE for current group, but ${fullCounts.incomplete} item(s) remain. Continuing...\x1b[0m`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
else if (loopMode) {
|
|
1040
1050
|
console.log("\n" + "=".repeat(50));
|
|
1041
1051
|
console.log("PRD iteration complete. Waiting for new items...");
|
|
1042
1052
|
console.log(`(Checking every ${POLL_INTERVAL_MS / 1000} seconds. Press Ctrl+C to stop)`);
|
|
@@ -1054,9 +1064,8 @@ export async function run(args) {
|
|
|
1054
1064
|
else {
|
|
1055
1065
|
console.log("\n" + "=".repeat(50));
|
|
1056
1066
|
if (allMode) {
|
|
1057
|
-
const counts = countPrdItems(paths.prd, category);
|
|
1058
1067
|
console.log("PRD COMPLETE - All tasks finished!");
|
|
1059
|
-
console.log(`Final Status: ${
|
|
1068
|
+
console.log(`Final Status: ${fullCounts.complete}/${fullCounts.total} complete`);
|
|
1060
1069
|
}
|
|
1061
1070
|
else {
|
|
1062
1071
|
console.log("PRD COMPLETE - All features implemented!");
|
package/dist/providers/slack.js
CHANGED
|
@@ -475,7 +475,7 @@ export class SlackChatClient {
|
|
|
475
475
|
// Handle the unified /ralph command
|
|
476
476
|
// Subcommands: help, status, run, stop, add, exec, action
|
|
477
477
|
// Anything else is treated as a prompt for Claude
|
|
478
|
-
const knownSubcommands = ["help", "status", "run", "stop", "add", "exec", "action"];
|
|
478
|
+
const knownSubcommands = ["help", "status", "run", "stop", "add", "exec", "action", "branch"];
|
|
479
479
|
if (this.debug) {
|
|
480
480
|
console.log(`[slack] Registering command: /ralph`);
|
|
481
481
|
}
|