@rigour-labs/core 2.21.2 → 3.0.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.
Files changed (101) 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 +4 -2
  7. package/dist/gates/base.js +5 -1
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/content.js +1 -1
  11. package/dist/gates/context-window-artifacts.d.ts +34 -0
  12. package/dist/gates/context-window-artifacts.js +214 -0
  13. package/dist/gates/context.d.ts +2 -1
  14. package/dist/gates/context.js +4 -3
  15. package/dist/gates/coverage.js +3 -1
  16. package/dist/gates/dependency.js +5 -5
  17. package/dist/gates/duplication-drift.d.ts +33 -0
  18. package/dist/gates/duplication-drift.js +190 -0
  19. package/dist/gates/environment.js +4 -4
  20. package/dist/gates/file.js +1 -1
  21. package/dist/gates/hallucinated-imports.d.ts +63 -0
  22. package/dist/gates/hallucinated-imports.js +406 -0
  23. package/dist/gates/inconsistent-error-handling.d.ts +39 -0
  24. package/dist/gates/inconsistent-error-handling.js +236 -0
  25. package/dist/gates/promise-safety.d.ts +68 -0
  26. package/dist/gates/promise-safety.js +509 -0
  27. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  28. package/dist/gates/retry-loop-breaker.js +2 -1
  29. package/dist/gates/runner.js +62 -1
  30. package/dist/gates/safety.d.ts +2 -1
  31. package/dist/gates/safety.js +2 -1
  32. package/dist/gates/security-patterns.d.ts +2 -1
  33. package/dist/gates/security-patterns.js +2 -1
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/services/fix-packet-service.d.ts +0 -1
  38. package/dist/services/fix-packet-service.js +9 -14
  39. package/dist/services/score-history.d.ts +54 -0
  40. package/dist/services/score-history.js +122 -0
  41. package/dist/templates/index.js +195 -0
  42. package/dist/types/fix-packet.d.ts +5 -5
  43. package/dist/types/fix-packet.js +1 -1
  44. package/dist/types/index.d.ts +430 -0
  45. package/dist/types/index.js +57 -0
  46. package/package.json +21 -1
  47. package/src/context.test.ts +0 -256
  48. package/src/discovery.test.ts +0 -88
  49. package/src/discovery.ts +0 -112
  50. package/src/environment.test.ts +0 -115
  51. package/src/gates/agent-team.test.ts +0 -134
  52. package/src/gates/agent-team.ts +0 -210
  53. package/src/gates/ast-handlers/base.ts +0 -13
  54. package/src/gates/ast-handlers/python.ts +0 -145
  55. package/src/gates/ast-handlers/python_parser.py +0 -181
  56. package/src/gates/ast-handlers/typescript.ts +0 -264
  57. package/src/gates/ast-handlers/universal.ts +0 -184
  58. package/src/gates/ast.ts +0 -54
  59. package/src/gates/base.ts +0 -27
  60. package/src/gates/checkpoint.test.ts +0 -135
  61. package/src/gates/checkpoint.ts +0 -311
  62. package/src/gates/content.ts +0 -50
  63. package/src/gates/context.ts +0 -267
  64. package/src/gates/coverage.ts +0 -74
  65. package/src/gates/dependency.ts +0 -108
  66. package/src/gates/environment.ts +0 -94
  67. package/src/gates/file.ts +0 -42
  68. package/src/gates/retry-loop-breaker.ts +0 -151
  69. package/src/gates/runner.ts +0 -156
  70. package/src/gates/safety.ts +0 -56
  71. package/src/gates/security-patterns.test.ts +0 -162
  72. package/src/gates/security-patterns.ts +0 -305
  73. package/src/gates/structure.ts +0 -36
  74. package/src/index.ts +0 -13
  75. package/src/pattern-index/embeddings.ts +0 -84
  76. package/src/pattern-index/index.ts +0 -59
  77. package/src/pattern-index/indexer.test.ts +0 -276
  78. package/src/pattern-index/indexer.ts +0 -1023
  79. package/src/pattern-index/matcher.test.ts +0 -293
  80. package/src/pattern-index/matcher.ts +0 -493
  81. package/src/pattern-index/overrides.ts +0 -235
  82. package/src/pattern-index/security.ts +0 -151
  83. package/src/pattern-index/staleness.test.ts +0 -313
  84. package/src/pattern-index/staleness.ts +0 -568
  85. package/src/pattern-index/types.ts +0 -339
  86. package/src/safety.test.ts +0 -53
  87. package/src/services/adaptive-thresholds.test.ts +0 -189
  88. package/src/services/adaptive-thresholds.ts +0 -275
  89. package/src/services/context-engine.ts +0 -104
  90. package/src/services/fix-packet-service.ts +0 -42
  91. package/src/services/state-service.ts +0 -138
  92. package/src/smoke.test.ts +0 -18
  93. package/src/templates/index.ts +0 -312
  94. package/src/types/fix-packet.ts +0 -32
  95. package/src/types/index.ts +0 -159
  96. package/src/utils/logger.ts +0 -43
  97. package/src/utils/scanner.test.ts +0 -37
  98. package/src/utils/scanner.ts +0 -43
  99. package/tsconfig.json +0 -10
  100. package/vitest.config.ts +0 -7
  101. package/vitest.setup.ts +0 -30
