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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.js +55 -0
- package/dist/db/schema.d.ts +540 -0
- package/dist/db/schema.js +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +278 -0
- package/dist/store/state.d.ts +55 -0
- package/dist/store/state.js +205 -0
- package/dist/tools/get.d.ts +40 -0
- package/dist/tools/get.js +48 -0
- package/dist/tools/merge.d.ts +50 -0
- package/dist/tools/merge.js +384 -0
- package/dist/tools/set-agent-id.d.ts +18 -0
- package/dist/tools/set-agent-id.js +24 -0
- package/dist/tools/start.d.ts +42 -0
- package/dist/tools/start.js +96 -0
- package/dist/tools/status.d.ts +35 -0
- package/dist/tools/status.js +53 -0
- package/dist/tools/stop.d.ts +24 -0
- package/dist/tools/stop.js +52 -0
- package/dist/tools/update.d.ts +28 -0
- package/dist/tools/update.js +71 -0
- package/dist/utils/agent.d.ts +22 -0
- package/dist/utils/agent.js +110 -0
- package/dist/utils/merge-helpers.d.ts +48 -0
- package/dist/utils/merge-helpers.js +213 -0
- package/dist/utils/prd-parser.d.ts +22 -0
- package/dist/utils/prd-parser.js +137 -0
- package/dist/utils/worktree.d.ts +34 -0
- package/dist/utils/worktree.js +136 -0
- package/package.json +54 -0
|
@@ -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;
|