ralph-mcp 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.
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { removeWorktree } from "../utils/worktree.js";
3
+ import { deleteExecution, findExecutionByBranch, updateExecution, } from "../store/state.js";
4
+ export const stopInputSchema = z.object({
5
+ branch: z.string().describe("Branch name to stop (e.g., ralph/task1-agent)"),
6
+ cleanup: z.boolean().default(false).describe("Also remove the worktree"),
7
+ deleteRecord: z.boolean().default(false).describe("Delete the execution record from database"),
8
+ });
9
+ export async function stop(input) {
10
+ // Find execution
11
+ const exec = await findExecutionByBranch(input.branch);
12
+ if (!exec) {
13
+ throw new Error(`No execution found for branch: ${input.branch}`);
14
+ }
15
+ const previousStatus = exec.status;
16
+ // Optionally clean up worktree
17
+ let cleanedUp = false;
18
+ if (input.cleanup && exec.worktreePath) {
19
+ try {
20
+ await removeWorktree(exec.projectRoot, exec.worktreePath);
21
+ cleanedUp = true;
22
+ }
23
+ catch (e) {
24
+ console.error("Failed to remove worktree:", e);
25
+ }
26
+ }
27
+ // Delete or update
28
+ let deleted = false;
29
+ if (input.deleteRecord) {
30
+ await deleteExecution(exec.id);
31
+ deleted = true;
32
+ }
33
+ else {
34
+ // Just update status
35
+ await updateExecution(exec.id, { status: "stopped", updatedAt: new Date() });
36
+ }
37
+ const actions = [];
38
+ if (deleted)
39
+ actions.push("deleted record");
40
+ if (cleanedUp)
41
+ actions.push("removed worktree");
42
+ if (!deleted)
43
+ actions.push("stopped");
44
+ return {
45
+ success: true,
46
+ branch: input.branch,
47
+ previousStatus,
48
+ cleanedUp,
49
+ deleted,
50
+ message: `${input.branch}: ${actions.join(", ")}`,
51
+ };
52
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ export declare const updateInputSchema: z.ZodObject<{
3
+ branch: z.ZodString;
4
+ storyId: z.ZodString;
5
+ passes: z.ZodBoolean;
6
+ notes: z.ZodOptional<z.ZodString>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ branch: string;
9
+ storyId: string;
10
+ passes: boolean;
11
+ notes?: string | undefined;
12
+ }, {
13
+ branch: string;
14
+ storyId: string;
15
+ passes: boolean;
16
+ notes?: string | undefined;
17
+ }>;
18
+ export type UpdateInput = z.infer<typeof updateInputSchema>;
19
+ export interface UpdateResult {
20
+ success: boolean;
21
+ branch: string;
22
+ storyId: string;
23
+ passes: boolean;
24
+ allComplete: boolean;
25
+ progress: string;
26
+ addedToMergeQueue: boolean;
27
+ }
28
+ export declare function update(input: UpdateInput): Promise<UpdateResult>;
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ import notifier from "node-notifier";
3
+ import { findExecutionByBranch, findMergeQueueItemByExecutionId, findUserStoryById, insertMergeQueueItem, listMergeQueue, listUserStoriesByExecutionId, updateExecution, updateUserStory, } from "../store/state.js";
4
+ export const updateInputSchema = z.object({
5
+ branch: z.string().describe("Branch name (e.g., ralph/task1-agent)"),
6
+ storyId: z.string().describe("Story ID (e.g., US-001)"),
7
+ passes: z.boolean().describe("Whether the story passes"),
8
+ notes: z.string().optional().describe("Implementation notes"),
9
+ });
10
+ export async function update(input) {
11
+ // Find execution by branch
12
+ const exec = await findExecutionByBranch(input.branch);
13
+ if (!exec) {
14
+ throw new Error(`No execution found for branch: ${input.branch}`);
15
+ }
16
+ // Find and update the story
17
+ const storyKey = `${exec.id}:${input.storyId}`;
18
+ const story = await findUserStoryById(storyKey);
19
+ if (!story) {
20
+ throw new Error(`No story found with ID ${input.storyId} for branch ${input.branch}`);
21
+ }
22
+ // Update story
23
+ await updateUserStory(storyKey, {
24
+ passes: input.passes,
25
+ notes: input.notes || story.notes,
26
+ });
27
+ // Update execution timestamp and status
28
+ const allStories = await listUserStoriesByExecutionId(exec.id);
29
+ // Check if this update completes all stories
30
+ const updatedStories = allStories.map((s) => s.id === storyKey ? { ...s, passes: input.passes } : s);
31
+ const allComplete = updatedStories.every((s) => s.passes);
32
+ const completedCount = updatedStories.filter((s) => s.passes).length;
33
+ // Update execution status
34
+ const newStatus = allComplete ? "completed" : "running";
35
+ await updateExecution(exec.id, { status: newStatus, updatedAt: new Date() });
36
+ // Auto add to merge queue if enabled and all complete
37
+ let addedToMergeQueue = false;
38
+ if (allComplete && exec.autoMerge) {
39
+ // Check if already in queue
40
+ const existingInQueue = await findMergeQueueItemByExecutionId(exec.id);
41
+ if (!existingInQueue) {
42
+ const queue = await listMergeQueue();
43
+ const maxPosition = queue.length > 0 ? Math.max(...queue.map((q) => q.position)) : 0;
44
+ const nextPosition = maxPosition + 1;
45
+ await insertMergeQueueItem({
46
+ executionId: exec.id,
47
+ position: nextPosition,
48
+ status: "pending",
49
+ createdAt: new Date(),
50
+ });
51
+ addedToMergeQueue = true;
52
+ }
53
+ }
54
+ // Send Windows toast notification when all complete (if enabled)
55
+ if (allComplete && exec.notifyOnComplete) {
56
+ notifier.notify({
57
+ title: "Ralph PRD Complete",
58
+ message: `${exec.branch} - All ${allStories.length} stories done!`,
59
+ sound: true,
60
+ });
61
+ }
62
+ return {
63
+ success: true,
64
+ branch: input.branch,
65
+ storyId: input.storyId,
66
+ passes: input.passes,
67
+ allComplete,
68
+ progress: `${completedCount}/${allStories.length} US`,
69
+ addedToMergeQueue,
70
+ };
71
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Generate agent prompt for PRD execution
3
+ */
4
+ export declare function generateAgentPrompt(branch: string, description: string, worktreePath: string, stories: Array<{
5
+ storyId: string;
6
+ title: string;
7
+ description: string;
8
+ acceptanceCriteria: string[];
9
+ priority: number;
10
+ passes: boolean;
11
+ }>): string;
12
+ /**
13
+ * Generate merge agent prompt for conflict resolution
14
+ */
15
+ export declare function generateMergeAgentPrompt(projectRoot: string, branch: string, description: string, conflictFiles: string[], prdPath?: string): string;
16
+ /**
17
+ * Start a Claude agent via CLI (for merge conflicts)
18
+ */
19
+ export declare function startMergeAgent(projectRoot: string, prompt: string): Promise<{
20
+ success: boolean;
21
+ output: string;
22
+ }>;
@@ -0,0 +1,110 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ const execAsync = promisify(exec);
6
+ /**
7
+ * Generate agent prompt for PRD execution
8
+ */
9
+ export function generateAgentPrompt(branch, description, worktreePath, stories) {
10
+ const pendingStories = stories
11
+ .filter((s) => !s.passes)
12
+ .sort((a, b) => a.priority - b.priority);
13
+ if (pendingStories.length === 0) {
14
+ return "All user stories are complete. No action needed.";
15
+ }
16
+ const storiesText = pendingStories
17
+ .map((s) => `
18
+ ### ${s.storyId}: ${s.title}
19
+ ${s.description}
20
+
21
+ **Acceptance Criteria:**
22
+ ${s.acceptanceCriteria.map((ac) => `- ${ac}`).join("\n")}
23
+ `)
24
+ .join("\n");
25
+ return `You are an autonomous coding agent working on the "${branch}" branch.
26
+
27
+ ## Working Directory
28
+ ${worktreePath}
29
+
30
+ ## PRD: ${description}
31
+
32
+ ## Pending User Stories
33
+ ${storiesText}
34
+
35
+ ## Instructions
36
+
37
+ 1. Work on ONE user story at a time, starting with the highest priority
38
+ 2. Implement the feature to satisfy all acceptance criteria
39
+ 3. Run quality checks: \`pnpm check-types\` and \`pnpm --filter api build\`
40
+ 4. Commit changes with message: \`feat: [${pendingStories[0].storyId}] - ${pendingStories[0].title}\`
41
+ 5. After completing a story, call \`ralph_update\` to mark it as passed:
42
+ \`ralph_update({ branch: "${branch}", storyId: "${pendingStories[0].storyId}", passes: true, notes: "..." })\`
43
+ 6. Continue to the next story until all are complete
44
+
45
+ ## Quality Requirements
46
+ - ALL commits must pass typecheck and build
47
+ - Keep changes focused and minimal
48
+ - Follow existing code patterns
49
+
50
+ ## Stop Condition
51
+ When all stories are complete, report completion.
52
+ `;
53
+ }
54
+ /**
55
+ * Generate merge agent prompt for conflict resolution
56
+ */
57
+ export function generateMergeAgentPrompt(projectRoot, branch, description, conflictFiles, prdPath) {
58
+ // Read CLAUDE.md for architecture context
59
+ let architectureContext = "";
60
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
61
+ if (existsSync(claudeMdPath)) {
62
+ architectureContext = readFileSync(claudeMdPath, "utf-8");
63
+ }
64
+ // Read PRD content if available
65
+ let prdContent = "";
66
+ if (prdPath && existsSync(prdPath)) {
67
+ prdContent = readFileSync(prdPath, "utf-8");
68
+ }
69
+ return `You are a Git merge expert. Please resolve the following merge conflicts.
70
+
71
+ ## Project Architecture
72
+ ${architectureContext || "No CLAUDE.md found. Use your best judgment based on the code."}
73
+
74
+ ## PRD Context
75
+ ${prdContent || `Branch: ${branch}\nDescription: ${description}`}
76
+
77
+ ## Conflict Files
78
+ ${conflictFiles.map((f) => `- ${f}`).join("\n")}
79
+
80
+ ## Tasks
81
+
82
+ 1. Read each conflict file to understand both sides of the conflict
83
+ 2. Analyze the intent of changes from both branches based on the PRD
84
+ 3. Resolve conflicts by keeping valuable changes from both sides
85
+ 4. Ensure the PRD requirements are satisfied
86
+ 5. Run \`git add <file>\` for each resolved file
87
+ 6. Run \`git commit -m "resolve: merge conflicts for ${branch}"\`
88
+
89
+ ## Guidelines
90
+ - Prefer keeping both changes when they don't conflict logically
91
+ - If changes conflict logically, prefer the feature branch changes (they implement the PRD)
92
+ - Ensure the code compiles after resolution
93
+ - Run \`pnpm check-types\` to verify
94
+ `;
95
+ }
96
+ /**
97
+ * Start a Claude agent via CLI (for merge conflicts)
98
+ */
99
+ export async function startMergeAgent(projectRoot, prompt) {
100
+ try {
101
+ const { stdout, stderr } = await execAsync(`echo "${prompt.replace(/"/g, '\\"')}" | claude --dangerously-skip-permissions --print`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
102
+ return { success: true, output: stdout || stderr };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ success: false,
107
+ output: error instanceof Error ? error.message : String(error),
108
+ };
109
+ }
110
+ }
@@ -0,0 +1,48 @@
1
+ export interface QualityCheckResult {
2
+ success: boolean;
3
+ typeCheck: {
4
+ success: boolean;
5
+ output: string;
6
+ };
7
+ build: {
8
+ success: boolean;
9
+ output: string;
10
+ };
11
+ }
12
+ export interface SyncResult {
13
+ success: boolean;
14
+ hasConflicts: boolean;
15
+ conflictFiles?: string[];
16
+ message: string;
17
+ }
18
+ /**
19
+ * Sync main branch to feature branch before merge
20
+ */
21
+ export declare function syncMainToBranch(worktreePath: string, branch: string): Promise<SyncResult>;
22
+ /**
23
+ * Run quality checks (type check and build)
24
+ */
25
+ export declare function runQualityChecks(worktreePath: string): Promise<QualityCheckResult>;
26
+ /**
27
+ * Generate commit message with US list
28
+ */
29
+ export declare function generateCommitMessage(branch: string, description: string, completedStories: {
30
+ id: string;
31
+ title: string;
32
+ }[]): string;
33
+ /**
34
+ * Get list of commits on branch since diverging from main
35
+ */
36
+ export declare function getBranchCommits(projectRoot: string, branch: string): Promise<string[]>;
37
+ /**
38
+ * Update TODO.md to mark PRD as completed
39
+ */
40
+ export declare function updateTodoDoc(projectRoot: string, branch: string, description: string): boolean;
41
+ /**
42
+ * Update PROJECT-STATUS.md with merge info
43
+ */
44
+ export declare function updateProjectStatus(projectRoot: string, branch: string, description: string, commitHash: string): boolean;
45
+ /**
46
+ * Handle schema.prisma conflicts specially
47
+ */
48
+ export declare function handleSchemaConflict(projectRoot: string): Promise<boolean>;
@@ -0,0 +1,213 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ const execAsync = promisify(exec);
6
+ /**
7
+ * Sync main branch to feature branch before merge
8
+ */
9
+ export async function syncMainToBranch(worktreePath, branch) {
10
+ try {
11
+ // Fetch latest main
12
+ await execAsync("git fetch origin main", { cwd: worktreePath });
13
+ // Try to merge main into feature branch
14
+ try {
15
+ await execAsync("git merge origin/main --no-edit", { cwd: worktreePath });
16
+ return {
17
+ success: true,
18
+ hasConflicts: false,
19
+ message: "Successfully synced main to feature branch",
20
+ };
21
+ }
22
+ catch (mergeError) {
23
+ // Check for conflicts
24
+ const { stdout: status } = await execAsync("git status --porcelain", {
25
+ cwd: worktreePath,
26
+ });
27
+ const conflictFiles = status
28
+ .split("\n")
29
+ .filter((line) => line.startsWith("UU ") || line.startsWith("AA "))
30
+ .map((line) => line.slice(3));
31
+ if (conflictFiles.length > 0) {
32
+ return {
33
+ success: false,
34
+ hasConflicts: true,
35
+ conflictFiles,
36
+ message: `Conflicts when syncing main: ${conflictFiles.join(", ")}`,
37
+ };
38
+ }
39
+ throw mergeError;
40
+ }
41
+ }
42
+ catch (error) {
43
+ return {
44
+ success: false,
45
+ hasConflicts: false,
46
+ message: `Failed to sync main: ${error}`,
47
+ };
48
+ }
49
+ }
50
+ /**
51
+ * Run quality checks (type check and build)
52
+ */
53
+ export async function runQualityChecks(worktreePath) {
54
+ const result = {
55
+ success: false,
56
+ typeCheck: { success: false, output: "" },
57
+ build: { success: false, output: "" },
58
+ };
59
+ // Run type check
60
+ try {
61
+ const { stdout, stderr } = await execAsync("pnpm check-types", {
62
+ cwd: worktreePath,
63
+ timeout: 120000, // 2 minutes
64
+ });
65
+ result.typeCheck = { success: true, output: stdout || stderr };
66
+ }
67
+ catch (error) {
68
+ const execError = error;
69
+ result.typeCheck = {
70
+ success: false,
71
+ output: execError.stdout || execError.stderr || String(error),
72
+ };
73
+ return result;
74
+ }
75
+ // Run build (only API for now, as it's the critical one)
76
+ try {
77
+ const { stdout, stderr } = await execAsync("pnpm --filter api build", {
78
+ cwd: worktreePath,
79
+ timeout: 180000, // 3 minutes
80
+ });
81
+ result.build = { success: true, output: stdout || stderr };
82
+ }
83
+ catch (error) {
84
+ const execError = error;
85
+ result.build = {
86
+ success: false,
87
+ output: execError.stdout || execError.stderr || String(error),
88
+ };
89
+ return result;
90
+ }
91
+ result.success = true;
92
+ return result;
93
+ }
94
+ /**
95
+ * Generate commit message with US list
96
+ */
97
+ export function generateCommitMessage(branch, description, completedStories) {
98
+ const storyList = completedStories
99
+ .map((s) => `- ${s.id}: ${s.title}`)
100
+ .join("\n");
101
+ return `merge: ${branch} - ${description}
102
+
103
+ Completed User Stories:
104
+ ${storyList || "- No stories tracked"}
105
+
106
+ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>`;
107
+ }
108
+ /**
109
+ * Get list of commits on branch since diverging from main
110
+ */
111
+ export async function getBranchCommits(projectRoot, branch) {
112
+ try {
113
+ const { stdout } = await execAsync(`git log main..${branch} --oneline --no-merges`, { cwd: projectRoot });
114
+ return stdout.split("\n").filter(Boolean);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ /**
121
+ * Update TODO.md to mark PRD as completed
122
+ */
123
+ export function updateTodoDoc(projectRoot, branch, description) {
124
+ const todoPath = join(projectRoot, "docs", "TODO.md");
125
+ if (!existsSync(todoPath)) {
126
+ return false;
127
+ }
128
+ try {
129
+ let content = readFileSync(todoPath, "utf-8");
130
+ // Find and update the PRD entry (mark as completed)
131
+ // Pattern: - [ ] PRD description -> - [x] PRD description
132
+ const prdPattern = new RegExp(`- \\[ \\] (.*${description.slice(0, 30)}.*|.*${branch}.*)`);
133
+ if (prdPattern.test(content)) {
134
+ content = content.replace(prdPattern, "- [x] $1");
135
+ writeFileSync(todoPath, content, "utf-8");
136
+ return true;
137
+ }
138
+ return false;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ /**
145
+ * Update PROJECT-STATUS.md with merge info
146
+ */
147
+ export function updateProjectStatus(projectRoot, branch, description, commitHash) {
148
+ const statusPath = join(projectRoot, "docs", "PROJECT-STATUS.md");
149
+ if (!existsSync(statusPath)) {
150
+ return false;
151
+ }
152
+ try {
153
+ let content = readFileSync(statusPath, "utf-8");
154
+ // Add to recent merges section or create one
155
+ const date = new Date().toISOString().split("T")[0];
156
+ const mergeEntry = `- ${date}: ${branch} - ${description} (${commitHash.slice(0, 7)})`;
157
+ const recentMergesPattern = /## Recent Merges\n/;
158
+ if (recentMergesPattern.test(content)) {
159
+ content = content.replace(recentMergesPattern, `## Recent Merges\n${mergeEntry}\n`);
160
+ }
161
+ else {
162
+ // Add section at the end
163
+ content += `\n## Recent Merges\n${mergeEntry}\n`;
164
+ }
165
+ writeFileSync(statusPath, content, "utf-8");
166
+ return true;
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
172
+ /**
173
+ * Handle schema.prisma conflicts specially
174
+ */
175
+ export async function handleSchemaConflict(projectRoot) {
176
+ const schemaPath = join(projectRoot, "apps", "api", "src", "infra", "prisma", "schema.prisma");
177
+ if (!existsSync(schemaPath)) {
178
+ return false;
179
+ }
180
+ try {
181
+ // Read the conflicted file
182
+ let content = readFileSync(schemaPath, "utf-8");
183
+ // Check if there are conflict markers
184
+ if (!content.includes("<<<<<<<") && !content.includes(">>>>>>>")) {
185
+ return true; // No conflicts
186
+ }
187
+ // For schema.prisma, we typically want to keep both changes
188
+ // Remove conflict markers and keep all content
189
+ content = content
190
+ .replace(/<<<<<<< HEAD\n/g, "")
191
+ .replace(/=======\n/g, "")
192
+ .replace(/>>>>>>> .+\n/g, "");
193
+ // Remove duplicate model definitions (keep first occurrence)
194
+ const modelPattern = /model (\w+) \{[\s\S]*?\n\}/g;
195
+ const seenModels = new Set();
196
+ content = content.replace(modelPattern, (match, modelName) => {
197
+ if (seenModels.has(modelName)) {
198
+ return ""; // Remove duplicate
199
+ }
200
+ seenModels.add(modelName);
201
+ return match;
202
+ });
203
+ // Clean up extra blank lines
204
+ content = content.replace(/\n{3,}/g, "\n\n");
205
+ writeFileSync(schemaPath, content, "utf-8");
206
+ // Stage the resolved file
207
+ await execAsync(`git add "${schemaPath}"`, { cwd: projectRoot });
208
+ return true;
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
@@ -0,0 +1,22 @@
1
+ export interface ParsedUserStory {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ acceptanceCriteria: string[];
6
+ priority: number;
7
+ }
8
+ export interface ParsedPrd {
9
+ title: string;
10
+ description: string;
11
+ branchName: string;
12
+ userStories: ParsedUserStory[];
13
+ }
14
+ /**
15
+ * Parse a PRD markdown file into structured data.
16
+ * Supports both markdown format and JSON format.
17
+ */
18
+ export declare function parsePrdFile(filePath: string): ParsedPrd;
19
+ /**
20
+ * Generate branch name from PRD title
21
+ */
22
+ export declare function generateBranchName(title: string): string;