@@ -1,74 +0,0 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import { Gate, GateContext } from './base.js';
4
- import { Failure, Gates } from '../types/index.js';
5
- import { globby } from 'globby';
6
-
7
- export class CoverageGate extends Gate {
8
- constructor(private config: Gates) {
9
- super('coverage-guard', 'Dynamic Coverage Guard');
10
- }
11
-
12
- async run(context: GateContext): Promise<Failure[]> {
13
- const failures: Failure[] = [];
14
-
15
- // 1. Locate coverage report (lcov.info is standard)
16
- const reports = await globby(['**/lcov.info', '**/coverage-final.json'], {
17
- cwd: context.cwd,
18
- ignore: ['node_modules/**']
19
- });
20
-
21
- if (reports.length === 0) {
22
- // If no reports found, and coverage is required, we could flag it.
23
- // But for now, we'll just skip silently if not configured.
24
- return [];
25
- }
26
-
27
- // 2. Parse coverage (Simplified LCOV parser for demonstration)
28
- const coverageData = await this.parseLcov(path.join(context.cwd, reports[0]));
29
-
30
- // 3. Quality Handshake: SME SME LOGIC
31
- // We look for files that have high complexity but low coverage.
32
- // In a real implementation, we would share data between ASTGate and CoverageGate.
33
- // For this demo, we'll implement a standalone check.
34
-
35
- for (const [file, stats] of Object.entries(coverageData)) {
36
- const coverage = (stats.hit / stats.found) * 100;
37
- const threshold = stats.isComplex ? 80 : 50; // SME logic: Complex files need higher coverage
38
-
39
- if (coverage < threshold) {
40
- failures.push({
41
- id: 'DYNAMIC_COVERAGE_LOW',
42
- title: `Low coverage for high-risk file: ${file}`,
43
- details: `Current coverage: ${coverage.toFixed(2)}%. Required: ${threshold}% due to structural risk.`,
44
- files: [file],
45
- hint: `Add dynamic tests to cover complex logical branches in this file.`
46
- });
47
- }
48
- }
49
-
50
- return failures;
51
- }
52
-
53
- private async parseLcov(reportPath: string): Promise<Record<string, { found: number, hit: number, isComplex: boolean }>> {
54
- const content = await fs.readFile(reportPath, 'utf-8');
55
- const results: Record<string, { found: number, hit: number, isComplex: boolean }> = {};
56
- let currentFile = '';
57
-
58
- for (const line of content.split('\n')) {
59
- if (line.startsWith('SF:')) {
60
- currentFile = line.substring(3);
61
- results[currentFile] = { found: 0, hit: 0, isComplex: false };
62
- } else if (line.startsWith('LF:')) {
63
- const found = parseInt(line.substring(3));
64
- results[currentFile].found = found;
65
- // SME Logic: If a file has > 100 logical lines, it's considered "Complex"
66
- // and triggers the higher (80%) coverage requirement.
67
- if (found > 100) results[currentFile].isComplex = true;
68
- } else if (line.startsWith('LH:')) {
69
- results[currentFile].hit = parseInt(line.substring(3));
70
- }
71
- }
72
- return results;
73
- }
74
- }
@@ -1,108 +0,0 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import { Failure, Config } from '../types/index.js';
4
- import { Gate, GateContext } from './base.js';
5
-
6
- export class DependencyGate extends Gate {
7
- constructor(private config: Config) {
8
- super('dependency-guardian', 'Dependency Guardian');
9
- }
10
-
11
- async run(context: GateContext): Promise<Failure[]> {
12
- const failures: Failure[] = [];
13
- const forbidden = this.config.gates.dependencies?.forbid || [];
14
-
15
- if (forbidden.length === 0) return [];
16
-
17
- const { cwd } = context;
18
-
19
- // 1. Scan Node.js (package.json)
20
- const pkgPath = path.join(cwd, 'package.json');
21
- if (await fs.pathExists(pkgPath)) {
22
- try {
23
- const pkg = await fs.readJson(pkgPath);
24
- const allDeps = {
25
- ...(pkg.dependencies || {}),
26
- ...(pkg.devDependencies || {}),
27
- ...(pkg.peerDependencies || {}),
28
- };
29
-
30
- for (const dep of forbidden) {
31
- if (allDeps[dep]) {
32
- failures.push(this.createFailure(
33
- `The package '${dep}' is forbidden by project standards.`,
34
- ['package.json'],
35
- `Remove '${dep}' from package.json and use approved alternatives.`,
36
- 'Forbidden Dependency'
37
- ));
38
- }
39
- }
40
- } catch (e) { }
41
- }
42
-
43
- // 2. Scan Python (requirements.txt, pyproject.toml)
44
- const reqPath = path.join(cwd, 'requirements.txt');
45
- if (await fs.pathExists(reqPath)) {
46
- const content = await fs.readFile(reqPath, 'utf-8');
47
- for (const dep of forbidden) {
48
- if (new RegExp(`^${dep}([=<>! ]|$)`, 'm').test(content)) {
49
- failures.push(this.createFailure(
50
- `The Python package '${dep}' is forbidden.`,
51
- ['requirements.txt'],
52
- `Remove '${dep}' from requirements.txt.`,
53
- 'Forbidden Dependency'
54
- ));
55
- }
56
- }
57
- }
58
-
59
- const pyprojPath = path.join(cwd, 'pyproject.toml');
60
- if (await fs.pathExists(pyprojPath)) {
61
- const content = await fs.readFile(pyprojPath, 'utf-8');
62
- for (const dep of forbidden) {
63
- if (new RegExp(`^${dep}\\s*=`, 'm').test(content)) {
64
- failures.push(this.createFailure(
65
- `The Python package '${dep}' is forbidden in pyproject.toml.`,
66
- ['pyproject.toml'],
67
- `Remove '${dep}' from pyproject.toml dependencies.`,
68
- 'Forbidden Dependency'
69
- ));
70
- }
71
- }
72
- }
73
-
74
- // 3. Scan Go (go.mod)
75
- const goModPath = path.join(cwd, 'go.mod');
76
- if (await fs.pathExists(goModPath)) {
77
- const content = await fs.readFile(goModPath, 'utf-8');
78
- for (const dep of forbidden) {
79
- if (content.includes(dep)) {
80
- failures.push(this.createFailure(
81
- `The Go module '${dep}' is forbidden.`,
82
- ['go.mod'],
83
- `Remove '${dep}' from go.mod.`,
84
- 'Forbidden Dependency'
85
- ));
86
- }
87
- }
88
- }
89
-
90
- // 4. Scan Java (pom.xml)
91
- const pomPath = path.join(cwd, 'pom.xml');
92
- if (await fs.pathExists(pomPath)) {
93
- const content = await fs.readFile(pomPath, 'utf-8');
94
- for (const dep of forbidden) {
95
- if (content.includes(`<artifactId>${dep}</artifactId>`)) {
96
- failures.push(this.createFailure(
97
- `The Java artifact '${dep}' is forbidden.`,
98
- ['pom.xml'],
99
- `Remove '${dep}' from pom.xml.`,
100
- 'Forbidden Dependency'
101
- ));
102
- }
103
- }
104
- }
105
-
106
- return failures;
107
- }
108
- }
@@ -1,94 +0,0 @@
1
- import { Gate, GateContext } from './base.js';
2
- import { Failure, Gates } from '../types/index.js';
3
- import { execa } from 'execa';
4
- import semver from 'semver';
5
- import fs from 'fs-extra';
6
- import path from 'path';
7
-
8
- export class EnvironmentGate extends Gate {
9
- constructor(private config: Gates) {
10
- super('environment-alignment', 'Environment & Tooling Alignment');
11
- }
12
-
13
- async run(context: GateContext): Promise<Failure[]> {
14
- const failures: Failure[] = [];
15
- const envConfig = this.config.environment;
16
- if (!envConfig || !envConfig.enabled) return [];
17
-
18
- const contracts = envConfig.enforce_contracts ? await this.discoverContracts(context.cwd) : {};
19
- const toolsToCheck = { ...contracts, ...(envConfig.tools || {}) };
20
-
21
- // 1. Verify Tool Versions
22
- for (const [tool, range] of Object.entries(toolsToCheck)) {
23
- // Ensure range is a string
24
- const semverRange = String(range);
25
- try {
26
- const { stdout } = await execa(tool, ['--version'], { shell: true });
27
- const versionMatch = stdout.match(/(\d+\.\d+\.\d+)/);
28
-
29
- if (versionMatch) {
30
- const version = versionMatch[1];
31
- if (!semver.satisfies(version, semverRange)) {
32
- failures.push(this.createFailure(
33
- `Environment Alignment: Tool '${tool}' version mismatch.`,
34
- [],
35
- `Project requires '${tool} ${semverRange}' (discovered from contract), but found version '${version}'. Please align your local environment to prevent drift.`
36
- ));
37
- }
38
- } else {
39
- failures.push(this.createFailure(
40
- `Environment Alignment: Could not determine version for '${tool}'.`,
41
- [],
42
- `Ensure '${tool} --version' returns a standard SemVer string.`
43
- ));
44
- }
45
- } catch (e) {
46
- failures.push(this.createFailure(
47
- `Environment Alignment: Required tool '${tool}' is missing.`,
48
- [],
49
- `Install '${tool}' and ensure it is in your $PATH.`
50
- ));
51
- }
52
- }
53
-
54
- // 2. Verify Required Env Vars
55
- const requiredEnv = envConfig.required_env || [];
56
- for (const envVar of requiredEnv) {
57
- if (!process.env[envVar]) {
58
- failures.push(this.createFailure(
59
- `Environment Alignment: Missing required environment variable '${envVar}'.`,
60
- [],
61
- `Ensure '${envVar}' is defined in your environment or .env file.`
62
- ));
63
- }
64
- }
65
-
66
- return failures;
67
- }
68
-
69
- private async discoverContracts(cwd: string): Promise<Record<string, string>> {
70
- const contracts: Record<string, string> = {};
71
-
72
- // 1. Scan pyproject.toml (for ruff, mypy)
73
- const pyprojectPath = path.join(cwd, 'pyproject.toml');
74
- if (await fs.pathExists(pyprojectPath)) {
75
- const content = await fs.readFile(pyprojectPath, 'utf-8');
76
- // SME Logic: Look for ruff and mypy version constraints
77
- // Handle both ruff = "^0.14.0" and ruff = { version = "^0.14.0" }
78
- const ruffMatch = content.match(/ruff\s*=\s*(?:['"]([^'"]+)['"]|\{\s*version\s*=\s*['"]([^'"]+)['"]\s*\})/);
79
- if (ruffMatch) contracts['ruff'] = ruffMatch[1] || ruffMatch[2];
80
-
81
- const mypyMatch = content.match(/mypy\s*=\s*(?:['"]([^'"]+)['"]|\{\s*version\s*=\s*['"]([^'"]+)['"]\s*\})/);
82
- if (mypyMatch) contracts['mypy'] = mypyMatch[1] || mypyMatch[2];
83
- }
84
-
85
- // 2. Scan package.json (for node/npm/pnpm)
86
- const pkgPath = path.join(cwd, 'package.json');
87
- if (await fs.pathExists(pkgPath)) {
88
- const pkg = await fs.readJson(pkgPath);
89
- if (pkg.engines?.node) contracts['node'] = pkg.engines.node;
90
- }
91
-
92
- return contracts;
93
- }
94
- }
package/src/gates/file.ts DELETED
@@ -1,42 +0,0 @@
1
- import { Gate, GateContext } from './base.js';
2
- import { Failure } from '../types/index.js';
3
- import { FileScanner } from '../utils/scanner.js';
4
-
5
- export interface FileGateConfig {
6
- maxLines: number;
7
- }
8
-
9
- export class FileGate extends Gate {
10
- constructor(private config: FileGateConfig) {
11
- super('file-size', 'File Size Limit');
12
- }
13
-
14
- async run(context: GateContext): Promise<Failure[]> {
15
- const files = await FileScanner.findFiles({
16
- cwd: context.cwd,
17
- ignore: context.ignore,
18
- patterns: context.patterns
19
- });
20
- const contents = await FileScanner.readFiles(context.cwd, files);
21
-
22
- const violations: string[] = [];
23
- for (const [file, content] of contents) {
24
- const lines = content.split('\n').length;
25
- if (lines > this.config.maxLines) {
26
- violations.push(`${file} (${lines} lines)`);
27
- }
28
- }
29
-
30
- if (violations.length > 0) {
31
- return [
32
- this.createFailure(
33
- `The following files exceed the maximum limit of ${this.config.maxLines} lines:`,
34
- violations,
35
- 'Break these files into smaller, more modular components to improve maintainability (SOLID - Single Responsibility Principle).'
36
- ),
37
- ];
38
- }
39
-
40
- return [];
41
- }
42
- }
@@ -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,156 +0,0 @@
1
- import { Gate } from './base.js';
2
- import { Failure, Config, Report, Status } 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 { execa } from 'execa';
18
- import { Logger } from '../utils/logger.js';
19
-
20
- export class GateRunner {
21
- private gates: Gate[] = [];
22
-
23
- constructor(private config: Config) {
24
- this.initializeGates();
25
- }
26
-
27
- private initializeGates() {
28
- // Retry Loop Breaker Gate - HIGHEST PRIORITY (runs first)
29
- if (this.config.gates.retry_loop_breaker?.enabled !== false) {
30
- this.gates.push(new RetryLoopBreakerGate(this.config.gates.retry_loop_breaker));
31
- }
32
-
33
- if (this.config.gates.max_file_lines) {
34
- this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
35
- }
36
- this.gates.push(
37
- new ContentGate({
38
- forbidTodos: !!this.config.gates.forbid_todos,
39
- forbidFixme: !!this.config.gates.forbid_fixme,
40
- })
41
- );
42
- if (this.config.gates.required_files) {
43
- this.gates.push(new StructureGate({ requiredFiles: this.config.gates.required_files }));
44
- }
45
- this.gates.push(new ASTGate(this.config.gates));
46
- this.gates.push(new DependencyGate(this.config));
47
- this.gates.push(new FileGuardGate(this.config.gates));
48
- this.gates.push(new CoverageGate(this.config.gates));
49
-
50
- if (this.config.gates.context?.enabled) {
51
- this.gates.push(new ContextGate(this.config.gates));
52
- }
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) — enabled by default since v2.15
65
- if (this.config.gates.security?.enabled !== false) {
66
- this.gates.push(new SecurityPatternsGate(this.config.gates.security));
67
- }
68
-
69
- // Environment Alignment Gate (Should be prioritized)
70
- if (this.config.gates.environment?.enabled) {
71
- this.gates.unshift(new EnvironmentGate(this.config.gates));
72
- }
73
- }
74
-
75
- /**
76
- * Allows adding custom gates dynamically (SOLID - Open/Closed Principle)
77
- */
78
- addGate(gate: Gate) {
79
- this.gates.push(gate);
80
- }
81
-
82
- async run(cwd: string, patterns?: string[]): Promise<Report> {
83
- const start = Date.now();
84
- const failures: Failure[] = [];
85
- const summary: Record<string, Status> = {};
86
-
87
- const ignore = this.config.ignore;
88
-
89
- // 0. Run Context Discovery
90
- let record;
91
- if (this.config.gates.context?.enabled) {
92
- const engine = new ContextEngine(this.config);
93
- record = await engine.discover(cwd);
94
- }
95
-
96
- // 1. Run internal gates
97
- for (const gate of this.gates) {
98
- try {
99
- const gateFailures = await gate.run({ cwd, record, ignore, patterns });
100
- if (gateFailures.length > 0) {
101
- failures.push(...gateFailures);
102
- summary[gate.id] = 'FAIL';
103
- } else {
104
- summary[gate.id] = 'PASS';
105
- }
106
- } catch (error: any) {
107
- Logger.error(`Gate ${gate.id} failed with error: ${error.message}`);
108
- summary[gate.id] = 'ERROR';
109
- failures.push({
110
- id: gate.id,
111
- title: `Gate Error: ${gate.title}`,
112
- details: error.message,
113
- hint: 'There was an internal error running this gate. Check the logs.',
114
- });
115
- }
116
- }
117
-
118
- // 2. Run command gates (lint, test, etc.)
119
- const commands = this.config.commands;
120
- if (commands) {
121
- for (const [key, cmd] of Object.entries(commands)) {
122
- if (!cmd) {
123
- summary[key] = 'SKIP';
124
- continue;
125
- }
126
-
127
- try {
128
- Logger.info(`Running command gate: ${key} (${cmd})`);
129
- await execa(cmd, { shell: true, cwd });
130
- summary[key] = 'PASS';
131
- } catch (error: any) {
132
- summary[key] = 'FAIL';
133
- failures.push({
134
- id: key,
135
- title: `${key.toUpperCase()} Check Failed`,
136
- details: error.stderr || error.stdout || error.message,
137
- hint: `Fix the issues reported by \`${cmd}\`. Use rigorous standards (SOLID, DRY) in your resolution.`,
138
- });
139
- }
140
- }
141
- }
142
-
143
- const status: Status = failures.length > 0 ? 'FAIL' : 'PASS';
144
- const score = Math.max(0, 100 - (failures.length * 5)); // Basic SME scoring logic
145
-
146
- return {
147
- status,
148
- summary,
149
- failures,
150
- stats: {
151
- duration_ms: Date.now() - start,
152
- score,
153
- },
154
- };
155
- }
156
- }
@@ -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
- }