@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.
@@ -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
+ }
@@ -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'; // [NEW]
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';
@@ -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
  }[];
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
+ }
@@ -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'; // [NEW]
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';
@@ -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',
@@ -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({