claude-ralph 1.0.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.
package/dist/cli.js ADDED
@@ -0,0 +1,775 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs7 from 'fs-extra';
4
+ import path7 from 'path';
5
+ import chalk4 from 'chalk';
6
+ import { cosmiconfig } from 'cosmiconfig';
7
+ import { z } from 'zod';
8
+ import { execa, execaSync } from 'execa';
9
+
10
+ var logger = {
11
+ info(message) {
12
+ console.log(chalk4.blue("[INFO]"), message);
13
+ },
14
+ success(message) {
15
+ console.log(chalk4.green("[SUCCESS]"), message);
16
+ },
17
+ warning(message) {
18
+ console.log(chalk4.yellow("[WARNING]"), message);
19
+ },
20
+ error(message) {
21
+ console.log(chalk4.red("[ERROR]"), message);
22
+ },
23
+ log(message) {
24
+ console.log(message);
25
+ },
26
+ header(title) {
27
+ console.log("");
28
+ console.log(chalk4.bold("========================================"));
29
+ console.log(chalk4.bold(` ${title}`));
30
+ console.log(chalk4.bold("========================================"));
31
+ console.log("");
32
+ },
33
+ divider() {
34
+ console.log(chalk4.gray("----------------------------------------"));
35
+ },
36
+ list(items, indent = 2) {
37
+ const prefix = " ".repeat(indent) + "- ";
38
+ for (const item of items) {
39
+ console.log(prefix + item);
40
+ }
41
+ },
42
+ keyValue(key, value) {
43
+ console.log(` ${chalk4.cyan(key)}: ${value}`);
44
+ }
45
+ };
46
+ var RepositoryConfigSchema = z.object({
47
+ path: z.string(),
48
+ defaultBranch: z.string().default("main"),
49
+ checks: z.array(z.string()).default([])
50
+ });
51
+ var AgentConfigSchema = z.object({
52
+ maxIterations: z.number().default(50),
53
+ timeout: z.number().default(600)
54
+ });
55
+ var RalphConfigSchema = z.object({
56
+ $schema: z.string().optional(),
57
+ version: z.string().optional(),
58
+ project: z.string(),
59
+ description: z.string().optional(),
60
+ repositories: z.record(z.string(), RepositoryConfigSchema),
61
+ agent: AgentConfigSchema.optional().default({})
62
+ });
63
+ var PrdRepositorySchema = z.object({
64
+ branchName: z.string(),
65
+ activeBranch: z.string().optional()
66
+ });
67
+ var UserStorySchema = z.object({
68
+ id: z.string(),
69
+ title: z.string(),
70
+ repo: z.string(),
71
+ description: z.string(),
72
+ acceptanceCriteria: z.array(z.string()),
73
+ priority: z.number(),
74
+ passes: z.boolean().default(false),
75
+ fork: z.boolean().default(false),
76
+ notes: z.string().default("")
77
+ });
78
+ var PrdSchema = z.object({
79
+ project: z.string(),
80
+ description: z.string().optional(),
81
+ repositories: z.record(z.string(), PrdRepositorySchema),
82
+ userStories: z.array(UserStorySchema)
83
+ });
84
+
85
+ // src/core/config.ts
86
+ var MODULE_NAME = "ralph";
87
+ var explorer = cosmiconfig(MODULE_NAME, {
88
+ searchPlaces: [
89
+ "ralph.config.json",
90
+ ".ralphrc",
91
+ ".ralphrc.json",
92
+ ".ralphrc.yaml",
93
+ ".ralphrc.yml",
94
+ "package.json"
95
+ ]
96
+ });
97
+ var ConfigNotFoundError = class extends Error {
98
+ constructor(searchPath) {
99
+ super(
100
+ `No Ralph configuration found. Run 'ralph init' to create one, or create ralph.config.json manually.
101
+ Searched from: ${searchPath}`
102
+ );
103
+ this.name = "ConfigNotFoundError";
104
+ }
105
+ };
106
+ var ConfigValidationError = class extends Error {
107
+ constructor(message) {
108
+ super(`Invalid configuration: ${message}`);
109
+ this.name = "ConfigValidationError";
110
+ }
111
+ };
112
+ async function loadConfig(cwd = process.cwd()) {
113
+ const result = await explorer.search(cwd);
114
+ if (!result || !result.config) {
115
+ throw new ConfigNotFoundError(cwd);
116
+ }
117
+ const parsed = RalphConfigSchema.safeParse(result.config);
118
+ if (!parsed.success) {
119
+ throw new ConfigValidationError(parsed.error.message);
120
+ }
121
+ return parsed.data;
122
+ }
123
+ async function configExists(cwd = process.cwd()) {
124
+ const result = await explorer.search(cwd);
125
+ return result !== null && result.config !== void 0;
126
+ }
127
+ function getDefaultConfig(projectName) {
128
+ return {
129
+ version: "1.0",
130
+ project: projectName,
131
+ description: "Project description",
132
+ repositories: {
133
+ main: {
134
+ path: ".",
135
+ defaultBranch: "main",
136
+ checks: ["npm run build", "npm run lint", "npm test"]
137
+ }
138
+ },
139
+ agent: {
140
+ maxIterations: 50,
141
+ timeout: 600
142
+ }
143
+ };
144
+ }
145
+
146
+ // src/commands/init.ts
147
+ async function initCommand(options = {}) {
148
+ const cwd = process.cwd();
149
+ const configPath = path7.join(cwd, "ralph.config.json");
150
+ if (await configExists(cwd)) {
151
+ if (!options.force) {
152
+ logger.warning("Configuration already exists. Use --force to overwrite.");
153
+ return;
154
+ }
155
+ logger.info("Overwriting existing configuration...");
156
+ }
157
+ let projectName = path7.basename(cwd);
158
+ const packageJsonPath = path7.join(cwd, "package.json");
159
+ if (await fs7.pathExists(packageJsonPath)) {
160
+ try {
161
+ const pkg = await fs7.readJSON(packageJsonPath);
162
+ if (pkg.name) {
163
+ projectName = pkg.name;
164
+ }
165
+ } catch {
166
+ }
167
+ }
168
+ const config = getDefaultConfig(projectName);
169
+ await fs7.writeJSON(configPath, config, { spaces: 2 });
170
+ logger.success(`Created ralph.config.json`);
171
+ logger.log("");
172
+ logger.log("Next steps:");
173
+ logger.log(" 1. Edit ralph.config.json to configure your repositories");
174
+ logger.log(" 2. Run: ralph plan <feature>");
175
+ logger.log(" 3. Run: ralph prd");
176
+ logger.log(" 4. Run: ralph run");
177
+ const gitignorePath = path7.join(cwd, ".gitignore");
178
+ if (await fs7.pathExists(gitignorePath)) {
179
+ const gitignore = await fs7.readFile(gitignorePath, "utf-8");
180
+ const entries = ["plan.md", "prd.json", "progress.txt", "archive/", ".last-project"];
181
+ const toAdd = entries.filter((e) => !gitignore.includes(e));
182
+ if (toAdd.length > 0) {
183
+ const addition = `
184
+ # Ralph
185
+ ${toAdd.join("\n")}
186
+ `;
187
+ await fs7.appendFile(gitignorePath, addition);
188
+ logger.info(`Added ${toAdd.length} entries to .gitignore`);
189
+ }
190
+ }
191
+ }
192
+ async function run(command, args = [], options = {}) {
193
+ const result = await execa(command, args, {
194
+ reject: false,
195
+ ...options
196
+ });
197
+ return {
198
+ stdout: String(result.stdout ?? ""),
199
+ stderr: String(result.stderr ?? ""),
200
+ exitCode: result.exitCode ?? 1
201
+ };
202
+ }
203
+ function isCommandAvailable(command) {
204
+ try {
205
+ const result = execaSync("which", [command], { reject: false });
206
+ return result.exitCode === 0;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ // src/core/claude.ts
213
+ var ClaudeNotFoundError = class extends Error {
214
+ constructor() {
215
+ super(
216
+ "Claude Code CLI not found. Install it from: https://claude.ai/code"
217
+ );
218
+ this.name = "ClaudeNotFoundError";
219
+ }
220
+ };
221
+ function isClaudeInstalled() {
222
+ return isCommandAvailable("claude");
223
+ }
224
+ function checkClaudeInstalled() {
225
+ if (!isClaudeInstalled()) {
226
+ throw new ClaudeNotFoundError();
227
+ }
228
+ }
229
+ async function invokeClaudeStreaming(prompt, options = {}) {
230
+ checkClaudeInstalled();
231
+ const args = [];
232
+ if (options.print) {
233
+ args.push("--print");
234
+ }
235
+ if (options.skipPermissions) {
236
+ args.push("--dangerously-skip-permissions");
237
+ }
238
+ const finalPrompt = options.ultrathink ? `ultrathink ${prompt}` : prompt;
239
+ if (options.print) {
240
+ args.push(finalPrompt);
241
+ }
242
+ const subprocess = execa("claude", args, {
243
+ cwd: options.cwd,
244
+ timeout: options.timeout ? options.timeout * 1e3 : void 0,
245
+ reject: false,
246
+ input: options.print ? void 0 : finalPrompt
247
+ });
248
+ subprocess.stderr?.pipe(process.stderr);
249
+ const result = await subprocess;
250
+ return result.stdout;
251
+ }
252
+
253
+ // src/templates/embedded/prompt.ts
254
+ var PROMPT_TEMPLATE = '# Ralph Agent Instructions\n\nYou are an autonomous coding agent executing user stories from a PRD.\n\n## Your Task\n\n1. Read the PRD at `prd.json`\n2. Read the progress log at `progress.txt` (check Codebase Patterns section first)\n3. Read `ralph.config.json` to understand the project structure\n4. Pick the **highest priority** user story where `passes: false`\n5. Note which `repo` the story targets\n6. Ensure you\'re on the correct branch in that repo\n7. Implement that single user story\n8. Run quality checks for the target repo\n9. If checks pass, commit changes with message: `feat: [Story ID] - [Story Title]`\n10. Update `prd.json` to set `passes: true` for the completed story\n11. Append your progress to `progress.txt`\n\n## Configuration\n\nRead `ralph.config.json` to understand:\n- Available repositories and their paths\n- Quality checks to run for each repo\n- Project structure\n\nExample configuration:\n```json\n{\n "repositories": {\n "backend": {\n "path": "./backend",\n "checks": ["pytest", "mypy ."]\n },\n "frontend": {\n "path": "./frontend",\n "checks": ["npm run build", "npm run lint"]\n }\n }\n}\n```\n\n## Branch Management\n\nEach story specifies a `repo` field. Before implementing:\n\n1. **Check the story\'s `repo` field** (e.g., `"repo": "backend"`)\n2. **Get the repo path** from `ralph.config.json`\n3. **Navigate to that repo** and check/create the branch:\n ```bash\n cd <repo-path>\n git checkout -b feature/my-feature # or checkout existing branch\n ```\n4. **After implementing, commit in that repo:**\n ```bash\n cd <repo-path>\n git add .\n git commit -m "feat: US-001 - Add status field"\n ```\n\nThe PRD contains a `repositories` section with branch names for each repo:\n```json\n{\n "repositories": {\n "backend": { "branchName": "feature/task-status" },\n "frontend": { "branchName": "feature/task-status" }\n }\n}\n```\n\n## Branch Forking\n\nSome stories may request a "fork" - creating a new branch from the current one.\n\n### When to Fork\n\nCheck the story\'s `fork` field:\n- `"fork": true` - Create a new branch before implementing\n- `"fork": false` or not specified - Use the existing branch\n\n### Fork Algorithm\n\nIf `fork: true` for the current story:\n\n1. **Check if already forked**:\n - If `repositories[repo].activeBranch` exists and differs from `branchName`, the fork already happened\n - Just checkout `activeBranch` and continue\n\n2. **Detect next branch number:**\n ```bash\n cd <repo-path>\n BASE_BRANCH=$(jq -r \'.repositories["<repo>"].branchName\' prd.json)\n EXISTING=$(git branch --list "${BASE_BRANCH}-*" | tr -d \' *\')\n # Find max number and increment\n NEW_BRANCH="${BASE_BRANCH}-${NEXT_NUM}"\n ```\n\n3. **Create and checkout new branch:**\n ```bash\n git checkout -b "$NEW_BRANCH"\n ```\n\n4. **Update PRD** to reflect the new active branch\n\n## Quality Requirements\n\nBefore committing, run the quality checks from `ralph.config.json`:\n\n```bash\ncd <repo-path>\n# Run each check from config\n<check-command-1>\n<check-command-2>\n```\n\n**General Rules:**\n- ALL commits must pass quality checks\n- Do NOT commit broken code\n- Keep changes focused and minimal\n- Follow existing code patterns\n\n## Progress Report Format\n\nAPPEND to `progress.txt` (never replace, always append):\n```\n## [Date/Time] - [Story ID] ([repo])\n- What was implemented\n- Files changed\n- Commit: [commit hash]\n- **Learnings for future iterations:**\n - Patterns discovered\n - Gotchas encountered\n---\n```\n\n## Consolidate Patterns\n\nIf you discover a **reusable pattern**, add it to the `## Codebase Patterns` section at the TOP of `progress.txt`:\n\n```\n## Codebase Patterns\n\n### [repo-name]\n- Example: Use Pydantic models for request/response\n- Example: Routes are in `routers/` directory\n```\n\n## Update CLAUDE.md Files\n\nBefore committing, check if learnings should be added to the target repo\'s `CLAUDE.md` file.\n\n## Browser Testing (For Frontend Stories)\n\nFor any story that changes UI:\n\n1. Start the dev server\n2. Navigate to the relevant page\n3. Verify the UI changes work as expected\n4. A frontend story is NOT complete until browser verification passes\n\n## Stop Condition\n\nAfter completing a user story, check if ALL stories have `passes: true`.\n\nIf ALL stories are complete and passing, reply with:\n<promise>COMPLETE</promise>\n\nIf there are still stories with `passes: false`, end your response normally (another iteration will pick up the next story).\n\n## Important\n\n- Work on ONE story per iteration\n- Each story targets ONE repository\n- Read the story\'s `repo` field to know which repo to work in\n- Use the correct branch for each repo (from PRD `repositories` section)\n- Read the Codebase Patterns section in `progress.txt` before starting\n- Read the relevant CLAUDE.md for the target repo if it exists\n';
255
+
256
+ // src/templates/embedded/skills-ralph.ts
257
+ var RALPH_SKILL = '# Ralph - Plan Generator\n\nGenerate a structured implementation plan from a feature description.\n\n---\n\n## The Job\n\n1. Read the project configuration from `ralph.config.json`\n2. Analyze the user\'s feature request\n3. Ask 3-5 clarifying questions to understand scope\n4. Generate a detailed plan in `plan.md`\n5. Offer to validate or modify the plan\n\n---\n\n## Step 1: Read Configuration\n\nFirst, read `ralph.config.json` to understand:\n- Project name and description\n- Available repositories and their paths\n- Quality checks for each repository\n\nIf `ralph.config.json` doesn\'t exist, ask the user to create one or offer to generate a default configuration.\n\n---\n\n## Step 2: Clarifying Questions\n\nAsk 3-5 essential questions to understand the feature. Format questions with lettered options:\n\n```\n1. What is the primary goal of this feature?\n A) Add new functionality\n B) Improve existing functionality\n C) Fix a bug\n D) Refactor/cleanup\n\n2. Which parts of the codebase will be affected?\n A) Backend only\n B) Frontend only\n C) Both backend and frontend\n D) Other (specify)\n\n3. Are there any external dependencies required?\n A) No new dependencies\n B) Yes (specify which)\n```\n\nWait for the user to respond with format like "1A, 2C, 3A" before proceeding.\n\n---\n\n## Step 3: Generate Plan\n\nBased on the answers, generate `plan.md` with this structure:\n\n```markdown\n# [Project Name] - [Feature Name]\n\n[Brief description of the feature]\n\n---\n\n## Summary\n\n[2-3 sentences explaining what this plan accomplishes]\n\n---\n\n## User Stories\n\n### [Repository 1] Stories\n\n#### US-001: [Title]\n**Repository:** `repo-name`\n**Description:** As a [role], I want [feature] so that [benefit].\n\n**Acceptance Criteria:**\n- [ ] Specific, verifiable criterion\n- [ ] Another criterion\n- [ ] Tests pass (pytest/npm test/etc.)\n\n[Repeat for each story...]\n\n---\n\n## Technical Considerations\n\n- [Architecture decisions]\n- [Dependencies]\n- [Breaking changes]\n\n---\n\n## Open Questions\n\n- [Any unresolved questions]\n```\n\n---\n\n## Step 4: Story Guidelines\n\n### Story Size\n- Each story must be completable in ONE Claude Code iteration\n- If it can\'t be described in 2-3 sentences, split it\n- Rule: One story = One focused change in ONE repository\n\n### Story Order\nStories should be ordered by dependency:\n1. Database/schema changes first\n2. API/backend logic second\n3. Frontend components third\n4. Integration/polish last\n\n### Acceptance Criteria Rules\n- Must be objectively verifiable (not "works well" or "good UX")\n- Backend stories MUST include: "Tests pass"\n- Frontend stories MUST include: "Build passes" AND "Verify in browser"\n- Include specific checks relevant to the change\n\n---\n\n## Step 5: Validation\n\nAfter generating the plan, present it to the user and ask:\n\n```\nPlan generated and saved to `plan.md`.\n\nWould you like to:\nA) Validate and proceed to PRD generation (/prd)\nB) Make modifications (describe what to change)\nC) Start over with different requirements\n```\n\nIf user chooses B, make the requested modifications and re-validate.\n\n---\n\n## Output\n\n**File:** `plan.md`\n\nThe plan file will be used by the `/prd` command to generate the executable `prd.json`.\n\n---\n\n## Example Usage\n\nUser: `/ralph Add user authentication with OAuth`\n';
258
+
259
+ // src/commands/plan.ts
260
+ async function planCommand(feature, options = {}) {
261
+ const cwd = process.cwd();
262
+ const outputPath = options.output ?? path7.join(cwd, "plan.md");
263
+ checkClaudeInstalled();
264
+ let config;
265
+ try {
266
+ config = await loadConfig(cwd);
267
+ } catch (error) {
268
+ if (error instanceof ConfigNotFoundError) {
269
+ logger.warning("No configuration found. Run 'ralph init' first, or create ralph.config.json manually.");
270
+ logger.info("Continuing without configuration...");
271
+ config = null;
272
+ } else {
273
+ throw error;
274
+ }
275
+ }
276
+ logger.header("Ralph - Plan Generator");
277
+ logger.keyValue("Feature", feature);
278
+ if (config) {
279
+ logger.keyValue("Project", config.project);
280
+ }
281
+ logger.log("");
282
+ let prompt = RALPH_SKILL;
283
+ if (config) {
284
+ prompt += `
285
+
286
+ ## Current Configuration
287
+
288
+ \`\`\`json
289
+ ${JSON.stringify(config, null, 2)}
290
+ \`\`\``;
291
+ }
292
+ prompt += `
293
+
294
+ ## Feature Request
295
+
296
+ ${feature}`;
297
+ logger.info("Starting interactive plan generation...");
298
+ logger.info("Claude will ask clarifying questions. Please respond in the terminal.");
299
+ logger.log("");
300
+ const { execa: execa3 } = await import('execa');
301
+ const subprocess = execa3("claude", [`ultrathink ${prompt}`], {
302
+ cwd,
303
+ stdio: "inherit",
304
+ reject: false
305
+ });
306
+ await subprocess;
307
+ if (await fs7.pathExists(outputPath)) {
308
+ logger.log("");
309
+ logger.success(`Plan generated: ${outputPath}`);
310
+ logger.log("");
311
+ logger.log("Next steps:");
312
+ logger.log(" 1. Review and edit plan.md if needed");
313
+ logger.log(" 2. Run: ralph prd");
314
+ logger.log(" 3. Run: ralph run");
315
+ } else {
316
+ logger.warning("Plan file was not created. You may need to re-run the command.");
317
+ }
318
+ }
319
+ var PrdNotFoundError = class extends Error {
320
+ constructor(filePath) {
321
+ super(
322
+ `PRD file not found: ${filePath}
323
+ Run 'ralph plan <feature>' and 'ralph prd' to generate one.`
324
+ );
325
+ this.name = "PrdNotFoundError";
326
+ }
327
+ };
328
+ var PrdValidationError = class extends Error {
329
+ constructor(message) {
330
+ super(`Invalid PRD: ${message}`);
331
+ this.name = "PrdValidationError";
332
+ }
333
+ };
334
+ async function loadPrd(cwd = process.cwd()) {
335
+ const prdPath = path7.join(cwd, "prd.json");
336
+ if (!await fs7.pathExists(prdPath)) {
337
+ throw new PrdNotFoundError(prdPath);
338
+ }
339
+ const content = await fs7.readJSON(prdPath);
340
+ const parsed = PrdSchema.safeParse(content);
341
+ if (!parsed.success) {
342
+ throw new PrdValidationError(parsed.error.message);
343
+ }
344
+ return parsed.data;
345
+ }
346
+ async function savePrd(prd, cwd = process.cwd()) {
347
+ const prdPath = path7.join(cwd, "prd.json");
348
+ await fs7.writeJSON(prdPath, prd, { spaces: 2 });
349
+ }
350
+ async function prdExists(cwd = process.cwd()) {
351
+ const prdPath = path7.join(cwd, "prd.json");
352
+ return fs7.pathExists(prdPath);
353
+ }
354
+ function getPendingStories(prd) {
355
+ return prd.userStories.filter((story) => !story.passes).sort((a, b) => a.priority - b.priority);
356
+ }
357
+ function isAllComplete(prd) {
358
+ return prd.userStories.every((story) => story.passes);
359
+ }
360
+ function getStoriesByRepo(prd) {
361
+ const byRepo = {};
362
+ for (const story of prd.userStories) {
363
+ if (!byRepo[story.repo]) {
364
+ byRepo[story.repo] = [];
365
+ }
366
+ byRepo[story.repo].push(story);
367
+ }
368
+ return byRepo;
369
+ }
370
+ function getCompletionStats(prd) {
371
+ const total = prd.userStories.length;
372
+ const completed = prd.userStories.filter((s) => s.passes).length;
373
+ const pending = total - completed;
374
+ const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
375
+ return { total, completed, pending, percentage };
376
+ }
377
+
378
+ // src/commands/prd.ts
379
+ async function prdCommand(options = {}) {
380
+ const cwd = process.cwd();
381
+ const inputPath = options.input ?? path7.join(cwd, "plan.md");
382
+ const outputDir = options.output ? path7.dirname(options.output) : cwd;
383
+ if (!await fs7.pathExists(inputPath)) {
384
+ logger.error(`Plan file not found: ${inputPath}`);
385
+ logger.info("Run 'ralph plan <feature>' to generate a plan first.");
386
+ process.exit(1);
387
+ }
388
+ let config;
389
+ try {
390
+ config = await loadConfig(cwd);
391
+ } catch (error) {
392
+ if (error instanceof ConfigNotFoundError) {
393
+ logger.error("No configuration found. Run 'ralph init' first.");
394
+ process.exit(1);
395
+ }
396
+ throw error;
397
+ }
398
+ logger.header("Ralph - PRD Generator");
399
+ const planContent = await fs7.readFile(inputPath, "utf-8");
400
+ logger.info("Parsing plan.md...");
401
+ const prd = parsePlanToPrd(planContent, config);
402
+ await savePrd(prd, outputDir);
403
+ logger.log("");
404
+ logger.success("PRD Generated Successfully!");
405
+ logger.log("");
406
+ logger.keyValue("Project", prd.project);
407
+ logger.keyValue("Stories", prd.userStories.length.toString());
408
+ const byRepo = getStoriesByRepo(prd);
409
+ for (const [repo, stories] of Object.entries(byRepo)) {
410
+ logger.log(` - ${repo}: ${stories.length} stories`);
411
+ }
412
+ logger.log("");
413
+ logger.log("Repositories:");
414
+ for (const [repo, info] of Object.entries(prd.repositories)) {
415
+ logger.log(` - ${repo}: ${chalk4.cyan(info.branchName)}`);
416
+ }
417
+ logger.log("");
418
+ logger.log("Next steps:");
419
+ logger.log(" 1. Review prd.json if needed");
420
+ logger.log(" 2. Run: ralph run");
421
+ }
422
+ function parsePlanToPrd(content, config) {
423
+ const h1Match = content.match(/^#\s+(.+)$/m);
424
+ const projectName = h1Match ? h1Match[1].trim() : config.project;
425
+ const descMatch = content.match(/^#\s+.+\n\n(.+?)(?:\n\n|---)/s);
426
+ const description = descMatch ? descMatch[1].trim() : "";
427
+ const branchSlug = projectName.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").substring(0, 50);
428
+ const branchName = `feature/${branchSlug}`;
429
+ const stories = [];
430
+ const storyRegex = /####\s+(US-\d+):\s*(.+?)(?:\n|$)/g;
431
+ let match;
432
+ let priority = 1;
433
+ while ((match = storyRegex.exec(content)) !== null) {
434
+ const id = match[1];
435
+ const title = match[2].trim();
436
+ const storyStart = match.index;
437
+ const nextStoryMatch = content.slice(storyStart + match[0].length).match(/####\s+US-\d+/);
438
+ const storyEnd = nextStoryMatch ? storyStart + match[0].length + nextStoryMatch.index : content.length;
439
+ const storyContent = content.slice(storyStart, storyEnd);
440
+ const repoMatch = storyContent.match(/\*\*Repository:\*\*\s*`?(\w+)`?/);
441
+ const repo = repoMatch ? repoMatch[1] : "main";
442
+ const descMatch2 = storyContent.match(/\*\*Description:\*\*\s*(.+?)(?:\n\n|\*\*)/s);
443
+ const storyDesc = descMatch2 ? descMatch2[1].trim() : title;
444
+ const criteriaMatch = storyContent.match(/\*\*Acceptance Criteria:\*\*\s*([\s\S]*?)(?:\n\n---|\n\n####|$)/);
445
+ const criteria = [];
446
+ if (criteriaMatch) {
447
+ const criteriaLines = criteriaMatch[1].split("\n");
448
+ for (const line of criteriaLines) {
449
+ const item = line.replace(/^-\s*\[.\]\s*/, "").replace(/^-\s*/, "").trim();
450
+ if (item) {
451
+ criteria.push(item);
452
+ }
453
+ }
454
+ }
455
+ const isFork = storyContent.toLowerCase().includes("experimental") || storyContent.toLowerCase().includes("fork");
456
+ stories.push({
457
+ id,
458
+ title,
459
+ repo,
460
+ description: storyDesc,
461
+ acceptanceCriteria: criteria.length > 0 ? criteria : ["Implementation complete"],
462
+ priority: priority++,
463
+ passes: false,
464
+ fork: isFork,
465
+ notes: ""
466
+ });
467
+ }
468
+ const repositories = {};
469
+ const repoKeys = new Set(stories.map((s) => s.repo));
470
+ for (const repoKey of repoKeys) {
471
+ repositories[repoKey] = { branchName };
472
+ }
473
+ for (const repoKey of Object.keys(config.repositories)) {
474
+ if (!repositories[repoKey]) {
475
+ repositories[repoKey] = { branchName };
476
+ }
477
+ }
478
+ return {
479
+ project: projectName,
480
+ description,
481
+ repositories,
482
+ userStories: stories
483
+ };
484
+ }
485
+ var PROGRESS_FILE = "progress.txt";
486
+ async function initProgressFile(projectName, cwd = process.cwd()) {
487
+ const progressPath = path7.join(cwd, PROGRESS_FILE);
488
+ if (await fs7.pathExists(progressPath)) {
489
+ return;
490
+ }
491
+ const content = `# Ralph Progress Log
492
+
493
+ Project: ${projectName}
494
+ Started: ${(/* @__PURE__ */ new Date()).toISOString()}
495
+
496
+ ## Codebase Patterns
497
+
498
+ (Add discovered patterns here as you work)
499
+
500
+ ---
501
+
502
+ `;
503
+ await fs7.writeFile(progressPath, content);
504
+ }
505
+ async function progressExists(cwd = process.cwd()) {
506
+ const progressPath = path7.join(cwd, PROGRESS_FILE);
507
+ return fs7.pathExists(progressPath);
508
+ }
509
+ async function readProgress(cwd = process.cwd()) {
510
+ const progressPath = path7.join(cwd, PROGRESS_FILE);
511
+ if (!await fs7.pathExists(progressPath)) {
512
+ return "";
513
+ }
514
+ return fs7.readFile(progressPath, "utf-8");
515
+ }
516
+ async function getRecentProgress(lines = 50, cwd = process.cwd()) {
517
+ const content = await readProgress(cwd);
518
+ const allLines = content.split("\n");
519
+ return allLines.slice(-lines).join("\n");
520
+ }
521
+ async function isGitRepo(dir) {
522
+ const gitDir = path7.join(dir, ".git");
523
+ return fs7.pathExists(gitDir);
524
+ }
525
+ async function getCurrentBranch(cwd) {
526
+ if (!await isGitRepo(cwd)) {
527
+ return null;
528
+ }
529
+ const result = await run("git", ["branch", "--show-current"], { cwd });
530
+ if (result.exitCode !== 0) {
531
+ return null;
532
+ }
533
+ return result.stdout.trim() || "detached";
534
+ }
535
+
536
+ // src/commands/run.ts
537
+ var COMPLETE_SIGNAL = "<promise>COMPLETE</promise>";
538
+ var ARCHIVE_DIR = "archive";
539
+ var LAST_PROJECT_FILE = ".last-project";
540
+ async function runCommand(options = {}) {
541
+ const cwd = process.cwd();
542
+ checkClaudeInstalled();
543
+ let config;
544
+ try {
545
+ config = await loadConfig(cwd);
546
+ } catch (error) {
547
+ if (error instanceof ConfigNotFoundError) {
548
+ logger.error("No configuration found. Run 'ralph init' first.");
549
+ process.exit(1);
550
+ }
551
+ throw error;
552
+ }
553
+ if (!await prdExists(cwd)) {
554
+ logger.error("PRD file not found.");
555
+ logger.info("Run 'ralph plan <feature>' and 'ralph prd' to generate one.");
556
+ process.exit(1);
557
+ }
558
+ const prd = await loadPrd(cwd);
559
+ const maxIterations = options.maxIterations ?? config.agent?.maxIterations ?? 50;
560
+ await handleProjectArchive(cwd, prd.project);
561
+ await initProgressFile(prd.project, cwd);
562
+ showStatus(config, prd, maxIterations, cwd);
563
+ if (isAllComplete(prd)) {
564
+ logger.success("All stories already completed!");
565
+ return;
566
+ }
567
+ await runLoop(maxIterations, cwd);
568
+ }
569
+ async function handleProjectArchive(cwd, currentProject) {
570
+ const lastProjectPath = path7.join(cwd, LAST_PROJECT_FILE);
571
+ const prdPath = path7.join(cwd, "prd.json");
572
+ const progressPath = path7.join(cwd, "progress.txt");
573
+ if (await fs7.pathExists(lastProjectPath)) {
574
+ const lastProject = (await fs7.readFile(lastProjectPath, "utf-8")).trim();
575
+ if (lastProject && lastProject !== currentProject) {
576
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
577
+ const folderName = lastProject.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
578
+ const archivePath = path7.join(cwd, ARCHIVE_DIR, `${date}-${folderName}`);
579
+ logger.info(`Archiving previous run: ${lastProject}`);
580
+ await fs7.ensureDir(archivePath);
581
+ if (await fs7.pathExists(prdPath)) {
582
+ await fs7.copy(prdPath, path7.join(archivePath, "prd.json"));
583
+ }
584
+ if (await fs7.pathExists(progressPath)) {
585
+ await fs7.copy(progressPath, path7.join(archivePath, "progress.txt"));
586
+ await fs7.remove(progressPath);
587
+ }
588
+ logger.success(`Archived to: ${archivePath}`);
589
+ }
590
+ }
591
+ await fs7.writeFile(lastProjectPath, currentProject);
592
+ }
593
+ async function showStatus(config, prd, maxIterations, cwd) {
594
+ logger.header("Ralph - Autonomous Agent Loop");
595
+ logger.keyValue("Project", prd.project);
596
+ logger.keyValue("Max iterations", maxIterations.toString());
597
+ logger.log("");
598
+ logger.log("Repositories:");
599
+ for (const [repoKey, repoConfig] of Object.entries(config.repositories)) {
600
+ const repoPath = path7.resolve(cwd, repoConfig.path);
601
+ const branch = await getCurrentBranch(repoPath);
602
+ const branchInfo = branch ?? "(not a git repo)";
603
+ logger.log(` - ${repoKey}: ${repoConfig.path} (${branchInfo})`);
604
+ }
605
+ logger.log("");
606
+ const pending = getPendingStories(prd);
607
+ if (pending.length > 0) {
608
+ logger.log("Pending stories:");
609
+ for (const story of pending) {
610
+ const forkTag = story.fork ? " (FORK)" : "";
611
+ logger.log(` - ${story.id} [${story.repo}]${forkTag}: ${story.title}`);
612
+ }
613
+ logger.log("");
614
+ }
615
+ }
616
+ async function runLoop(maxIterations, cwd) {
617
+ for (let i = 1; i <= maxIterations; i++) {
618
+ logger.log("");
619
+ logger.log(chalk4.bold("========================================"));
620
+ logger.log(chalk4.bold(` Iteration ${i} of ${maxIterations}`));
621
+ logger.log(chalk4.bold("========================================"));
622
+ logger.info("Starting Claude Code iteration...");
623
+ const output = await invokeClaudeStreaming(PROMPT_TEMPLATE, {
624
+ print: false,
625
+ // Must be false to allow Claude to execute tools
626
+ skipPermissions: true,
627
+ cwd
628
+ });
629
+ if (output.includes(COMPLETE_SIGNAL)) {
630
+ logger.log("");
631
+ logger.log(chalk4.bold("========================================"));
632
+ logger.success("Ralph completed all tasks!");
633
+ logger.log(` Completed at iteration ${i} of ${maxIterations}`);
634
+ logger.log(chalk4.bold("========================================"));
635
+ logger.log("");
636
+ await showFinalBranchStatus(cwd);
637
+ return;
638
+ }
639
+ logger.info(`Iteration ${i} complete. Continuing...`);
640
+ await sleep(2e3);
641
+ }
642
+ logger.log("");
643
+ logger.warning(`Ralph reached max iterations (${maxIterations}) without completing all tasks.`);
644
+ logger.info("Check progress.txt for status.");
645
+ const prd = await loadPrd(cwd);
646
+ const pending = getPendingStories(prd);
647
+ if (pending.length > 0) {
648
+ logger.log("");
649
+ logger.log("Remaining stories:");
650
+ for (const story of pending) {
651
+ const forkTag = story.fork ? " (FORK)" : "";
652
+ logger.log(` - ${story.id} [${story.repo}]${forkTag}: ${story.title}`);
653
+ }
654
+ }
655
+ process.exit(1);
656
+ }
657
+ async function showFinalBranchStatus(cwd) {
658
+ try {
659
+ const config = await loadConfig(cwd);
660
+ logger.log("Final branch status:");
661
+ for (const [repoKey, repoConfig] of Object.entries(config.repositories)) {
662
+ const repoPath = path7.resolve(cwd, repoConfig.path);
663
+ const branch = await getCurrentBranch(repoPath);
664
+ if (branch) {
665
+ logger.log(` - ${repoKey}: ${branch}`);
666
+ }
667
+ }
668
+ } catch {
669
+ }
670
+ }
671
+ function sleep(ms) {
672
+ return new Promise((resolve) => setTimeout(resolve, ms));
673
+ }
674
+ async function statusCommand(options = {}) {
675
+ const cwd = process.cwd();
676
+ logger.header("Ralph - Status");
677
+ let config;
678
+ try {
679
+ config = await loadConfig(cwd);
680
+ logger.keyValue("Project", config.project);
681
+ } catch (error) {
682
+ if (error instanceof ConfigNotFoundError) {
683
+ logger.warning("No configuration found. Run 'ralph init' first.");
684
+ return;
685
+ }
686
+ throw error;
687
+ }
688
+ if (!await prdExists(cwd)) {
689
+ logger.warning("No PRD found. Run 'ralph plan <feature>' and 'ralph prd' first.");
690
+ return;
691
+ }
692
+ const prd = await loadPrd(cwd);
693
+ const stats = getCompletionStats(prd);
694
+ logger.log("");
695
+ logger.log(chalk4.bold("Progress:"));
696
+ const progressBar = createProgressBar(stats.percentage);
697
+ logger.log(` ${progressBar} ${stats.percentage}%`);
698
+ logger.log(` ${chalk4.green(stats.completed.toString())} completed / ${chalk4.yellow(stats.pending.toString())} pending / ${stats.total} total`);
699
+ logger.log("");
700
+ logger.log(chalk4.bold("Repositories:"));
701
+ for (const [repoKey, repoConfig] of Object.entries(config.repositories)) {
702
+ const repoPath = path7.resolve(cwd, repoConfig.path);
703
+ const branch = await getCurrentBranch(repoPath);
704
+ const branchInfo = branch ? chalk4.cyan(branch) : chalk4.gray("(not a git repo)");
705
+ const prdBranch = prd.repositories[repoKey]?.branchName;
706
+ let branchDisplay = branchInfo;
707
+ if (prdBranch && branch && branch !== prdBranch) {
708
+ branchDisplay = `${branchInfo} ${chalk4.yellow(`(expected: ${prdBranch})`)}`;
709
+ }
710
+ logger.log(` ${chalk4.bold(repoKey)}: ${repoConfig.path} [${branchDisplay}]`);
711
+ }
712
+ const pending = getPendingStories(prd);
713
+ if (pending.length > 0) {
714
+ logger.log("");
715
+ logger.log(chalk4.bold("Pending Stories:"));
716
+ for (const story of pending) {
717
+ const forkTag = story.fork ? chalk4.magenta(" [FORK]") : "";
718
+ logger.log(` ${chalk4.yellow(story.id)} [${story.repo}]${forkTag}: ${story.title}`);
719
+ }
720
+ } else {
721
+ logger.log("");
722
+ logger.success("All stories completed!");
723
+ }
724
+ if (options.verbose) {
725
+ const byRepo = getStoriesByRepo(prd);
726
+ logger.log("");
727
+ logger.log(chalk4.bold("Stories by Repository:"));
728
+ for (const [repo, stories] of Object.entries(byRepo)) {
729
+ const completed = stories.filter((s) => s.passes).length;
730
+ logger.log(` ${chalk4.bold(repo)}: ${completed}/${stories.length} completed`);
731
+ for (const story of stories) {
732
+ const status = story.passes ? chalk4.green("\u2713") : chalk4.yellow("\u25CB");
733
+ logger.log(` ${status} ${story.id}: ${story.title}`);
734
+ }
735
+ }
736
+ }
737
+ if (options.verbose && await progressExists(cwd)) {
738
+ const recent = await getRecentProgress(10, cwd);
739
+ if (recent.trim()) {
740
+ logger.log("");
741
+ logger.log(chalk4.bold("Recent Progress:"));
742
+ logger.log(chalk4.gray(recent));
743
+ }
744
+ }
745
+ }
746
+ function createProgressBar(percentage, width = 20) {
747
+ const filled = Math.round(percentage / 100 * width);
748
+ const empty = width - filled;
749
+ const bar = chalk4.green("\u2588".repeat(filled)) + chalk4.gray("\u2591".repeat(empty));
750
+ return `[${bar}]`;
751
+ }
752
+
753
+ // src/cli.ts
754
+ var program = new Command();
755
+ program.name("ralph").description("Autonomous AI agent loop for Claude Code").version("1.0.0");
756
+ program.command("init").description("Initialize Ralph configuration in the current directory").option("-f, --force", "Overwrite existing configuration").action(async (options) => {
757
+ await initCommand(options);
758
+ });
759
+ program.command("plan <feature>").description("Generate a structured implementation plan").option("-o, --output <path>", "Output path for plan.md").action(async (feature, options) => {
760
+ await planCommand(feature, options);
761
+ });
762
+ program.command("prd").description("Convert plan.md into prd.json").option("-i, --input <path>", "Input path for plan.md").option("-o, --output <path>", "Output path for prd.json").action(async (options) => {
763
+ await prdCommand(options);
764
+ });
765
+ program.command("run").description("Run the autonomous agent loop").option("-m, --max-iterations <number>", "Maximum number of iterations", parseInt).action(async (options) => {
766
+ await runCommand({
767
+ maxIterations: options.maxIterations
768
+ });
769
+ });
770
+ program.command("status").description("Show current progress and status").option("-v, --verbose", "Show detailed information").action(async (options) => {
771
+ await statusCommand(options);
772
+ });
773
+ program.parse();
774
+ //# sourceMappingURL=cli.js.map
775
+ //# sourceMappingURL=cli.js.map