ralph-mcp 1.0.13 → 1.1.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.
@@ -15,6 +15,11 @@ export interface ExecutionRecord {
15
15
  autoMerge: boolean;
16
16
  notifyOnComplete: boolean;
17
17
  dependencies: string[];
18
+ loopCount: number;
19
+ consecutiveNoProgress: number;
20
+ consecutiveErrors: number;
21
+ lastError: string | null;
22
+ lastFilesChanged: number;
18
23
  createdAt: Date;
19
24
  updatedAt: Date;
20
25
  }
@@ -66,3 +71,35 @@ export declare function areDependenciesSatisfied(execution: ExecutionRecord): Pr
66
71
  pending: string[];
67
72
  completed: string[];
68
73
  }>;
74
+ /**
75
+ * Stagnation detection thresholds (matching original ralph-claude-code)
76
+ */
77
+ export declare const STAGNATION_THRESHOLDS: {
78
+ NO_PROGRESS_THRESHOLD: number;
79
+ SAME_ERROR_THRESHOLD: number;
80
+ MAX_LOOPS_PER_STORY: number;
81
+ };
82
+ export type StagnationType = "no_progress" | "repeated_error" | "max_loops" | null;
83
+ export interface StagnationCheckResult {
84
+ isStagnant: boolean;
85
+ type: StagnationType;
86
+ message: string;
87
+ metrics: {
88
+ loopCount: number;
89
+ consecutiveNoProgress: number;
90
+ consecutiveErrors: number;
91
+ lastError: string | null;
92
+ };
93
+ }
94
+ /**
95
+ * Check if an execution is stagnant (stuck in a loop).
96
+ */
97
+ export declare function checkStagnation(executionId: string): Promise<StagnationCheckResult>;
98
+ /**
99
+ * Record a loop result for stagnation tracking.
100
+ */
101
+ export declare function recordLoopResult(executionId: string, filesChanged: number, error: string | null): Promise<StagnationCheckResult>;
102
+ /**
103
+ * Reset stagnation counters (e.g., after manual intervention).
104
+ */
105
+ export declare function resetStagnation(executionId: string): Promise<void>;
@@ -46,6 +46,12 @@ function deserializeState(file) {
46
46
  executions: file.executions.map((e) => ({
47
47
  ...e,
48
48
  dependencies: Array.isArray(e.dependencies) ? e.dependencies : [],
49
+ // Stagnation detection defaults for backward compatibility
50
+ loopCount: typeof e.loopCount === "number" ? e.loopCount : 0,
51
+ consecutiveNoProgress: typeof e.consecutiveNoProgress === "number" ? e.consecutiveNoProgress : 0,
52
+ consecutiveErrors: typeof e.consecutiveErrors === "number" ? e.consecutiveErrors : 0,
53
+ lastError: typeof e.lastError === "string" ? e.lastError : null,
54
+ lastFilesChanged: typeof e.lastFilesChanged === "number" ? e.lastFilesChanged : 0,
49
55
  createdAt: parseDate(e.createdAt, "executions.createdAt"),
50
56
  updatedAt: parseDate(e.updatedAt, "executions.updatedAt"),
51
57
  })),
@@ -236,3 +242,152 @@ export async function areDependenciesSatisfied(execution) {
236
242
  };
237
243
  });
238
244
  }
