ralph-cli-sandboxed 0.4.2 → 0.5.0

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,6 +1,6 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
- import { getCliProviders } from "../templates/prompts.js";
3
+ import { getCliProviders, DEFAULT_PRD_YAML, DEFAULT_PROGRESS } from "../templates/prompts.js";
4
4
  export const DEFAULT_CLI_CONFIG = {
5
5
  command: "claude",
6
6
  args: [],
@@ -41,8 +41,70 @@ export function getCliConfig(config) {
41
41
  const RALPH_DIR = ".ralph";
42
42
  const CONFIG_FILE = "config.json";
43
43
  const PROMPT_FILE = "prompt.md";
44
- const PRD_FILE = "prd.json";
44
+ const PRD_FILE_JSON = "prd.json";
45
+ const PRD_FILE_YAML = "prd.yaml";
45
46
  const PROGRESS_FILE = "progress.txt";
47
+ /**
48
+ * Gets the PRD file path(s) that exist.
49
+ * Returns an object with:
50
+ * - primary: The main PRD file path to use (yaml preferred over json)
51
+ * - secondary: The secondary PRD file path if both exist (for merging)
52
+ * - jsonOnly: True if only prd.json exists (shows migration notice)
53
+ * - yamlOnly: True if only prd.yaml exists (happy path)
54
+ * - both: True if both files exist (merge mode)
55
+ * - none: True if no PRD file exists
56
+ */
57
+ export function getPrdFiles() {
58
+ const ralphDir = getRalphDir();
59
+ const jsonPath = join(ralphDir, PRD_FILE_JSON);
60
+ const yamlPath = join(ralphDir, PRD_FILE_YAML);
61
+ const hasJson = existsSync(jsonPath);
62
+ const hasYaml = existsSync(yamlPath);
63
+ if (hasYaml && hasJson) {
64
+ // Both exist - merge mode (YAML is primary)
65
+ return {
66
+ primary: yamlPath,
67
+ secondary: jsonPath,
68
+ jsonOnly: false,
69
+ yamlOnly: false,
70
+ both: true,
71
+ none: false,
72
+ };
73
+ }
74
+ else if (hasYaml) {
75
+ // Only YAML exists - happy path
76
+ return {
77
+ primary: yamlPath,
78
+ secondary: null,
79
+ jsonOnly: false,
80
+ yamlOnly: true,
81
+ both: false,
82
+ none: false,
83
+ };
84
+ }
85
+ else if (hasJson) {
86
+ // Only JSON exists - show migration notice
87
+ return {
88
+ primary: jsonPath,
89
+ secondary: null,
90
+ jsonOnly: true,
91
+ yamlOnly: false,
92
+ both: false,
93
+ none: false,
94
+ };
95
+ }
96
+ else {
97
+ // No PRD file exists
98
+ return {
99
+ primary: null,
100
+ secondary: null,
101
+ jsonOnly: false,
102
+ yamlOnly: false,
103
+ both: false,
104
+ none: true,
105
+ };
106
+ }
107
+ }
46
108
  export function getRalphDir() {
47
109
  return join(process.cwd(), RALPH_DIR);
48
110
  }
@@ -66,20 +128,38 @@ export function checkFilesExist() {
66
128
  if (!existsSync(ralphDir)) {
67
129
  throw new Error(".ralph/ directory not found. Run 'ralph init' first.");
68
130
  }
69
- const requiredFiles = [CONFIG_FILE, PROMPT_FILE, PRD_FILE, PROGRESS_FILE];
131
+ // Check config and prompt files (these are critical and must exist)
132
+ const requiredFiles = [CONFIG_FILE, PROMPT_FILE];
70
133
  for (const file of requiredFiles) {
71
134
  if (!existsSync(join(ralphDir, file))) {
72
135
  throw new Error(`.ralph/${file} not found. Run 'ralph init' first.`);
73
136
  }
74
137
  }
138
+ // Create progress.txt if it doesn't exist (user may have cleaned up)
139
+ const progressPath = join(ralphDir, PROGRESS_FILE);
140
+ if (!existsSync(progressPath)) {
141
+ writeFileSync(progressPath, DEFAULT_PROGRESS);
142
+ console.log(`Created ${progressPath}`);
143
+ }
144
+ // Create prd.yaml if no PRD file exists (user may have cleaned up)
145
+ const prdFiles = getPrdFiles();
146
+ if (prdFiles.none) {
147
+ const prdPath = join(ralphDir, PRD_FILE_YAML);
148
+ writeFileSync(prdPath, DEFAULT_PRD_YAML);
149
+ console.log(`Created ${prdPath}`);
150
+ }
75
151
  }
76
152
  export function getPaths() {
77
153
  const ralphDir = getRalphDir();
154
+ const prdFiles = getPrdFiles();
155
+ // Use the primary PRD file path (yaml preferred over json, fallback to json for backwards compat)
156
+ const prdPath = prdFiles.primary || join(ralphDir, PRD_FILE_JSON);
78
157
  return {
79
158
  dir: ralphDir,
80
159
  config: join(ralphDir, CONFIG_FILE),
81
160
  prompt: join(ralphDir, PROMPT_FILE),
82
- prd: join(ralphDir, PRD_FILE),
161
+ prd: prdPath,
162
+ prdSecondary: prdFiles.secondary, // Second PRD file if merging
83
163
  progress: join(ralphDir, PROGRESS_FILE),
84
164
  };
85
165
  }
@@ -40,11 +40,13 @@ export declare function smartMerge(original: PrdEntry[], corrupted: unknown): Me
40
40
  export declare function attemptRecovery(corrupted: unknown): PrdEntry[] | null;
41
41
  /**
42
42
  * Creates a timestamped backup of the PRD file.
43
+ * Preserves the original file extension (.json or .yaml/.yml).
43
44
  * Returns the backup path.
44
45
  */
45
46
  export declare function createBackup(prdPath: string): string;
46
47
  /**
47
48
  * Finds the most recent backup file.
49
+ * Searches for both .json and .yaml/.yml backup files.
48
50
  * Returns the path or null if no backups exist.
49
51
  */
50
52
  export declare function findLatestBackup(prdPath: string): string | null;
@@ -55,7 +57,16 @@ export declare function findLatestBackup(prdPath: string): string | null;
55
57
  */
56
58
  export declare function createTemplatePrd(backupPath?: string): PrdEntry[];
57
59
  /**
58
- * Reads and parses a PRD file, handling potential JSON errors.
60
+ * Reads and parses a YAML PRD file.
61
+ * Returns the parsed content or null if it couldn't be parsed.
62
+ */
63
+ export declare function readYamlPrdFile(prdPath: string): {
64
+ content: unknown;
65
+ raw: string;
66
+ } | null;
67
+ /**
68
+ * Reads and parses a PRD file, handling potential JSON/YAML errors.
69
+ * Detects file format based on extension (.yaml/.yml uses YAML, .json uses JSON).
59
70
  * Returns the parsed content or null if it couldn't be parsed.
60
71
  */
61
72
  export declare function readPrdFile(prdPath: string): {
@@ -63,9 +74,17 @@ export declare function readPrdFile(prdPath: string): {
63
74
  raw: string;
64
75
  } | null;
65
76
  /**
66
- * Writes a PRD to file.
77
+ * Writes a PRD to file in JSON format.
67
78
  */
68
79
  export declare function writePrd(prdPath: string, entries: PrdEntry[]): void;
80
+ /**
81
+ * Writes a PRD to file in YAML format.
82
+ */
83
+ export declare function writePrdYaml(prdPath: string, entries: PrdEntry[]): void;
84
+ /**
85
+ * Writes a PRD to file, detecting format from file extension.
86
+ */
87
+ export declare function writePrdAuto(prdPath: string, entries: PrdEntry[]): void;
69
88
  /**
70
89
  * Expands @{filepath} patterns in a string with actual file contents.
71
90
  * Similar to curl's @ syntax for including file contents.
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
2
- import { join, dirname } from "path";
2
+ import { join, dirname, extname } from "path";
3
+ import YAML from "yaml";
3
4
  const VALID_CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
4
5
  /**
5
6
  * Validates that a PRD structure is correct.
@@ -340,18 +341,23 @@ function attemptArrayRecovery(items) {
340
341
  }
341
342
  /**
342
343
  * Creates a timestamped backup of the PRD file.
344
+ * Preserves the original file extension (.json or .yaml/.yml).
343
345
  * Returns the backup path.
344
346
  */
345
347
  export function createBackup(prdPath) {
346
348
  const content = readFileSync(prdPath, "utf-8");
347
349
  const dir = dirname(prdPath);
348
350
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
349
- const backupPath = join(dir, `backup.prd.${timestamp}.json`);
351
+ const ext = extname(prdPath).toLowerCase();
352
+ // Preserve original extension, default to .json if unknown
353
+ const backupExt = ext === ".yaml" || ext === ".yml" ? ext : ".json";
354
+ const backupPath = join(dir, `backup.prd.${timestamp}${backupExt}`);
350
355
  writeFileSync(backupPath, content);
351
356
  return backupPath;
352
357
  }
353
358
  /**
354
359
  * Finds the most recent backup file.
360
+ * Searches for both .json and .yaml/.yml backup files.
355
361
  * Returns the path or null if no backups exist.
356
362
  */
357
363
  export function findLatestBackup(prdPath) {
@@ -361,7 +367,8 @@ export function findLatestBackup(prdPath) {
361
367
  }
362
368
  const files = readdirSync(dir);
363
369
  const backups = files
364
- .filter((f) => f.startsWith("backup.prd.") && f.endsWith(".json"))
370
+ .filter((f) => f.startsWith("backup.prd.") &&
371
+ (f.endsWith(".json") || f.endsWith(".yaml") || f.endsWith(".yml")))
365
372
  .sort()
366
373
  .reverse();
367
374
  if (backups.length === 0) {
@@ -384,7 +391,7 @@ export function createTemplatePrd(backupPath) {
384
391
  description: "Fix the PRD entries",
385
392
  steps: [
386
393
  `Recreate PRD entries based on this corrupted backup content:\n\n@{${absolutePath}}`,
387
- "Write valid entries to .ralph/prd.json with format: category (string), description (string), steps (array of strings), passes (boolean)",
394
+ "Write valid entries to .ralph/prd.yaml with format: category (string), description (string), steps (array of strings), passes (boolean)",
388
395
  ],
389
396
  passes: false,
390
397
  },
@@ -395,7 +402,7 @@ export function createTemplatePrd(backupPath) {
395
402
  category: "setup",
396
403
  description: "Add PRD entries",
397
404
  steps: [
398
- "Add requirements using 'ralph add' or edit .ralph/prd.json directly",
405
+ "Add requirements using 'ralph add' or edit .ralph/prd.yaml directly",
399
406
  "Verify format: category (string), description (string), steps (array of strings), passes (boolean)",
400
407
  ],
401
408
  passes: false,
@@ -403,13 +410,37 @@ export function createTemplatePrd(backupPath) {
403
410
  ];
404
411
  }
405
412
  /**
406
- * Reads and parses a PRD file, handling potential JSON errors.
413
+ * Reads and parses a YAML PRD file.
414
+ * Returns the parsed content or null if it couldn't be parsed.
415
+ */
416
+ export function readYamlPrdFile(prdPath) {
417
+ try {
418
+ const raw = readFileSync(prdPath, "utf-8");
419
+ const content = YAML.parse(raw);
420
+ return { content, raw };
421
+ }
422
+ catch {
423
+ return null;
424
+ }
425
+ }
426
+ /**
427
+ * Reads and parses a PRD file, handling potential JSON/YAML errors.
428
+ * Detects file format based on extension (.yaml/.yml uses YAML, .json uses JSON).
407
429
  * Returns the parsed content or null if it couldn't be parsed.
408
430
  */
409
431
  export function readPrdFile(prdPath) {
410
432
  try {
411
433
  const raw = readFileSync(prdPath, "utf-8");
412
- const content = JSON.parse(raw);
434
+ const ext = extname(prdPath).toLowerCase();
435
+ // Parse based on file extension
436
+ let content;
437
+ if (ext === ".yaml" || ext === ".yml") {
438
+ content = YAML.parse(raw);
439
+ }
440
+ else {
441
+ // Default to JSON for .json or any other extension
442
+ content = JSON.parse(raw);
443
+ }
413
444
  return { content, raw };
414
445
  }
415
446
  catch {
@@ -417,17 +448,39 @@ export function readPrdFile(prdPath) {
417
448
  }
418
449
  }
419
450
  /**
420
- * Writes a PRD to file.
451
+ * Writes a PRD to file in JSON format.
421
452
  */
422
453
  export function writePrd(prdPath, entries) {
423
454
  writeFileSync(prdPath, JSON.stringify(entries, null, 2) + "\n");
424
455
  }
456
+ /**
457
+ * Writes a PRD to file in YAML format.
458
+ */
459
+ export function writePrdYaml(prdPath, entries) {
460
+ writeFileSync(prdPath, YAML.stringify(entries));
461
+ }
462
+ /**
463
+ * Writes a PRD to file, detecting format from file extension.
464
+ */
465
+ export function writePrdAuto(prdPath, entries) {
466
+ const ext = extname(prdPath).toLowerCase();
467
+ if (ext === ".yaml" || ext === ".yml") {
468
+ writePrdYaml(prdPath, entries);
469
+ }
470
+ else {
471
+ writePrd(prdPath, entries);
472
+ }
473
+ }
425
474
  /**
426
475
  * Expands @{filepath} patterns in a string with actual file contents.
427
476
  * Similar to curl's @ syntax for including file contents.
428
477
  * Paths are resolved relative to the .ralph directory.
429
478
  */
430
479
  export function expandFileReferences(text, baseDir) {
480
+ // Handle null/undefined text
481
+ if (typeof text !== "string") {
482
+ return text ?? "";
483
+ }
431
484
  // Match @{filepath} patterns
432
485
  const pattern = /@\{([^}]+)\}/g;
433
486
  return text.replace(pattern, (match, filepath) => {
@@ -4,19 +4,16 @@ This guide explains how to write Product Requirement Documents (PRDs) that Ralph
4
4
 
5
5
  ## PRD Structure
6
6
 
7
- Each PRD item is a JSON object with four fields:
8
-
9
- ```json
10
- {
11
- "category": "feature",
12
- "description": "Short imperative description of what to implement",
13
- "steps": [
14
- "First concrete action to take",
15
- "Second concrete action to take",
16
- "Verification step to confirm completion"
17
- ],
18
- "passes": false
19
- }
7
+ Each PRD entry is a YAML object with four fields:
8
+
9
+ ```yaml
10
+ - category: feature
11
+ description: Short imperative description of what to implement
12
+ steps:
13
+ - First concrete action to take
14
+ - Second concrete action to take
15
+ - Verification step to confirm completion
16
+ passes: false
20
17
  ```
21
18
 
22
19
  ## Categories
@@ -74,90 +71,80 @@ Steps tell the AI agent exactly **how** to verify or implement the requirement.
74
71
  ### Step Patterns
75
72
 
76
73
  **For features:**
77
- ```json
78
- "steps": [
79
- "Implement X in src/path/file.ts",
80
- "Add Y functionality that does Z",
81
- "Run `command` and confirm expected output"
82
- ]
74
+ ```yaml
75
+ steps:
76
+ - Implement X in src/path/file.ts
77
+ - Add Y functionality that does Z
78
+ - Run `command` and confirm expected output
83
79
  ```
84
80
 
85
81
  **For bug fixes:**
86
- ```json
87
- "steps": [
88
- "Identify the cause of X in src/path/file.ts",
89
- "Fix by doing Y",
90
- "Run `command` and verify the bug is resolved"
91
- ]
82
+ ```yaml
83
+ steps:
84
+ - Identify the cause of X in src/path/file.ts
85
+ - Fix by doing Y
86
+ - Run `command` and verify the bug is resolved
92
87
  ```
93
88
 
94
89
  **For documentation:**
95
- ```json
96
- "steps": [
97
- "Add section 'X' to README.md",
98
- "Include explanation of Y",
99
- "Include example showing Z"
100
- ]
90
+ ```yaml
91
+ steps:
92
+ - Add section 'X' to README.md
93
+ - Include explanation of Y
94
+ - Include example showing Z
101
95
  ```
102
96
 
103
97
  **For releases:**
104
- ```json
105
- "steps": [
106
- "Update version in package.json to 'X.Y.Z'",
107
- "Run `npm run build` to verify no errors",
108
- "Run `command --version` and confirm it shows X.Y.Z"
109
- ]
98
+ ```yaml
99
+ steps:
100
+ - Update version in package.json to 'X.Y.Z'
101
+ - Run `npm run build` to verify no errors
102
+ - Run `command --version` and confirm it shows X.Y.Z
110
103
  ```
111
104
 
112
105
  ## Anti-Patterns to Avoid
113
106
 
114
107
  ### Vague Steps
115
- ```json
116
- // Bad
117
- "steps": [
118
- "Make it work",
119
- "Test it",
120
- "Verify it's good"
121
- ]
122
-
123
- // Good
124
- "steps": [
125
- "Add error handling for null input in parseConfig()",
126
- "Run `npm test` and confirm all tests pass",
127
- "Run `ralph init` with missing config and verify helpful error message"
128
- ]
108
+ ```yaml
109
+ # Bad
110
+ steps:
111
+ - Make it work
112
+ - Test it
113
+ - Verify it's good
114
+
115
+ # Good
116
+ steps:
117
+ - Add error handling for null input in parseConfig()
118
+ - Run `npm test` and confirm all tests pass
119
+ - Run `ralph init` with missing config and verify helpful error message
129
120
  ```
130
121
 
131
122
  ### Steps That Require Human Judgment
132
- ```json
133
- // Bad
134
- "steps": [
135
- "Understand the codebase",
136
- "Decide the best approach",
137
- "Implement your solution"
138
- ]
139
-
140
- // Good
141
- "steps": [
142
- "Add retry logic with exponential backoff to fetchData() in src/api.ts",
143
- "Set max retries to 3 with initial delay of 1000ms",
144
- "Run `npm test` and verify retry tests pass"
145
- ]
123
+ ```yaml
124
+ # Bad
125
+ steps:
126
+ - Understand the codebase
127
+ - Decide the best approach
128
+ - Implement your solution
129
+
130
+ # Good
131
+ steps:
132
+ - Add retry logic with exponential backoff to fetchData() in src/api.ts
133
+ - Set max retries to 3 with initial delay of 1000ms
134
+ - Run `npm test` and verify retry tests pass
146
135
  ```
147
136
 
148
137
  ### Missing Verification
149
- ```json
150
- // Bad
151
- "steps": [
152
- "Update the version number"
153
- ]
154
-
155
- // Good
156
- "steps": [
157
- "Update version in package.json to '1.2.3'",
158
- "Run `npm run build` to verify no errors",
159
- "Run `ralph --version` and confirm output shows '1.2.3'"
160
- ]
138
+ ```yaml
139
+ # Bad
140
+ steps:
141
+ - Update the version number
142
+
143
+ # Good
144
+ steps:
145
+ - Update version in package.json to '1.2.3'
146
+ - Run `npm run build` to verify no errors
147
+ - Run `ralph --version` and confirm output shows '1.2.3'
161
148
  ```
162
149
 
163
150
  ## Priority Through Ordering
@@ -176,39 +163,34 @@ Recommended ordering:
176
163
 
177
164
  Break large features into smaller, independently completable items. Each item should be achievable in a single Ralph iteration.
178
165
 
179
- ```json
180
- // Too large
181
- {
182
- "description": "Implement user authentication system",
183
- "steps": ["Add login, logout, registration, password reset, OAuth..."]
184
- }
185
-
186
- // Better: Split into multiple items
187
- {
188
- "description": "Add user registration endpoint POST /api/register",
189
- "steps": [...]
190
- },
191
- {
192
- "description": "Add user login endpoint POST /api/login",
193
- "steps": [...]
194
- },
195
- {
196
- "description": "Add JWT token generation and validation",
197
- "steps": [...]
198
- }
166
+ ```yaml
167
+ # Too large
168
+ - description: Implement user authentication system
169
+ steps:
170
+ - Add login, logout, registration, password reset, OAuth...
171
+
172
+ # Better: Split into multiple items
173
+ - description: Add user registration endpoint POST /api/register
174
+ steps:
175
+ - ...
176
+
177
+ - description: Add user login endpoint POST /api/login
178
+ steps:
179
+ - ...
180
+
181
+ - description: Add JWT token generation and validation
182
+ steps:
183
+ - ...
199
184
  ```
200
185
 
201
186
  ## Quick Reference
202
187
 
203
- ```json
204
- {
205
- "category": "setup|feature|bugfix|refactor|docs|test|release|config|ui|integration",
206
- "description": "Imperative verb + specific what + where (context)",
207
- "steps": [
208
- "Concrete action with `commands` and file paths",
209
- "Another specific action",
210
- "Verification: Run `command` and confirm expected result"
211
- ],
212
- "passes": false
213
- }
188
+ ```yaml
189
+ - category: setup|feature|bugfix|refactor|docs|test|release|config|ui|integration
190
+ description: Imperative verb + specific what + where (context)
191
+ steps:
192
+ - Concrete action with `commands` and file paths
193
+ - Another specific action
194
+ - Verification: Run `command` and confirm expected result
195
+ passes: false
214
196
  ```