ralph-cli-sandboxed 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -24
- package/dist/commands/docker.d.ts +5 -0
- package/dist/commands/docker.js +44 -0
- package/dist/commands/fix-prd.d.ts +1 -0
- package/dist/commands/fix-prd.js +201 -0
- package/dist/commands/help.js +8 -0
- package/dist/commands/init.js +8 -5
- package/dist/commands/once.js +24 -4
- package/dist/commands/run.js +151 -22
- package/dist/config/cli-providers.json +36 -8
- package/dist/index.js +2 -0
- package/dist/templates/prompts.d.ts +5 -0
- package/dist/templates/prompts.js +3 -3
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.js +17 -15
- package/dist/utils/prd-validator.d.ts +80 -0
- package/dist/utils/prd-validator.js +417 -0
- package/package.json +1 -1
package/dist/commands/run.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
4
|
import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
|
|
6
5
|
import { resolvePromptVariables } from "../templates/prompts.js";
|
|
6
|
+
import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
|
|
7
7
|
const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
|
|
8
8
|
/**
|
|
9
9
|
* Creates a filtered PRD file containing only incomplete items (passes: false).
|
|
10
10
|
* Optionally filters by category if specified.
|
|
11
|
+
* Expands @{filepath} references to include file contents.
|
|
11
12
|
* Returns the path to the temp file, or null if all items pass.
|
|
12
13
|
*/
|
|
13
|
-
function createFilteredPrd(prdPath, category) {
|
|
14
|
+
function createFilteredPrd(prdPath, baseDir, category) {
|
|
14
15
|
const content = readFileSync(prdPath, "utf-8");
|
|
15
16
|
const items = JSON.parse(content);
|
|
16
17
|
let filteredItems = items.filter(item => item.passes === false);
|
|
@@ -18,17 +19,62 @@ function createFilteredPrd(prdPath, category) {
|
|
|
18
19
|
if (category) {
|
|
19
20
|
filteredItems = filteredItems.filter(item => item.category === category);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Expand @{filepath} references in description and steps
|
|
23
|
+
const expandedItems = expandPrdFileReferences(filteredItems, baseDir);
|
|
24
|
+
// Write to .ralph/prd-tasks.json so LLMs see a sensible path
|
|
25
|
+
const tempPath = join(baseDir, "prd-tasks.json");
|
|
26
|
+
writeFileSync(tempPath, JSON.stringify(expandedItems, null, 2));
|
|
23
27
|
return {
|
|
24
28
|
tempPath,
|
|
25
29
|
hasIncomplete: filteredItems.length > 0
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Syncs passes flags from prd-tasks.json back to prd.json.
|
|
34
|
+
* If the LLM marked any item as passes: true in prd-tasks.json,
|
|
35
|
+
* find the matching item in prd.json and update it.
|
|
36
|
+
* Returns the number of items synced.
|
|
37
|
+
*/
|
|
38
|
+
function syncPassesFromTasks(tasksPath, prdPath) {
|
|
39
|
+
// Check if tasks file exists
|
|
40
|
+
if (!existsSync(tasksPath)) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const tasksContent = readFileSync(tasksPath, "utf-8");
|
|
45
|
+
const tasks = JSON.parse(tasksContent);
|
|
46
|
+
const prdContent = readFileSync(prdPath, "utf-8");
|
|
47
|
+
const prd = JSON.parse(prdContent);
|
|
48
|
+
let synced = 0;
|
|
49
|
+
// Find tasks that were marked as passing
|
|
50
|
+
for (const task of tasks) {
|
|
51
|
+
if (task.passes === true) {
|
|
52
|
+
// Find matching item in prd by description
|
|
53
|
+
const match = prd.find(item => item.description === task.description ||
|
|
54
|
+
item.description.includes(task.description) ||
|
|
55
|
+
task.description.includes(item.description));
|
|
56
|
+
if (match && !match.passes) {
|
|
57
|
+
match.passes = true;
|
|
58
|
+
synced++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Write back if any items were synced
|
|
63
|
+
if (synced > 0) {
|
|
64
|
+
writeFileSync(prdPath, JSON.stringify(prd, null, 2) + "\n");
|
|
65
|
+
console.log(`\x1b[32mSynced ${synced} completed item(s) from prd-tasks.json to prd.json\x1b[0m`);
|
|
66
|
+
}
|
|
67
|
+
return synced;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Ignore errors - the validation step will handle any issues
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model) {
|
|
29
75
|
return new Promise((resolve, reject) => {
|
|
30
76
|
let output = "";
|
|
31
|
-
// Build CLI arguments: config args + yolo args + prompt args
|
|
77
|
+
// Build CLI arguments: config args + yolo args + model args + prompt args
|
|
32
78
|
const cliArgs = [
|
|
33
79
|
...(cliConfig.args ?? []),
|
|
34
80
|
];
|
|
@@ -38,6 +84,10 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
|
|
|
38
84
|
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
39
85
|
cliArgs.push(...yoloArgs);
|
|
40
86
|
}
|
|
87
|
+
// Add model args if model is specified
|
|
88
|
+
if (model && cliConfig.modelArgs) {
|
|
89
|
+
cliArgs.push(...cliConfig.modelArgs, model);
|
|
90
|
+
}
|
|
41
91
|
// Use the filtered PRD (only incomplete items) for the prompt
|
|
42
92
|
// promptArgs specifies flags to use (e.g., ["-p"] for Claude, [] for positional)
|
|
43
93
|
const promptArgs = cliConfig.promptArgs ?? ["-p"];
|
|
@@ -105,9 +155,53 @@ function countPrdItems(prdPath, category) {
|
|
|
105
155
|
incomplete
|
|
106
156
|
};
|
|
107
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Validates the PRD after an iteration and recovers if corrupted.
|
|
160
|
+
* Uses the validPrd as the source of truth and merges passes flags from the current file.
|
|
161
|
+
* Returns true if the PRD was corrupted and recovered.
|
|
162
|
+
*/
|
|
163
|
+
function validateAndRecoverPrd(prdPath, validPrd) {
|
|
164
|
+
const parsed = readPrdFile(prdPath);
|
|
165
|
+
// If we can't even parse the JSON, restore from valid copy
|
|
166
|
+
if (!parsed) {
|
|
167
|
+
console.log("\n\x1b[33mWarning: PRD corrupted (invalid JSON) - restored from memory.\x1b[0m");
|
|
168
|
+
writePrd(prdPath, validPrd);
|
|
169
|
+
return { recovered: true, itemsUpdated: 0 };
|
|
170
|
+
}
|
|
171
|
+
// Validate the structure
|
|
172
|
+
const validation = validatePrd(parsed.content);
|
|
173
|
+
if (validation.valid) {
|
|
174
|
+
// PRD is valid, no recovery needed
|
|
175
|
+
return { recovered: false, itemsUpdated: 0 };
|
|
176
|
+
}
|
|
177
|
+
// PRD is corrupted - use smart merge to extract passes flags
|
|
178
|
+
console.log("\n\x1b[33mWarning: PRD format corrupted by LLM - recovering...\x1b[0m");
|
|
179
|
+
const mergeResult = smartMerge(validPrd, parsed.content);
|
|
180
|
+
// Write the valid structure back
|
|
181
|
+
writePrd(prdPath, mergeResult.merged);
|
|
182
|
+
if (mergeResult.itemsUpdated > 0) {
|
|
183
|
+
console.log(`\x1b[32mRecovered: merged ${mergeResult.itemsUpdated} passes flag(s) into valid PRD structure.\x1b[0m`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log("\x1b[32mRecovered: restored valid PRD structure.\x1b[0m");
|
|
187
|
+
}
|
|
188
|
+
if (mergeResult.warnings.length > 0) {
|
|
189
|
+
mergeResult.warnings.forEach(w => console.log(` \x1b[33m${w}\x1b[0m`));
|
|
190
|
+
}
|
|
191
|
+
return { recovered: true, itemsUpdated: mergeResult.itemsUpdated };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Loads a valid copy of the PRD to keep in memory.
|
|
195
|
+
* Returns the validated PRD entries.
|
|
196
|
+
*/
|
|
197
|
+
function loadValidPrd(prdPath) {
|
|
198
|
+
const content = readFileSync(prdPath, "utf-8");
|
|
199
|
+
return JSON.parse(content);
|
|
200
|
+
}
|
|
108
201
|
export async function run(args) {
|
|
109
202
|
// Parse flags
|
|
110
203
|
let category;
|
|
204
|
+
let model;
|
|
111
205
|
let loopMode = false;
|
|
112
206
|
let allModeExplicit = false;
|
|
113
207
|
let debug = false;
|
|
@@ -124,6 +218,16 @@ export async function run(args) {
|
|
|
124
218
|
process.exit(1);
|
|
125
219
|
}
|
|
126
220
|
}
|
|
221
|
+
else if (args[i] === "--model" || args[i] === "-m") {
|
|
222
|
+
if (i + 1 < args.length) {
|
|
223
|
+
model = args[i + 1];
|
|
224
|
+
i++; // Skip the model value
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.error("Error: --model requires a value");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
127
231
|
else if (args[i] === "--loop" || args[i] === "-l") {
|
|
128
232
|
loopMode = true;
|
|
129
233
|
}
|
|
@@ -149,8 +253,6 @@ export async function run(args) {
|
|
|
149
253
|
// - Otherwise, default to --all mode (run until all tasks complete)
|
|
150
254
|
const hasIterationArg = filteredArgs.length > 0 && !isNaN(parseInt(filteredArgs[0])) && parseInt(filteredArgs[0]) >= 1;
|
|
151
255
|
const allMode = !loopMode && (allModeExplicit || !hasIterationArg);
|
|
152
|
-
// In loop mode or all mode, iterations argument is optional (defaults to unlimited)
|
|
153
|
-
const iterations = (loopMode || allMode) ? (parseInt(filteredArgs[0]) || Infinity) : parseInt(filteredArgs[0]);
|
|
154
256
|
requireContainer("run");
|
|
155
257
|
checkFilesExist();
|
|
156
258
|
const config = loadConfig();
|
|
@@ -163,6 +265,10 @@ export async function run(args) {
|
|
|
163
265
|
});
|
|
164
266
|
const paths = getPaths();
|
|
165
267
|
const cliConfig = getCliConfig(config);
|
|
268
|
+
// Safety margin for iteration limit (recalculated dynamically each iteration)
|
|
269
|
+
const ITERATION_SAFETY_MARGIN = 3;
|
|
270
|
+
// Get requested iteration count (may be adjusted dynamically)
|
|
271
|
+
const requestedIterations = parseInt(filteredArgs[0]) || Infinity;
|
|
166
272
|
// Container is required, so always run with skip-permissions
|
|
167
273
|
const sandboxed = true;
|
|
168
274
|
if (allMode) {
|
|
@@ -174,7 +280,7 @@ export async function run(args) {
|
|
|
174
280
|
console.log("Starting ralph in loop mode (runs until interrupted)...");
|
|
175
281
|
}
|
|
176
282
|
else {
|
|
177
|
-
console.log(`Starting ${
|
|
283
|
+
console.log(`Starting ralph iterations (requested: ${requestedIterations})...`);
|
|
178
284
|
}
|
|
179
285
|
if (category) {
|
|
180
286
|
console.log(`Filtering PRD items by category: ${category}`);
|
|
@@ -187,22 +293,40 @@ export async function run(args) {
|
|
|
187
293
|
const startTime = Date.now();
|
|
188
294
|
let consecutiveFailures = 0;
|
|
189
295
|
let lastExitCode = 0;
|
|
296
|
+
let iterationCount = 0;
|
|
190
297
|
try {
|
|
191
|
-
|
|
298
|
+
while (true) {
|
|
299
|
+
iterationCount++;
|
|
300
|
+
// Dynamic iteration limit: recalculate based on current incomplete count
|
|
301
|
+
// This allows the limit to expand if tasks are added during the run
|
|
302
|
+
const currentCounts = countPrdItems(paths.prd, category);
|
|
303
|
+
const dynamicMaxIterations = currentCounts.incomplete + ITERATION_SAFETY_MARGIN;
|
|
304
|
+
// Check if we should stop (not in loop mode)
|
|
305
|
+
if (!loopMode) {
|
|
306
|
+
if (allMode && iterationCount > dynamicMaxIterations) {
|
|
307
|
+
console.log(`\nStopping: reached iteration limit (${dynamicMaxIterations}) with ${currentCounts.incomplete} tasks remaining.`);
|
|
308
|
+
console.log("This may indicate tasks are not completing. Check the PRD and progress.");
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (!allMode && iterationCount > Math.min(requestedIterations, dynamicMaxIterations)) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
192
315
|
console.log(`\n${"=".repeat(50)}`);
|
|
193
316
|
if (allMode) {
|
|
194
|
-
|
|
195
|
-
console.log(`Iteration ${i} | Progress: ${counts.complete}/${counts.total} complete`);
|
|
317
|
+
console.log(`Iteration ${iterationCount} | Progress: ${currentCounts.complete}/${currentCounts.total} complete`);
|
|
196
318
|
}
|
|
197
|
-
else if (loopMode
|
|
198
|
-
console.log(`Iteration ${
|
|
319
|
+
else if (loopMode) {
|
|
320
|
+
console.log(`Iteration ${iterationCount}`);
|
|
199
321
|
}
|
|
200
322
|
else {
|
|
201
|
-
console.log(`Iteration ${
|
|
323
|
+
console.log(`Iteration ${iterationCount} of ${Math.min(requestedIterations, dynamicMaxIterations)}`);
|
|
202
324
|
}
|
|
203
325
|
console.log(`${"=".repeat(50)}\n`);
|
|
326
|
+
// Load a valid copy of the PRD before handing to the LLM
|
|
327
|
+
const validPrd = loadValidPrd(paths.prd);
|
|
204
328
|
// Create a fresh filtered PRD for each iteration (in case items were completed)
|
|
205
|
-
const { tempPath, hasIncomplete } = createFilteredPrd(paths.prd, category);
|
|
329
|
+
const { tempPath, hasIncomplete } = createFilteredPrd(paths.prd, paths.dir, category);
|
|
206
330
|
filteredPrdPath = tempPath;
|
|
207
331
|
if (!hasIncomplete) {
|
|
208
332
|
// Clean up temp file since we're not using it
|
|
@@ -227,14 +351,14 @@ export async function run(args) {
|
|
|
227
351
|
// Poll for new items
|
|
228
352
|
while (true) {
|
|
229
353
|
await sleep(POLL_INTERVAL_MS);
|
|
230
|
-
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, category);
|
|
354
|
+
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, paths.dir, category);
|
|
231
355
|
if (newItems) {
|
|
232
356
|
console.log("\nNew incomplete item(s) detected! Resuming...");
|
|
233
357
|
break;
|
|
234
358
|
}
|
|
235
359
|
}
|
|
236
|
-
// Decrement
|
|
237
|
-
|
|
360
|
+
// Decrement so we don't count waiting as an iteration
|
|
361
|
+
iterationCount--;
|
|
238
362
|
continue;
|
|
239
363
|
}
|
|
240
364
|
else {
|
|
@@ -259,7 +383,10 @@ export async function run(args) {
|
|
|
259
383
|
break;
|
|
260
384
|
}
|
|
261
385
|
}
|
|
262
|
-
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug);
|
|
386
|
+
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model);
|
|
387
|
+
// Sync any completed items from prd-tasks.json back to prd.json
|
|
388
|
+
// This catches cases where the LLM updated prd-tasks.json instead of prd.json
|
|
389
|
+
syncPassesFromTasks(filteredPrdPath, paths.prd);
|
|
263
390
|
// Clean up temp file after each iteration
|
|
264
391
|
try {
|
|
265
392
|
unlinkSync(filteredPrdPath);
|
|
@@ -268,6 +395,8 @@ export async function run(args) {
|
|
|
268
395
|
// Ignore cleanup errors
|
|
269
396
|
}
|
|
270
397
|
filteredPrdPath = null;
|
|
398
|
+
// Validate and recover PRD if the LLM corrupted it
|
|
399
|
+
validateAndRecoverPrd(paths.prd, validPrd);
|
|
271
400
|
if (exitCode !== 0) {
|
|
272
401
|
console.error(`\n${cliConfig.command} exited with code ${exitCode}`);
|
|
273
402
|
// Track consecutive failures to detect persistent errors (e.g., missing API key)
|
|
@@ -302,7 +431,7 @@ export async function run(args) {
|
|
|
302
431
|
// Poll for new items
|
|
303
432
|
while (true) {
|
|
304
433
|
await sleep(POLL_INTERVAL_MS);
|
|
305
|
-
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, category);
|
|
434
|
+
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, paths.dir, category);
|
|
306
435
|
if (newItems) {
|
|
307
436
|
console.log("\nNew incomplete item(s) detected! Resuming...");
|
|
308
437
|
break;
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
"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
12
|
},
|
|
13
13
|
"envVars": ["ANTHROPIC_API_KEY"],
|
|
14
|
-
"credentialMount": "~/.claude:/home/node/.claude"
|
|
14
|
+
"credentialMount": "~/.claude:/home/node/.claude",
|
|
15
|
+
"modelConfig": {
|
|
16
|
+
"envVar": "CLAUDE_MODEL",
|
|
17
|
+
"note": "Or use: ralph run --model <model>"
|
|
18
|
+
},
|
|
19
|
+
"modelArgs": ["--model"]
|
|
15
20
|
},
|
|
16
21
|
"aider": {
|
|
17
22
|
"name": "Aider",
|
|
@@ -25,7 +30,12 @@
|
|
|
25
30
|
"note": "Check 'aider --help' for available flags"
|
|
26
31
|
},
|
|
27
32
|
"envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
28
|
-
"credentialMount": null
|
|
33
|
+
"credentialMount": null,
|
|
34
|
+
"modelConfig": {
|
|
35
|
+
"envVar": "AIDER_MODEL",
|
|
36
|
+
"note": "Or use: ralph run --model <model>"
|
|
37
|
+
},
|
|
38
|
+
"modelArgs": ["--model"]
|
|
29
39
|
},
|
|
30
40
|
"codex": {
|
|
31
41
|
"name": "OpenAI Codex CLI",
|
|
@@ -39,7 +49,12 @@
|
|
|
39
49
|
"note": "Check 'codex --help' for available flags"
|
|
40
50
|
},
|
|
41
51
|
"envVars": ["OPENAI_API_KEY"],
|
|
42
|
-
"credentialMount": null
|
|
52
|
+
"credentialMount": null,
|
|
53
|
+
"modelConfig": {
|
|
54
|
+
"envVar": "CODEX_MODEL",
|
|
55
|
+
"note": "Or use: ralph run --model <model>"
|
|
56
|
+
},
|
|
57
|
+
"modelArgs": ["--model"]
|
|
43
58
|
},
|
|
44
59
|
"gemini": {
|
|
45
60
|
"name": "Gemini CLI",
|
|
@@ -53,7 +68,12 @@
|
|
|
53
68
|
"note": "Check 'gemini --help' for available flags"
|
|
54
69
|
},
|
|
55
70
|
"envVars": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
56
|
-
"credentialMount": "~/.gemini:/home/node/.gemini"
|
|
71
|
+
"credentialMount": "~/.gemini:/home/node/.gemini",
|
|
72
|
+
"modelConfig": {
|
|
73
|
+
"envVar": "GEMINI_MODEL",
|
|
74
|
+
"note": "Or use: ralph run --model <model>"
|
|
75
|
+
},
|
|
76
|
+
"modelArgs": ["--model"]
|
|
57
77
|
},
|
|
58
78
|
"opencode": {
|
|
59
79
|
"name": "OpenCode",
|
|
@@ -67,21 +87,29 @@
|
|
|
67
87
|
"note": "Check 'opencode --help' for available flags"
|
|
68
88
|
},
|
|
69
89
|
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
70
|
-
"credentialMount": null
|
|
90
|
+
"credentialMount": null,
|
|
91
|
+
"modelConfig": {
|
|
92
|
+
"note": "Or use: ralph run --model <model>"
|
|
93
|
+
},
|
|
94
|
+
"modelArgs": ["--model"]
|
|
71
95
|
},
|
|
72
96
|
"amp": {
|
|
73
97
|
"name": "AMP CLI",
|
|
74
98
|
"description": "Sourcegraph's AMP coding agent",
|
|
75
99
|
"command": "amp",
|
|
76
100
|
"defaultArgs": [],
|
|
77
|
-
"yoloArgs": ["--
|
|
78
|
-
"promptArgs": [],
|
|
101
|
+
"yoloArgs": ["--dangerously-allow-all"],
|
|
102
|
+
"promptArgs": ["-x"],
|
|
79
103
|
"docker": {
|
|
80
104
|
"install": "# Install AMP CLI (as node user)\nRUN su - node -c 'curl -fsSL https://ampcode.com/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.amp/bin:$PATH\"' >> /home/node/.zshrc",
|
|
81
105
|
"note": "Check 'amp --help' for available flags"
|
|
82
106
|
},
|
|
83
107
|
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
84
|
-
"credentialMount": null
|
|
108
|
+
"credentialMount": null,
|
|
109
|
+
"modelConfig": {
|
|
110
|
+
"note": "Or use: ralph run --model <model>"
|
|
111
|
+
},
|
|
112
|
+
"modelArgs": ["--model"]
|
|
85
113
|
},
|
|
86
114
|
"custom": {
|
|
87
115
|
"name": "Custom CLI",
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { run } from "./commands/run.js";
|
|
|
9
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
|
+
import { fixPrd } from "./commands/fix-prd.js";
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = dirname(__filename);
|
|
14
15
|
function getPackageInfo() {
|
|
@@ -24,6 +25,7 @@ const commands = {
|
|
|
24
25
|
prd,
|
|
25
26
|
prompt,
|
|
26
27
|
docker,
|
|
28
|
+
"fix-prd": (args) => fixPrd(args),
|
|
27
29
|
// Top-level PRD commands (shortcuts)
|
|
28
30
|
add: () => prdAdd(),
|
|
29
31
|
list: (args) => {
|
|
@@ -34,11 +34,16 @@ export interface CliProviderConfig {
|
|
|
34
34
|
defaultArgs: string[];
|
|
35
35
|
yoloArgs: string[];
|
|
36
36
|
promptArgs: string[];
|
|
37
|
+
modelArgs?: string[];
|
|
37
38
|
docker: {
|
|
38
39
|
install: string;
|
|
39
40
|
};
|
|
40
41
|
envVars: string[];
|
|
41
42
|
credentialMount: string | null;
|
|
43
|
+
modelConfig?: {
|
|
44
|
+
envVar?: string;
|
|
45
|
+
note?: string;
|
|
46
|
+
};
|
|
42
47
|
}
|
|
43
48
|
interface CliProvidersJson {
|
|
44
49
|
providers: Record<string, CliProviderConfig>;
|
|
@@ -81,13 +81,13 @@ TECHNOLOGY STACK:
|
|
|
81
81
|
- Technologies: $technologies
|
|
82
82
|
|
|
83
83
|
INSTRUCTIONS:
|
|
84
|
-
1. Read the
|
|
84
|
+
1. Read the provided PRD tasks file to find the first incomplete feature
|
|
85
85
|
2. Implement that feature completely
|
|
86
86
|
3. Verify your changes work by running:
|
|
87
87
|
- Type/build check: $checkCommand
|
|
88
88
|
- Tests: $testCommand
|
|
89
|
-
4. Update
|
|
90
|
-
5. Append a brief note about what you did to
|
|
89
|
+
4. Update .ralph/prd.json to set "passes": true for the completed feature
|
|
90
|
+
5. Append a brief note about what you did to .ralph/progress.txt
|
|
91
91
|
6. Create a git commit with a descriptive message for this feature
|
|
92
92
|
7. Only work on ONE feature per execution
|
|
93
93
|
|
package/dist/utils/config.d.ts
CHANGED
package/dist/utils/config.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { getCliProviders } from "../templates/prompts.js";
|
|
3
4
|
export const DEFAULT_CLI_CONFIG = {
|
|
4
5
|
command: "claude",
|
|
5
6
|
args: ["--permission-mode", "acceptEdits"],
|
|
6
7
|
promptArgs: ["-p"],
|
|
7
8
|
};
|
|
8
|
-
// Lazy import to avoid circular dependency
|
|
9
|
-
let _getCliProviders = null;
|
|
10
9
|
export function getCliConfig(config) {
|
|
11
10
|
const cliConfig = config.cli ?? DEFAULT_CLI_CONFIG;
|
|
12
|
-
//
|
|
13
|
-
if (cliConfig.promptArgs !== undefined) {
|
|
14
|
-
return cliConfig;
|
|
15
|
-
}
|
|
16
|
-
// Look up promptArgs from cliProvider if available
|
|
11
|
+
// Look up promptArgs and modelArgs from cliProvider if available
|
|
17
12
|
if (config.cliProvider) {
|
|
18
|
-
|
|
19
|
-
// Dynamic import to avoid circular dependency
|
|
20
|
-
_getCliProviders = require("../templates/prompts.js").getCliProviders;
|
|
21
|
-
}
|
|
22
|
-
const providers = _getCliProviders();
|
|
13
|
+
const providers = getCliProviders();
|
|
23
14
|
const provider = providers[config.cliProvider];
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
const result = { ...cliConfig };
|
|
16
|
+
// Use provider's promptArgs if not already set
|
|
17
|
+
if (result.promptArgs === undefined && provider?.promptArgs !== undefined) {
|
|
18
|
+
result.promptArgs = provider.promptArgs;
|
|
19
|
+
}
|
|
20
|
+
// Use provider's modelArgs if not already set
|
|
21
|
+
if (result.modelArgs === undefined && provider?.modelArgs !== undefined) {
|
|
22
|
+
result.modelArgs = provider.modelArgs;
|
|
23
|
+
}
|
|
24
|
+
// Default promptArgs for backwards compatibility
|
|
25
|
+
if (result.promptArgs === undefined) {
|
|
26
|
+
result.promptArgs = ["-p"];
|
|
26
27
|
}
|
|
28
|
+
return result;
|
|
27
29
|
}
|
|
28
30
|
// Default to -p for backwards compatibility
|
|
29
|
-
return { ...cliConfig, promptArgs: ["-p"] };
|
|
31
|
+
return { ...cliConfig, promptArgs: cliConfig.promptArgs ?? ["-p"] };
|
|
30
32
|
}
|
|
31
33
|
const RALPH_DIR = ".ralph";
|
|
32
34
|
const CONFIG_FILE = "config.json";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface PrdEntry {
|
|
2
|
+
category: string;
|
|
3
|
+
description: string;
|
|
4
|
+
steps: string[];
|
|
5
|
+
passes: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
errors: string[];
|
|
10
|
+
data?: PrdEntry[];
|
|
11
|
+
}
|
|
12
|
+
export interface MergeResult {
|
|
13
|
+
merged: PrdEntry[];
|
|
14
|
+
itemsUpdated: number;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
interface ExtractedItem {
|
|
18
|
+
description: string;
|
|
19
|
+
passes: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validates that a PRD structure is correct.
|
|
23
|
+
* Returns validation result with parsed data if valid.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validatePrd(content: unknown): ValidationResult;
|
|
26
|
+
/**
|
|
27
|
+
* Extracts items marked as passing from a corrupted PRD structure.
|
|
28
|
+
* Handles various malformed structures LLMs might create.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractPassingItems(corrupted: unknown): ExtractedItem[];
|
|
31
|
+
/**
|
|
32
|
+
* Smart merge: applies passes flags from corrupted PRD to valid original.
|
|
33
|
+
* Only updates items that were marked as passing in the corrupted version.
|
|
34
|
+
*/
|
|
35
|
+
export declare function smartMerge(original: PrdEntry[], corrupted: unknown): MergeResult;
|
|
36
|
+
/**
|
|
37
|
+
* Attempts to recover a valid PRD from corrupted content.
|
|
38
|
+
* Returns the recovered PRD entries or null if recovery failed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function attemptRecovery(corrupted: unknown): PrdEntry[] | null;
|
|
41
|
+
/**
|
|
42
|
+
* Creates a timestamped backup of the PRD file.
|
|
43
|
+
* Returns the backup path.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createBackup(prdPath: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Finds the most recent backup file.
|
|
48
|
+
* Returns the path or null if no backups exist.
|
|
49
|
+
*/
|
|
50
|
+
export declare function findLatestBackup(prdPath: string): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Creates a PRD template with a recovery entry that instructs the LLM to fix the PRD.
|
|
53
|
+
* Uses @{filepath} syntax to include backup content when expanded.
|
|
54
|
+
* @param backupPath - Absolute path to the backup file containing the corrupted PRD
|
|
55
|
+
*/
|
|
56
|
+
export declare function createTemplatePrd(backupPath?: string): PrdEntry[];
|
|
57
|
+
/**
|
|
58
|
+
* Reads and parses a PRD file, handling potential JSON errors.
|
|
59
|
+
* Returns the parsed content or null if it couldn't be parsed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readPrdFile(prdPath: string): {
|
|
62
|
+
content: unknown;
|
|
63
|
+
raw: string;
|
|
64
|
+
} | null;
|
|
65
|
+
/**
|
|
66
|
+
* Writes a PRD to file.
|
|
67
|
+
*/
|
|
68
|
+
export declare function writePrd(prdPath: string, entries: PrdEntry[]): void;
|
|
69
|
+
/**
|
|
70
|
+
* Expands @{filepath} patterns in a string with actual file contents.
|
|
71
|
+
* Similar to curl's @ syntax for including file contents.
|
|
72
|
+
* Paths are resolved relative to the .ralph directory.
|
|
73
|
+
*/
|
|
74
|
+
export declare function expandFileReferences(text: string, baseDir: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Expands file references in all string fields of PRD entries.
|
|
77
|
+
* Returns a new array with expanded content.
|
|
78
|
+
*/
|
|
79
|
+
export declare function expandPrdFileReferences(entries: PrdEntry[], baseDir: string): PrdEntry[];
|
|
80
|
+
export {};
|