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.
@@ -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
- const tempPath = join(tmpdir(), `ralph-prd-filtered-${Date.now()}.json`);
22
- writeFileSync(tempPath, JSON.stringify(filteredItems, null, 2));
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, debug) {
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 ${iterations} ralph iteration(s)...`);
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
- for (let i = 1; i <= iterations; i++) {
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
- const counts = countPrdItems(paths.prd, category);
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 && iterations === Infinity) {
198
- console.log(`Iteration ${i}`);
319
+ else if (loopMode) {
320
+ console.log(`Iteration ${iterationCount}`);
199
321
  }
200
322
  else {
201
- console.log(`Iteration ${i} of ${iterations}`);
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 i so we don't skip an iteration count
237
- i--;
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": ["--yolo"],
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 @.ralph/prd.json file to find the highest priority feature that has "passes": false
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 the PRD entry to set "passes": true once verified
90
- 5. Append a brief note about what you did to @.ralph/progress.txt
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
 
@@ -3,6 +3,7 @@ export interface CliConfig {
3
3
  args?: string[];
4
4
  yoloArgs?: string[];
5
5
  promptArgs?: string[];
6
+ modelArgs?: string[];
6
7
  }
7
8
  export interface RalphConfig {
8
9
  language: string;
@@ -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
- // If promptArgs is already set, use it
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
- if (!_getCliProviders) {
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
- if (provider?.promptArgs !== undefined) {
25
- return { ...cliConfig, promptArgs: provider.promptArgs };
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 {};