ralph-mcp 1.0.8 → 1.0.10
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/README.md +3 -0
- package/dist/index.js +53 -0
- package/dist/store/state.d.ts +13 -0
- package/dist/store/state.js +33 -0
- package/dist/tools/batch-start.d.ts +50 -0
- package/dist/tools/batch-start.js +225 -0
- package/dist/tools/start.d.ts +3 -0
- package/dist/tools/start.js +12 -3
- package/dist/tools/update.d.ts +4 -0
- package/dist/tools/update.js +34 -1
- package/dist/utils/package-manager.d.ts +8 -0
- package/dist/utils/package-manager.js +44 -0
- package/dist/utils/prd-parser.d.ts +1 -0
- package/dist/utils/prd-parser.js +54 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ Claude: ralph_start → Task Agent handles everything automatically
|
|
|
39
39
|
- **2-Step Workflow** - Just create PRD and run `ralph_start`, everything else is automatic
|
|
40
40
|
- **Parallel Execution** - Run 5+ PRDs simultaneously with Claude Code Task tool
|
|
41
41
|
- **Git Worktree Isolation** - Each PRD runs in its own worktree, zero conflicts
|
|
42
|
+
- **Agent Memory** - Persistent "Progress Log" learns from mistakes across User Stories
|
|
43
|
+
- **Context Injection** - Inject project rules (CLAUDE.md) into agent context
|
|
42
44
|
- **Auto Quality Gates** - Type check, lint, build before every commit
|
|
43
45
|
- **Auto Merge** - Merges to main when all User Stories pass
|
|
44
46
|
- **Doc Sync** - Automatically updates TODO.md with completed items
|
|
@@ -276,6 +278,7 @@ Override data directory with `RALPH_DATA_DIR` environment variable.
|
|
|
276
278
|
| `autoMerge` | `false` | Auto-merge when all stories pass |
|
|
277
279
|
| `notifyOnComplete` | `true` | Show Windows notification on completion |
|
|
278
280
|
| `onConflict` | `"agent"` | Conflict resolution: `auto_theirs`, `auto_ours`, `notify`, `agent` |
|
|
281
|
+
| `contextInjectionPath` | `undefined` | Optional path to file (e.g. CLAUDE.md) to inject into prompt |
|
|
279
282
|
|
|
280
283
|
### Example with options
|
|
281
284
|
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { start, startInputSchema } from "./tools/start.js";
|
|
6
|
+
import { batchStart, batchStartInputSchema } from "./tools/batch-start.js";
|
|
6
7
|
import { status, statusInputSchema } from "./tools/status.js";
|
|
7
8
|
import { get, getInputSchema } from "./tools/get.js";
|
|
8
9
|
import { update, updateInputSchema } from "./tools/update.js";
|
|
@@ -208,6 +209,55 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
208
209
|
required: ["branch", "agentTaskId"],
|
|
209
210
|
},
|
|
210
211
|
},
|
|
212
|
+
{
|
|
213
|
+
name: "ralph_batch_start",
|
|
214
|
+
description: "Start multiple PRDs with dependency resolution. Parses all PRDs, creates worktrees, runs pnpm install serially (avoids store lock), and returns agent prompts for PRDs whose dependencies are satisfied.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
prdPaths: {
|
|
219
|
+
type: "array",
|
|
220
|
+
items: { type: "string" },
|
|
221
|
+
description: "Array of paths to PRD markdown files",
|
|
222
|
+
},
|
|
223
|
+
projectRoot: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Project root directory (defaults to cwd)",
|
|
226
|
+
},
|
|
227
|
+
worktree: {
|
|
228
|
+
type: "boolean",
|
|
229
|
+
description: "Create worktrees for isolation (default: true)",
|
|
230
|
+
default: true,
|
|
231
|
+
},
|
|
232
|
+
autoMerge: {
|
|
233
|
+
type: "boolean",
|
|
234
|
+
description: "Auto add to merge queue when all stories pass (default: true)",
|
|
235
|
+
default: true,
|
|
236
|
+
},
|
|
237
|
+
notifyOnComplete: {
|
|
238
|
+
type: "boolean",
|
|
239
|
+
description: "Show Windows notification when all stories complete (default: true)",
|
|
240
|
+
default: true,
|
|
241
|
+
},
|
|
242
|
+
onConflict: {
|
|
243
|
+
type: "string",
|
|
244
|
+
enum: ["auto_theirs", "auto_ours", "notify", "agent"],
|
|
245
|
+
description: "Conflict resolution strategy for merge (default: agent)",
|
|
246
|
+
default: "agent",
|
|
247
|
+
},
|
|
248
|
+
contextInjectionPath: {
|
|
249
|
+
type: "string",
|
|
250
|
+
description: "Path to a file (e.g., CLAUDE.md) to inject into the agent prompt",
|
|
251
|
+
},
|
|
252
|
+
preheat: {
|
|
253
|
+
type: "boolean",
|
|
254
|
+
description: "Run pnpm install serially before starting agents (default: true)",
|
|
255
|
+
default: true,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
required: ["prdPaths"],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
211
261
|
],
|
|
212
262
|
};
|
|
213
263
|
});
|
|
@@ -241,6 +291,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
241
291
|
case "ralph_set_agent_id":
|
|
242
292
|
result = await setAgentId(setAgentIdInputSchema.parse(args));
|
|
243
293
|
break;
|
|
294
|
+
case "ralph_batch_start":
|
|
295
|
+
result = await batchStart(batchStartInputSchema.parse(args));
|
|
296
|
+
break;
|
|
244
297
|
default:
|
|
245
298
|
throw new Error(`Unknown tool: ${name}`);
|
|
246
299
|
}
|
package/dist/store/state.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ExecutionRecord {
|
|
|
14
14
|
onConflict: ConflictStrategy | null;
|
|
15
15
|
autoMerge: boolean;
|
|
16
16
|
notifyOnComplete: boolean;
|
|
17
|
+
dependencies: string[];
|
|
17
18
|
createdAt: Date;
|
|
18
19
|
updatedAt: Date;
|
|
19
20
|
}
|
|
@@ -53,3 +54,15 @@ export declare function findMergeQueueItemByExecutionId(executionId: string): Pr
|
|
|
53
54
|
export declare function insertMergeQueueItem(item: Omit<MergeQueueItem, "id">): Promise<MergeQueueItem>;
|
|
54
55
|
export declare function updateMergeQueueItem(id: number, patch: Partial<Omit<MergeQueueItem, "id" | "executionId" | "createdAt">>): Promise<void>;
|
|
55
56
|
export declare function deleteMergeQueueByExecutionId(executionId: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Find all executions that depend on a given branch.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findExecutionsDependingOn(branch: string): Promise<ExecutionRecord[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Check if all dependencies of an execution are completed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function areDependenciesSatisfied(execution: ExecutionRecord): Promise<{
|
|
65
|
+
satisfied: boolean;
|
|
66
|
+
pending: string[];
|
|
67
|
+
completed: string[];
|
|
68
|
+
}>;
|
package/dist/store/state.js
CHANGED
|
@@ -45,6 +45,7 @@ function deserializeState(file) {
|
|
|
45
45
|
return {
|
|
46
46
|
executions: file.executions.map((e) => ({
|
|
47
47
|
...e,
|
|
48
|
+
dependencies: Array.isArray(e.dependencies) ? e.dependencies : [],
|
|
48
49
|
createdAt: parseDate(e.createdAt, "executions.createdAt"),
|
|
49
50
|
updatedAt: parseDate(e.updatedAt, "executions.updatedAt"),
|
|
50
51
|
})),
|
|
@@ -203,3 +204,35 @@ export async function deleteMergeQueueByExecutionId(executionId) {
|
|
|
203
204
|
s.mergeQueue = s.mergeQueue.filter((q) => q.executionId !== executionId);
|
|
204
205
|
});
|
|
205
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Find all executions that depend on a given branch.
|
|
209
|
+
*/
|
|
210
|
+
export async function findExecutionsDependingOn(branch) {
|
|
211
|
+
return readState((s) => s.executions.filter((e) => e.dependencies.includes(branch)));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Check if all dependencies of an execution are completed.
|
|
215
|
+
*/
|
|
216
|
+
export async function areDependenciesSatisfied(execution) {
|
|
217
|
+
if (!execution.dependencies || execution.dependencies.length === 0) {
|
|
218
|
+
return { satisfied: true, pending: [], completed: [] };
|
|
219
|
+
}
|
|
220
|
+
return readState((s) => {
|
|
221
|
+
const pending = [];
|
|
222
|
+
const completed = [];
|
|
223
|
+
for (const depBranch of execution.dependencies) {
|
|
224
|
+
const depExec = s.executions.find((e) => e.branch === depBranch);
|
|
225
|
+
if (depExec && depExec.status === "completed") {
|
|
226
|
+
completed.push(depBranch);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
pending.push(depBranch);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
satisfied: pending.length === 0,
|
|
234
|
+
pending,
|
|
235
|
+
completed,
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const batchStartInputSchema: z.ZodObject<{
|
|
3
|
+
prdPaths: z.ZodArray<z.ZodString, "many">;
|
|
4
|
+
projectRoot: z.ZodOptional<z.ZodString>;
|
|
5
|
+
worktree: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
autoMerge: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
notifyOnComplete: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
onConflict: z.ZodDefault<z.ZodEnum<["auto_theirs", "auto_ours", "notify", "agent"]>>;
|
|
9
|
+
contextInjectionPath: z.ZodOptional<z.ZodString>;
|
|
10
|
+
preheat: z.ZodDefault<z.ZodBoolean>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
onConflict: "auto_theirs" | "auto_ours" | "notify" | "agent";
|
|
13
|
+
autoMerge: boolean;
|
|
14
|
+
notifyOnComplete: boolean;
|
|
15
|
+
worktree: boolean;
|
|
16
|
+
prdPaths: string[];
|
|
17
|
+
preheat: boolean;
|
|
18
|
+
projectRoot?: string | undefined;
|
|
19
|
+
contextInjectionPath?: string | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
prdPaths: string[];
|
|
22
|
+
projectRoot?: string | undefined;
|
|
23
|
+
onConflict?: "auto_theirs" | "auto_ours" | "notify" | "agent" | undefined;
|
|
24
|
+
autoMerge?: boolean | undefined;
|
|
25
|
+
notifyOnComplete?: boolean | undefined;
|
|
26
|
+
worktree?: boolean | undefined;
|
|
27
|
+
contextInjectionPath?: string | undefined;
|
|
28
|
+
preheat?: boolean | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
export type BatchStartInput = z.infer<typeof batchStartInputSchema>;
|
|
31
|
+
export interface BatchStartResult {
|
|
32
|
+
total: number;
|
|
33
|
+
created: number;
|
|
34
|
+
skipped: Array<{
|
|
35
|
+
prdPath: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
}>;
|
|
38
|
+
readyToStart: Array<{
|
|
39
|
+
branch: string;
|
|
40
|
+
agentPrompt: string;
|
|
41
|
+
dependencies: string[];
|
|
42
|
+
}>;
|
|
43
|
+
waitingForDependencies: Array<{
|
|
44
|
+
branch: string;
|
|
45
|
+
pendingDependencies: string[];
|
|
46
|
+
}>;
|
|
47
|
+
dependencyGraph: Record<string, string[]>;
|
|
48
|
+
preheatCompleted: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function batchStart(input: BatchStartInput): Promise<BatchStartResult>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolve, basename } from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { parsePrdFile } from "../utils/prd-parser.js";
|
|
6
|
+
import { createWorktree } from "../utils/worktree.js";
|
|
7
|
+
import { generateAgentPrompt } from "../utils/agent.js";
|
|
8
|
+
import { detectPackageManager, getInstallCommand, } from "../utils/package-manager.js";
|
|
9
|
+
import { areDependenciesSatisfied, findExecutionByBranch, insertExecution, insertUserStories, } from "../store/state.js";
|
|
10
|
+
export const batchStartInputSchema = z.object({
|
|
11
|
+
prdPaths: z.array(z.string()).describe("Array of paths to PRD markdown files"),
|
|
12
|
+
projectRoot: z.string().optional().describe("Project root directory (defaults to cwd)"),
|
|
13
|
+
worktree: z.boolean().default(true).describe("Create worktrees for isolation"),
|
|
14
|
+
autoMerge: z.boolean().default(true).describe("Auto add to merge queue when all stories pass"),
|
|
15
|
+
notifyOnComplete: z.boolean().default(true).describe("Show Windows notification when all stories complete"),
|
|
16
|
+
onConflict: z
|
|
17
|
+
.enum(["auto_theirs", "auto_ours", "notify", "agent"])
|
|
18
|
+
.default("agent")
|
|
19
|
+
.describe("Conflict resolution strategy for merge"),
|
|
20
|
+
contextInjectionPath: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Path to a file (e.g., CLAUDE.md) to inject into the agent prompt"),
|
|
24
|
+
preheat: z.boolean().default(true).describe("Run install command serially before starting agents (avoids store lock)"),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Topological sort to determine execution order based on dependencies.
|
|
28
|
+
*/
|
|
29
|
+
function topologicalSort(prds) {
|
|
30
|
+
const branchToPrd = new Map();
|
|
31
|
+
for (const prd of prds) {
|
|
32
|
+
branchToPrd.set(prd.branch, prd);
|
|
33
|
+
}
|
|
34
|
+
const visited = new Set();
|
|
35
|
+
const result = [];
|
|
36
|
+
function visit(branch) {
|
|
37
|
+
if (visited.has(branch))
|
|
38
|
+
return;
|
|
39
|
+
visited.add(branch);
|
|
40
|
+
const prd = branchToPrd.get(branch);
|
|
41
|
+
if (!prd)
|
|
42
|
+
return;
|
|
43
|
+
// Visit dependencies first
|
|
44
|
+
for (const dep of prd.dependencies) {
|
|
45
|
+
if (branchToPrd.has(dep)) {
|
|
46
|
+
visit(dep);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
result.push(prd);
|
|
50
|
+
}
|
|
51
|
+
for (const prd of prds) {
|
|
52
|
+
visit(prd.branch);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run install command in a worktree directory.
|
|
58
|
+
*/
|
|
59
|
+
function preheatWorktree(worktreePath, installCmd) {
|
|
60
|
+
const cmd = `${installCmd.command} ${installCmd.args.join(" ")}`;
|
|
61
|
+
try {
|
|
62
|
+
execSync(cmd, {
|
|
63
|
+
cwd: worktreePath,
|
|
64
|
+
stdio: "pipe",
|
|
65
|
+
timeout: 300000, // 5 minutes
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// Try fallback if available
|
|
70
|
+
if (installCmd.fallbackArgs.length > 0) {
|
|
71
|
+
const fallbackCmd = `${installCmd.command} ${installCmd.fallbackArgs.join(" ")}`;
|
|
72
|
+
execSync(fallbackCmd, {
|
|
73
|
+
cwd: worktreePath,
|
|
74
|
+
stdio: "pipe",
|
|
75
|
+
timeout: 300000,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function batchStart(input) {
|
|
84
|
+
const projectRoot = input.projectRoot || process.cwd();
|
|
85
|
+
const projectName = basename(projectRoot);
|
|
86
|
+
const contextPath = input.contextInjectionPath
|
|
87
|
+
? resolve(projectRoot, input.contextInjectionPath)
|
|
88
|
+
: undefined;
|
|
89
|
+
// Detect package manager
|
|
90
|
+
const pm = detectPackageManager(projectRoot);
|
|
91
|
+
const installCmd = getInstallCommand(pm);
|
|
92
|
+
const skipped = [];
|
|
93
|
+
const prdInfos = [];
|
|
94
|
+
// Phase 1: Parse all PRDs and create execution records
|
|
95
|
+
for (const prdPath of input.prdPaths) {
|
|
96
|
+
const fullPath = resolve(projectRoot, prdPath);
|
|
97
|
+
try {
|
|
98
|
+
const prd = parsePrdFile(fullPath);
|
|
99
|
+
// Check if execution already exists
|
|
100
|
+
const existing = await findExecutionByBranch(prd.branchName);
|
|
101
|
+
if (existing) {
|
|
102
|
+
skipped.push({
|
|
103
|
+
prdPath,
|
|
104
|
+
reason: `Execution already exists for branch ${prd.branchName}`,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Create worktree if requested
|
|
109
|
+
let worktreePath = null;
|
|
110
|
+
if (input.worktree) {
|
|
111
|
+
worktreePath = await createWorktree(projectRoot, prd.branchName);
|
|
112
|
+
}
|
|
113
|
+
// Create execution record
|
|
114
|
+
const executionId = randomUUID();
|
|
115
|
+
const now = new Date();
|
|
116
|
+
await insertExecution({
|
|
117
|
+
id: executionId,
|
|
118
|
+
project: projectName,
|
|
119
|
+
branch: prd.branchName,
|
|
120
|
+
description: prd.description,
|
|
121
|
+
prdPath: fullPath,
|
|
122
|
+
projectRoot: projectRoot,
|
|
123
|
+
worktreePath: worktreePath,
|
|
124
|
+
status: "pending",
|
|
125
|
+
agentTaskId: null,
|
|
126
|
+
onConflict: input.onConflict,
|
|
127
|
+
autoMerge: input.autoMerge,
|
|
128
|
+
notifyOnComplete: input.notifyOnComplete,
|
|
129
|
+
dependencies: prd.dependencies,
|
|
130
|
+
createdAt: now,
|
|
131
|
+
updatedAt: now,
|
|
132
|
+
});
|
|
133
|
+
// Create user story records
|
|
134
|
+
const storyRecords = prd.userStories.map((story) => ({
|
|
135
|
+
id: `${executionId}:${story.id}`,
|
|
136
|
+
executionId: executionId,
|
|
137
|
+
storyId: story.id,
|
|
138
|
+
title: story.title,
|
|
139
|
+
description: story.description,
|
|
140
|
+
acceptanceCriteria: story.acceptanceCriteria,
|
|
141
|
+
priority: story.priority,
|
|
142
|
+
passes: false,
|
|
143
|
+
notes: "",
|
|
144
|
+
}));
|
|
145
|
+
if (storyRecords.length > 0) {
|
|
146
|
+
await insertUserStories(storyRecords);
|
|
147
|
+
}
|
|
148
|
+
prdInfos.push({
|
|
149
|
+
prdPath,
|
|
150
|
+
branch: prd.branchName,
|
|
151
|
+
dependencies: prd.dependencies,
|
|
152
|
+
worktreePath,
|
|
153
|
+
executionId,
|
|
154
|
+
stories: storyRecords.map((s) => ({
|
|
155
|
+
storyId: s.storyId,
|
|
156
|
+
title: s.title,
|
|
157
|
+
description: s.description,
|
|
158
|
+
acceptanceCriteria: s.acceptanceCriteria,
|
|
159
|
+
priority: s.priority,
|
|
160
|
+
passes: s.passes,
|
|
161
|
+
})),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
skipped.push({
|
|
166
|
+
prdPath,
|
|
167
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Phase 2: Topological sort for preheat order
|
|
172
|
+
const sortedPrds = topologicalSort(prdInfos);
|
|
173
|
+
// Phase 3: Preheat worktrees serially (avoids store lock contention)
|
|
174
|
+
let preheatCompleted = false;
|
|
175
|
+
if (input.preheat && input.worktree) {
|
|
176
|
+
for (const prd of sortedPrds) {
|
|
177
|
+
if (prd.worktreePath) {
|
|
178
|
+
try {
|
|
179
|
+
preheatWorktree(prd.worktreePath, installCmd);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error(`Preheat failed for ${prd.branch}:`, error);
|
|
183
|
+
// Continue with other worktrees
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
preheatCompleted = true;
|
|
188
|
+
}
|
|
189
|
+
// Phase 4: Determine which PRDs can start immediately
|
|
190
|
+
const readyToStart = [];
|
|
191
|
+
const waitingForDependencies = [];
|
|
192
|
+
for (const prd of prdInfos) {
|
|
193
|
+
const tempExec = { dependencies: prd.dependencies };
|
|
194
|
+
const depStatus = await areDependenciesSatisfied(tempExec);
|
|
195
|
+
if (depStatus.satisfied) {
|
|
196
|
+
const agentPrompt = generateAgentPrompt(prd.branch, "", // description not stored in prdInfo
|
|
197
|
+
prd.worktreePath || projectRoot, prd.stories, contextPath);
|
|
198
|
+
readyToStart.push({
|
|
199
|
+
branch: prd.branch,
|
|
200
|
+
agentPrompt,
|
|
201
|
+
dependencies: prd.dependencies,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
waitingForDependencies.push({
|
|
206
|
+
branch: prd.branch,
|
|
207
|
+
pendingDependencies: depStatus.pending,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Build dependency graph for visualization
|
|
212
|
+
const dependencyGraph = {};
|
|
213
|
+
for (const prd of prdInfos) {
|
|
214
|
+
dependencyGraph[prd.branch] = prd.dependencies;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
total: input.prdPaths.length,
|
|
218
|
+
created: prdInfos.length,
|
|
219
|
+
skipped,
|
|
220
|
+
readyToStart,
|
|
221
|
+
waitingForDependencies,
|
|
222
|
+
dependencyGraph,
|
|
223
|
+
preheatCompleted,
|
|
224
|
+
};
|
|
225
|
+
}
|
package/dist/tools/start.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ export interface StartResult {
|
|
|
33
33
|
branch: string;
|
|
34
34
|
worktreePath: string | null;
|
|
35
35
|
agentPrompt: string | null;
|
|
36
|
+
dependencies: string[];
|
|
37
|
+
dependenciesSatisfied: boolean;
|
|
38
|
+
pendingDependencies: string[];
|
|
36
39
|
stories: Array<{
|
|
37
40
|
storyId: string;
|
|
38
41
|
title: string;
|
package/dist/tools/start.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parsePrdFile } from "../utils/prd-parser.js";
|
|
|
4
4
|
import { createWorktree } from "../utils/worktree.js";
|
|
5
5
|
import { generateAgentPrompt } from "../utils/agent.js";
|
|
6
6
|
import { resolve, basename } from "path";
|
|
7
|
-
import { findExecutionByBranch, insertExecution, insertUserStories, } from "../store/state.js";
|
|
7
|
+
import { areDependenciesSatisfied, findExecutionByBranch, insertExecution, insertUserStories, } from "../store/state.js";
|
|
8
8
|
export const startInputSchema = z.object({
|
|
9
9
|
prdPath: z.string().describe("Path to the PRD markdown file"),
|
|
10
10
|
projectRoot: z.string().optional().describe("Project root directory (defaults to cwd)"),
|
|
@@ -53,6 +53,7 @@ export async function start(input) {
|
|
|
53
53
|
onConflict: input.onConflict,
|
|
54
54
|
autoMerge: input.autoMerge,
|
|
55
55
|
notifyOnComplete: input.notifyOnComplete,
|
|
56
|
+
dependencies: prd.dependencies,
|
|
56
57
|
createdAt: now,
|
|
57
58
|
updatedAt: now,
|
|
58
59
|
});
|
|
@@ -71,9 +72,14 @@ export async function start(input) {
|
|
|
71
72
|
if (storyRecords.length > 0) {
|
|
72
73
|
await insertUserStories(storyRecords);
|
|
73
74
|
}
|
|
74
|
-
//
|
|
75
|
+
// Check if dependencies are satisfied
|
|
76
|
+
const tempExec = {
|
|
77
|
+
dependencies: prd.dependencies,
|
|
78
|
+
};
|
|
79
|
+
const depStatus = await areDependenciesSatisfied(tempExec);
|
|
80
|
+
// Generate agent prompt only if auto-start AND dependencies are satisfied
|
|
75
81
|
let agentPrompt = null;
|
|
76
|
-
if (input.autoStart) {
|
|
82
|
+
if (input.autoStart && depStatus.satisfied) {
|
|
77
83
|
const contextPath = input.contextInjectionPath
|
|
78
84
|
? resolve(projectRoot, input.contextInjectionPath)
|
|
79
85
|
: undefined;
|
|
@@ -91,6 +97,9 @@ export async function start(input) {
|
|
|
91
97
|
branch: prd.branchName,
|
|
92
98
|
worktreePath,
|
|
93
99
|
agentPrompt,
|
|
100
|
+
dependencies: prd.dependencies,
|
|
101
|
+
dependenciesSatisfied: depStatus.satisfied,
|
|
102
|
+
pendingDependencies: depStatus.pending,
|
|
94
103
|
stories: storyRecords.map((s) => ({
|
|
95
104
|
storyId: s.storyId,
|
|
96
105
|
title: s.title,
|
package/dist/tools/update.d.ts
CHANGED
|
@@ -24,5 +24,9 @@ export interface UpdateResult {
|
|
|
24
24
|
allComplete: boolean;
|
|
25
25
|
progress: string;
|
|
26
26
|
addedToMergeQueue: boolean;
|
|
27
|
+
triggeredDependents: Array<{
|
|
28
|
+
branch: string;
|
|
29
|
+
agentPrompt: string | null;
|
|
30
|
+
}>;
|
|
27
31
|
}
|
|
28
32
|
export declare function update(input: UpdateInput): Promise<UpdateResult>;
|
package/dist/tools/update.js
CHANGED
|
@@ -3,8 +3,9 @@ import notifier from "node-notifier";
|
|
|
3
3
|
import { appendFile, mkdir } from "fs/promises";
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
|
-
import { findExecutionByBranch, findMergeQueueItemByExecutionId, findUserStoryById, insertMergeQueueItem, listMergeQueue, listUserStoriesByExecutionId, updateExecution, updateUserStory, } from "../store/state.js";
|
|
6
|
+
import { areDependenciesSatisfied, findExecutionByBranch, findExecutionsDependingOn, findMergeQueueItemByExecutionId, findUserStoryById, insertMergeQueueItem, listMergeQueue, listUserStoriesByExecutionId, updateExecution, updateUserStory, } from "../store/state.js";
|
|
7
7
|
import { mergeQueueAction } from "./merge.js";
|
|
8
|
+
import { generateAgentPrompt } from "../utils/agent.js";
|
|
8
9
|
export const updateInputSchema = z.object({
|
|
9
10
|
branch: z.string().describe("Branch name (e.g., ralph/task1-agent)"),
|
|
10
11
|
storyId: z.string().describe("Story ID (e.g., US-001)"),
|
|
@@ -99,6 +100,37 @@ export async function update(input) {
|
|
|
99
100
|
sound: true,
|
|
100
101
|
});
|
|
101
102
|
}
|
|
103
|
+
// Trigger dependent executions when this PRD completes
|
|
104
|
+
const triggeredDependents = [];
|
|
105
|
+
if (allComplete) {
|
|
106
|
+
const dependents = await findExecutionsDependingOn(exec.branch);
|
|
107
|
+
for (const dep of dependents) {
|
|
108
|
+
// Skip if already running or completed
|
|
109
|
+
if (dep.status !== "pending") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Check if all dependencies are now satisfied
|
|
113
|
+
const depStatus = await areDependenciesSatisfied(dep);
|
|
114
|
+
if (depStatus.satisfied) {
|
|
115
|
+
// Get user stories for this dependent execution
|
|
116
|
+
const depStories = await listUserStoriesByExecutionId(dep.id);
|
|
117
|
+
// Generate agent prompt for the dependent
|
|
118
|
+
const agentPrompt = generateAgentPrompt(dep.branch, dep.description, dep.worktreePath || dep.projectRoot, depStories.map((s) => ({
|
|
119
|
+
storyId: s.storyId,
|
|
120
|
+
title: s.title,
|
|
121
|
+
description: s.description,
|
|
122
|
+
acceptanceCriteria: s.acceptanceCriteria,
|
|
123
|
+
priority: s.priority,
|
|
124
|
+
passes: s.passes,
|
|
125
|
+
})), undefined // contextPath not stored, would need to re-parse PRD if needed
|
|
126
|
+
);
|
|
127
|
+
triggeredDependents.push({
|
|
128
|
+
branch: dep.branch,
|
|
129
|
+
agentPrompt,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
102
134
|
return {
|
|
103
135
|
success: true,
|
|
104
136
|
branch: input.branch,
|
|
@@ -107,5 +139,6 @@ export async function update(input) {
|
|
|
107
139
|
allComplete,
|
|
108
140
|
progress: `${completedCount}/${allStories.length} US`,
|
|
109
141
|
addedToMergeQueue,
|
|
142
|
+
triggeredDependents,
|
|
110
143
|
};
|
|
111
144
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
2
|
+
export interface InstallCommand {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
fallbackArgs: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function detectPackageManager(projectRoot: string): PackageManager;
|
|
8
|
+
export declare function getInstallCommand(pm: PackageManager): InstallCommand;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
export function detectPackageManager(projectRoot) {
|
|
4
|
+
if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
|
|
5
|
+
return "pnpm";
|
|
6
|
+
}
|
|
7
|
+
if (existsSync(join(projectRoot, "yarn.lock"))) {
|
|
8
|
+
return "yarn";
|
|
9
|
+
}
|
|
10
|
+
if (existsSync(join(projectRoot, "bun.lockb"))) {
|
|
11
|
+
return "bun";
|
|
12
|
+
}
|
|
13
|
+
// Default to npm if package-lock.json exists or as fallback
|
|
14
|
+
return "npm";
|
|
15
|
+
}
|
|
16
|
+
export function getInstallCommand(pm) {
|
|
17
|
+
switch (pm) {
|
|
18
|
+
case "pnpm":
|
|
19
|
+
return {
|
|
20
|
+
command: "pnpm",
|
|
21
|
+
args: ["install", "--frozen-lockfile"],
|
|
22
|
+
fallbackArgs: ["install"],
|
|
23
|
+
};
|
|
24
|
+
case "yarn":
|
|
25
|
+
return {
|
|
26
|
+
command: "yarn",
|
|
27
|
+
args: ["install", "--frozen-lockfile"],
|
|
28
|
+
fallbackArgs: ["install"],
|
|
29
|
+
};
|
|
30
|
+
case "bun":
|
|
31
|
+
return {
|
|
32
|
+
command: "bun",
|
|
33
|
+
args: ["install", "--frozen-lockfile"],
|
|
34
|
+
fallbackArgs: ["install"],
|
|
35
|
+
};
|
|
36
|
+
case "npm":
|
|
37
|
+
default:
|
|
38
|
+
return {
|
|
39
|
+
command: "npm",
|
|
40
|
+
args: ["ci"],
|
|
41
|
+
fallbackArgs: ["install"],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/utils/prd-parser.js
CHANGED
|
@@ -25,6 +25,7 @@ function parsePrdJson(content) {
|
|
|
25
25
|
acceptanceCriteria: us.acceptanceCriteria || [],
|
|
26
26
|
priority: us.priority || index + 1,
|
|
27
27
|
})),
|
|
28
|
+
dependencies: Array.isArray(data.dependencies) ? data.dependencies : [],
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
function parsePrdMarkdown(content) {
|
|
@@ -39,6 +40,8 @@ function parsePrdMarkdown(content) {
|
|
|
39
40
|
// Extract description
|
|
40
41
|
const descMatch = body.match(/##\s*(?:Description|描述|Overview|概述)\s*\n([\s\S]*?)(?=\n##|\n$)/i);
|
|
41
42
|
const description = descMatch?.[1]?.trim() || title;
|
|
43
|
+
// Extract dependencies from frontmatter or body
|
|
44
|
+
const dependencies = extractDependencies(frontmatter, body);
|
|
42
45
|
// Extract user stories
|
|
43
46
|
const userStories = extractUserStories(body);
|
|
44
47
|
return {
|
|
@@ -46,8 +49,59 @@ function parsePrdMarkdown(content) {
|
|
|
46
49
|
description,
|
|
47
50
|
branchName,
|
|
48
51
|
userStories,
|
|
52
|
+
dependencies,
|
|
49
53
|
};
|
|
50
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract dependencies from frontmatter or body.
|
|
57
|
+
* Supports:
|
|
58
|
+
* - Frontmatter: `dependencies: [ralph/prd-a, ralph/prd-b]`
|
|
59
|
+
* - Body section: `## Dependencies\n- depends_on: prd-a.md`
|
|
60
|
+
*/
|
|
61
|
+
function extractDependencies(frontmatter, body) {
|
|
62
|
+
const deps = [];
|
|
63
|
+
// From frontmatter (array of branch names)
|
|
64
|
+
if (Array.isArray(frontmatter.dependencies)) {
|
|
65
|
+
for (const dep of frontmatter.dependencies) {
|
|
66
|
+
if (typeof dep === "string" && dep.trim()) {
|
|
67
|
+
deps.push(normalizeDependency(dep.trim()));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// From body: ## Dependencies section
|
|
72
|
+
const depsSection = body.match(/##\s*(?:Dependencies|依赖)\s*\n([\s\S]*?)(?=\n##[^#]|$)/i);
|
|
73
|
+
if (depsSection) {
|
|
74
|
+
// Match patterns like:
|
|
75
|
+
// - depends_on: prd-shared-logic.md
|
|
76
|
+
// - ralph/prd-shared-logic
|
|
77
|
+
// - prd-shared-logic
|
|
78
|
+
const depPattern = /[-*]\s*(?:depends_on:\s*)?(.+?)(?:\n|$)/gi;
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = depPattern.exec(depsSection[1])) !== null) {
|
|
81
|
+
const dep = match[1].trim();
|
|
82
|
+
if (dep && !dep.startsWith("#")) {
|
|
83
|
+
deps.push(normalizeDependency(dep));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...new Set(deps)]; // Deduplicate
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalize dependency to branch name format.
|
|
91
|
+
* - "prd-shared-logic.md" -> "ralph/prd-shared-logic"
|
|
92
|
+
* - "ralph/prd-shared-logic" -> "ralph/prd-shared-logic"
|
|
93
|
+
* - "prd-shared-logic" -> "ralph/prd-shared-logic"
|
|
94
|
+
*/
|
|
95
|
+
function normalizeDependency(dep) {
|
|
96
|
+
// Remove .md extension if present
|
|
97
|
+
dep = dep.replace(/\.md$/i, "");
|
|
98
|
+
// If already has ralph/ prefix, return as-is
|
|
99
|
+
if (dep.startsWith("ralph/")) {
|
|
100
|
+
return dep;
|
|
101
|
+
}
|
|
102
|
+
// Add ralph/ prefix
|
|
103
|
+
return `ralph/${dep}`;
|
|
104
|
+
}
|
|
51
105
|
function extractUserStories(content) {
|
|
52
106
|
const stories = [];
|
|
53
107
|
// First, try to find the User Stories section
|
package/package.json
CHANGED