ralph-cli-sandboxed 0.2.1 → 0.2.3
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 +83 -23
- 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.d.ts +1 -1
- package/dist/commands/once.js +28 -4
- package/dist/commands/prd.js +2 -2
- package/dist/commands/run.js +134 -21
- 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/docs/run-state-machine.md +68 -0
- package/package.json +4 -4
package/dist/commands/run.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { 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,20 @@ 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
|
-
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig) {
|
|
32
|
+
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model) {
|
|
29
33
|
return new Promise((resolve, reject) => {
|
|
30
34
|
let output = "";
|
|
31
|
-
// Build CLI arguments: config args + yolo args + prompt args
|
|
35
|
+
// Build CLI arguments: config args + yolo args + model args + prompt args
|
|
32
36
|
const cliArgs = [
|
|
33
37
|
...(cliConfig.args ?? []),
|
|
34
38
|
];
|
|
@@ -38,11 +42,18 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
|
|
|
38
42
|
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
39
43
|
cliArgs.push(...yoloArgs);
|
|
40
44
|
}
|
|
45
|
+
// Add model args if model is specified
|
|
46
|
+
if (model && cliConfig.modelArgs) {
|
|
47
|
+
cliArgs.push(...cliConfig.modelArgs, model);
|
|
48
|
+
}
|
|
41
49
|
// Use the filtered PRD (only incomplete items) for the prompt
|
|
42
50
|
// promptArgs specifies flags to use (e.g., ["-p"] for Claude, [] for positional)
|
|
43
51
|
const promptArgs = cliConfig.promptArgs ?? ["-p"];
|
|
44
52
|
const promptValue = `@${filteredPrdPath} @${paths.progress} ${prompt}`;
|
|
45
53
|
cliArgs.push(...promptArgs, promptValue);
|
|
54
|
+
if (debug) {
|
|
55
|
+
console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
|
|
56
|
+
}
|
|
46
57
|
const proc = spawn(cliConfig.command, cliArgs, {
|
|
47
58
|
stdio: ["inherit", "pipe", "inherit"],
|
|
48
59
|
});
|
|
@@ -102,11 +113,56 @@ function countPrdItems(prdPath, category) {
|
|
|
102
113
|
incomplete
|
|
103
114
|
};
|
|
104
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Validates the PRD after an iteration and recovers if corrupted.
|
|
118
|
+
* Uses the validPrd as the source of truth and merges passes flags from the current file.
|
|
119
|
+
* Returns true if the PRD was corrupted and recovered.
|
|
120
|
+
*/
|
|
121
|
+
function validateAndRecoverPrd(prdPath, validPrd) {
|
|
122
|
+
const parsed = readPrdFile(prdPath);
|
|
123
|
+
// If we can't even parse the JSON, restore from valid copy
|
|
124
|
+
if (!parsed) {
|
|
125
|
+
console.log("\n\x1b[33mWarning: PRD corrupted (invalid JSON) - restored from memory.\x1b[0m");
|
|
126
|
+
writePrd(prdPath, validPrd);
|
|
127
|
+
return { recovered: true, itemsUpdated: 0 };
|
|
128
|
+
}
|
|
129
|
+
// Validate the structure
|
|
130
|
+
const validation = validatePrd(parsed.content);
|
|
131
|
+
if (validation.valid) {
|
|
132
|
+
// PRD is valid, no recovery needed
|
|
133
|
+
return { recovered: false, itemsUpdated: 0 };
|
|
134
|
+
}
|
|
135
|
+
// PRD is corrupted - use smart merge to extract passes flags
|
|
136
|
+
console.log("\n\x1b[33mWarning: PRD format corrupted by LLM - recovering...\x1b[0m");
|
|
137
|
+
const mergeResult = smartMerge(validPrd, parsed.content);
|
|
138
|
+
// Write the valid structure back
|
|
139
|
+
writePrd(prdPath, mergeResult.merged);
|
|
140
|
+
if (mergeResult.itemsUpdated > 0) {
|
|
141
|
+
console.log(`\x1b[32mRecovered: merged ${mergeResult.itemsUpdated} passes flag(s) into valid PRD structure.\x1b[0m`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log("\x1b[32mRecovered: restored valid PRD structure.\x1b[0m");
|
|
145
|
+
}
|
|
146
|
+
if (mergeResult.warnings.length > 0) {
|
|
147
|
+
mergeResult.warnings.forEach(w => console.log(` \x1b[33m${w}\x1b[0m`));
|
|
148
|
+
}
|
|
149
|
+
return { recovered: true, itemsUpdated: mergeResult.itemsUpdated };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Loads a valid copy of the PRD to keep in memory.
|
|
153
|
+
* Returns the validated PRD entries.
|
|
154
|
+
*/
|
|
155
|
+
function loadValidPrd(prdPath) {
|
|
156
|
+
const content = readFileSync(prdPath, "utf-8");
|
|
157
|
+
return JSON.parse(content);
|
|
158
|
+
}
|
|
105
159
|
export async function run(args) {
|
|
106
160
|
// Parse flags
|
|
107
161
|
let category;
|
|
162
|
+
let model;
|
|
108
163
|
let loopMode = false;
|
|
109
164
|
let allModeExplicit = false;
|
|
165
|
+
let debug = false;
|
|
110
166
|
const filteredArgs = [];
|
|
111
167
|
for (let i = 0; i < args.length; i++) {
|
|
112
168
|
if (args[i] === "--category" || args[i] === "-c") {
|
|
@@ -120,12 +176,25 @@ export async function run(args) {
|
|
|
120
176
|
process.exit(1);
|
|
121
177
|
}
|
|
122
178
|
}
|
|
179
|
+
else if (args[i] === "--model" || args[i] === "-m") {
|
|
180
|
+
if (i + 1 < args.length) {
|
|
181
|
+
model = args[i + 1];
|
|
182
|
+
i++; // Skip the model value
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.error("Error: --model requires a value");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
123
189
|
else if (args[i] === "--loop" || args[i] === "-l") {
|
|
124
190
|
loopMode = true;
|
|
125
191
|
}
|
|
126
192
|
else if (args[i] === "--all" || args[i] === "-a") {
|
|
127
193
|
allModeExplicit = true;
|
|
128
194
|
}
|
|
195
|
+
else if (args[i] === "--debug" || args[i] === "-d") {
|
|
196
|
+
debug = true;
|
|
197
|
+
}
|
|
129
198
|
else {
|
|
130
199
|
filteredArgs.push(args[i]);
|
|
131
200
|
}
|
|
@@ -142,8 +211,6 @@ export async function run(args) {
|
|
|
142
211
|
// - Otherwise, default to --all mode (run until all tasks complete)
|
|
143
212
|
const hasIterationArg = filteredArgs.length > 0 && !isNaN(parseInt(filteredArgs[0])) && parseInt(filteredArgs[0]) >= 1;
|
|
144
213
|
const allMode = !loopMode && (allModeExplicit || !hasIterationArg);
|
|
145
|
-
// In loop mode or all mode, iterations argument is optional (defaults to unlimited)
|
|
146
|
-
const iterations = (loopMode || allMode) ? (parseInt(filteredArgs[0]) || Infinity) : parseInt(filteredArgs[0]);
|
|
147
214
|
requireContainer("run");
|
|
148
215
|
checkFilesExist();
|
|
149
216
|
const config = loadConfig();
|
|
@@ -156,6 +223,10 @@ export async function run(args) {
|
|
|
156
223
|
});
|
|
157
224
|
const paths = getPaths();
|
|
158
225
|
const cliConfig = getCliConfig(config);
|
|
226
|
+
// Safety margin for iteration limit (recalculated dynamically each iteration)
|
|
227
|
+
const ITERATION_SAFETY_MARGIN = 3;
|
|
228
|
+
// Get requested iteration count (may be adjusted dynamically)
|
|
229
|
+
const requestedIterations = parseInt(filteredArgs[0]) || Infinity;
|
|
159
230
|
// Container is required, so always run with skip-permissions
|
|
160
231
|
const sandboxed = true;
|
|
161
232
|
if (allMode) {
|
|
@@ -167,7 +238,7 @@ export async function run(args) {
|
|
|
167
238
|
console.log("Starting ralph in loop mode (runs until interrupted)...");
|
|
168
239
|
}
|
|
169
240
|
else {
|
|
170
|
-
console.log(`Starting ${
|
|
241
|
+
console.log(`Starting ralph iterations (requested: ${requestedIterations})...`);
|
|
171
242
|
}
|
|
172
243
|
if (category) {
|
|
173
244
|
console.log(`Filtering PRD items by category: ${category}`);
|
|
@@ -176,23 +247,44 @@ export async function run(args) {
|
|
|
176
247
|
// Track temp file for cleanup
|
|
177
248
|
let filteredPrdPath = null;
|
|
178
249
|
const POLL_INTERVAL_MS = 30000; // 30 seconds between checks when waiting for new items
|
|
250
|
+
const MAX_CONSECUTIVE_FAILURES = 3; // Stop after this many consecutive failures
|
|
179
251
|
const startTime = Date.now();
|
|
252
|
+
let consecutiveFailures = 0;
|
|
253
|
+
let lastExitCode = 0;
|
|
254
|
+
let iterationCount = 0;
|
|
180
255
|
try {
|
|
181
|
-
|
|
256
|
+
while (true) {
|
|
257
|
+
iterationCount++;
|
|
258
|
+
// Dynamic iteration limit: recalculate based on current incomplete count
|
|
259
|
+
// This allows the limit to expand if tasks are added during the run
|
|
260
|
+
const currentCounts = countPrdItems(paths.prd, category);
|
|
261
|
+
const dynamicMaxIterations = currentCounts.incomplete + ITERATION_SAFETY_MARGIN;
|
|
262
|
+
// Check if we should stop (not in loop mode)
|
|
263
|
+
if (!loopMode) {
|
|
264
|
+
if (allMode && iterationCount > dynamicMaxIterations) {
|
|
265
|
+
console.log(`\nStopping: reached iteration limit (${dynamicMaxIterations}) with ${currentCounts.incomplete} tasks remaining.`);
|
|
266
|
+
console.log("This may indicate tasks are not completing. Check the PRD and progress.");
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (!allMode && iterationCount > Math.min(requestedIterations, dynamicMaxIterations)) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
182
273
|
console.log(`\n${"=".repeat(50)}`);
|
|
183
274
|
if (allMode) {
|
|
184
|
-
|
|
185
|
-
console.log(`Iteration ${i} | Progress: ${counts.complete}/${counts.total} complete`);
|
|
275
|
+
console.log(`Iteration ${iterationCount} | Progress: ${currentCounts.complete}/${currentCounts.total} complete`);
|
|
186
276
|
}
|
|
187
|
-
else if (loopMode
|
|
188
|
-
console.log(`Iteration ${
|
|
277
|
+
else if (loopMode) {
|
|
278
|
+
console.log(`Iteration ${iterationCount}`);
|
|
189
279
|
}
|
|
190
280
|
else {
|
|
191
|
-
console.log(`Iteration ${
|
|
281
|
+
console.log(`Iteration ${iterationCount} of ${Math.min(requestedIterations, dynamicMaxIterations)}`);
|
|
192
282
|
}
|
|
193
283
|
console.log(`${"=".repeat(50)}\n`);
|
|
284
|
+
// Load a valid copy of the PRD before handing to the LLM
|
|
285
|
+
const validPrd = loadValidPrd(paths.prd);
|
|
194
286
|
// Create a fresh filtered PRD for each iteration (in case items were completed)
|
|
195
|
-
const { tempPath, hasIncomplete } = createFilteredPrd(paths.prd, category);
|
|
287
|
+
const { tempPath, hasIncomplete } = createFilteredPrd(paths.prd, paths.dir, category);
|
|
196
288
|
filteredPrdPath = tempPath;
|
|
197
289
|
if (!hasIncomplete) {
|
|
198
290
|
// Clean up temp file since we're not using it
|
|
@@ -217,14 +309,14 @@ export async function run(args) {
|
|
|
217
309
|
// Poll for new items
|
|
218
310
|
while (true) {
|
|
219
311
|
await sleep(POLL_INTERVAL_MS);
|
|
220
|
-
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, category);
|
|
312
|
+
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, paths.dir, category);
|
|
221
313
|
if (newItems) {
|
|
222
314
|
console.log("\nNew incomplete item(s) detected! Resuming...");
|
|
223
315
|
break;
|
|
224
316
|
}
|
|
225
317
|
}
|
|
226
|
-
// Decrement
|
|
227
|
-
|
|
318
|
+
// Decrement so we don't count waiting as an iteration
|
|
319
|
+
iterationCount--;
|
|
228
320
|
continue;
|
|
229
321
|
}
|
|
230
322
|
else {
|
|
@@ -249,7 +341,7 @@ export async function run(args) {
|
|
|
249
341
|
break;
|
|
250
342
|
}
|
|
251
343
|
}
|
|
252
|
-
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig);
|
|
344
|
+
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model);
|
|
253
345
|
// Clean up temp file after each iteration
|
|
254
346
|
try {
|
|
255
347
|
unlinkSync(filteredPrdPath);
|
|
@@ -258,10 +350,31 @@ export async function run(args) {
|
|
|
258
350
|
// Ignore cleanup errors
|
|
259
351
|
}
|
|
260
352
|
filteredPrdPath = null;
|
|
353
|
+
// Validate and recover PRD if the LLM corrupted it
|
|
354
|
+
validateAndRecoverPrd(paths.prd, validPrd);
|
|
261
355
|
if (exitCode !== 0) {
|
|
262
356
|
console.error(`\n${cliConfig.command} exited with code ${exitCode}`);
|
|
357
|
+
// Track consecutive failures to detect persistent errors (e.g., missing API key)
|
|
358
|
+
if (exitCode === lastExitCode) {
|
|
359
|
+
consecutiveFailures++;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
consecutiveFailures = 1;
|
|
363
|
+
lastExitCode = exitCode;
|
|
364
|
+
}
|
|
365
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
366
|
+
console.error(`\nStopping: ${cliConfig.command} failed ${consecutiveFailures} times in a row with exit code ${exitCode}.`);
|
|
367
|
+
console.error("This usually indicates a configuration error (e.g., missing API key).");
|
|
368
|
+
console.error("Please check your CLI configuration and try again.");
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
263
371
|
console.log("Continuing to next iteration...");
|
|
264
372
|
}
|
|
373
|
+
else {
|
|
374
|
+
// Reset failure tracking on success
|
|
375
|
+
consecutiveFailures = 0;
|
|
376
|
+
lastExitCode = 0;
|
|
377
|
+
}
|
|
265
378
|
// Check for completion signal
|
|
266
379
|
if (output.includes("<promise>COMPLETE</promise>")) {
|
|
267
380
|
if (loopMode) {
|
|
@@ -273,7 +386,7 @@ export async function run(args) {
|
|
|
273
386
|
// Poll for new items
|
|
274
387
|
while (true) {
|
|
275
388
|
await sleep(POLL_INTERVAL_MS);
|
|
276
|
-
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, category);
|
|
389
|
+
const { hasIncomplete: newItems } = createFilteredPrd(paths.prd, paths.dir, category);
|
|
277
390
|
if (newItems) {
|
|
278
391
|
console.log("\nNew incomplete item(s) detected! Resuming...");
|
|
279
392
|
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",
|
|
@@ -61,13 +81,17 @@
|
|
|
61
81
|
"command": "opencode",
|
|
62
82
|
"defaultArgs": [],
|
|
63
83
|
"yoloArgs": ["--yolo"],
|
|
64
|
-
"promptArgs": [],
|
|
84
|
+
"promptArgs": ["run"],
|
|
65
85
|
"docker": {
|
|
66
86
|
"install": "# Install OpenCode (as node user)\nRUN su - node -c 'curl -fsSL https://opencode.ai/install | bash' \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
|
|
67
87
|
"note": "Check 'opencode --help' for available flags"
|
|
68
88
|
},
|
|
69
|
-
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "
|
|
70
|
-
"credentialMount": null
|
|
89
|
+
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
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",
|
|
@@ -81,7 +105,11 @@
|
|
|
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 {};
|