@rigour-labs/core 2.22.0 → 3.0.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.
Files changed (117) hide show
  1. package/README.md +58 -0
  2. package/dist/context.test.js +2 -3
  3. package/dist/environment.test.js +2 -1
  4. package/dist/gates/agent-team.d.ts +2 -1
  5. package/dist/gates/agent-team.js +1 -0
  6. package/dist/gates/base.d.ts +3 -1
  7. package/dist/gates/base.js +3 -0
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/context-window-artifacts.d.ts +2 -1
  11. package/dist/gates/context-window-artifacts.js +6 -3
  12. package/dist/gates/context.d.ts +2 -1
  13. package/dist/gates/context.js +1 -0
  14. package/dist/gates/coverage.js +3 -1
  15. package/dist/gates/dependency.js +5 -5
  16. package/dist/gates/duplication-drift.d.ts +2 -1
  17. package/dist/gates/duplication-drift.js +4 -1
  18. package/dist/gates/environment.js +4 -4
  19. package/dist/gates/hallucinated-imports.d.ts +21 -2
  20. package/dist/gates/hallucinated-imports.js +116 -2
  21. package/dist/gates/inconsistent-error-handling.d.ts +2 -1
  22. package/dist/gates/inconsistent-error-handling.js +21 -7
  23. package/dist/gates/promise-safety.d.ts +68 -0
  24. package/dist/gates/promise-safety.js +509 -0
  25. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  26. package/dist/gates/retry-loop-breaker.js +2 -1
  27. package/dist/gates/runner.js +34 -1
  28. package/dist/gates/safety.d.ts +2 -1
  29. package/dist/gates/safety.js +2 -1
  30. package/dist/gates/security-patterns-owasp.test.d.ts +1 -0
  31. package/dist/gates/security-patterns-owasp.test.js +171 -0
  32. package/dist/gates/security-patterns.d.ts +6 -1
  33. package/dist/gates/security-patterns.js +101 -0
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/hooks/checker.d.ts +23 -0
  36. package/dist/hooks/checker.js +222 -0
  37. package/dist/hooks/checker.test.d.ts +1 -0
  38. package/dist/hooks/checker.test.js +132 -0
  39. package/dist/hooks/index.d.ts +9 -0
  40. package/dist/hooks/index.js +8 -0
  41. package/dist/hooks/standalone-checker.d.ts +15 -0
  42. package/dist/hooks/standalone-checker.js +106 -0
  43. package/dist/hooks/templates.d.ts +22 -0
  44. package/dist/hooks/templates.js +232 -0
  45. package/dist/hooks/types.d.ts +34 -0
  46. package/dist/hooks/types.js +21 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +2 -0
  49. package/dist/services/fix-packet-service.d.ts +0 -1
  50. package/dist/services/fix-packet-service.js +9 -14
  51. package/dist/services/score-history.d.ts +54 -0
  52. package/dist/services/score-history.js +122 -0
  53. package/dist/templates/index.js +176 -0
  54. package/dist/types/fix-packet.d.ts +5 -5
  55. package/dist/types/fix-packet.js +1 -1
  56. package/dist/types/index.d.ts +207 -0
  57. package/dist/types/index.js +32 -0
  58. package/package.json +21 -1
  59. package/src/context.test.ts +0 -256
  60. package/src/discovery.test.ts +0 -88
  61. package/src/discovery.ts +0 -112
  62. package/src/environment.test.ts +0 -115
  63. package/src/gates/agent-team.test.ts +0 -134
  64. package/src/gates/agent-team.ts +0 -210
  65. package/src/gates/ast-handlers/base.ts +0 -13
  66. package/src/gates/ast-handlers/python.ts +0 -145
  67. package/src/gates/ast-handlers/python_parser.py +0 -181
  68. package/src/gates/ast-handlers/typescript.ts +0 -264
  69. package/src/gates/ast-handlers/universal.ts +0 -184
  70. package/src/gates/ast.ts +0 -54
  71. package/src/gates/base.ts +0 -28
  72. package/src/gates/checkpoint.test.ts +0 -135
  73. package/src/gates/checkpoint.ts +0 -311
  74. package/src/gates/content.ts +0 -51
  75. package/src/gates/context-window-artifacts.ts +0 -277
  76. package/src/gates/context.ts +0 -270
  77. package/src/gates/coverage.ts +0 -74
  78. package/src/gates/dependency.ts +0 -108
  79. package/src/gates/duplication-drift.ts +0 -231
  80. package/src/gates/environment.ts +0 -94
  81. package/src/gates/file.ts +0 -46
  82. package/src/gates/hallucinated-imports.ts +0 -361
  83. package/src/gates/inconsistent-error-handling.ts +0 -254
  84. package/src/gates/retry-loop-breaker.ts +0 -151
  85. package/src/gates/runner.ts +0 -188
  86. package/src/gates/safety.ts +0 -56
  87. package/src/gates/security-patterns.test.ts +0 -162
  88. package/src/gates/security-patterns.ts +0 -306
  89. package/src/gates/structure.ts +0 -36
  90. package/src/index.ts +0 -13
  91. package/src/pattern-index/embeddings.ts +0 -84
  92. package/src/pattern-index/index.ts +0 -59
  93. package/src/pattern-index/indexer.test.ts +0 -276
  94. package/src/pattern-index/indexer.ts +0 -1023
  95. package/src/pattern-index/matcher.test.ts +0 -293
  96. package/src/pattern-index/matcher.ts +0 -493
  97. package/src/pattern-index/overrides.ts +0 -235
  98. package/src/pattern-index/security.ts +0 -151
  99. package/src/pattern-index/staleness.test.ts +0 -313
  100. package/src/pattern-index/staleness.ts +0 -568
  101. package/src/pattern-index/types.ts +0 -339
  102. package/src/safety.test.ts +0 -53
  103. package/src/services/adaptive-thresholds.test.ts +0 -189
  104. package/src/services/adaptive-thresholds.ts +0 -275
  105. package/src/services/context-engine.ts +0 -104
  106. package/src/services/fix-packet-service.ts +0 -42
  107. package/src/services/state-service.ts +0 -138
  108. package/src/smoke.test.ts +0 -18
  109. package/src/templates/index.ts +0 -338
  110. package/src/types/fix-packet.ts +0 -32
  111. package/src/types/index.ts +0 -200
  112. package/src/utils/logger.ts +0 -43
  113. package/src/utils/scanner.test.ts +0 -37
  114. package/src/utils/scanner.ts +0 -43
  115. package/tsconfig.json +0 -10
  116. package/vitest.config.ts +0 -7
  117. package/vitest.setup.ts +0 -30
