ralph-mcp 1.0.14 → 1.1.1
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/dist/index.js +49 -0
- package/dist/store/state.d.ts +37 -0
- package/dist/store/state.js +155 -0
- package/dist/tools/batch-start.js +6 -0
- package/dist/tools/get.d.ts +8 -0
- package/dist/tools/get.js +23 -0
- package/dist/tools/reset-stagnation.d.ts +24 -0
- package/dist/tools/reset-stagnation.js +40 -0
- package/dist/tools/retry.d.ts +26 -0
- package/dist/tools/retry.js +65 -0
- package/dist/tools/start.js +6 -0
- package/dist/tools/status.d.ts +5 -0
- package/dist/tools/status.js +6 -0
- package/dist/tools/update.d.ts +11 -0
- package/dist/tools/update.js +25 -1
- package/dist/utils/agent.d.ts +6 -1
- package/dist/utils/agent.js +39 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { update, updateInputSchema } from "./tools/update.js";
|
|
|
10
10
|
import { stop, stopInputSchema } from "./tools/stop.js";
|
|
11
11
|
import { merge, mergeInputSchema, mergeQueueAction, mergeQueueInputSchema } from "./tools/merge.js";
|
|
12
12
|
import { setAgentId, setAgentIdInputSchema } from "./tools/set-agent-id.js";
|
|
13
|
+
import { resetStagnationTool, resetStagnationInputSchema } from "./tools/reset-stagnation.js";
|
|
14
|
+
import { retry, retryInputSchema } from "./tools/retry.js";
|
|
13
15
|
const server = new Server({
|
|
14
16
|
name: "ralph",
|
|
15
17
|
version: "1.0.0",
|
|
@@ -120,6 +122,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
120
122
|
type: "string",
|
|
121
123
|
description: "Implementation notes",
|
|
122
124
|
},
|
|
125
|
+
filesChanged: {
|
|
126
|
+
type: "number",
|
|
127
|
+
description: "Number of files changed (for stagnation detection)",
|
|
128
|
+
},
|
|
129
|
+
error: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Error message if stuck (for stagnation detection)",
|
|
132
|
+
},
|
|
123
133
|
},
|
|
124
134
|
required: ["branch", "storyId", "passes"],
|
|
125
135
|
},
|
|
@@ -258,6 +268,39 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
258
268
|
required: ["prdPaths"],
|
|
259
269
|
},
|
|
260
270
|
},
|
|
271
|
+
{
|
|
272
|
+
name: "ralph_reset_stagnation",
|
|
273
|
+
description: "Reset stagnation counters for an execution. Use after manual intervention to allow the agent to continue.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
branch: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Branch name (e.g., ralph/task1-agent)",
|
|
280
|
+
},
|
|
281
|
+
resumeExecution: {
|
|
282
|
+
type: "boolean",
|
|
283
|
+
description: "Also set status back to 'running' if currently 'failed' (default: true)",
|
|
284
|
+
default: true,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
required: ["branch"],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "ralph_retry",
|
|
292
|
+
description: "Retry a failed PRD execution. Resets stagnation counters and generates a new agent prompt to continue from where it left off.",
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
branch: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "Branch name (e.g., ralph/task1-agent)",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
required: ["branch"],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
261
304
|
],
|
|
262
305
|
};
|
|
263
306
|
});
|
|
@@ -294,6 +337,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
294
337
|
case "ralph_batch_start":
|
|
295
338
|
result = await batchStart(batchStartInputSchema.parse(args));
|
|
296
339
|
break;
|
|
340
|
+
case "ralph_reset_stagnation":
|
|
341
|
+
result = await resetStagnationTool(resetStagnationInputSchema.parse(args));
|
|
342
|
+
break;
|
|
343
|
+
case "ralph_retry":
|
|
344
|
+
result = await retry(retryInputSchema.parse(args));
|
|
345
|
+
break;
|
|
297
346
|
default:
|
|
298
347
|
throw new Error(`Unknown tool: ${name}`);
|
|
299
348
|
}
|
package/dist/store/state.d.ts
CHANGED
|
@@ -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>;
|
package/dist/store/state.js
CHANGED
|
@@ -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
|
});
|
package/dist/tools/get.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const resetStagnationInputSchema: z.ZodObject<{
|
|
3
|
+
branch: z.ZodString;
|
|
4
|
+
resumeExecution: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
branch: string;
|
|
7
|
+
resumeExecution: boolean;
|
|
8
|
+
}, {
|
|
9
|
+
branch: string;
|
|
10
|
+
resumeExecution?: boolean | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type ResetStagnationInput = z.infer<typeof resetStagnationInputSchema>;
|
|
13
|
+
export interface ResetStagnationResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
branch: string;
|
|
16
|
+
message: string;
|
|
17
|
+
previousStatus: string;
|
|
18
|
+
newStatus: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reset stagnation counters for an execution.
|
|
22
|
+
* Use this after manual intervention to allow the agent to continue.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resetStagnationTool(input: ResetStagnationInput): Promise<ResetStagnationResult>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { findExecutionByBranch, resetStagnation, updateExecution, } from "../store/state.js";
|
|
3
|
+
export const resetStagnationInputSchema = z.object({
|
|
4
|
+
branch: z.string().describe("Branch name (e.g., ralph/task1-agent)"),
|
|
5
|
+
resumeExecution: z
|
|
6
|
+
.boolean()
|
|
7
|
+
.default(true)
|
|
8
|
+
.describe("Also set status back to 'running' if currently 'failed'"),
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Reset stagnation counters for an execution.
|
|
12
|
+
* Use this after manual intervention to allow the agent to continue.
|
|
13
|
+
*/
|
|
14
|
+
export async function resetStagnationTool(input) {
|
|
15
|
+
const exec = await findExecutionByBranch(input.branch);
|
|
16
|
+
if (!exec) {
|
|
17
|
+
throw new Error(`No execution found for branch: ${input.branch}`);
|
|
18
|
+
}
|
|
19
|
+
const previousStatus = exec.status;
|
|
20
|
+
// Reset stagnation counters
|
|
21
|
+
await resetStagnation(exec.id);
|
|
22
|
+
// Optionally resume execution
|
|
23
|
+
let newStatus = previousStatus;
|
|
24
|
+
if (input.resumeExecution && previousStatus === "failed") {
|
|
25
|
+
await updateExecution(exec.id, {
|
|
26
|
+
status: "running",
|
|
27
|
+
updatedAt: new Date(),
|
|
28
|
+
});
|
|
29
|
+
newStatus = "running";
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
branch: input.branch,
|
|
34
|
+
message: previousStatus === "failed" && input.resumeExecution
|
|
35
|
+
? "Stagnation counters reset and execution resumed"
|
|
36
|
+
: "Stagnation counters reset",
|
|
37
|
+
previousStatus,
|
|
38
|
+
newStatus,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const retryInputSchema: z.ZodObject<{
|
|
3
|
+
branch: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
branch: string;
|
|
6
|
+
}, {
|
|
7
|
+
branch: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type RetryInput = z.infer<typeof retryInputSchema>;
|
|
10
|
+
export interface RetryResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
branch: string;
|
|
13
|
+
message: string;
|
|
14
|
+
previousStatus: string;
|
|
15
|
+
agentPrompt: string | null;
|
|
16
|
+
progress: {
|
|
17
|
+
completed: number;
|
|
18
|
+
total: number;
|
|
19
|
+
percentage: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retry a failed PRD execution.
|
|
24
|
+
* Resets stagnation counters and generates a new agent prompt.
|
|
25
|
+
*/
|
|
26
|
+
export declare function retry(input: RetryInput): Promise<RetryResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { findExecutionByBranch, listUserStoriesByExecutionId, resetStagnation, updateExecution, } from "../store/state.js";
|
|
3
|
+
import { generateAgentPrompt } from "../utils/agent.js";
|
|
4
|
+
export const retryInputSchema = z.object({
|
|
5
|
+
branch: z.string().describe("Branch name (e.g., ralph/task1-agent)"),
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* Retry a failed PRD execution.
|
|
9
|
+
* Resets stagnation counters and generates a new agent prompt.
|
|
10
|
+
*/
|
|
11
|
+
export async function retry(input) {
|
|
12
|
+
const exec = await findExecutionByBranch(input.branch);
|
|
13
|
+
if (!exec) {
|
|
14
|
+
throw new Error(`No execution found for branch: ${input.branch}`);
|
|
15
|
+
}
|
|
16
|
+
const previousStatus = exec.status;
|
|
17
|
+
// Only allow retry for failed or stopped executions
|
|
18
|
+
if (previousStatus !== "failed" && previousStatus !== "stopped") {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
branch: input.branch,
|
|
22
|
+
message: `Cannot retry execution with status '${previousStatus}'. Only 'failed' or 'stopped' executions can be retried.`,
|
|
23
|
+
previousStatus,
|
|
24
|
+
agentPrompt: null,
|
|
25
|
+
progress: { completed: 0, total: 0, percentage: 0 },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Reset stagnation counters
|
|
29
|
+
await resetStagnation(exec.id);
|
|
30
|
+
// Set status back to running
|
|
31
|
+
await updateExecution(exec.id, {
|
|
32
|
+
status: "running",
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
});
|
|
35
|
+
// Get stories and generate new agent prompt
|
|
36
|
+
const stories = await listUserStoriesByExecutionId(exec.id);
|
|
37
|
+
const completed = stories.filter((s) => s.passes).length;
|
|
38
|
+
const total = stories.length;
|
|
39
|
+
const agentPrompt = generateAgentPrompt(exec.branch, exec.description, exec.worktreePath || exec.projectRoot, stories.map((s) => ({
|
|
40
|
+
storyId: s.storyId,
|
|
41
|
+
title: s.title,
|
|
42
|
+
description: s.description,
|
|
43
|
+
acceptanceCriteria: s.acceptanceCriteria,
|
|
44
|
+
priority: s.priority,
|
|
45
|
+
passes: s.passes,
|
|
46
|
+
})), undefined, // contextPath - would need to re-read from PRD
|
|
47
|
+
{
|
|
48
|
+
loopCount: 0, // Reset loop context for fresh start
|
|
49
|
+
consecutiveNoProgress: 0,
|
|
50
|
+
consecutiveErrors: 0,
|
|
51
|
+
lastError: null,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
branch: input.branch,
|
|
56
|
+
message: `Execution retried. ${total - completed} stories remaining.`,
|
|
57
|
+
previousStatus,
|
|
58
|
+
agentPrompt,
|
|
59
|
+
progress: {
|
|
60
|
+
completed,
|
|
61
|
+
total,
|
|
62
|
+
percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
package/dist/tools/start.js
CHANGED
|
@@ -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
|
});
|
package/dist/tools/status.d.ts
CHANGED
|
@@ -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[];
|
|
@@ -30,6 +34,7 @@ export interface StatusResult {
|
|
|
30
34
|
completed: number;
|
|
31
35
|
failed: number;
|
|
32
36
|
pending: number;
|
|
37
|
+
atRisk: number;
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
40
|
export declare function status(input: StatusInput): Promise<StatusResult>;
|
package/dist/tools/status.js
CHANGED
|
@@ -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)
|
|
@@ -45,6 +50,7 @@ export async function status(input) {
|
|
|
45
50
|
completed: executionStatuses.filter((e) => e.status === "completed").length,
|
|
46
51
|
failed: executionStatuses.filter((e) => e.status === "failed").length,
|
|
47
52
|
pending: executionStatuses.filter((e) => e.status === "pending").length,
|
|
53
|
+
atRisk: executionStatuses.filter((e) => e.consecutiveNoProgress >= 2 || e.consecutiveErrors >= 3).length,
|
|
48
54
|
};
|
|
49
55
|
return {
|
|
50
56
|
executions: executionStatuses,
|
package/dist/tools/update.d.ts
CHANGED
|
@@ -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>;
|
package/dist/tools/update.js
CHANGED
|
@@ -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,
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
package/dist/utils/agent.js
CHANGED
|
@@ -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` : ""}
|
|
@@ -83,8 +100,22 @@ Before implementing, verify the story is small enough to complete in ONE context
|
|
|
83
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\`
|
|
87
|
-
\`
|
|
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
|
|
@@ -107,6 +138,11 @@ Provide structured learnings in the \`notes\` field:
|
|
|
107
138
|
- Follow existing code patterns
|
|
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