245
+ // =============================================================================
246
+ // STAGNATION DETECTION
247
+ // =============================================================================
248
+ /**
249
+ * Stagnation detection thresholds (matching original ralph-claude-code)
250
+ */
251
+ export const STAGNATION_THRESHOLDS = {
252
+ NO_PROGRESS_THRESHOLD: 3, // Open circuit after 3 loops with no file changes
253
+ SAME_ERROR_THRESHOLD: 5, // Open circuit after 5 loops with repeated errors
254
+ MAX_LOOPS_PER_STORY: 10, // Safety limit per story
255
+ };
256
+ /**
257
+ * Check if an execution is stagnant (stuck in a loop).
258
+ */
259
+ export async function checkStagnation(executionId) {
260
+ return readState((s) => {
261
+ const exec = s.executions.find((e) => e.id === executionId);
262
+ if (!exec) {
263
+ return {
264
+ isStagnant: false,
265
+ type: null,
266
+ message: "Execution not found",
267
+ metrics: { loopCount: 0, consecutiveNoProgress: 0, consecutiveErrors: 0, lastError: null },
268
+ };
269
+ }
270
+ const metrics = {
271
+ loopCount: exec.loopCount,
272
+ consecutiveNoProgress: exec.consecutiveNoProgress,
273
+ consecutiveErrors: exec.consecutiveErrors,
274
+ lastError: exec.lastError,
275
+ };
276
+ // Check no progress threshold
277
+ if (exec.consecutiveNoProgress >= STAGNATION_THRESHOLDS.NO_PROGRESS_THRESHOLD) {
278
+ return {
279
+ isStagnant: true,
280
+ type: "no_progress",
281
+ message: `No file changes for ${exec.consecutiveNoProgress} consecutive loops (threshold: ${STAGNATION_THRESHOLDS.NO_PROGRESS_THRESHOLD})`,
282
+ metrics,
283
+ };
284
+ }
285
+ // Check repeated error threshold
286
+ if (exec.consecutiveErrors >= STAGNATION_THRESHOLDS.SAME_ERROR_THRESHOLD) {
287
+ return {
288
+ isStagnant: true,
289
+ type: "repeated_error",
290
+ message: `Same error repeated ${exec.consecutiveErrors} times (threshold: ${STAGNATION_THRESHOLDS.SAME_ERROR_THRESHOLD}): ${exec.lastError?.slice(0, 100)}`,
291
+ metrics,
292
+ };
293
+ }
294
+ // Check max loops per story
295
+ const stories = s.userStories.filter((st) => st.executionId === executionId);
296
+ const pendingStories = stories.filter((st) => !st.passes);
297
+ if (pendingStories.length > 0 && exec.loopCount >= STAGNATION_THRESHOLDS.MAX_LOOPS_PER_STORY * pendingStories.length) {
298
+ return {
299
+ isStagnant: true,
300
+ type: "max_loops",
301
+ message: `Exceeded max loops (${exec.loopCount}) for ${pendingStories.length} pending stories`,
302
+ metrics,
303
+ };
304
+ }
305
+ return {
306
+ isStagnant: false,
307
+ type: null,
308
+ message: "OK",
309
+ metrics,
310
+ };
311
+ });
312
+ }
313
+ /**
314
+ * Record a loop result for stagnation tracking.
315
+ */
316
+ export async function recordLoopResult(executionId, filesChanged, error) {
317
+ return mutateState(async (s) => {
318
+ const exec = s.executions.find((e) => e.id === executionId);
319
+ if (!exec) {
320
+ throw new Error(`No execution found with id: ${executionId}`);
321
+ }
322
+ // Increment loop count
323
+ exec.loopCount++;
324
+ exec.lastFilesChanged = filesChanged;
325
+ exec.updatedAt = new Date();
326
+ // Track no progress
327
+ if (filesChanged === 0) {
328
+ exec.consecutiveNoProgress++;
329
+ }
330
+ else {
331
+ exec.consecutiveNoProgress = 0;
332
+ }
333
+ // Track repeated errors
334
+ if (error) {
335
+ if (exec.lastError === error) {
336
+ exec.consecutiveErrors++;
337
+ }
338
+ else {
339
+ exec.consecutiveErrors = 1;
340
+ exec.lastError = error;
341
+ }
342
+ }
343
+ else {
344
+ exec.consecutiveErrors = 0;
345
+ exec.lastError = null;
346
+ }
347
+ // Check stagnation after recording
348
+ const metrics = {
349
+ loopCount: exec.loopCount,
350
+ consecutiveNoProgress: exec.consecutiveNoProgress,
351
+ consecutiveErrors: exec.consecutiveErrors,
352
+ lastError: exec.lastError,
353
+ };
354
+ if (exec.consecutiveNoProgress >= STAGNATION_THRESHOLDS.NO_PROGRESS_THRESHOLD) {
355
+ exec.status = "failed";
356
+ return {
357
+ isStagnant: true,
358
+ type: "no_progress",
359
+ message: `Stagnation detected: No file changes for ${exec.consecutiveNoProgress} consecutive loops`,
360
+ metrics,
361
+ };
362
+ }
363
+ if (exec.consecutiveErrors >= STAGNATION_THRESHOLDS.SAME_ERROR_THRESHOLD) {
364
+ exec.status = "failed";
365
+ return {
366
+ isStagnant: true,
367
+ type: "repeated_error",
368
+ message: `Stagnation detected: Same error repeated ${exec.consecutiveErrors} times`,
369
+ metrics,
370
+ };
371
+ }
372
+ return {
373
+ isStagnant: false,
374
+ type: null,
375
+ message: "OK",
376
+ metrics,
377
+ };
378
+ });
379
+ }
380
+ /**
381
+ * Reset stagnation counters (e.g., after manual intervention).
382
+ */
383
+ export async function resetStagnation(executionId) {
384
+ return mutateState((s) => {
385
+ const exec = s.executions.find((e) => e.id === executionId);
386
+ if (!exec)
387
+ throw new Error(`No execution found with id: ${executionId}`);
388
+ exec.consecutiveNoProgress = 0;
389
+ exec.consecutiveErrors = 0;
390
+ exec.lastError = null;
391
+ exec.updatedAt = new Date();
392
+ });
393
+ }
@@ -127,6 +127,12 @@ export async function batchStart(input) {
127
127
  autoMerge: input.autoMerge,
128
128
  notifyOnComplete: input.notifyOnComplete,
129
129
  dependencies: prd.dependencies,
130
+ // Stagnation detection fields
131
+ loopCount: 0,
132
+ consecutiveNoProgress: 0,
133
+ consecutiveErrors: 0,
134
+ lastError: null,
135
+ lastFilesChanged: 0,
130
136
  createdAt: now,
131
137
  updatedAt: now,
132
138
  });