@@ -1,254 +0,0 @@
1
- /**
2
- * Inconsistent Error Handling Gate
3
- *
4
- * Detects when the same error type is handled differently across the codebase.
5
- * This is an AI-specific failure mode — each agent session writes error handling
6
- * from scratch, leading to 4 different patterns for the same error type.
7
- *
8
- * Detection strategy:
9
- * 1. Extract all try-catch blocks and error handling patterns
10
- * 2. Cluster by caught error type (e.g. "Error", "TypeError", custom errors)
11
- * 3. Compare handling strategies within each cluster
12
- * 4. Flag types with >2 distinct handling patterns across files
13
- *
14
- * Examples of inconsistency:
15
- * - File A: catch(e) { console.log(e) }
16
- * - File B: catch(e) { throw new AppError(e) }
17
- * - File C: catch(e) { return null }
18
- * - File D: catch(e) { [empty] }
19
- *
20
- * @since v2.16.0
21
- */
22
-
23
- import { Gate, GateContext } from './base.js';
24
- import { Failure } from '../types/index.js';
25
- import { FileScanner } from '../utils/scanner.js';
26
- import { Logger } from '../utils/logger.js';
27
- import fs from 'fs-extra';
28
- import path from 'path';
29
-
30
- interface ErrorHandler {
31
- file: string;
32
- line: number;
33
- errorType: string; // What's caught: 'Error', 'TypeError', 'any', etc.
34
- strategy: string; // Classified handling strategy
35
- rawPattern: string; // First line of catch body for display
36
- }
37
-
38
- export interface InconsistentErrorHandlingConfig {
39
- enabled?: boolean;
40
- max_strategies_per_type?: number; // Flag if more than N strategies, default 2
41
- min_occurrences?: number; // Minimum catch blocks to analyze, default 3
42
- ignore_empty_catches?: boolean; // Whether to count empty catches as a strategy
43
- }
44
-
45
- export class InconsistentErrorHandlingGate extends Gate {
46
- private config: Required<InconsistentErrorHandlingConfig>;
47
-
48
- constructor(config: InconsistentErrorHandlingConfig = {}) {
49
- super('inconsistent-error-handling', 'Inconsistent Error Handling Detection');
50
- this.config = {
51
- enabled: config.enabled ?? true,
52
- max_strategies_per_type: config.max_strategies_per_type ?? 2,
53
- min_occurrences: config.min_occurrences ?? 3,
54
- ignore_empty_catches: config.ignore_empty_catches ?? false,
55
- };
56
- }
57
-
58
- async run(context: GateContext): Promise<Failure[]> {
59
- if (!this.config.enabled) return [];
60
-
61
- const failures: Failure[] = [];
62
- const handlers: ErrorHandler[] = [];
63
-
64
- const files = await FileScanner.findFiles({
65
- cwd: context.cwd,
66
- patterns: ['**/*.{ts,js,tsx,jsx}'],
67
- ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
68
- });
69
-
70
- Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
71
-
72
- for (const file of files) {
73
- try {
74
- const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
75
- this.extractErrorHandlers(content, file, handlers);
76
- } catch (e) { }
77
- }
78
-
79
- // Group by error type
80
- const byType = new Map<string, ErrorHandler[]>();
81
- for (const handler of handlers) {
82
- const existing = byType.get(handler.errorType) || [];
83
- existing.push(handler);
84
- byType.set(handler.errorType, existing);
85
- }
86
-
87
- // Analyze each error type for inconsistent handling
88
- for (const [errorType, typeHandlers] of byType) {
89
- if (typeHandlers.length < this.config.min_occurrences) continue;
90
-
91
- // Count unique strategies
92
- const strategies = new Map<string, ErrorHandler[]>();
93
- for (const handler of typeHandlers) {
94
- const existing = strategies.get(handler.strategy) || [];
95
- existing.push(handler);
96
- strategies.set(handler.strategy, existing);
97
- }
98
-
99
- // Filter out empty catches if configured
100
- const activeStrategies = this.config.ignore_empty_catches
101
- ? new Map([...strategies].filter(([k]) => k !== 'swallow'))
102
- : strategies;
103
-
104
- if (activeStrategies.size > this.config.max_strategies_per_type) {
105
- // Only flag if handlers span multiple files
106
- const uniqueFiles = new Set(typeHandlers.map(h => h.file));
107
- if (uniqueFiles.size < 2) continue;
108
-
109
- const strategyBreakdown = [...activeStrategies.entries()]
110
- .map(([strategy, handlers]) => {
111
- const files = [...new Set(handlers.map(h => h.file))].slice(0, 3);
112
- return ` • ${strategy} (${handlers.length}x): ${files.join(', ')}`;
113
- })
114
- .join('\n');
115
-
116
- failures.push(this.createFailure(
117
- `Inconsistent error handling for '${errorType}': ${activeStrategies.size} different strategies found across ${uniqueFiles.size} files:\n${strategyBreakdown}`,
118
- [...uniqueFiles].slice(0, 5),
119
- `Standardize error handling for '${errorType}'. Create a shared error handler or establish a project convention. AI agents often write error handling from scratch each session, leading to divergent patterns.`,
120
- 'Inconsistent Error Handling',
121
- typeHandlers[0].line,
122
- undefined,
123
- 'high'
124
- ));
125
- }
126
- }
127
-
128
- return failures;
129
- }
130
-
131
- private extractErrorHandlers(content: string, file: string, handlers: ErrorHandler[]): void {
132
- const lines = content.split('\n');
133
-
134
- for (let i = 0; i < lines.length; i++) {
135
- // Match catch clauses: catch (e), catch (error: Error), catch (e: TypeError)
136
- const catchMatch = lines[i].match(/\bcatch\s*\(\s*(\w+)(?:\s*:\s*(\w+))?\s*\)/);
137
- if (!catchMatch) continue;
138
-
139
- const varName = catchMatch[1];
140
- const explicitType = catchMatch[2] || 'any';
141
-
142
- // Extract catch body (up to next closing brace at same level)
143
- const body = this.extractCatchBody(lines, i);
144
- if (!body) continue;
145
-
146
- const strategy = this.classifyStrategy(body, varName);
147
- const rawPattern = body.split('\n')[0]?.trim() || '';
148
-
149
- handlers.push({
150
- file,
151
- line: i + 1,
152
- errorType: explicitType,
153
- strategy,
154
- rawPattern,
155
- });
156
- }
157
-
158
- // Also detect .catch() promise patterns
159
- for (let i = 0; i < lines.length; i++) {
160
- const catchMatch = lines[i].match(/\.catch\s*\(\s*(?:async\s+)?(?:\(\s*)?(\w+)/);
161
- if (!catchMatch) continue;
162
-
163
- const varName = catchMatch[1];
164
-
165
- // Extract the callback body
166
- const body = this.extractCatchCallbackBody(lines, i);
167
- if (!body) continue;
168
-
169
- const strategy = this.classifyStrategy(body, varName);
170
-
171
- handlers.push({
172
- file,
173
- line: i + 1,
174
- errorType: 'Promise',
175
- strategy,
176
- rawPattern: body.split('\n')[0]?.trim() || '',
177
- });
178
- }
179
- }
180
-
181
- private classifyStrategy(body: string, varName: string): string {
182
- const trimmed = body.trim();
183
-
184
- // Empty catch (swallow)
185
- if (!trimmed || trimmed === '{}' || trimmed === '') {
186
- return 'swallow';
187
- }
188
-
189
- // Re-throw
190
- if (/\bthrow\b/.test(trimmed)) {
191
- if (/\bthrow\s+new\b/.test(trimmed)) return 'wrap-and-throw';
192
- if (new RegExp(`\\bthrow\\s+${varName}\\b`).test(trimmed)) return 'rethrow';
193
- return 'throw-new';
194
- }
195
-
196
- // Return patterns
197
- if (/\breturn\s+null\b/.test(trimmed)) return 'return-null';
198
- if (/\breturn\s+undefined\b/.test(trimmed) || /\breturn\s*;/.test(trimmed)) return 'return-undefined';
199
- if (/\breturn\s+\[\s*\]/.test(trimmed)) return 'return-empty-array';
200
- if (/\breturn\s+\{\s*\}/.test(trimmed)) return 'return-empty-object';
201
- if (/\breturn\s+false\b/.test(trimmed)) return 'return-false';
202
- if (/\breturn\s+/.test(trimmed)) return 'return-value';
203
-
204
- // Logging patterns
205
- if (/console\.(error|warn)\b/.test(trimmed)) return 'log-error';
206
- if (/console\.log\b/.test(trimmed)) return 'log-info';
207
- if (/\blogger\b/i.test(trimmed) || /\blog\b/i.test(trimmed)) return 'log-custom';
208
-
209
- // Process.exit
210
- if (/process\.exit\b/.test(trimmed)) return 'exit';
211
-
212
- // Response patterns (Express/HTTP)
213
- if (/\bres\s*\.\s*status\b/.test(trimmed) || /\bres\s*\.\s*json\b/.test(trimmed)) return 'http-response';
214
-
215
- // Notification patterns
216
- if (/\bnotif|toast|alert|modal\b/i.test(trimmed)) return 'user-notification';
217
-
218
- return 'other';
219
- }
220
-
221
- private extractCatchBody(lines: string[], catchLine: number): string | null {
222
- let braceDepth = 0;
223
- let started = false;
224
- const body: string[] = [];
225
-
226
- for (let i = catchLine; i < lines.length; i++) {
227
- for (const ch of lines[i]) {
228
- if (ch === '{') { braceDepth++; started = true; }
229
- if (ch === '}') braceDepth--;
230
- }
231
- if (started && i > catchLine) body.push(lines[i]);
232
- if (started && braceDepth === 0) break;
233
- }
234
-
235
- return body.length > 0 ? body.join('\n') : null;
236
- }
237
-
238
- private extractCatchCallbackBody(lines: string[], startLine: number): string | null {
239
- let depth = 0;
240
- let started = false;
241
- const body: string[] = [];
242
-
243
- for (let i = startLine; i < Math.min(startLine + 20, lines.length); i++) {
244
- for (const ch of lines[i]) {
245
- if (ch === '{' || ch === '(') { depth++; started = true; }
246
- if (ch === '}' || ch === ')') depth--;
247
- }
248
- if (started && i > startLine) body.push(lines[i]);
249
- if (started && depth <= 0) break;
250
- }
251
-
252
- return body.length > 0 ? body.join('\n') : null;
253
- }
254
- }
@@ -1,151 +0,0 @@
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
- }
@@ -1,188 +0,0 @@
1
- import { Gate } from './base.js';
2
- import { Failure, Config, Report, Status, Severity, SEVERITY_WEIGHTS } from '../types/index.js';
3
- import { FileGate } from './file.js';
4
- import { ContentGate } from './content.js';
5
- import { StructureGate } from './structure.js';
6
- import { ASTGate } from './ast.js';
7
- import { FileGuardGate } from './safety.js';
8
- import { DependencyGate } from './dependency.js';
9
- import { CoverageGate } from './coverage.js';
10
- import { ContextGate } from './context.js';
11
- import { ContextEngine } from '../services/context-engine.js';
12
- import { EnvironmentGate } from './environment.js';
13
- import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
14
- import { AgentTeamGate } from './agent-team.js';
15
- import { CheckpointGate } from './checkpoint.js';
16
- import { SecurityPatternsGate } from './security-patterns.js';
17
- import { DuplicationDriftGate } from './duplication-drift.js';
18
- import { HallucinatedImportsGate } from './hallucinated-imports.js';
19
- import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
20
- import { ContextWindowArtifactsGate } from './context-window-artifacts.js';
21
- import { execa } from 'execa';
22
- import { Logger } from '../utils/logger.js';
23
-
24
- export class GateRunner {
25
- private gates: Gate[] = [];
26
-
27
- constructor(private config: Config) {
28
- this.initializeGates();
29
- }
30
-
31
- private initializeGates() {
32
- // Retry Loop Breaker Gate - HIGHEST PRIORITY (runs first)
33
- if (this.config.gates.retry_loop_breaker?.enabled !== false) {
34
- this.gates.push(new RetryLoopBreakerGate(this.config.gates.retry_loop_breaker));
35
- }
36
-
37
- if (this.config.gates.max_file_lines) {
38
- this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
39
- }
40
- this.gates.push(
41
- new ContentGate({
42
- forbidTodos: !!this.config.gates.forbid_todos,
43
- forbidFixme: !!this.config.gates.forbid_fixme,
44
- })
45
- );
46
- if (this.config.gates.required_files) {
47
- this.gates.push(new StructureGate({ requiredFiles: this.config.gates.required_files }));
48
- }
49
- this.gates.push(new ASTGate(this.config.gates));
50
- this.gates.push(new DependencyGate(this.config));
51
- this.gates.push(new FileGuardGate(this.config.gates));
52
- this.gates.push(new CoverageGate(this.config.gates));
53
-
54
- if (this.config.gates.context?.enabled) {
55
- this.gates.push(new ContextGate(this.config.gates));
56
- }
57
-
58
- // Agent Team Governance Gate (for Opus 4.6 / GPT-5.3 multi-agent workflows)
59
- if (this.config.gates.agent_team?.enabled) {
60
- this.gates.push(new AgentTeamGate(this.config.gates.agent_team));
61
- }
62
-
63
- // Checkpoint Supervision Gate (for long-running GPT-5.3 coworking mode)
64
- if (this.config.gates.checkpoint?.enabled) {
65
- this.gates.push(new CheckpointGate(this.config.gates.checkpoint));
66
- }
67
-
68
- // Security Patterns Gate (code-level vulnerability detection) — enabled by default since v2.15
69
- if (this.config.gates.security?.enabled !== false) {
70
- this.gates.push(new SecurityPatternsGate(this.config.gates.security));
71
- }
72
-
73
- // v2.16+ AI-Native Drift Detection Gates (enabled by default)
74
- if (this.config.gates.duplication_drift?.enabled !== false) {
75
- this.gates.push(new DuplicationDriftGate(this.config.gates.duplication_drift));
76
- }
77
-
78
- if (this.config.gates.hallucinated_imports?.enabled !== false) {
79
- this.gates.push(new HallucinatedImportsGate(this.config.gates.hallucinated_imports));
80
- }
81
-
82
- if (this.config.gates.inconsistent_error_handling?.enabled !== false) {
83
- this.gates.push(new InconsistentErrorHandlingGate(this.config.gates.inconsistent_error_handling));
84
- }
85
-
86
- if (this.config.gates.context_window_artifacts?.enabled !== false) {
87
- this.gates.push(new ContextWindowArtifactsGate(this.config.gates.context_window_artifacts));
88
- }
89
-
90
- // Environment Alignment Gate (Should be prioritized)
91
- if (this.config.gates.environment?.enabled) {
92
- this.gates.unshift(new EnvironmentGate(this.config.gates));
93
- }
94
- }
95
-
96
- /**
97
- * Allows adding custom gates dynamically (SOLID - Open/Closed Principle)
98
- */
99
- addGate(gate: Gate) {
100
- this.gates.push(gate);
101
- }
102
-
103
- async run(cwd: string, patterns?: string[]): Promise<Report> {
104
- const start = Date.now();
105
- const failures: Failure[] = [];
106
- const summary: Record<string, Status> = {};
107
-
108
- const ignore = this.config.ignore;
109
-
110
- // 0. Run Context Discovery
111
- let record;
112
- if (this.config.gates.context?.enabled) {
113
- const engine = new ContextEngine(this.config);
114
- record = await engine.discover(cwd);
115
- }
116
-
117
- // 1. Run internal gates
118
- for (const gate of this.gates) {
119
- try {
120
- const gateFailures = await gate.run({ cwd, record, ignore, patterns });
121
- if (gateFailures.length > 0) {
122
- failures.push(...gateFailures);
123
- summary[gate.id] = 'FAIL';
124
- } else {
125
- summary[gate.id] = 'PASS';
126
- }
127
- } catch (error: any) {
128
- Logger.error(`Gate ${gate.id} failed with error: ${error.message}`);
129
- summary[gate.id] = 'ERROR';
130
- failures.push({
131
- id: gate.id,
132
- title: `Gate Error: ${gate.title}`,
133
- details: error.message,
134
- hint: 'There was an internal error running this gate. Check the logs.',
135
- });
136
- }
137
- }
138
-
139
- // 2. Run command gates (lint, test, etc.)
140
- const commands = this.config.commands;
141
- if (commands) {
142
- for (const [key, cmd] of Object.entries(commands)) {
143
- if (!cmd) {
144
- summary[key] = 'SKIP';
145
- continue;
146
- }
147
-
148
- try {
149
- Logger.info(`Running command gate: ${key} (${cmd})`);
150
- await execa(cmd, { shell: true, cwd });
151
- summary[key] = 'PASS';
152
- } catch (error: any) {
153
- summary[key] = 'FAIL';
154
- failures.push({
155
- id: key,
156
- title: `${key.toUpperCase()} Check Failed`,
157
- details: error.stderr || error.stdout || error.message,
158
- hint: `Fix the issues reported by \`${cmd}\`. Use rigorous standards (SOLID, DRY) in your resolution.`,
159
- });
160
- }
161
- }
162
- }
163
-
164
- const status: Status = failures.length > 0 ? 'FAIL' : 'PASS';
165
-
166
- // Severity-weighted scoring: each failure deducts based on its severity
167
- // critical=20, high=10, medium=5, low=2, info=0
168
- const severityBreakdown: Record<string, number> = {};
169
- let totalDeduction = 0;
170
- for (const f of failures) {
171
- const sev = (f.severity || 'medium') as Severity;
172
- severityBreakdown[sev] = (severityBreakdown[sev] || 0) + 1;
173
- totalDeduction += SEVERITY_WEIGHTS[sev] ?? 5;
174
- }
175
- const score = Math.max(0, 100 - totalDeduction);
176
-
177
- return {
178
- status,
179
- summary,
180
- failures,
181
- stats: {
182
- duration_ms: Date.now() - start,
183
- score,
184
- severity_breakdown: severityBreakdown,
185
- },
186
- };
187
- }
188
- }
@@ -1,56 +0,0 @@
1
- import { Gate, GateContext } from './base.js';
2
- import { Failure, Gates } from '../types/index.js';
3
- import { execa } from 'execa';
4
-
5
- export class FileGuardGate extends Gate {
6
- constructor(private config: Gates) {
7
- super('file-guard', 'File Guard — Protected Paths');
8
- }
9
-
10
- async run(context: GateContext): Promise<Failure[]> {
11
- const failures: Failure[] = [];
12
- const safety = this.config.safety || {};
13
- const protectedPaths = safety.protected_paths || [];
14
-
15
- if (protectedPaths.length === 0) return [];
16
-
17
- try {
18
- // Check for modified files in protected paths using git
19
- // File Guard - if an agent touched protected files, we fail.
20
- const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: context.cwd });
21
- const modifiedFiles = stdout.split('\n')
22
- .filter(line => {
23
- const status = line.slice(0, 2);
24
- // M: Modified, A: Added (staged), D: Deleted, R: Renamed
25
- // We ignore ?? (Untracked) to allow rigour init and new doc creation
26
- return /M|A|D|R/.test(status);
27
- })
28
- .map(line => line.slice(3).trim());
29
-
30
- for (const file of modifiedFiles) {
31
- if (this.isProtected(file, protectedPaths)) {
32
- const message = `Protected file '${file}' was modified.`;
33
- failures.push(this.createFailure(
34
- message,
35
- [file],
36
- `Agents are forbidden from modifying files in ${protectedPaths.join(', ')}.`,
37
- message
38
- ));
39
- }
40
- }
41
- } catch (error) {
42
- // If not a git repo, skip safety for now
43
- }
44
-
45
- return failures;
46
- }
47
-
48
- private isProtected(file: string, patterns: string[]): boolean {
49
- return patterns.some(p => {
50
- const cleanP = p.replace('/**', '').replace('/*', '');
51
- if (file === cleanP) return true;
52
- if (cleanP.endsWith('/')) return file.startsWith(cleanP);
53
- return file.startsWith(cleanP + '/');
54
- });
55
- }
56
- }