@rigour-labs/core 2.18.0 → 2.18.2
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/context.test.js +15 -1
- package/dist/discovery.js +13 -1
- package/dist/gates/agent-team.d.ts +50 -0
- package/dist/gates/agent-team.js +159 -0
- package/dist/gates/agent-team.test.d.ts +1 -0
- package/dist/gates/agent-team.test.js +113 -0
- package/dist/gates/checkpoint.d.ts +72 -0
- package/dist/gates/checkpoint.js +231 -0
- package/dist/gates/checkpoint.test.d.ts +1 -0
- package/dist/gates/checkpoint.test.js +102 -0
- package/dist/gates/context.d.ts +35 -0
- package/dist/gates/context.js +151 -2
- package/dist/gates/runner.js +15 -0
- package/dist/gates/security-patterns.d.ts +48 -0
- package/dist/gates/security-patterns.js +236 -0
- package/dist/gates/security-patterns.test.d.ts +1 -0
- package/dist/gates/security-patterns.test.js +133 -0
- package/dist/services/adaptive-thresholds.d.ts +63 -0
- package/dist/services/adaptive-thresholds.js +204 -0
- package/dist/services/adaptive-thresholds.test.d.ts +1 -0
- package/dist/services/adaptive-thresholds.test.js +129 -0
- package/dist/templates/index.js +34 -0
- package/dist/types/fix-packet.d.ts +4 -4
- package/dist/types/index.d.ts +404 -0
- package/dist/types/index.js +36 -0
- package/package.json +1 -1
- package/src/context.test.ts +15 -1
- package/src/discovery.ts +14 -2
- package/src/gates/agent-team.test.ts +134 -0
- package/src/gates/agent-team.ts +210 -0
- package/src/gates/checkpoint.test.ts +135 -0
- package/src/gates/checkpoint.ts +311 -0
- package/src/gates/context.ts +200 -2
- package/src/gates/runner.ts +18 -0
- package/src/gates/security-patterns.test.ts +162 -0
- package/src/gates/security-patterns.ts +303 -0
- package/src/services/adaptive-thresholds.test.ts +189 -0
- package/src/services/adaptive-thresholds.ts +275 -0
- package/src/templates/index.ts +34 -0
- package/src/types/index.ts +36 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Supervision Gate
|
|
3
|
+
*
|
|
4
|
+
* Monitors agent quality during extended execution for frontier models
|
|
5
|
+
* like GPT-5.3-Codex "coworking mode" that run autonomously for long periods.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Time-based checkpoint triggers
|
|
9
|
+
* - Quality score tracking
|
|
10
|
+
* - Drift detection (quality degradation over time)
|
|
11
|
+
* - Auto-save on failure
|
|
12
|
+
*
|
|
13
|
+
* @since v2.14.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Gate, GateContext } from './base.js';
|
|
17
|
+
import { Failure } from '../types/index.js';
|
|
18
|
+
import { Logger } from '../utils/logger.js';
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
|
|
22
|
+
export interface CheckpointEntry {
|
|
23
|
+
checkpointId: string;
|
|
24
|
+
timestamp: Date;
|
|
25
|
+
progressPct: number;
|
|
26
|
+
filesChanged: string[];
|
|
27
|
+
summary: string;
|
|
28
|
+
qualityScore: number;
|
|
29
|
+
warnings: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CheckpointSession {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
startedAt: Date;
|
|
35
|
+
lastCheckpoint?: Date;
|
|
36
|
+
checkpoints: CheckpointEntry[];
|
|
37
|
+
status: 'active' | 'completed' | 'aborted';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CheckpointConfig {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
interval_minutes?: number;
|
|
43
|
+
quality_threshold?: number;
|
|
44
|
+
drift_detection?: boolean;
|
|
45
|
+
auto_save_on_failure?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// In-memory checkpoint store (persisted to .rigour/checkpoint-session.json)
|
|
49
|
+
let currentCheckpointSession: CheckpointSession | null = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate unique checkpoint ID
|
|
53
|
+
*/
|
|
54
|
+
function generateCheckpointId(): string {
|
|
55
|
+
return `cp-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get or create checkpoint session
|
|
60
|
+
*/
|
|
61
|
+
export function getOrCreateCheckpointSession(cwd: string): CheckpointSession {
|
|
62
|
+
if (!currentCheckpointSession) {
|
|
63
|
+
loadCheckpointSession(cwd);
|
|
64
|
+
}
|
|
65
|
+
if (!currentCheckpointSession) {
|
|
66
|
+
currentCheckpointSession = {
|
|
67
|
+
sessionId: `chk-session-${Date.now()}`,
|
|
68
|
+
startedAt: new Date(),
|
|
69
|
+
checkpoints: [],
|
|
70
|
+
status: 'active',
|
|
71
|
+
};
|
|
72
|
+
persistCheckpointSession(cwd);
|
|
73
|
+
}
|
|
74
|
+
return currentCheckpointSession;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Record a checkpoint with quality evaluation
|
|
79
|
+
*/
|
|
80
|
+
export function recordCheckpoint(
|
|
81
|
+
cwd: string,
|
|
82
|
+
progressPct: number,
|
|
83
|
+
filesChanged: string[],
|
|
84
|
+
summary: string,
|
|
85
|
+
qualityScore: number
|
|
86
|
+
): { continue: boolean; warnings: string[]; checkpoint: CheckpointEntry } {
|
|
87
|
+
const session = getOrCreateCheckpointSession(cwd);
|
|
88
|
+
const warnings: string[] = [];
|
|
89
|
+
|
|
90
|
+
// Default threshold
|
|
91
|
+
const qualityThreshold = 80;
|
|
92
|
+
|
|
93
|
+
// Check if quality is below threshold
|
|
94
|
+
const shouldContinue = qualityScore >= qualityThreshold;
|
|
95
|
+
if (!shouldContinue) {
|
|
96
|
+
warnings.push(`Quality score ${qualityScore}% is below threshold ${qualityThreshold}%`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Detect drift (quality degradation over recent checkpoints)
|
|
100
|
+
if (session.checkpoints.length >= 2) {
|
|
101
|
+
const recentScores = session.checkpoints.slice(-3).map(cp => cp.qualityScore);
|
|
102
|
+
const avgRecent = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
|
|
103
|
+
|
|
104
|
+
if (qualityScore < avgRecent - 10) {
|
|
105
|
+
warnings.push(`Drift detected: quality dropped from avg ${avgRecent.toFixed(0)}% to ${qualityScore}%`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const checkpoint: CheckpointEntry = {
|
|
110
|
+
checkpointId: generateCheckpointId(),
|
|
111
|
+
timestamp: new Date(),
|
|
112
|
+
progressPct,
|
|
113
|
+
filesChanged,
|
|
114
|
+
summary,
|
|
115
|
+
qualityScore,
|
|
116
|
+
warnings,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
session.checkpoints.push(checkpoint);
|
|
120
|
+
session.lastCheckpoint = new Date();
|
|
121
|
+
persistCheckpointSession(cwd);
|
|
122
|
+
|
|
123
|
+
return { continue: shouldContinue, warnings, checkpoint };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get current checkpoint session
|
|
128
|
+
*/
|
|
129
|
+
export function getCheckpointSession(cwd: string): CheckpointSession | null {
|
|
130
|
+
if (!currentCheckpointSession) {
|
|
131
|
+
loadCheckpointSession(cwd);
|
|
132
|
+
}
|
|
133
|
+
return currentCheckpointSession;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Complete checkpoint session
|
|
138
|
+
*/
|
|
139
|
+
export function completeCheckpointSession(cwd: string): void {
|
|
140
|
+
if (currentCheckpointSession) {
|
|
141
|
+
currentCheckpointSession.status = 'completed';
|
|
142
|
+
persistCheckpointSession(cwd);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Abort checkpoint session (quality too low)
|
|
148
|
+
*/
|
|
149
|
+
export function abortCheckpointSession(cwd: string, reason: string): void {
|
|
150
|
+
if (currentCheckpointSession) {
|
|
151
|
+
currentCheckpointSession.status = 'aborted';
|
|
152
|
+
// Add final checkpoint with abort reason
|
|
153
|
+
currentCheckpointSession.checkpoints.push({
|
|
154
|
+
checkpointId: generateCheckpointId(),
|
|
155
|
+
timestamp: new Date(),
|
|
156
|
+
progressPct: currentCheckpointSession.checkpoints.length > 0
|
|
157
|
+
? currentCheckpointSession.checkpoints[currentCheckpointSession.checkpoints.length - 1].progressPct
|
|
158
|
+
: 0,
|
|
159
|
+
filesChanged: [],
|
|
160
|
+
summary: `Session aborted: ${reason}`,
|
|
161
|
+
qualityScore: 0,
|
|
162
|
+
warnings: [reason],
|
|
163
|
+
});
|
|
164
|
+
persistCheckpointSession(cwd);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear checkpoint session
|
|
170
|
+
*/
|
|
171
|
+
export function clearCheckpointSession(cwd: string): void {
|
|
172
|
+
currentCheckpointSession = null;
|
|
173
|
+
const sessionPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
|
|
174
|
+
if (fs.existsSync(sessionPath)) {
|
|
175
|
+
fs.unlinkSync(sessionPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function persistCheckpointSession(cwd: string): void {
|
|
180
|
+
const rigourDir = path.join(cwd, '.rigour');
|
|
181
|
+
if (!fs.existsSync(rigourDir)) {
|
|
182
|
+
fs.mkdirSync(rigourDir, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
const sessionPath = path.join(rigourDir, 'checkpoint-session.json');
|
|
185
|
+
fs.writeFileSync(sessionPath, JSON.stringify(currentCheckpointSession, null, 2));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function loadCheckpointSession(cwd: string): void {
|
|
189
|
+
const sessionPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
|
|
190
|
+
if (fs.existsSync(sessionPath)) {
|
|
191
|
+
try {
|
|
192
|
+
const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
193
|
+
currentCheckpointSession = {
|
|
194
|
+
...data,
|
|
195
|
+
startedAt: new Date(data.startedAt),
|
|
196
|
+
lastCheckpoint: data.lastCheckpoint ? new Date(data.lastCheckpoint) : undefined,
|
|
197
|
+
checkpoints: data.checkpoints.map((cp: any) => ({
|
|
198
|
+
...cp,
|
|
199
|
+
timestamp: new Date(cp.timestamp),
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
} catch (err) {
|
|
203
|
+
Logger.warn('Failed to load checkpoint session, starting fresh');
|
|
204
|
+
currentCheckpointSession = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Calculate time since last checkpoint
|
|
211
|
+
*/
|
|
212
|
+
function timeSinceLastCheckpoint(session: CheckpointSession): number {
|
|
213
|
+
const lastTime = session.lastCheckpoint || session.startedAt;
|
|
214
|
+
return (Date.now() - lastTime.getTime()) / 1000 / 60; // minutes
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Detect quality drift pattern
|
|
219
|
+
*/
|
|
220
|
+
function detectDrift(checkpoints: CheckpointEntry[]): { hasDrift: boolean; trend: 'improving' | 'stable' | 'degrading' } {
|
|
221
|
+
if (checkpoints.length < 3) {
|
|
222
|
+
return { hasDrift: false, trend: 'stable' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const recent = checkpoints.slice(-5);
|
|
226
|
+
const scores = recent.map(cp => cp.qualityScore);
|
|
227
|
+
|
|
228
|
+
// Calculate trend using simple linear regression
|
|
229
|
+
const n = scores.length;
|
|
230
|
+
const sumX = (n * (n - 1)) / 2;
|
|
231
|
+
const sumY = scores.reduce((a, b) => a + b, 0);
|
|
232
|
+
const sumXY = scores.reduce((sum, y, x) => sum + x * y, 0);
|
|
233
|
+
const sumX2 = (n * (n - 1) * (2 * n - 1)) / 6;
|
|
234
|
+
|
|
235
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
236
|
+
|
|
237
|
+
if (slope < -2) {
|
|
238
|
+
return { hasDrift: true, trend: 'degrading' };
|
|
239
|
+
} else if (slope > 2) {
|
|
240
|
+
return { hasDrift: false, trend: 'improving' };
|
|
241
|
+
}
|
|
242
|
+
return { hasDrift: false, trend: 'stable' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export class CheckpointGate extends Gate {
|
|
246
|
+
private config: CheckpointConfig;
|
|
247
|
+
|
|
248
|
+
constructor(config: CheckpointConfig = {}) {
|
|
249
|
+
super('checkpoint', 'Checkpoint Supervision');
|
|
250
|
+
this.config = {
|
|
251
|
+
enabled: config.enabled ?? false,
|
|
252
|
+
interval_minutes: config.interval_minutes ?? 15,
|
|
253
|
+
quality_threshold: config.quality_threshold ?? 80,
|
|
254
|
+
drift_detection: config.drift_detection ?? true,
|
|
255
|
+
auto_save_on_failure: config.auto_save_on_failure ?? true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async run(context: GateContext): Promise<Failure[]> {
|
|
260
|
+
if (!this.config.enabled) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const failures: Failure[] = [];
|
|
265
|
+
const session = getCheckpointSession(context.cwd);
|
|
266
|
+
|
|
267
|
+
if (!session || session.checkpoints.length === 0) {
|
|
268
|
+
// No checkpoints yet, skip
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Logger.info(`Checkpoint Gate: ${session.checkpoints.length} checkpoints in session`);
|
|
273
|
+
|
|
274
|
+
// Check 1: Time since last checkpoint
|
|
275
|
+
const minutesSinceLast = timeSinceLastCheckpoint(session);
|
|
276
|
+
if (minutesSinceLast > (this.config.interval_minutes ?? 15) * 2) {
|
|
277
|
+
failures.push(this.createFailure(
|
|
278
|
+
`No checkpoint in ${minutesSinceLast.toFixed(0)} minutes (expected every ${this.config.interval_minutes} min)`,
|
|
279
|
+
undefined,
|
|
280
|
+
'Ensure agent is reporting checkpoints via rigour_checkpoint MCP tool',
|
|
281
|
+
'Missing Checkpoint'
|
|
282
|
+
));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check 2: Quality threshold
|
|
286
|
+
const lastCheckpoint = session.checkpoints[session.checkpoints.length - 1];
|
|
287
|
+
if (lastCheckpoint.qualityScore < (this.config.quality_threshold ?? 80)) {
|
|
288
|
+
failures.push(this.createFailure(
|
|
289
|
+
`Quality score ${lastCheckpoint.qualityScore}% is below threshold ${this.config.quality_threshold}%`,
|
|
290
|
+
lastCheckpoint.filesChanged,
|
|
291
|
+
'Review recent changes and address quality issues before continuing',
|
|
292
|
+
'Quality Below Threshold'
|
|
293
|
+
));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check 3: Drift detection
|
|
297
|
+
if (this.config.drift_detection) {
|
|
298
|
+
const { hasDrift, trend } = detectDrift(session.checkpoints);
|
|
299
|
+
if (hasDrift && trend === 'degrading') {
|
|
300
|
+
failures.push(this.createFailure(
|
|
301
|
+
`Quality drift detected: scores are degrading over time`,
|
|
302
|
+
undefined,
|
|
303
|
+
'Agent performance is declining. Consider pausing and reviewing recent work.',
|
|
304
|
+
'Quality Drift Detected'
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return failures;
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/gates/context.ts
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
1
|
import { Gate, GateContext } from './base.js';
|
|
2
2
|
import { Failure, Gates } from '../types/index.js';
|
|
3
3
|
import { FileScanner } from '../utils/scanner.js';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
4
5
|
import fs from 'fs-extra';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Extended Context Configuration (v2.14+)
|
|
10
|
+
* For 1M token frontier models like Opus 4.6
|
|
11
|
+
*/
|
|
12
|
+
export interface ExtendedContextConfig {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
sensitivity?: number;
|
|
15
|
+
mining_depth?: number;
|
|
16
|
+
cross_file_patterns?: boolean; // NEW: Enable cross-file pattern analysis
|
|
17
|
+
naming_consistency?: boolean; // NEW: Check naming convention drift
|
|
18
|
+
import_relationships?: boolean; // NEW: Validate import patterns
|
|
19
|
+
max_cross_file_depth?: number; // NEW: How many related files to analyze
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
export class ContextGate extends Gate {
|
|
23
|
+
private extendedConfig: ExtendedContextConfig;
|
|
24
|
+
|
|
8
25
|
constructor(private config: Gates) {
|
|
9
26
|
super('context-drift', 'Context Awareness & Drift Detection');
|
|
27
|
+
this.extendedConfig = {
|
|
28
|
+
enabled: config.context?.enabled ?? false,
|
|
29
|
+
sensitivity: config.context?.sensitivity ?? 0.8,
|
|
30
|
+
mining_depth: config.context?.mining_depth ?? 100,
|
|
31
|
+
cross_file_patterns: true, // Default ON for frontier model support
|
|
32
|
+
naming_consistency: true,
|
|
33
|
+
import_relationships: true,
|
|
34
|
+
max_cross_file_depth: 50,
|
|
35
|
+
};
|
|
10
36
|
}
|
|
11
37
|
|
|
12
38
|
async run(context: GateContext): Promise<Failure[]> {
|
|
13
39
|
const failures: Failure[] = [];
|
|
14
40
|
const record = context.record;
|
|
15
|
-
if (!record || !this.
|
|
41
|
+
if (!record || !this.extendedConfig.enabled) return [];
|
|
16
42
|
|
|
17
43
|
const files = await FileScanner.findFiles({ cwd: context.cwd });
|
|
18
44
|
const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
|
|
19
45
|
|
|
46
|
+
// Collect all patterns across files for cross-file analysis
|
|
47
|
+
const namingPatterns: Map<string, { casing: string; file: string; count: number }[]> = new Map();
|
|
48
|
+
const importPatterns: Map<string, string[]> = new Map();
|
|
49
|
+
|
|
20
50
|
for (const file of files) {
|
|
21
51
|
try {
|
|
22
52
|
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
23
53
|
|
|
24
|
-
// 1. Detect Redundant Suffixes (The Golden Example)
|
|
54
|
+
// 1. Original: Detect Redundant Suffixes (The Golden Example)
|
|
25
55
|
this.checkEnvDrift(content, file, envAnchors, failures);
|
|
26
56
|
|
|
57
|
+
// 2. NEW: Cross-file pattern collection
|
|
58
|
+
if (this.extendedConfig.cross_file_patterns) {
|
|
59
|
+
this.collectNamingPatterns(content, file, namingPatterns);
|
|
60
|
+
this.collectImportPatterns(content, file, importPatterns);
|
|
61
|
+
}
|
|
62
|
+
|
|
27
63
|
} catch (e) { }
|
|
28
64
|
}
|
|
29
65
|
|
|
66
|
+
// 3. NEW: Analyze naming consistency across files
|
|
67
|
+
if (this.extendedConfig.naming_consistency) {
|
|
68
|
+
this.analyzeNamingConsistency(namingPatterns, failures);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. NEW: Analyze import relationship patterns
|
|
72
|
+
if (this.extendedConfig.import_relationships) {
|
|
73
|
+
this.analyzeImportPatterns(importPatterns, failures);
|
|
74
|
+
}
|
|
75
|
+
|
|
30
76
|
return failures;
|
|
31
77
|
}
|
|
32
78
|
|
|
@@ -52,4 +98,156 @@ export class ContextGate extends Gate {
|
|
|
52
98
|
}
|
|
53
99
|
}
|
|
54
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Collect naming patterns (function names, class names, variable names)
|
|
104
|
+
*/
|
|
105
|
+
private collectNamingPatterns(
|
|
106
|
+
content: string,
|
|
107
|
+
file: string,
|
|
108
|
+
patterns: Map<string, { casing: string; file: string; count: number }[]>
|
|
109
|
+
) {
|
|
110
|
+
// Function declarations
|
|
111
|
+
const funcMatches = content.matchAll(/(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=(]/g);
|
|
112
|
+
for (const match of funcMatches) {
|
|
113
|
+
const name = match[1];
|
|
114
|
+
const casing = this.detectCasing(name);
|
|
115
|
+
this.addPattern(patterns, 'function', { casing, file, count: 1 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Class declarations
|
|
119
|
+
const classMatches = content.matchAll(/class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
|
|
120
|
+
for (const match of classMatches) {
|
|
121
|
+
const casing = this.detectCasing(match[1]);
|
|
122
|
+
this.addPattern(patterns, 'class', { casing, file, count: 1 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Interface declarations (TypeScript)
|
|
126
|
+
const interfaceMatches = content.matchAll(/interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
|
|
127
|
+
for (const match of interfaceMatches) {
|
|
128
|
+
const casing = this.detectCasing(match[1]);
|
|
129
|
+
this.addPattern(patterns, 'interface', { casing, file, count: 1 });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Collect import patterns
|
|
135
|
+
*/
|
|
136
|
+
private collectImportPatterns(content: string, file: string, patterns: Map<string, string[]>) {
|
|
137
|
+
// ES6 imports
|
|
138
|
+
const importMatches = content.matchAll(/import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g);
|
|
139
|
+
for (const match of importMatches) {
|
|
140
|
+
const importPath = match[1];
|
|
141
|
+
if (!patterns.has(file)) {
|
|
142
|
+
patterns.set(file, []);
|
|
143
|
+
}
|
|
144
|
+
patterns.get(file)!.push(importPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Analyze naming consistency across files
|
|
150
|
+
*/
|
|
151
|
+
private analyzeNamingConsistency(
|
|
152
|
+
patterns: Map<string, { casing: string; file: string; count: number }[]>,
|
|
153
|
+
failures: Failure[]
|
|
154
|
+
) {
|
|
155
|
+
for (const [type, entries] of patterns) {
|
|
156
|
+
const casingCounts = new Map<string, number>();
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
casingCounts.set(entry.casing, (casingCounts.get(entry.casing) || 0) + entry.count);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Find dominant casing
|
|
162
|
+
let dominant = '';
|
|
163
|
+
let maxCount = 0;
|
|
164
|
+
for (const [casing, count] of casingCounts) {
|
|
165
|
+
if (count > maxCount) {
|
|
166
|
+
dominant = casing;
|
|
167
|
+
maxCount = count;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Report violations (non-dominant casing with significant usage)
|
|
172
|
+
const total = entries.reduce((sum, e) => sum + e.count, 0);
|
|
173
|
+
const threshold = total * (1 - (this.extendedConfig.sensitivity ?? 0.8));
|
|
174
|
+
|
|
175
|
+
for (const [casing, count] of casingCounts) {
|
|
176
|
+
if (casing !== dominant && count > threshold) {
|
|
177
|
+
const violatingFiles = entries.filter(e => e.casing === casing).map(e => e.file);
|
|
178
|
+
const uniqueFiles = [...new Set(violatingFiles)].slice(0, 5);
|
|
179
|
+
|
|
180
|
+
failures.push(this.createFailure(
|
|
181
|
+
`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`,
|
|
182
|
+
uniqueFiles,
|
|
183
|
+
`Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`,
|
|
184
|
+
'Naming Convention Drift'
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Analyze import patterns for consistency
|
|
193
|
+
*/
|
|
194
|
+
private analyzeImportPatterns(patterns: Map<string, string[]>, failures: Failure[]) {
|
|
195
|
+
// Check for mixed import styles (relative vs absolute)
|
|
196
|
+
const relativeCount = new Map<string, number>();
|
|
197
|
+
const absoluteCount = new Map<string, number>();
|
|
198
|
+
|
|
199
|
+
for (const [file, imports] of patterns) {
|
|
200
|
+
for (const imp of imports) {
|
|
201
|
+
if (imp.startsWith('.') || imp.startsWith('..')) {
|
|
202
|
+
relativeCount.set(file, (relativeCount.get(file) || 0) + 1);
|
|
203
|
+
} else if (!imp.startsWith('@') && !imp.includes('/')) {
|
|
204
|
+
// Skip external packages
|
|
205
|
+
} else {
|
|
206
|
+
absoluteCount.set(file, (absoluteCount.get(file) || 0) + 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Detect files with both relative AND absolute local imports
|
|
212
|
+
const mixedFiles: string[] = [];
|
|
213
|
+
for (const file of patterns.keys()) {
|
|
214
|
+
const hasRelative = (relativeCount.get(file) || 0) > 0;
|
|
215
|
+
const hasAbsolute = (absoluteCount.get(file) || 0) > 0;
|
|
216
|
+
if (hasRelative && hasAbsolute) {
|
|
217
|
+
mixedFiles.push(file);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (mixedFiles.length > 3) {
|
|
222
|
+
failures.push(this.createFailure(
|
|
223
|
+
`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`,
|
|
224
|
+
mixedFiles.slice(0, 5),
|
|
225
|
+
'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.',
|
|
226
|
+
'Import Pattern Drift'
|
|
227
|
+
));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Detect casing convention of an identifier
|
|
233
|
+
*/
|
|
234
|
+
private detectCasing(name: string): string {
|
|
235
|
+
if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'PascalCase';
|
|
236
|
+
if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'camelCase';
|
|
237
|
+
if (/^[a-z]+(_[a-z]+)+$/.test(name)) return 'snake_case';
|
|
238
|
+
if (/^[A-Z]+(_[A-Z]+)*$/.test(name)) return 'SCREAMING_SNAKE';
|
|
239
|
+
if (/^[A-Z][a-zA-Z]*$/.test(name)) return 'PascalCase';
|
|
240
|
+
return 'unknown';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private addPattern(
|
|
244
|
+
patterns: Map<string, { casing: string; file: string; count: number }[]>,
|
|
245
|
+
type: string,
|
|
246
|
+
entry: { casing: string; file: string; count: number }
|
|
247
|
+
) {
|
|
248
|
+
if (!patterns.has(type)) {
|
|
249
|
+
patterns.set(type, []);
|
|
250
|
+
}
|
|
251
|
+
patterns.get(type)!.push(entry);
|
|
252
|
+
}
|
|
55
253
|
}
|
package/src/gates/runner.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { ContextGate } from './context.js';
|
|
|
11
11
|
import { ContextEngine } from '../services/context-engine.js';
|
|
12
12
|
import { EnvironmentGate } from './environment.js';
|
|
13
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';
|
|
14
17
|
import { execa } from 'execa';
|
|
15
18
|
import { Logger } from '../utils/logger.js';
|
|
16
19
|
|
|
@@ -48,6 +51,21 @@ export class GateRunner {
|
|
|
48
51
|
this.gates.push(new ContextGate(this.config.gates));
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
// Agent Team Governance Gate (for Opus 4.6 / GPT-5.3 multi-agent workflows)
|
|
55
|
+
if (this.config.gates.agent_team?.enabled) {
|
|
56
|
+
this.gates.push(new AgentTeamGate(this.config.gates.agent_team));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Checkpoint Supervision Gate (for long-running GPT-5.3 coworking mode)
|
|
60
|
+
if (this.config.gates.checkpoint?.enabled) {
|
|
61
|
+
this.gates.push(new CheckpointGate(this.config.gates.checkpoint));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Security Patterns Gate (code-level vulnerability detection)
|
|
65
|
+
if (this.config.gates.security?.enabled) {
|
|
66
|
+
this.gates.push(new SecurityPatternsGate(this.config.gates.security));
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
// Environment Alignment Gate (Should be prioritized)
|
|
52
70
|
if (this.config.gates.environment?.enabled) {
|
|
53
71
|
this.gates.unshift(new EnvironmentGate(this.config.gates));
|