@@ -36,5 +36,13 @@ export interface GetResult {
36
36
  total: number;
37
37
  percentage: number;
38
38
  };
39
+ stagnation: {
40
+ loopCount: number;
41
+ consecutiveNoProgress: number;
42
+ consecutiveErrors: number;
43
+ lastError: string | null;
44
+ isAtRisk: boolean;
45
+ riskReason: string | null;
46
+ };
39
47
  }
40
48
  export declare function get(input: GetInput): Promise<GetResult>;
package/dist/tools/get.js CHANGED
@@ -15,6 +15,21 @@ export async function get(input) {
15
15
  stories.sort((a, b) => a.priority - b.priority);
16
16
  const completed = stories.filter((s) => s.passes).length;
17
17
  const total = stories.length;
18
+ // Calculate stagnation risk
19
+ const loopCount = exec.loopCount ?? 0;
20
+ const consecutiveNoProgress = exec.consecutiveNoProgress ?? 0;
21
+ const consecutiveErrors = exec.consecutiveErrors ?? 0;
22
+ const lastError = exec.lastError ?? null;
23
+ let isAtRisk = false;
24
+ let riskReason = null;
25
+ if (consecutiveNoProgress >= 2) {
26
+ isAtRisk = true;
27
+ riskReason = `No file changes for ${consecutiveNoProgress} consecutive updates (threshold: 3)`;
28
+ }
29
+ else if (consecutiveErrors >= 3) {
30
+ isAtRisk = true;
31
+ riskReason = `Same error repeated ${consecutiveErrors} times (threshold: 5)`;
32
+ }
18
33
  return {
19
34
  execution: {
20
35
  id: exec.id,
@@ -44,5 +59,13 @@ export async function get(input) {
44
59
  total,
45
60
  percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
46
61
  },
62
+ stagnation: {
63
+ loopCount,
64
+ consecutiveNoProgress,
65
+ consecutiveErrors,
66
+ lastError,
67
+ isAtRisk,
68
+ riskReason,
69
+ },
47
70
  };
48
71
  }
@@ -54,6 +54,12 @@ export async function start(input) {
54
54
  autoMerge: input.autoMerge,
55
55
  notifyOnComplete: input.notifyOnComplete,
56
56
  dependencies: prd.dependencies,
57
+ // Stagnation detection fields
58
+ loopCount: 0,
59
+ consecutiveNoProgress: 0,
60
+ consecutiveErrors: 0,
61
+ lastError: null,
62
+ lastFilesChanged: 0,
57
63
  createdAt: now,
58
64
  updatedAt: now,
59
65
  });
