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.
@@ -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
- 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) {
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 ${iterations} ralph iteration(s)...`);
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
- for (let i = 1; i <= iterations; i++) {
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
- const counts = countPrdItems(paths.prd, category);
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 && iterations === Infinity) {
188
- console.log(`Iteration ${i}`);
277
+ else if (loopMode) {
278
+ console.log(`Iteration ${iterationCount}`);
189
279
  }
190
280
  else {
191
- console.log(`Iteration ${i} of ${iterations}`);
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 i so we don't skip an iteration count
227
- i--;
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", "GOOGLE_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 @.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 {};