ralph-mcp 1.0.7 → 1.0.9

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 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
  }
@@ -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
+ }>;
@@ -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,214 @@
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 { areDependenciesSatisfied, findExecutionByBranch, insertExecution, insertUserStories, } from "../store/state.js";
9
+ export const batchStartInputSchema = z.object({
10
+ prdPaths: z.array(z.string()).describe("Array of paths to PRD markdown files"),
11
+ projectRoot: z.string().optional().describe("Project root directory (defaults to cwd)"),
12
+ worktree: z.boolean().default(true).describe("Create worktrees for isolation"),
13
+ autoMerge: z.boolean().default(true).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
+ contextInjectionPath: z
20
+ .string()
21
+ .optional()
22
+ .describe("Path to a file (e.g., CLAUDE.md) to inject into the agent prompt"),
23
+ preheat: z.boolean().default(true).describe("Run pnpm install serially before starting agents (avoids store lock)"),
24
+ });
25
+ /**
26
+ * Topological sort to determine execution order based on dependencies.
27
+ */
28
+ function topologicalSort(prds) {
29
+ const branchToPrd = new Map();
30
+ for (const prd of prds) {
31
+ branchToPrd.set(prd.branch, prd);
32
+ }
33
+ const visited = new Set();
34
+ const result = [];
35
+ function visit(branch) {
36
+ if (visited.has(branch))
37
+ return;
38
+ visited.add(branch);
39
+ const prd = branchToPrd.get(branch);
40
+ if (!prd)
41
+ return;
42
+ // Visit dependencies first
43
+ for (const dep of prd.dependencies) {
44
+ if (branchToPrd.has(dep)) {
45
+ visit(dep);
46
+ }
47
+ }
48
+ result.push(prd);
49
+ }
50
+ for (const prd of prds) {
51
+ visit(prd.branch);
52
+ }
53
+ return result;
54
+ }
55
+ /**
56
+ * Run pnpm install in a worktree directory.
57
+ */
58
+ function preheatWorktree(worktreePath) {
59
+ try {
60
+ execSync("pnpm install --frozen-lockfile", {
61
+ cwd: worktreePath,
62
+ stdio: "pipe",
63
+ timeout: 300000, // 5 minutes
64
+ });
65
+ }
66
+ catch (error) {
67
+ // Try without frozen lockfile
68
+ execSync("pnpm install", {
69
+ cwd: worktreePath,
70
+ stdio: "pipe",
71
+ timeout: 300000,
72
+ });
73
+ }
74
+ }
75
+ export async function batchStart(input) {
76
+ const projectRoot = input.projectRoot || process.cwd();
77
+ const projectName = basename(projectRoot);
78
+ const contextPath = input.contextInjectionPath
79
+ ? resolve(projectRoot, input.contextInjectionPath)
80
+ : undefined;
81
+ const skipped = [];
82
+ const prdInfos = [];
83
+ // Phase 1: Parse all PRDs and create execution records
84
+ for (const prdPath of input.prdPaths) {
85
+ const fullPath = resolve(projectRoot, prdPath);
86
+ try {
87
+ const prd = parsePrdFile(fullPath);
88
+ // Check if execution already exists
89
+ const existing = await findExecutionByBranch(prd.branchName);
90
+ if (existing) {
91
+ skipped.push({
92
+ prdPath,
93
+ reason: `Execution already exists for branch ${prd.branchName}`,
94
+ });
95
+ continue;
96
+ }
97
+ // Create worktree if requested
98
+ let worktreePath = null;
99
+ if (input.worktree) {
100
+ worktreePath = await createWorktree(projectRoot, prd.branchName);
101
+ }
102
+ // Create execution record
103
+ const executionId = randomUUID();
104
+ const now = new Date();
105
+ await insertExecution({
106
+ id: executionId,
107
+ project: projectName,
108
+ branch: prd.branchName,
109
+ description: prd.description,
110
+ prdPath: fullPath,
111
+ projectRoot: projectRoot,
112
+ worktreePath: worktreePath,
113
+ status: "pending",
114
+ agentTaskId: null,
115
+ onConflict: input.onConflict,
116
+ autoMerge: input.autoMerge,
117
+ notifyOnComplete: input.notifyOnComplete,
118
+ dependencies: prd.dependencies,
119
+ createdAt: now,
120
+ updatedAt: now,
121
+ });
122
+ // Create user story records
123
+ const storyRecords = prd.userStories.map((story) => ({
124
+ id: `${executionId}:${story.id}`,
125
+ executionId: executionId,
126
+ storyId: story.id,
127
+ title: story.title,
128
+ description: story.description,
129
+ acceptanceCriteria: story.acceptanceCriteria,
130
+ priority: story.priority,
131
+ passes: false,
132
+ notes: "",
133
+ }));
134
+ if (storyRecords.length > 0) {
135
+ await insertUserStories(storyRecords);
136
+ }
137
+ prdInfos.push({
138
+ prdPath,
139
+ branch: prd.branchName,
140
+ dependencies: prd.dependencies,
141
+ worktreePath,
142
+ executionId,
143
+ stories: storyRecords.map((s) => ({
144
+ storyId: s.storyId,
145
+ title: s.title,
146
+ description: s.description,
147
+ acceptanceCriteria: s.acceptanceCriteria,
148
+ priority: s.priority,
149
+ passes: s.passes,
150
+ })),
151
+ });
152
+ }
153
+ catch (error) {
154
+ skipped.push({
155
+ prdPath,
156
+ reason: error instanceof Error ? error.message : String(error),
157
+ });
158
+ }
159
+ }
160
+ // Phase 2: Topological sort for preheat order
161
+ const sortedPrds = topologicalSort(prdInfos);
162
+ // Phase 3: Preheat worktrees serially (avoids pnpm store lock contention)
163
+ let preheatCompleted = false;
164
+ if (input.preheat && input.worktree) {
165
+ for (const prd of sortedPrds) {
166
+ if (prd.worktreePath) {
167
+ try {
168
+ preheatWorktree(prd.worktreePath);
169
+ }
170
+ catch (error) {
171
+ console.error(`Preheat failed for ${prd.branch}:`, error);
172
+ // Continue with other worktrees
173
+ }
174
+ }
175
+ }
176
+ preheatCompleted = true;
177
+ }
178
+ // Phase 4: Determine which PRDs can start immediately
179
+ const readyToStart = [];
180
+ const waitingForDependencies = [];
181
+ for (const prd of prdInfos) {
182
+ const tempExec = { dependencies: prd.dependencies };
183
+ const depStatus = await areDependenciesSatisfied(tempExec);
184
+ if (depStatus.satisfied) {
185
+ const agentPrompt = generateAgentPrompt(prd.branch, "", // description not stored in prdInfo
186
+ prd.worktreePath || projectRoot, prd.stories, contextPath);
187
+ readyToStart.push({
188
+ branch: prd.branch,
189
+ agentPrompt,
190
+ dependencies: prd.dependencies,
191
+ });
192
+ }
193
+ else {
194
+ waitingForDependencies.push({
195
+ branch: prd.branch,
196
+ pendingDependencies: depStatus.pending,
197
+ });
198
+ }
199
+ }
200
+ // Build dependency graph for visualization
201
+ const dependencyGraph = {};
202
+ for (const prd of prdInfos) {
203
+ dependencyGraph[prd.branch] = prd.dependencies;
204
+ }
205
+ return {
206
+ total: input.prdPaths.length,
207
+ created: prdInfos.length,
208
+ skipped,
209
+ readyToStart,
210
+ waitingForDependencies,
211
+ dependencyGraph,
212
+ preheatCompleted,
213
+ };
214
+ }
@@ -7,6 +7,7 @@ export declare const startInputSchema: z.ZodObject<{
7
7
  autoMerge: z.ZodDefault<z.ZodBoolean>;
8
8
  notifyOnComplete: z.ZodDefault<z.ZodBoolean>;
9
9
  onConflict: z.ZodDefault<z.ZodEnum<["auto_theirs", "auto_ours", "notify", "agent"]>>;
10
+ contextInjectionPath: z.ZodOptional<z.ZodString>;
10
11
  }, "strip", z.ZodTypeAny, {
11
12
  prdPath: string;
12
13
  onConflict: "auto_theirs" | "auto_ours" | "notify" | "agent";
@@ -15,6 +16,7 @@ export declare const startInputSchema: z.ZodObject<{
15
16
  worktree: boolean;
16
17
  autoStart: boolean;
17
18
  projectRoot?: string | undefined;
19
+ contextInjectionPath?: string | undefined;
18
20
  }, {
19
21
  prdPath: string;
20
22
  projectRoot?: string | undefined;
@@ -23,6 +25,7 @@ export declare const startInputSchema: z.ZodObject<{
23
25
  notifyOnComplete?: boolean | undefined;
24
26
  worktree?: boolean | undefined;
25
27
  autoStart?: boolean | undefined;
28
+ contextInjectionPath?: string | undefined;
26
29
  }>;
27
30
  export type StartInput = z.infer<typeof startInputSchema>;
28
31
  export interface StartResult {
@@ -30,6 +33,9 @@ export interface StartResult {
30
33
  branch: string;
31
34
  worktreePath: string | null;
32
35
  agentPrompt: string | null;
36
+ dependencies: string[];
37
+ dependenciesSatisfied: boolean;
38
+ pendingDependencies: string[];
33
39
  stories: Array<{
34
40
  storyId: string;
35
41
  title: string;
@@ -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)"),
@@ -16,6 +16,10 @@ export const startInputSchema = z.object({
16
16
  .enum(["auto_theirs", "auto_ours", "notify", "agent"])
17
17
  .default("agent")
18
18
  .describe("Conflict resolution strategy for merge"),
19
+ contextInjectionPath: z
20
+ .string()
21
+ .optional()
22
+ .describe("Path to a file (e.g., CLAUDE.md) to inject into the agent prompt"),
19
23
  });
20
24
  export async function start(input) {
21
25
  const projectRoot = input.projectRoot || process.cwd();
@@ -49,6 +53,7 @@ export async function start(input) {
49
53
  onConflict: input.onConflict,
50
54
  autoMerge: input.autoMerge,
51
55
  notifyOnComplete: input.notifyOnComplete,
56
+ dependencies: prd.dependencies,
52
57
  createdAt: now,
53
58
  updatedAt: now,
54
59
  });
@@ -67,9 +72,17 @@ export async function start(input) {
67
72
  if (storyRecords.length > 0) {
68
73
  await insertUserStories(storyRecords);
69
74
  }
70
- // Generate agent prompt if auto-start
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
71
81
  let agentPrompt = null;
72
- if (input.autoStart) {
82
+ if (input.autoStart && depStatus.satisfied) {
83
+ const contextPath = input.contextInjectionPath
84
+ ? resolve(projectRoot, input.contextInjectionPath)
85
+ : undefined;
73
86
  agentPrompt = generateAgentPrompt(prd.branchName, prd.description, worktreePath || projectRoot, storyRecords.map((s) => ({
74
87
  storyId: s.storyId,
75
88
  title: s.title,
@@ -77,13 +90,16 @@ export async function start(input) {
77
90
  acceptanceCriteria: s.acceptanceCriteria,
78
91
  priority: s.priority,
79
92
  passes: s.passes,
80
- })));
93
+ })), contextPath);
81
94
  }
82
95
  return {
83
96
  executionId,
84
97
  branch: prd.branchName,
85
98
  worktreePath,
86
99
  agentPrompt,
100
+ dependencies: prd.dependencies,
101
+ dependenciesSatisfied: depStatus.satisfied,
102
+ pendingDependencies: depStatus.pending,
87
103
  stories: storyRecords.map((s) => ({
88
104
  storyId: s.storyId,
89
105
  title: s.title,
@@ -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>;
@@ -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
  }
@@ -8,7 +8,7 @@ export declare function generateAgentPrompt(branch: string, description: string,
8
8
  acceptanceCriteria: string[];
9
9
  priority: number;
10
10
  passes: boolean;
11
- }>): string;
11
+ }>, contextInjectionPath?: string): string;
12
12
  /**
13
13
  * Generate merge agent prompt for conflict resolution
14
14
  */
@@ -6,7 +6,7 @@ const execAsync = promisify(exec);
6
6
  /**
7
7
  * Generate agent prompt for PRD execution
8
8
  */
9
- export function generateAgentPrompt(branch, description, worktreePath, stories) {
9
+ export function generateAgentPrompt(branch, description, worktreePath, stories, contextInjectionPath) {
10
10
  const pendingStories = stories
11
11
  .filter((s) => !s.passes)
12
12
  .sort((a, b) => a.priority - b.priority);
@@ -33,6 +33,16 @@ ${s.acceptanceCriteria.map((ac) => `- ${ac}`).join("\n")}
33
33
  // Ignore read errors
34
34
  }
35
35
  }
36
+ // Read injected context if provided
37
+ let injectedContext = "";
38
+ if (contextInjectionPath && existsSync(contextInjectionPath)) {
39
+ try {
40
+ injectedContext = readFileSync(contextInjectionPath, "utf-8");
41
+ }
42
+ catch (e) {
43
+ // Ignore read errors
44
+ }
45
+ }
36
46
  return `You are an autonomous coding agent working on the "${branch}" branch.
37
47
 
38
48
  ## Working Directory
@@ -40,6 +50,8 @@ ${worktreePath}
40
50
 
41
51
  ## PRD: ${description}
42
52
 
53
+ ${injectedContext ? `## Project Context\n${injectedContext}\n` : ""}
54
+
43
55
  ${progressLog ? `## Progress & Learnings\n${progressLog}\n` : ""}
44
56
 
45
57
  ## Pending User Stories
@@ -10,6 +10,7 @@ export interface ParsedPrd {
10
10
  description: string;
11
11
  branchName: string;
12
12
  userStories: ParsedUserStory[];
13
+ dependencies: string[];
13
14
  }
14
15
  /**
15
16
  * Parse a PRD markdown file into structured data.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "MCP server for autonomous PRD execution with Claude Code. Git worktree isolation, progress tracking, auto-merge.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "user-story",
21
21
  "merge-queue"
22
22
  ],
23
- "author": "G0d2i11a",
23
+ "author": "Godzi11a <2492186627@qq.com>",
24
24
  "license": "MIT",
25
25
  "repository": {
26
26
  "type": "git",