@@ -21,6 +21,10 @@ export interface ExecutionStatus {
21
21
  agentTaskId: string | null;
22
22
  lastActivity: string;
23
23
  createdAt: string;
24
+ loopCount: number;
25
+ consecutiveNoProgress: number;
26
+ consecutiveErrors: number;
27
+ lastError: string | null;
24
28
  }
25
29
  export interface StatusResult {
26
30
  executions: ExecutionStatus[];
@@ -34,6 +34,11 @@ export async function status(input) {
34
34
  agentTaskId: exec.agentTaskId,
35
35
  lastActivity: exec.updatedAt.toISOString(),
36
36
  createdAt: exec.createdAt.toISOString(),
37
+ // Stagnation metrics
38
+ loopCount: exec.loopCount ?? 0,
39
+ consecutiveNoProgress: exec.consecutiveNoProgress ?? 0,
40
+ consecutiveErrors: exec.consecutiveErrors ?? 0,
41
+ lastError: exec.lastError ?? null,
37
42
  });
38
43
  }
39
44
  // Sort by last activity (most recent first)
@@ -4,16 +4,22 @@ export declare const updateInputSchema: z.ZodObject<{
4
4
  storyId: z.ZodString;
5
5
  passes: z.ZodBoolean;
6
6
  notes: z.ZodOptional<z.ZodString>;
7
+ filesChanged: z.ZodOptional<z.ZodNumber>;
8
+ error: z.ZodOptional<z.ZodString>;
7
9
  }, "strip", z.ZodTypeAny, {
8
10
  branch: string;
9
11
  storyId: string;
10
12
  passes: boolean;
11
13
  notes?: string | undefined;
14
+ filesChanged?: number | undefined;
15
+ error?: string | undefined;
12
16
  }, {
13
17
  branch: string;
14
18
  storyId: string;
15
19
  passes: boolean;
16
20
  notes?: string | undefined;
21
+ filesChanged?: number | undefined;
22
+ error?: string | undefined;
17
23
  }>;
18
24
  export type UpdateInput = z.infer<typeof updateInputSchema>;
19
25
  export interface UpdateResult {
@@ -28,5 +34,10 @@ export interface UpdateResult {
28
34
  branch: string;
29
35
  agentPrompt: string | null;
30
36
  }>;
37
+ stagnation?: {
38
+ isStagnant: boolean;
39
+ type: string | null;
40
+ message: string;
41
+ };
31
42
  }
32
43
  export declare function update(input: UpdateInput): Promise<UpdateResult>;
@@ -3,7 +3,7 @@ import notifier from "node-notifier";
3
3
  import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
4
4
  import { existsSync } from "fs";
5
5
  import { join, dirname } from "path";
6
- import { areDependenciesSatisfied, findExecutionByBranch, findExecutionsDependingOn, findMergeQueueItemByExecutionId, findUserStoryById, insertMergeQueueItem, listMergeQueue, listUserStoriesByExecutionId, updateExecution, updateUserStory, } from "../store/state.js";
6
+ import { areDependenciesSatisfied, findExecutionByBranch, findExecutionsDependingOn, findMergeQueueItemByExecutionId, findUserStoryById, insertMergeQueueItem, listMergeQueue, listUserStoriesByExecutionId, recordLoopResult, updateExecution, updateUserStory, } from "../store/state.js";
7
7
  import { mergeQueueAction } from "./merge.js";
8
8
  import { generateAgentPrompt } from "../utils/agent.js";
