@rigour-labs/core 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gates/retry-loop-breaker.d.ts +49 -0
- package/dist/gates/retry-loop-breaker.js +121 -0
- package/dist/gates/runner.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/templates/index.js +6 -0
- package/dist/types/fix-packet.d.ts +2 -2
- package/dist/types/index.d.ts +68 -0
- package/dist/types/index.js +6 -0
- package/package.json +1 -1
- package/src/gates/retry-loop-breaker.ts +151 -0
- package/src/gates/runner.ts +7 -1
- package/src/index.ts +1 -0
- package/src/templates/index.ts +6 -0
- package/src/types/index.ts +6 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Gate, GateContext } from './base.js';
|
|
2
|
+
import { Failure, Gates } from '../types/index.js';
|
|
3
|
+
interface FailureRecord {
|
|
4
|
+
category: string;
|
|
5
|
+
count: number;
|
|
6
|
+
lastError: string;
|
|
7
|
+
lastTimestamp: string;
|
|
8
|
+
}
|
|
9
|
+
interface RigourState {
|
|
10
|
+
failureHistory: Record<string, FailureRecord>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Retry Loop Breaker Gate
|
|
14
|
+
*
|
|
15
|
+
* Detects when an agent is stuck in a retry loop and forces them to consult
|
|
16
|
+
* official documentation before continuing. This gate is universal and works
|
|
17
|
+
* with any type of failure, not just specific tools or languages.
|
|
18
|
+
*/
|
|
19
|
+
export declare class RetryLoopBreakerGate extends Gate {
|
|
20
|
+
private options;
|
|
21
|
+
constructor(options: Gates['retry_loop_breaker']);
|
|
22
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Classify an error message into a category based on patterns.
|
|
25
|
+
*/
|
|
26
|
+
static classifyError(errorMessage: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Record a failure for retry loop detection.
|
|
29
|
+
* Call this when an operation fails.
|
|
30
|
+
*/
|
|
31
|
+
static recordFailure(cwd: string, errorMessage: string, category?: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Clear failure history for a specific category after successful resolution.
|
|
34
|
+
*/
|
|
35
|
+
static clearFailure(cwd: string, category: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Clear all failure history.
|
|
38
|
+
*/
|
|
39
|
+
static clearAllFailures(cwd: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Get the current failure state for inspection.
|
|
42
|
+
*/
|
|
43
|
+
static getState(cwd: string): Promise<RigourState>;
|
|
44
|
+
private getDefaultDocUrl;
|
|
45
|
+
private loadState;
|
|
46
|
+
private static loadStateStatic;
|
|
47
|
+
private static saveStateStatic;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Gate } from './base.js';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const ERROR_PATTERNS = [
|
|
5
|
+
[/ERR_REQUIRE_ESM|Cannot find module|MODULE_NOT_FOUND/i, 'module_resolution'],
|
|
6
|
+
[/FUNCTION_INVOCATION_FAILED|Build Failed|deploy.*fail/i, 'deployment'],
|
|
7
|
+
[/TypeError|SyntaxError|ReferenceError|compilation.*error/i, 'runtime_error'],
|
|
8
|
+
[/Connection refused|ECONNREFUSED|timeout|ETIMEDOUT/i, 'network'],
|
|
9
|
+
[/Permission denied|EACCES|EPERM/i, 'permissions'],
|
|
10
|
+
[/ENOMEM|heap out of memory|OOM/i, 'resources'],
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Retry Loop Breaker Gate
|
|
14
|
+
*
|
|
15
|
+
* Detects when an agent is stuck in a retry loop and forces them to consult
|
|
16
|
+
* official documentation before continuing. This gate is universal and works
|
|
17
|
+
* with any type of failure, not just specific tools or languages.
|
|
18
|
+
*/
|
|
19
|
+
export class RetryLoopBreakerGate extends Gate {
|
|
20
|
+
options;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
super('retry_loop_breaker', 'Retry Loop Breaker');
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
25
|
+
async run(context) {
|
|
26
|
+
const state = await this.loadState(context.cwd);
|
|
27
|
+
const failures = [];
|
|
28
|
+
for (const [category, record] of Object.entries(state.failureHistory)) {
|
|
29
|
+
if (record.count >= (this.options?.max_retries ?? 3)) {
|
|
30
|
+
const docUrl = this.options?.doc_sources?.[category] || this.getDefaultDocUrl(category);
|
|
31
|
+
failures.push(this.createFailure(`Operation '${category}' has failed ${record.count} times consecutively. Last error: ${record.lastError}`, undefined, `STOP RETRYING. You are in a loop. Consult the official documentation: ${docUrl}. Extract the canonical solution pattern and apply it.`, `Retry Loop Detected: ${category}`));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return failures;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Classify an error message into a category based on patterns.
|
|
38
|
+
*/
|
|
39
|
+
static classifyError(errorMessage) {
|
|
40
|
+
for (const [pattern, category] of ERROR_PATTERNS) {
|
|
41
|
+
if (pattern.test(errorMessage)) {
|
|
42
|
+
return category;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return 'general';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Record a failure for retry loop detection.
|
|
49
|
+
* Call this when an operation fails.
|
|
50
|
+
*/
|
|
51
|
+
static async recordFailure(cwd, errorMessage, category) {
|
|
52
|
+
const resolvedCategory = category || this.classifyError(errorMessage);
|
|
53
|
+
const state = await this.loadStateStatic(cwd);
|
|
54
|
+
const existing = state.failureHistory[resolvedCategory] || {
|
|
55
|
+
category: resolvedCategory,
|
|
56
|
+
count: 0,
|
|
57
|
+
lastError: '',
|
|
58
|
+
lastTimestamp: ''
|
|
59
|
+
};
|
|
60
|
+
existing.count += 1;
|
|
61
|
+
existing.lastError = errorMessage.slice(0, 500); // Truncate for storage
|
|
62
|
+
existing.lastTimestamp = new Date().toISOString();
|
|
63
|
+
state.failureHistory[resolvedCategory] = existing;
|
|
64
|
+
await this.saveStateStatic(cwd, state);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clear failure history for a specific category after successful resolution.
|
|
68
|
+
*/
|
|
69
|
+
static async clearFailure(cwd, category) {
|
|
70
|
+
const state = await this.loadStateStatic(cwd);
|
|
71
|
+
delete state.failureHistory[category];
|
|
72
|
+
await this.saveStateStatic(cwd, state);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Clear all failure history.
|
|
76
|
+
*/
|
|
77
|
+
static async clearAllFailures(cwd) {
|
|
78
|
+
const state = await this.loadStateStatic(cwd);
|
|
79
|
+
state.failureHistory = {};
|
|
80
|
+
await this.saveStateStatic(cwd, state);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the current failure state for inspection.
|
|
84
|
+
*/
|
|
85
|
+
static async getState(cwd) {
|
|
86
|
+
return this.loadStateStatic(cwd);
|
|
87
|
+
}
|
|
88
|
+
getDefaultDocUrl(category) {
|
|
89
|
+
const defaults = {
|
|
90
|
+
module_resolution: 'https://nodejs.org/api/esm.html',
|
|
91
|
+
deployment: 'Check the deployment platform\'s official documentation',
|
|
92
|
+
runtime_error: 'Check the language\'s official documentation',
|
|
93
|
+
network: 'Check network configuration and firewall rules',
|
|
94
|
+
permissions: 'Check file/directory permissions and ownership',
|
|
95
|
+
resources: 'Check system resource limits and memory allocation',
|
|
96
|
+
general: 'Consult the relevant official documentation',
|
|
97
|
+
};
|
|
98
|
+
return defaults[category] || defaults.general;
|
|
99
|
+
}
|
|
100
|
+
async loadState(cwd) {
|
|
101
|
+
return RetryLoopBreakerGate.loadStateStatic(cwd);
|
|
102
|
+
}
|
|
103
|
+
static async loadStateStatic(cwd) {
|
|
104
|
+
const statePath = path.join(cwd, '.rigour', 'state.json');
|
|
105
|
+
if (await fs.pathExists(statePath)) {
|
|
106
|
+
try {
|
|
107
|
+
const data = await fs.readJson(statePath);
|
|
108
|
+
return { failureHistory: data.failureHistory || {}, ...data };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { failureHistory: {} };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { failureHistory: {} };
|
|
115
|
+
}
|
|
116
|
+
static async saveStateStatic(cwd, state) {
|
|
117
|
+
const statePath = path.join(cwd, '.rigour', 'state.json');
|
|
118
|
+
await fs.ensureDir(path.dirname(statePath));
|
|
119
|
+
await fs.writeJson(statePath, state, { spaces: 2 });
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/gates/runner.js
CHANGED
|
@@ -7,7 +7,8 @@ import { DependencyGate } from './dependency.js';
|
|
|
7
7
|
import { CoverageGate } from './coverage.js';
|
|
8
8
|
import { ContextGate } from './context.js';
|
|
9
9
|
import { ContextEngine } from '../services/context-engine.js';
|
|
10
|
-
import { EnvironmentGate } from './environment.js';
|
|
10
|
+
import { EnvironmentGate } from './environment.js';
|
|
11
|
+
import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
|
|
11
12
|
import { execa } from 'execa';
|
|
12
13
|
import { Logger } from '../utils/logger.js';
|
|
13
14
|
export class GateRunner {
|
|
@@ -18,6 +19,10 @@ export class GateRunner {
|
|
|
18
19
|
this.initializeGates();
|
|
19
20
|
}
|
|
20
21
|
initializeGates() {
|
|
22
|
+
// Retry Loop Breaker Gate - HIGHEST PRIORITY (runs first)
|
|
23
|
+
if (this.config.gates.retry_loop_breaker?.enabled !== false) {
|
|
24
|
+
this.gates.push(new RetryLoopBreakerGate(this.config.gates.retry_loop_breaker));
|
|
25
|
+
}
|
|
21
26
|
if (this.config.gates.max_file_lines) {
|
|
22
27
|
this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
|
|
23
28
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export * from './services/fix-packet-service.js';
|
|
|
5
5
|
export * from './templates/index.js';
|
|
6
6
|
export * from './types/fix-packet.js';
|
|
7
7
|
export { Gate, GateContext } from './gates/base.js';
|
|
8
|
+
export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
8
9
|
export * from './utils/logger.js';
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export * from './services/fix-packet-service.js';
|
|
|
5
5
|
export * from './templates/index.js';
|
|
6
6
|
export * from './types/fix-packet.js';
|
|
7
7
|
export { Gate } from './gates/base.js';
|
|
8
|
+
export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
8
9
|
export * from './utils/logger.js';
|
package/dist/templates/index.js
CHANGED
|
@@ -186,6 +186,12 @@ export const UNIVERSAL_CONFIG = {
|
|
|
186
186
|
tools: {},
|
|
187
187
|
required_env: [],
|
|
188
188
|
},
|
|
189
|
+
retry_loop_breaker: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
max_retries: 3,
|
|
192
|
+
auto_classify: true,
|
|
193
|
+
doc_sources: {},
|
|
194
|
+
},
|
|
189
195
|
},
|
|
190
196
|
output: {
|
|
191
197
|
report_path: 'rigour-report.json',
|
|
@@ -35,8 +35,8 @@ export declare const FixPacketV2Schema: z.ZodObject<{
|
|
|
35
35
|
gate: string;
|
|
36
36
|
files?: string[] | undefined;
|
|
37
37
|
hint?: string | undefined;
|
|
38
|
-
severity?: "low" | "medium" | "high" | "critical" | undefined;
|
|
39
38
|
category?: string | undefined;
|
|
39
|
+
severity?: "low" | "medium" | "high" | "critical" | undefined;
|
|
40
40
|
instructions?: string[] | undefined;
|
|
41
41
|
metrics?: Record<string, any> | undefined;
|
|
42
42
|
}>, "many">;
|
|
@@ -94,8 +94,8 @@ export declare const FixPacketV2Schema: z.ZodObject<{
|
|
|
94
94
|
gate: string;
|
|
95
95
|
files?: string[] | undefined;
|
|
96
96
|
hint?: string | undefined;
|
|
97
|
-
severity?: "low" | "medium" | "high" | "critical" | undefined;
|
|
98
97
|
category?: string | undefined;
|
|
98
|
+
severity?: "low" | "medium" | "high" | "critical" | undefined;
|
|
99
99
|
instructions?: string[] | undefined;
|
|
100
100
|
metrics?: Record<string, any> | undefined;
|
|
101
101
|
}[];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -109,6 +109,22 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
109
109
|
tools?: Record<string, string> | undefined;
|
|
110
110
|
required_env?: string[] | undefined;
|
|
111
111
|
}>>>;
|
|
112
|
+
retry_loop_breaker: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
113
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
114
|
+
max_retries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
115
|
+
auto_classify: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
116
|
+
doc_sources: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
117
|
+
}, "strip", z.ZodTypeAny, {
|
|
118
|
+
enabled: boolean;
|
|
119
|
+
max_retries: number;
|
|
120
|
+
auto_classify: boolean;
|
|
121
|
+
doc_sources: Record<string, string>;
|
|
122
|
+
}, {
|
|
123
|
+
enabled?: boolean | undefined;
|
|
124
|
+
max_retries?: number | undefined;
|
|
125
|
+
auto_classify?: boolean | undefined;
|
|
126
|
+
doc_sources?: Record<string, string> | undefined;
|
|
127
|
+
}>>>;
|
|
112
128
|
}, "strip", z.ZodTypeAny, {
|
|
113
129
|
max_file_lines: number;
|
|
114
130
|
forbid_todos: boolean;
|
|
@@ -151,6 +167,12 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
151
167
|
tools: Record<string, string>;
|
|
152
168
|
required_env: string[];
|
|
153
169
|
};
|
|
170
|
+
retry_loop_breaker: {
|
|
171
|
+
enabled: boolean;
|
|
172
|
+
max_retries: number;
|
|
173
|
+
auto_classify: boolean;
|
|
174
|
+
doc_sources: Record<string, string>;
|
|
175
|
+
};
|
|
154
176
|
}, {
|
|
155
177
|
max_file_lines?: number | undefined;
|
|
156
178
|
forbid_todos?: boolean | undefined;
|
|
@@ -193,6 +215,12 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
193
215
|
tools?: Record<string, string> | undefined;
|
|
194
216
|
required_env?: string[] | undefined;
|
|
195
217
|
} | undefined;
|
|
218
|
+
retry_loop_breaker?: {
|
|
219
|
+
enabled?: boolean | undefined;
|
|
220
|
+
max_retries?: number | undefined;
|
|
221
|
+
auto_classify?: boolean | undefined;
|
|
222
|
+
doc_sources?: Record<string, string> | undefined;
|
|
223
|
+
} | undefined;
|
|
196
224
|
}>;
|
|
197
225
|
export declare const CommandsSchema: z.ZodObject<{
|
|
198
226
|
format: z.ZodOptional<z.ZodString>;
|
|
@@ -340,6 +368,22 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
340
368
|
tools?: Record<string, string> | undefined;
|
|
341
369
|
required_env?: string[] | undefined;
|
|
342
370
|
}>>>;
|
|
371
|
+
retry_loop_breaker: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
372
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
373
|
+
max_retries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
374
|
+
auto_classify: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
375
|
+
doc_sources: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
376
|
+
}, "strip", z.ZodTypeAny, {
|
|
377
|
+
enabled: boolean;
|
|
378
|
+
max_retries: number;
|
|
379
|
+
auto_classify: boolean;
|
|
380
|
+
doc_sources: Record<string, string>;
|
|
381
|
+
}, {
|
|
382
|
+
enabled?: boolean | undefined;
|
|
383
|
+
max_retries?: number | undefined;
|
|
384
|
+
auto_classify?: boolean | undefined;
|
|
385
|
+
doc_sources?: Record<string, string> | undefined;
|
|
386
|
+
}>>>;
|
|
343
387
|
}, "strip", z.ZodTypeAny, {
|
|
344
388
|
max_file_lines: number;
|
|
345
389
|
forbid_todos: boolean;
|
|
@@ -382,6 +426,12 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
382
426
|
tools: Record<string, string>;
|
|
383
427
|
required_env: string[];
|
|
384
428
|
};
|
|
429
|
+
retry_loop_breaker: {
|
|
430
|
+
enabled: boolean;
|
|
431
|
+
max_retries: number;
|
|
432
|
+
auto_classify: boolean;
|
|
433
|
+
doc_sources: Record<string, string>;
|
|
434
|
+
};
|
|
385
435
|
}, {
|
|
386
436
|
max_file_lines?: number | undefined;
|
|
387
437
|
forbid_todos?: boolean | undefined;
|
|
@@ -424,6 +474,12 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
424
474
|
tools?: Record<string, string> | undefined;
|
|
425
475
|
required_env?: string[] | undefined;
|
|
426
476
|
} | undefined;
|
|
477
|
+
retry_loop_breaker?: {
|
|
478
|
+
enabled?: boolean | undefined;
|
|
479
|
+
max_retries?: number | undefined;
|
|
480
|
+
auto_classify?: boolean | undefined;
|
|
481
|
+
doc_sources?: Record<string, string> | undefined;
|
|
482
|
+
} | undefined;
|
|
427
483
|
}>>>;
|
|
428
484
|
output: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
429
485
|
report_path: z.ZodDefault<z.ZodString>;
|
|
@@ -485,6 +541,12 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
485
541
|
tools: Record<string, string>;
|
|
486
542
|
required_env: string[];
|
|
487
543
|
};
|
|
544
|
+
retry_loop_breaker: {
|
|
545
|
+
enabled: boolean;
|
|
546
|
+
max_retries: number;
|
|
547
|
+
auto_classify: boolean;
|
|
548
|
+
doc_sources: Record<string, string>;
|
|
549
|
+
};
|
|
488
550
|
};
|
|
489
551
|
output: {
|
|
490
552
|
report_path: string;
|
|
@@ -545,6 +607,12 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
545
607
|
tools?: Record<string, string> | undefined;
|
|
546
608
|
required_env?: string[] | undefined;
|
|
547
609
|
} | undefined;
|
|
610
|
+
retry_loop_breaker?: {
|
|
611
|
+
enabled?: boolean | undefined;
|
|
612
|
+
max_retries?: number | undefined;
|
|
613
|
+
auto_classify?: boolean | undefined;
|
|
614
|
+
doc_sources?: Record<string, string> | undefined;
|
|
615
|
+
} | undefined;
|
|
548
616
|
} | undefined;
|
|
549
617
|
output?: {
|
|
550
618
|
report_path?: string | undefined;
|
package/dist/types/index.js
CHANGED
|
@@ -46,6 +46,12 @@ export const GatesSchema = z.object({
|
|
|
46
46
|
tools: z.record(z.string()).optional().default({}), // Explicit overrides
|
|
47
47
|
required_env: z.array(z.string()).optional().default([]),
|
|
48
48
|
}).optional().default({}),
|
|
49
|
+
retry_loop_breaker: z.object({
|
|
50
|
+
enabled: z.boolean().optional().default(true),
|
|
51
|
+
max_retries: z.number().optional().default(3), // Fail after 3 consecutive failures in same category
|
|
52
|
+
auto_classify: z.boolean().optional().default(true), // Auto-detect failure category from error message
|
|
53
|
+
doc_sources: z.record(z.string()).optional().default({}), // Custom doc URLs per category
|
|
54
|
+
}).optional().default({}),
|
|
49
55
|
});
|
|
50
56
|
export const CommandsSchema = z.object({
|
|
51
57
|
format: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Gate, GateContext } from './base.js';
|
|
2
|
+
import { Failure, Gates } from '../types/index.js';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
interface FailureRecord {
|
|
7
|
+
category: string;
|
|
8
|
+
count: number;
|
|
9
|
+
lastError: string;
|
|
10
|
+
lastTimestamp: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RigourState {
|
|
14
|
+
failureHistory: Record<string, FailureRecord>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ERROR_PATTERNS: [RegExp, string][] = [
|
|
18
|
+
[/ERR_REQUIRE_ESM|Cannot find module|MODULE_NOT_FOUND/i, 'module_resolution'],
|
|
19
|
+
[/FUNCTION_INVOCATION_FAILED|Build Failed|deploy.*fail/i, 'deployment'],
|
|
20
|
+
[/TypeError|SyntaxError|ReferenceError|compilation.*error/i, 'runtime_error'],
|
|
21
|
+
[/Connection refused|ECONNREFUSED|timeout|ETIMEDOUT/i, 'network'],
|
|
22
|
+
[/Permission denied|EACCES|EPERM/i, 'permissions'],
|
|
23
|
+
[/ENOMEM|heap out of memory|OOM/i, 'resources'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Retry Loop Breaker Gate
|
|
28
|
+
*
|
|
29
|
+
* Detects when an agent is stuck in a retry loop and forces them to consult
|
|
30
|
+
* official documentation before continuing. This gate is universal and works
|
|
31
|
+
* with any type of failure, not just specific tools or languages.
|
|
32
|
+
*/
|
|
33
|
+
export class RetryLoopBreakerGate extends Gate {
|
|
34
|
+
constructor(private options: Gates['retry_loop_breaker']) {
|
|
35
|
+
super('retry_loop_breaker', 'Retry Loop Breaker');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async run(context: GateContext): Promise<Failure[]> {
|
|
39
|
+
const state = await this.loadState(context.cwd);
|
|
40
|
+
const failures: Failure[] = [];
|
|
41
|
+
|
|
42
|
+
for (const [category, record] of Object.entries(state.failureHistory)) {
|
|
43
|
+
if (record.count >= (this.options?.max_retries ?? 3)) {
|
|
44
|
+
const docUrl = this.options?.doc_sources?.[category] || this.getDefaultDocUrl(category);
|
|
45
|
+
failures.push(this.createFailure(
|
|
46
|
+
`Operation '${category}' has failed ${record.count} times consecutively. Last error: ${record.lastError}`,
|
|
47
|
+
undefined,
|
|
48
|
+
`STOP RETRYING. You are in a loop. Consult the official documentation: ${docUrl}. Extract the canonical solution pattern and apply it.`,
|
|
49
|
+
`Retry Loop Detected: ${category}`
|
|
50
|
+
));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return failures;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Classify an error message into a category based on patterns.
|
|
59
|
+
*/
|
|
60
|
+
static classifyError(errorMessage: string): string {
|
|
61
|
+
for (const [pattern, category] of ERROR_PATTERNS) {
|
|
62
|
+
if (pattern.test(errorMessage)) {
|
|
63
|
+
return category;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return 'general';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record a failure for retry loop detection.
|
|
71
|
+
* Call this when an operation fails.
|
|
72
|
+
*/
|
|
73
|
+
static async recordFailure(cwd: string, errorMessage: string, category?: string): Promise<void> {
|
|
74
|
+
const resolvedCategory = category || this.classifyError(errorMessage);
|
|
75
|
+
const state = await this.loadStateStatic(cwd);
|
|
76
|
+
|
|
77
|
+
const existing = state.failureHistory[resolvedCategory] || {
|
|
78
|
+
category: resolvedCategory,
|
|
79
|
+
count: 0,
|
|
80
|
+
lastError: '',
|
|
81
|
+
lastTimestamp: ''
|
|
82
|
+
};
|
|
83
|
+
existing.count += 1;
|
|
84
|
+
existing.lastError = errorMessage.slice(0, 500); // Truncate for storage
|
|
85
|
+
existing.lastTimestamp = new Date().toISOString();
|
|
86
|
+
state.failureHistory[resolvedCategory] = existing;
|
|
87
|
+
|
|
88
|
+
await this.saveStateStatic(cwd, state);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clear failure history for a specific category after successful resolution.
|
|
93
|
+
*/
|
|
94
|
+
static async clearFailure(cwd: string, category: string): Promise<void> {
|
|
95
|
+
const state = await this.loadStateStatic(cwd);
|
|
96
|
+
delete state.failureHistory[category];
|
|
97
|
+
await this.saveStateStatic(cwd, state);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clear all failure history.
|
|
102
|
+
*/
|
|
103
|
+
static async clearAllFailures(cwd: string): Promise<void> {
|
|
104
|
+
const state = await this.loadStateStatic(cwd);
|
|
105
|
+
state.failureHistory = {};
|
|
106
|
+
await this.saveStateStatic(cwd, state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the current failure state for inspection.
|
|
111
|
+
*/
|
|
112
|
+
static async getState(cwd: string): Promise<RigourState> {
|
|
113
|
+
return this.loadStateStatic(cwd);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private getDefaultDocUrl(category: string): string {
|
|
117
|
+
const defaults: Record<string, string> = {
|
|
118
|
+
module_resolution: 'https://nodejs.org/api/esm.html',
|
|
119
|
+
deployment: 'Check the deployment platform\'s official documentation',
|
|
120
|
+
runtime_error: 'Check the language\'s official documentation',
|
|
121
|
+
network: 'Check network configuration and firewall rules',
|
|
122
|
+
permissions: 'Check file/directory permissions and ownership',
|
|
123
|
+
resources: 'Check system resource limits and memory allocation',
|
|
124
|
+
general: 'Consult the relevant official documentation',
|
|
125
|
+
};
|
|
126
|
+
return defaults[category] || defaults.general;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async loadState(cwd: string): Promise<RigourState> {
|
|
130
|
+
return RetryLoopBreakerGate.loadStateStatic(cwd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static async loadStateStatic(cwd: string): Promise<RigourState> {
|
|
134
|
+
const statePath = path.join(cwd, '.rigour', 'state.json');
|
|
135
|
+
if (await fs.pathExists(statePath)) {
|
|
136
|
+
try {
|
|
137
|
+
const data = await fs.readJson(statePath);
|
|
138
|
+
return { failureHistory: data.failureHistory || {}, ...data };
|
|
139
|
+
} catch {
|
|
140
|
+
return { failureHistory: {} };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { failureHistory: {} };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static async saveStateStatic(cwd: string, state: RigourState): Promise<void> {
|
|
147
|
+
const statePath = path.join(cwd, '.rigour', 'state.json');
|
|
148
|
+
await fs.ensureDir(path.dirname(statePath));
|
|
149
|
+
await fs.writeJson(statePath, state, { spaces: 2 });
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/gates/runner.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { DependencyGate } from './dependency.js';
|
|
|
9
9
|
import { CoverageGate } from './coverage.js';
|
|
10
10
|
import { ContextGate } from './context.js';
|
|
11
11
|
import { ContextEngine } from '../services/context-engine.js';
|
|
12
|
-
import { EnvironmentGate } from './environment.js';
|
|
12
|
+
import { EnvironmentGate } from './environment.js';
|
|
13
|
+
import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
|
|
13
14
|
import { execa } from 'execa';
|
|
14
15
|
import { Logger } from '../utils/logger.js';
|
|
15
16
|
|
|
@@ -21,6 +22,11 @@ export class GateRunner {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
private initializeGates() {
|
|
25
|
+
// Retry Loop Breaker Gate - HIGHEST PRIORITY (runs first)
|
|
26
|
+
if (this.config.gates.retry_loop_breaker?.enabled !== false) {
|
|
27
|
+
this.gates.push(new RetryLoopBreakerGate(this.config.gates.retry_loop_breaker));
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
if (this.config.gates.max_file_lines) {
|
|
25
31
|
this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
|
|
26
32
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,4 +5,5 @@ export * from './services/fix-packet-service.js';
|
|
|
5
5
|
export * from './templates/index.js';
|
|
6
6
|
export * from './types/fix-packet.js';
|
|
7
7
|
export { Gate, GateContext } from './gates/base.js';
|
|
8
|
+
export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
|
|
8
9
|
export * from './utils/logger.js';
|
package/src/templates/index.ts
CHANGED
|
@@ -202,6 +202,12 @@ export const UNIVERSAL_CONFIG: Config = {
|
|
|
202
202
|
tools: {},
|
|
203
203
|
required_env: [],
|
|
204
204
|
},
|
|
205
|
+
retry_loop_breaker: {
|
|
206
|
+
enabled: true,
|
|
207
|
+
max_retries: 3,
|
|
208
|
+
auto_classify: true,
|
|
209
|
+
doc_sources: {},
|
|
210
|
+
},
|
|
205
211
|
},
|
|
206
212
|
output: {
|
|
207
213
|
report_path: 'rigour-report.json',
|
package/src/types/index.ts
CHANGED
|
@@ -47,6 +47,12 @@ export const GatesSchema = z.object({
|
|
|
47
47
|
tools: z.record(z.string()).optional().default({}), // Explicit overrides
|
|
48
48
|
required_env: z.array(z.string()).optional().default([]),
|
|
49
49
|
}).optional().default({}),
|
|
50
|
+
retry_loop_breaker: z.object({
|
|
51
|
+
enabled: z.boolean().optional().default(true),
|
|
52
|
+
max_retries: z.number().optional().default(3), // Fail after 3 consecutive failures in same category
|
|
53
|
+
auto_classify: z.boolean().optional().default(true), // Auto-detect failure category from error message
|
|
54
|
+
doc_sources: z.record(z.string()).optional().default({}), // Custom doc URLs per category
|
|
55
|
+
}).optional().default({}),
|
|
50
56
|
});
|
|
51
57
|
|
|
52
58
|
export const CommandsSchema = z.object({
|