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,384 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { removeWorktree, abortMerge, } from "../utils/worktree.js";
|
|
3
|
+
import { generateMergeAgentPrompt, startMergeAgent, } from "../utils/agent.js";
|
|
4
|
+
import { syncMainToBranch, runQualityChecks, generateCommitMessage, updateTodoDoc, updateProjectStatus, handleSchemaConflict, } from "../utils/merge-helpers.js";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { deleteMergeQueueByExecutionId, findExecutionByBranch, findExecutionById, insertMergeQueueItem, listExecutions, listMergeQueue, listUserStoriesByExecutionId, updateExecution, updateMergeQueueItem, } from "../store/state.js";
|
|
7
|
+
export const mergeInputSchema = z.object({
|
|
8
|
+
branch: z.string().describe("Branch name to merge (e.g., ralph/task1-agent)"),
|
|
9
|
+
force: z.boolean().default(false).describe("Skip verification checks"),
|
|
10
|
+
skipQualityChecks: z.boolean().default(false).describe("Skip type check and build"),
|
|
11
|
+
onConflict: z
|
|
12
|
+
.enum(["auto_theirs", "auto_ours", "notify", "agent"])
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Override conflict resolution strategy"),
|
|
15
|
+
});
|
|
16
|
+
export async function merge(input) {
|
|
17
|
+
// Find execution
|
|
18
|
+
const exec = await findExecutionByBranch(input.branch);
|
|
19
|
+
if (!exec) {
|
|
20
|
+
throw new Error(`No execution found for branch: ${input.branch}`);
|
|
21
|
+
}
|
|
22
|
+
// Get completed stories for commit message
|
|
23
|
+
const stories = await listUserStoriesByExecutionId(exec.id);
|
|
24
|
+
const completedStories = stories
|
|
25
|
+
.filter((s) => s.passes)
|
|
26
|
+
.map((s) => ({ id: s.storyId, title: s.title }));
|
|
27
|
+
// Check if all stories are complete (unless force)
|
|
28
|
+
if (!input.force) {
|
|
29
|
+
const { get } = await import("./get.js");
|
|
30
|
+
const status = await get({ branch: input.branch });
|
|
31
|
+
if (status.progress.completed < status.progress.total) {
|
|
32
|
+
throw new Error(`Cannot merge: ${status.progress.completed}/${status.progress.total} stories complete. Use force=true to override.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Update status to merging
|
|
36
|
+
await updateExecution(exec.id, { status: "merging", updatedAt: new Date() });
|
|
37
|
+
const onConflict = input.onConflict || exec.onConflict || "agent";
|
|
38
|
+
try {
|
|
39
|
+
// Step 1: Sync main to feature branch (in worktree)
|
|
40
|
+
if (exec.worktreePath) {
|
|
41
|
+
console.log(">>> Syncing main to feature branch...");
|
|
42
|
+
const syncResult = await syncMainToBranch(exec.worktreePath, exec.branch);
|
|
43
|
+
if (!syncResult.success) {
|
|
44
|
+
if (syncResult.hasConflicts && syncResult.conflictFiles) {
|
|
45
|
+
// Try to handle schema conflicts automatically
|
|
46
|
+
const hasSchemaConflict = syncResult.conflictFiles.some((f) => f.includes("schema.prisma"));
|
|
47
|
+
if (hasSchemaConflict) {
|
|
48
|
+
console.log(">>> Attempting to resolve schema.prisma conflict...");
|
|
49
|
+
const schemaResolved = await handleSchemaConflict(exec.worktreePath);
|
|
50
|
+
if (!schemaResolved) {
|
|
51
|
+
throw new Error(`Failed to resolve schema.prisma conflict during sync`);
|
|
52
|
+
}
|
|
53
|
+
// Continue with remaining conflicts if any
|
|
54
|
+
const remainingConflicts = syncResult.conflictFiles.filter((f) => !f.includes("schema.prisma"));
|
|
55
|
+
if (remainingConflicts.length > 0) {
|
|
56
|
+
throw new Error(`Sync conflicts in: ${remainingConflicts.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(syncResult.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw new Error(syncResult.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Step 2: Run quality checks (unless skipped)
|
|
69
|
+
if (!input.skipQualityChecks && exec.worktreePath) {
|
|
70
|
+
console.log(">>> Running quality checks...");
|
|
71
|
+
const qualityResult = await runQualityChecks(exec.worktreePath);
|
|
72
|
+
if (!qualityResult.success) {
|
|
73
|
+
const failedChecks = [];
|
|
74
|
+
if (!qualityResult.typeCheck.success)
|
|
75
|
+
failedChecks.push("typeCheck");
|
|
76
|
+
if (!qualityResult.build.success)
|
|
77
|
+
failedChecks.push("build");
|
|
78
|
+
await updateExecution(exec.id, { status: "failed", updatedAt: new Date() });
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
branch: input.branch,
|
|
82
|
+
cleanedUp: false,
|
|
83
|
+
qualityChecks: {
|
|
84
|
+
typeCheck: qualityResult.typeCheck.success,
|
|
85
|
+
build: qualityResult.build.success,
|
|
86
|
+
},
|
|
87
|
+
message: `Quality checks failed: ${failedChecks.join(", ")}. Fix issues before merging.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Step 3: Generate commit message
|
|
92
|
+
const commitMessage = generateCommitMessage(exec.branch, exec.description, completedStories);
|
|
93
|
+
// Step 4: Attempt merge to main
|
|
94
|
+
console.log(">>> Merging to main...");
|
|
95
|
+
const mergeResult = await mergeBranchWithMessage(exec.projectRoot, exec.branch, commitMessage, onConflict);
|
|
96
|
+
if (mergeResult.success) {
|
|
97
|
+
// Step 5: Update docs
|
|
98
|
+
const docsUpdated = [];
|
|
99
|
+
if (updateTodoDoc(exec.projectRoot, exec.branch, exec.description)) {
|
|
100
|
+
docsUpdated.push("docs/TODO.md");
|
|
101
|
+
}
|
|
102
|
+
if (mergeResult.commitHash &&
|
|
103
|
+
updateProjectStatus(exec.projectRoot, exec.branch, exec.description, mergeResult.commitHash)) {
|
|
104
|
+
docsUpdated.push("docs/PROJECT-STATUS.md");
|
|
105
|
+
}
|
|
106
|
+
// Commit doc updates if any
|
|
107
|
+
if (docsUpdated.length > 0) {
|
|
108
|
+
try {
|
|
109
|
+
execSync(`git add ${docsUpdated.join(" ")} && git commit --amend --no-edit`, {
|
|
110
|
+
cwd: exec.projectRoot,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Ignore if no changes or amend fails
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Step 6: Clean up worktree
|
|
118
|
+
let cleanedUp = false;
|
|
119
|
+
if (exec.worktreePath) {
|
|
120
|
+
try {
|
|
121
|
+
await removeWorktree(exec.projectRoot, exec.worktreePath);
|
|
122
|
+
cleanedUp = true;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.error("Failed to remove worktree:", e);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Update status
|
|
129
|
+
await updateExecution(exec.id, { status: "completed", updatedAt: new Date() });
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
branch: input.branch,
|
|
133
|
+
commitHash: mergeResult.commitHash,
|
|
134
|
+
cleanedUp,
|
|
135
|
+
conflictResolution: "auto",
|
|
136
|
+
qualityChecks: input.skipQualityChecks
|
|
137
|
+
? undefined
|
|
138
|
+
: { typeCheck: true, build: true },
|
|
139
|
+
docsUpdated: docsUpdated.length > 0 ? docsUpdated : undefined,
|
|
140
|
+
mergedStories: completedStories.map((s) => s.id),
|
|
141
|
+
message: `Successfully merged ${input.branch} to main`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Handle conflicts
|
|
145
|
+
if (mergeResult.hasConflicts && mergeResult.conflictFiles) {
|
|
146
|
+
// Try schema conflict resolution first
|
|
147
|
+
const hasSchemaConflict = mergeResult.conflictFiles.some((f) => f.includes("schema.prisma"));
|
|
148
|
+
if (hasSchemaConflict) {
|
|
149
|
+
console.log(">>> Attempting to resolve schema.prisma conflict...");
|
|
150
|
+
const schemaResolved = await handleSchemaConflict(exec.projectRoot);
|
|
151
|
+
if (schemaResolved) {
|
|
152
|
+
// Check if there are remaining conflicts
|
|
153
|
+
const remainingConflicts = mergeResult.conflictFiles.filter((f) => !f.includes("schema.prisma"));
|
|
154
|
+
if (remainingConflicts.length === 0) {
|
|
155
|
+
// Complete the merge
|
|
156
|
+
try {
|
|
157
|
+
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
158
|
+
cwd: exec.projectRoot,
|
|
159
|
+
});
|
|
160
|
+
const commitHash = execSync("git rev-parse HEAD", {
|
|
161
|
+
cwd: exec.projectRoot,
|
|
162
|
+
encoding: "utf-8",
|
|
163
|
+
}).trim();
|
|
164
|
+
// Clean up
|
|
165
|
+
let cleanedUp = false;
|
|
166
|
+
if (exec.worktreePath) {
|
|
167
|
+
try {
|
|
168
|
+
await removeWorktree(exec.projectRoot, exec.worktreePath);
|
|
169
|
+
cleanedUp = true;
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
console.error("Failed to remove worktree:", e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await updateExecution(exec.id, { status: "completed", updatedAt: new Date() });
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
branch: input.branch,
|
|
179
|
+
commitHash,
|
|
180
|
+
cleanedUp,
|
|
181
|
+
conflictResolution: "auto",
|
|
182
|
+
mergedStories: completedStories.map((s) => s.id),
|
|
183
|
+
message: `Successfully merged ${input.branch} (schema conflict auto-resolved)`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Fall through to agent resolution
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (onConflict === "notify") {
|
|
193
|
+
await updateExecution(exec.id, { status: "failed", updatedAt: new Date() });
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
branch: input.branch,
|
|
197
|
+
cleanedUp: false,
|
|
198
|
+
conflictResolution: "pending",
|
|
199
|
+
message: `Merge conflicts detected in: ${mergeResult.conflictFiles.join(", ")}. Manual resolution required.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (onConflict === "agent") {
|
|
203
|
+
const prompt = generateMergeAgentPrompt(exec.projectRoot, exec.branch, exec.description, mergeResult.conflictFiles, exec.prdPath);
|
|
204
|
+
const agentResult = await startMergeAgent(exec.projectRoot, prompt);
|
|
205
|
+
if (agentResult.success) {
|
|
206
|
+
let cleanedUp = false;
|
|
207
|
+
if (exec.worktreePath) {
|
|
208
|
+
try {
|
|
209
|
+
await removeWorktree(exec.projectRoot, exec.worktreePath);
|
|
210
|
+
cleanedUp = true;
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
console.error("Failed to remove worktree:", e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const commitHash = execSync("git rev-parse HEAD", {
|
|
217
|
+
cwd: exec.projectRoot,
|
|
218
|
+
encoding: "utf-8",
|
|
219
|
+
}).trim();
|
|
220
|
+
await updateExecution(exec.id, { status: "completed", updatedAt: new Date() });
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
branch: input.branch,
|
|
224
|
+
commitHash,
|
|
225
|
+
cleanedUp,
|
|
226
|
+
conflictResolution: "agent",
|
|
227
|
+
mergedStories: completedStories.map((s) => s.id),
|
|
228
|
+
message: `Merge conflicts resolved by agent for ${input.branch}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
await abortMerge(exec.projectRoot);
|
|
233
|
+
await updateExecution(exec.id, { status: "failed", updatedAt: new Date() });
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
branch: input.branch,
|
|
237
|
+
cleanedUp: false,
|
|
238
|
+
conflictResolution: "pending",
|
|
239
|
+
message: `Merge agent failed: ${agentResult.output}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new Error("Unexpected merge state");
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
await updateExecution(exec.id, { status: "failed", updatedAt: new Date() });
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Merge branch with custom commit message
|
|
253
|
+
*/
|
|
254
|
+
async function mergeBranchWithMessage(projectRoot, branch, commitMessage, onConflict) {
|
|
255
|
+
const { exec: execAsync } = await import("child_process");
|
|
256
|
+
const { promisify } = await import("util");
|
|
257
|
+
const execPromise = promisify(execAsync);
|
|
258
|
+
// Checkout main and pull
|
|
259
|
+
await execPromise("git checkout main && git pull", { cwd: projectRoot });
|
|
260
|
+
// Build merge strategy
|
|
261
|
+
let mergeStrategy = "";
|
|
262
|
+
if (onConflict === "auto_theirs") {
|
|
263
|
+
mergeStrategy = "-X theirs";
|
|
264
|
+
}
|
|
265
|
+
else if (onConflict === "auto_ours") {
|
|
266
|
+
mergeStrategy = "-X ours";
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
// Use heredoc-style commit message to handle special characters
|
|
270
|
+
const escapedMessage = commitMessage.replace(/'/g, "'\\''");
|
|
271
|
+
await execPromise(`git merge --no-ff ${mergeStrategy} "${branch}" -m '${escapedMessage}'`, { cwd: projectRoot });
|
|
272
|
+
const { stdout: hash } = await execPromise("git rev-parse HEAD", {
|
|
273
|
+
cwd: projectRoot,
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
success: true,
|
|
277
|
+
commitHash: hash.trim(),
|
|
278
|
+
hasConflicts: false,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Check for conflicts
|
|
283
|
+
const { stdout: status } = await execPromise("git status --porcelain", {
|
|
284
|
+
cwd: projectRoot,
|
|
285
|
+
});
|
|
286
|
+
const conflictFiles = status
|
|
287
|
+
.split("\n")
|
|
288
|
+
.filter((line) => line.startsWith("UU ") || line.startsWith("AA "))
|
|
289
|
+
.map((line) => line.slice(3));
|
|
290
|
+
if (conflictFiles.length > 0) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
hasConflicts: true,
|
|
294
|
+
conflictFiles,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
throw new Error("Merge failed for unknown reason");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Merge queue management
|
|
301
|
+
export const mergeQueueInputSchema = z.object({
|
|
302
|
+
action: z
|
|
303
|
+
.enum(["list", "add", "remove", "process"])
|
|
304
|
+
.default("list")
|
|
305
|
+
.describe("Queue action"),
|
|
306
|
+
branch: z.string().optional().describe("Branch for add/remove actions"),
|
|
307
|
+
});
|
|
308
|
+
export async function mergeQueueAction(input) {
|
|
309
|
+
const queue = await listMergeQueue();
|
|
310
|
+
const current = queue.find((q) => q.status === "merging");
|
|
311
|
+
if (input.action === "list") {
|
|
312
|
+
const execs = await listExecutions();
|
|
313
|
+
const execById = new Map(execs.map((e) => [e.id, e]));
|
|
314
|
+
return {
|
|
315
|
+
queue: queue.map((q) => {
|
|
316
|
+
const exec = execById.get(q.executionId);
|
|
317
|
+
return exec?.branch || q.executionId;
|
|
318
|
+
}),
|
|
319
|
+
current: current
|
|
320
|
+
? execById.get(current.executionId)?.branch
|
|
321
|
+
: undefined,
|
|
322
|
+
message: `${queue.length} items in merge queue`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (input.action === "add" && input.branch) {
|
|
326
|
+
const exec = await findExecutionByBranch(input.branch);
|
|
327
|
+
if (!exec) {
|
|
328
|
+
throw new Error(`No execution found for branch: ${input.branch}`);
|
|
329
|
+
}
|
|
330
|
+
const maxPosition = queue.length > 0
|
|
331
|
+
? Math.max(...queue.map((q) => q.position))
|
|
332
|
+
: 0;
|
|
333
|
+
await insertMergeQueueItem({
|
|
334
|
+
executionId: exec.id,
|
|
335
|
+
position: maxPosition + 1,
|
|
336
|
+
status: "pending",
|
|
337
|
+
createdAt: new Date(),
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
queue: [...queue.map((q) => q.executionId), exec.id],
|
|
341
|
+
message: `Added ${input.branch} to merge queue at position ${maxPosition + 1}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (input.action === "remove" && input.branch) {
|
|
345
|
+
const exec = await findExecutionByBranch(input.branch);
|
|
346
|
+
if (exec) {
|
|
347
|
+
await deleteMergeQueueByExecutionId(exec.id);
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
queue: queue
|
|
351
|
+
.filter((q) => q.executionId !== exec?.id)
|
|
352
|
+
.map((q) => q.executionId),
|
|
353
|
+
message: `Removed ${input.branch} from merge queue`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (input.action === "process") {
|
|
357
|
+
// Process next item in queue
|
|
358
|
+
const next = queue.find((q) => q.status === "pending");
|
|
359
|
+
if (!next) {
|
|
360
|
+
return {
|
|
361
|
+
queue: [],
|
|
362
|
+
message: "No pending items in merge queue",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const exec = await findExecutionById(next.executionId);
|
|
366
|
+
if (exec) {
|
|
367
|
+
// Update queue status
|
|
368
|
+
await updateMergeQueueItem(next.id, { status: "merging" });
|
|
369
|
+
// Perform merge
|
|
370
|
+
const result = await merge({ branch: exec.branch, force: false, skipQualityChecks: false });
|
|
371
|
+
// Update queue status
|
|
372
|
+
await updateMergeQueueItem(next.id, { status: result.success ? "completed" : "failed" });
|
|
373
|
+
return {
|
|
374
|
+
queue: queue.slice(1).map((q) => q.executionId),
|
|
375
|
+
current: exec.branch,
|
|
376
|
+
message: result.message,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
queue: queue.map((q) => q.executionId),
|
|
382
|
+
message: "Unknown action",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const setAgentIdInputSchema: z.ZodObject<{
|
|
3
|
+
branch: z.ZodString;
|
|
4
|
+
agentTaskId: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
branch: string;
|
|
7
|
+
agentTaskId: string;
|
|
8
|
+
}, {
|
|
9
|
+
branch: string;
|
|
10
|
+
agentTaskId: string;
|
|
11
|
+
}>;
|
|
12
|
+
export type SetAgentIdInput = z.infer<typeof setAgentIdInputSchema>;
|
|
13
|
+
export interface SetAgentIdResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
branch: string;
|
|
16
|
+
agentTaskId: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function setAgentId(input: SetAgentIdInput): Promise<SetAgentIdResult>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { findExecutionByBranch, updateExecution } from "../store/state.js";
|
|
3
|
+
export const setAgentIdInputSchema = z.object({
|
|
4
|
+
branch: z.string().describe("Branch name"),
|
|
5
|
+
agentTaskId: z.string().describe("Claude Task agent ID"),
|
|
6
|
+
});
|
|
7
|
+
export async function setAgentId(input) {
|
|
8
|
+
// Find execution
|
|
9
|
+
const exec = await findExecutionByBranch(input.branch);
|
|
10
|
+
if (!exec) {
|
|
11
|
+
throw new Error(`No execution found for branch: ${input.branch}`);
|
|
12
|
+
}
|
|
13
|
+
// Update agent task ID and status
|
|
14
|
+
await updateExecution(exec.id, {
|
|
15
|
+
agentTaskId: input.agentTaskId,
|
|
16
|
+
status: "running",
|
|
17
|
+
updatedAt: new Date(),
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
success: true,
|
|
21
|
+
branch: input.branch,
|
|
22
|
+
agentTaskId: input.agentTaskId,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const startInputSchema: z.ZodObject<{
|
|
3
|
+
prdPath: z.ZodString;
|
|
4
|
+
projectRoot: z.ZodOptional<z.ZodString>;
|
|
5
|
+
worktree: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
autoStart: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
autoMerge: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
notifyOnComplete: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
onConflict: z.ZodDefault<z.ZodEnum<["auto_theirs", "auto_ours", "notify", "agent"]>>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
prdPath: string;
|
|
12
|
+
onConflict: "auto_theirs" | "auto_ours" | "notify" | "agent";
|
|
13
|
+
autoMerge: boolean;
|
|
14
|
+
notifyOnComplete: boolean;
|
|
15
|
+
worktree: boolean;
|
|
16
|
+
autoStart: boolean;
|
|
17
|
+
projectRoot?: string | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
prdPath: string;
|
|
20
|
+
projectRoot?: string | undefined;
|
|
21
|
+
onConflict?: "auto_theirs" | "auto_ours" | "notify" | "agent" | undefined;
|
|
22
|
+
autoMerge?: boolean | undefined;
|
|
23
|
+
notifyOnComplete?: boolean | undefined;
|
|
24
|
+
worktree?: boolean | undefined;
|
|
25
|
+
autoStart?: boolean | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
export type StartInput = z.infer<typeof startInputSchema>;
|
|
28
|
+
export interface StartResult {
|
|
29
|
+
executionId: string;
|
|
30
|
+
branch: string;
|
|
31
|
+
worktreePath: string | null;
|
|
32
|
+
agentPrompt: string | null;
|
|
33
|
+
stories: Array<{
|
|
34
|
+
storyId: string;
|
|
35
|
+
title: string;
|
|
36
|
+
description: string;
|
|
37
|
+
acceptanceCriteria: string[];
|
|
38
|
+
priority: number;
|
|
39
|
+
passes: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export declare function start(input: StartInput): Promise<StartResult>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { parsePrdFile } from "../utils/prd-parser.js";
|
|
4
|
+
import { createWorktree } from "../utils/worktree.js";
|
|
5
|
+
import { generateAgentPrompt } from "../utils/agent.js";
|
|
6
|
+
import { resolve, basename } from "path";
|
|
7
|
+
import { findExecutionByBranch, insertExecution, insertUserStories, } from "../store/state.js";
|
|
8
|
+
export const startInputSchema = z.object({
|
|
9
|
+
prdPath: z.string().describe("Path to the PRD markdown file"),
|
|
10
|
+
projectRoot: z.string().optional().describe("Project root directory (defaults to cwd)"),
|
|
11
|
+
worktree: z.boolean().default(true).describe("Create a worktree for isolation"),
|
|
12
|
+
autoStart: z.boolean().default(true).describe("Generate agent prompt for auto-start"),
|
|
13
|
+
autoMerge: z.boolean().default(false).describe("Auto add to merge queue when all stories pass"),
|
|
14
|
+
notifyOnComplete: z.boolean().default(true).describe("Show Windows notification when all stories complete"),
|
|
15
|
+
onConflict: z
|
|
16
|
+
.enum(["auto_theirs", "auto_ours", "notify", "agent"])
|
|
17
|
+
.default("agent")
|
|
18
|
+
.describe("Conflict resolution strategy for merge"),
|
|
19
|
+
});
|
|
20
|
+
export async function start(input) {
|
|
21
|
+
const projectRoot = input.projectRoot || process.cwd();
|
|
22
|
+
const prdPath = resolve(projectRoot, input.prdPath);
|
|
23
|
+
// Parse PRD file
|
|
24
|
+
const prd = parsePrdFile(prdPath);
|
|
25
|
+
// Check if execution already exists for this branch
|
|
26
|
+
const existing = await findExecutionByBranch(prd.branchName);
|
|
27
|
+
if (existing) {
|
|
28
|
+
throw new Error(`Execution already exists for branch ${prd.branchName}. Use ralph_get to check status or ralph_stop to stop it.`);
|
|
29
|
+
}
|
|
30
|
+
// Create worktree if requested
|
|
31
|
+
let worktreePath = null;
|
|
32
|
+
if (input.worktree) {
|
|
33
|
+
worktreePath = await createWorktree(projectRoot, prd.branchName);
|
|
34
|
+
}
|
|
35
|
+
// Create execution record
|
|
36
|
+
const executionId = randomUUID();
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const projectName = basename(projectRoot);
|
|
39
|
+
await insertExecution({
|
|
40
|
+
id: executionId,
|
|
41
|
+
project: projectName,
|
|
42
|
+
branch: prd.branchName,
|
|
43
|
+
description: prd.description,
|
|
44
|
+
prdPath: prdPath,
|
|
45
|
+
projectRoot: projectRoot,
|
|
46
|
+
worktreePath: worktreePath,
|
|
47
|
+
status: "pending",
|
|
48
|
+
agentTaskId: null,
|
|
49
|
+
onConflict: input.onConflict,
|
|
50
|
+
autoMerge: input.autoMerge,
|
|
51
|
+
notifyOnComplete: input.notifyOnComplete,
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
});
|
|
55
|
+
// Create user story records
|
|
56
|
+
const storyRecords = prd.userStories.map((story) => ({
|
|
57
|
+
id: `${executionId}:${story.id}`,
|
|
58
|
+
executionId: executionId,
|
|
59
|
+
storyId: story.id,
|
|
60
|
+
title: story.title,
|
|
61
|
+
description: story.description,
|
|
62
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
63
|
+
priority: story.priority,
|
|
64
|
+
passes: false,
|
|
65
|
+
notes: "",
|
|
66
|
+
}));
|
|
67
|
+
if (storyRecords.length > 0) {
|
|
68
|
+
await insertUserStories(storyRecords);
|
|
69
|
+
}
|
|
70
|
+
// Generate agent prompt if auto-start
|
|
71
|
+
let agentPrompt = null;
|
|
72
|
+
if (input.autoStart) {
|
|
73
|
+
agentPrompt = generateAgentPrompt(prd.branchName, prd.description, worktreePath || projectRoot, storyRecords.map((s) => ({
|
|
74
|
+
storyId: s.storyId,
|
|
75
|
+
title: s.title,
|
|
76
|
+
description: s.description,
|
|
77
|
+
acceptanceCriteria: s.acceptanceCriteria,
|
|
78
|
+
priority: s.priority,
|
|
79
|
+
passes: s.passes,
|
|
80
|
+
})));
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
executionId,
|
|
84
|
+
branch: prd.branchName,
|
|
85
|
+
worktreePath,
|
|
86
|
+
agentPrompt,
|
|
87
|
+
stories: storyRecords.map((s) => ({
|
|
88
|
+
storyId: s.storyId,
|
|
89
|
+
title: s.title,
|
|
90
|
+
description: s.description,
|
|
91
|
+
acceptanceCriteria: s.acceptanceCriteria,
|
|
92
|
+
priority: s.priority,
|
|
93
|
+
passes: s.passes,
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const statusInputSchema: z.ZodObject<{
|
|
3
|
+
project: z.ZodOptional<z.ZodString>;
|
|
4
|
+
status: z.ZodOptional<z.ZodEnum<["pending", "running", "completed", "failed", "stopped", "merging"]>>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
project?: string | undefined;
|
|
7
|
+
status?: "pending" | "running" | "completed" | "failed" | "stopped" | "merging" | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
project?: string | undefined;
|
|
10
|
+
status?: "pending" | "running" | "completed" | "failed" | "stopped" | "merging" | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type StatusInput = z.infer<typeof statusInputSchema>;
|
|
13
|
+
export interface ExecutionStatus {
|
|
14
|
+
branch: string;
|
|
15
|
+
description: string;
|
|
16
|
+
status: string;
|
|
17
|
+
progress: string;
|
|
18
|
+
completedStories: number;
|
|
19
|
+
totalStories: number;
|
|
20
|
+
worktreePath: string | null;
|
|
21
|
+
agentTaskId: string | null;
|
|
22
|
+
lastActivity: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
export interface StatusResult {
|
|
26
|
+
executions: ExecutionStatus[];
|
|
27
|
+
summary: {
|
|
28
|
+
total: number;
|
|
29
|
+
running: number;
|
|
30
|
+
completed: number;
|
|
31
|
+
failed: number;
|
|
32
|
+
pending: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function status(input: StatusInput): Promise<StatusResult>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { listExecutions, listUserStoriesByExecutionId } from "../store/state.js";
|
|
3
|
+
export const statusInputSchema = z.object({
|
|
4
|
+
project: z.string().optional().describe("Filter by project name"),
|
|
5
|
+
status: z
|
|
6
|
+
.enum(["pending", "running", "completed", "failed", "stopped", "merging"])
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Filter by status"),
|
|
9
|
+
});
|
|
10
|
+
export async function status(input) {
|
|
11
|
+
const allExecutions = await listExecutions();
|
|
12
|
+
// Filter in memory (simpler than building dynamic where clauses)
|
|
13
|
+
let filtered = allExecutions;
|
|
14
|
+
if (input.project) {
|
|
15
|
+
filtered = filtered.filter((e) => e.project === input.project);
|
|
16
|
+
}
|
|
17
|
+
if (input.status) {
|
|
18
|
+
filtered = filtered.filter((e) => e.status === input.status);
|
|
19
|
+
}
|
|
20
|
+
// Get story counts for each execution
|
|
21
|
+
const executionStatuses = [];
|
|
22
|
+
for (const exec of filtered) {
|
|
23
|
+
const stories = await listUserStoriesByExecutionId(exec.id);
|
|
24
|
+
const completedStories = stories.filter((s) => s.passes).length;
|
|
25
|
+
const totalStories = stories.length;
|
|
26
|
+
executionStatuses.push({
|
|
27
|
+
branch: exec.branch,
|
|
28
|
+
description: exec.description,
|
|
29
|
+
status: exec.status,
|
|
30
|
+
progress: `${completedStories}/${totalStories} US`,
|
|
31
|
+
completedStories,
|
|
32
|
+
totalStories,
|
|
33
|
+
worktreePath: exec.worktreePath,
|
|
34
|
+
agentTaskId: exec.agentTaskId,
|
|
35
|
+
lastActivity: exec.updatedAt.toISOString(),
|
|
36
|
+
createdAt: exec.createdAt.toISOString(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Sort by last activity (most recent first)
|
|
40
|
+
executionStatuses.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
41
|
+
// Calculate summary
|
|
42
|
+
const summary = {
|
|
43
|
+
total: executionStatuses.length,
|
|
44
|
+
running: executionStatuses.filter((e) => e.status === "running").length,
|
|
45
|
+
completed: executionStatuses.filter((e) => e.status === "completed").length,
|
|
46
|
+
failed: executionStatuses.filter((e) => e.status === "failed").length,
|
|
47
|
+
pending: executionStatuses.filter((e) => e.status === "pending").length,
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
executions: executionStatuses,
|
|
51
|
+
summary,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const stopInputSchema: z.ZodObject<{
|
|
3
|
+
branch: z.ZodString;
|
|
4
|
+
cleanup: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
deleteRecord: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
branch: string;
|
|
8
|
+
cleanup: boolean;
|
|
9
|
+
deleteRecord: boolean;
|
|
10
|
+
}, {
|
|
11
|
+
branch: string;
|
|
12
|
+
cleanup?: boolean | undefined;
|
|
13
|
+
deleteRecord?: boolean | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type StopInput = z.infer<typeof stopInputSchema>;
|
|
16
|
+
export interface StopResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
branch: string;
|
|
19
|
+
previousStatus: string;
|
|
20
|
+
cleanedUp: boolean;
|
|
21
|
+
deleted: boolean;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function stop(input: StopInput): Promise<StopResult>;
|