9
9
  export const updateInputSchema = z.object({
@@ -11,6 +11,8 @@ export const updateInputSchema = z.object({
11
11
  storyId: z.string().describe("Story ID (e.g., US-001)"),
12
12
  passes: z.boolean().describe("Whether the story passes"),
13
13
  notes: z.string().optional().describe("Implementation notes"),
14
+ filesChanged: z.number().optional().describe("Number of files changed (for stagnation detection)"),
15
+ error: z.string().optional().describe("Error message if stuck (for stagnation detection)"),
14
16
  });
15
17
  function formatDate(date) {
16
18
  const pad = (n) => n.toString().padStart(2, '0');
@@ -81,6 +83,28 @@ export async function update(input) {
81
83
  if (!story) {
82
84
  throw new Error(`No story found with ID ${input.storyId} for branch ${input.branch}`);
83
85
  }
86
+ // Record loop result for stagnation detection
87
+ const filesChanged = input.filesChanged ?? 0;
88
+ const error = input.error ?? null;
89
+ const stagnationResult = await recordLoopResult(exec.id, filesChanged, error);
90
+ // If stagnant, mark execution as failed and return early
91
+ if (stagnationResult.isStagnant) {
92
+ return {
93
+ success: false,
94
+ branch: input.branch,
95
+ storyId: input.storyId,
96
+ passes: false,
97
+ allComplete: false,
98
+ progress: `Stagnation detected`,
99
+ addedToMergeQueue: false,
100
+ triggeredDependents: [],
101
+ stagnation: {
102
+ isStagnant: true,
103
+ type: stagnationResult.type,
104
+ message: stagnationResult.message,
105
+ },
106
+ };
107
+ }
84
108
  // Update story
85
109
  await updateUserStory(storyKey, {
86
110
  passes: input.passes,
@@ -8,7 +8,12 @@ export declare function generateAgentPrompt(branch: string, description: string,
8
8
  acceptanceCriteria: string[];
9
9
  priority: number;
10
10
  passes: boolean;
11
- }>, contextInjectionPath?: string): string;
11
+ }>, contextInjectionPath?: string, loopContext?: {
12
+ loopCount: number;
13
+ consecutiveNoProgress: number;
14
+ consecutiveErrors: number;
15
+ lastError: string | null;
16
+ }): string;
12
17
  /**
13
18
  * Generate merge agent prompt for conflict resolution
14
19
  */
@@ -6,13 +6,15 @@ const execAsync = promisify(exec);
6
6
  /**
7
7
  * Generate agent prompt for PRD execution
8
8
  */
9
- export function generateAgentPrompt(branch, description, worktreePath, stories, contextInjectionPath) {
9
+ export function generateAgentPrompt(branch, description, worktreePath, stories, contextInjectionPath, loopContext) {
10
10
  const pendingStories = stories
11
11
  .filter((s) => !s.passes)
12
12
  .sort((a, b) => a.priority - b.priority);
13
13
  if (pendingStories.length === 0) {
14
14
  return "All user stories are complete. No action needed.";
15
15
  }
16
+ const completedCount = stories.filter((s) => s.passes).length;
17
+ const totalCount = stories.length;
16
18
  const storiesText = pendingStories
17
19
  .map((s) => `
18
20
  ### ${s.storyId}: ${s.title}
@@ -43,6 +45,16 @@ ${s.acceptanceCriteria.map((ac) => `- ${ac}`).join("\n")}
43
45
  // Ignore read errors
44
46
  }
45
47
  }
48
+ // Build loop context warning if stagnation is approaching
49
+ let loopWarning = "";
50
+ if (loopContext) {
51
+ if (loopContext.consecutiveNoProgress >= 2) {
52
+ loopWarning = `\n⚠️ **WARNING**: No file changes detected for ${loopContext.consecutiveNoProgress} consecutive updates. If stuck, try a different approach or mark the story as blocked.\n`;
53
+ }
54
+ if (loopContext.consecutiveErrors >= 3) {
55
+ loopWarning += `\n⚠️ **WARNING**: Same error repeated ${loopContext.consecutiveErrors} times. Consider a different approach.\nLast error: ${loopContext.lastError?.slice(0, 200)}\n`;
56
+ }
57
+ }
46
58
  return `You are an autonomous coding agent working on the "${branch}" branch.
47
59
 
48
60
  ## Working Directory
@@ -50,6 +62,11 @@ ${worktreePath}
50
62
 
51
63
  ## PRD: ${description}
52
64
 
65
+ ## Progress
66
+ - Completed: ${completedCount}/${totalCount} stories
67
+ - Current story: ${pendingStories[0].storyId}
68
+ ${loopContext ? `- Loop iteration: ${loopContext.loopCount}` : ""}
69
+ ${loopWarning}
53
70
  ${injectedContext ? `## Project Context\n${injectedContext}\n` : ""}
54
71
 
55
72
  ${progressLog ? `## Progress & Learnings\n${progressLog}\n` : ""}
@@ -80,11 +97,25 @@ Before implementing, verify the story is small enough to complete in ONE context
80
97
  2. ${progressLog ? "Review the 'Progress & Learnings' section above - especially the 'Codebase Patterns' section at the top." : "Check if 'ralph-progress.md' exists and review it for context."}
81
98
  3. Implement the feature to satisfy all acceptance criteria.
82
99
  4. Run quality checks: \`pnpm check-types\` and \`pnpm --filter api build\` (adjust for repo structure).
83
- 5. **Browser Testing (UI stories)**: If the story changes UI, verify in browser using Chrome DevTools MCP or similar tools. A frontend story is NOT complete until visually verified.
100
+ 5. **Testing**: Run relevant tests. For UI changes, run component tests if available. If no browser tools are available, note "Manual UI verification needed" in your update notes.
84
101
  6. Commit changes with message: \`feat: [${pendingStories[0].storyId}] - ${pendingStories[0].title}\`
85
102
  7. **Update Directory CLAUDE.md**: If you discovered reusable patterns, add them to the CLAUDE.md in the directory you modified (create if needed). Only add genuinely reusable knowledge, not story-specific details.
86
- 8. Call \`ralph_update\` to mark the story as passed. Include detailed notes:
87
- \`ralph_update({ branch: "${branch}", storyId: "${pendingStories[0].storyId}", passes: true, notes: "..." })\`
103
+ 8. Call \`ralph_update\` with structured status. Include:
104
+ - \`passes: true\` if story is complete, \`passes: false\` if blocked/incomplete
105
+ - \`filesChanged\`: number of files modified (for stagnation detection)
106
+ - \`error\`: error message if stuck (for stagnation detection)
107
+ - \`notes\`: detailed implementation notes
108
+
109
+ Example:
110
+ \`\`\`
111
+ ralph_update({
112
+ branch: "${branch}",
113
+ storyId: "${pendingStories[0].storyId}",
114
+ passes: true,
115
+ filesChanged: 5,
116
+ notes: "**Implemented:** ... **Files changed:** ... **Learnings:** ..."
117
+ })
118
+ \`\`\`
88
119
  9. Continue to the next story until all are complete.
89
120
 
90
121
  ## Notes Format for ralph_update
@@ -102,11 +133,16 @@ Provide structured learnings in the \`notes\` field:
102
133
 
103
134
  ## Quality Requirements (Feedback Loops)
104
135
  - ALL commits must pass typecheck and build - broken code compounds across iterations
136
+ - Run relevant tests before committing
105
137
  - Keep changes focused and minimal
106
138
  - Follow existing code patterns
107
- - UI changes must be browser-verified
108
139
  - Do NOT commit broken code - if checks fail, fix before committing
109
140
 
141
+ ## Stagnation Prevention
142
+ - If you're stuck on the same error 3+ times, try a different approach
143
+ - If no files are changing, you may be in a loop - step back and reassess
144
+ - It's OK to mark a story as \`passes: false\` with notes explaining the blocker
145
+
110
146
  ## Stop Condition
111
147
  When all stories are complete, report completion.
112
148
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-mcp",
3
- "version": "1.0.13",
3
+ "version": "1.1.0",
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",