@rigour-labs/core 2.22.0 → 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 (103) 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.d.ts +2 -1
  31. package/dist/gates/security-patterns.js +1 -0
  32. package/dist/gates/structure.js +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/services/fix-packet-service.d.ts +0 -1
  36. package/dist/services/fix-packet-service.js +9 -14
  37. package/dist/services/score-history.d.ts +54 -0
  38. package/dist/services/score-history.js +122 -0
  39. package/dist/templates/index.js +169 -0
  40. package/dist/types/fix-packet.d.ts +5 -5
  41. package/dist/types/fix-packet.js +1 -1
  42. package/dist/types/index.d.ts +153 -0
  43. package/dist/types/index.js +19 -0
  44. package/package.json +21 -1
  45. package/src/context.test.ts +0 -256
  46. package/src/discovery.test.ts +0 -88
  47. package/src/discovery.ts +0 -112
  48. package/src/environment.test.ts +0 -115
  49. package/src/gates/agent-team.test.ts +0 -134
  50. package/src/gates/agent-team.ts +0 -210
  51. package/src/gates/ast-handlers/base.ts +0 -13
  52. package/src/gates/ast-handlers/python.ts +0 -145
  53. package/src/gates/ast-handlers/python_parser.py +0 -181
  54. package/src/gates/ast-handlers/typescript.ts +0 -264
  55. package/src/gates/ast-handlers/universal.ts +0 -184
  56. package/src/gates/ast.ts +0 -54
  57. package/src/gates/base.ts +0 -28
  58. package/src/gates/checkpoint.test.ts +0 -135
  59. package/src/gates/checkpoint.ts +0 -311
  60. package/src/gates/content.ts +0 -51
  61. package/src/gates/context-window-artifacts.ts +0 -277
  62. package/src/gates/context.ts +0 -270
  63. package/src/gates/coverage.ts +0 -74
  64. package/src/gates/dependency.ts +0 -108
  65. package/src/gates/duplication-drift.ts +0 -231
  66. package/src/gates/environment.ts +0 -94
  67. package/src/gates/file.ts +0 -46
  68. package/src/gates/hallucinated-imports.ts +0 -361
  69. package/src/gates/inconsistent-error-handling.ts +0 -254
  70. package/src/gates/retry-loop-breaker.ts +0 -151
  71. package/src/gates/runner.ts +0 -188
  72. package/src/gates/safety.ts +0 -56
  73. package/src/gates/security-patterns.test.ts +0 -162
  74. package/src/gates/security-patterns.ts +0 -306
  75. package/src/gates/structure.ts +0 -36
  76. package/src/index.ts +0 -13
  77. package/src/pattern-index/embeddings.ts +0 -84
  78. package/src/pattern-index/index.ts +0 -59
  79. package/src/pattern-index/indexer.test.ts +0 -276
  80. package/src/pattern-index/indexer.ts +0 -1023
  81. package/src/pattern-index/matcher.test.ts +0 -293
  82. package/src/pattern-index/matcher.ts +0 -493
  83. package/src/pattern-index/overrides.ts +0 -235
  84. package/src/pattern-index/security.ts +0 -151
  85. package/src/pattern-index/staleness.test.ts +0 -313
  86. package/src/pattern-index/staleness.ts +0 -568
  87. package/src/pattern-index/types.ts +0 -339
  88. package/src/safety.test.ts +0 -53
  89. package/src/services/adaptive-thresholds.test.ts +0 -189
  90. package/src/services/adaptive-thresholds.ts +0 -275
  91. package/src/services/context-engine.ts +0 -104
  92. package/src/services/fix-packet-service.ts +0 -42
  93. package/src/services/state-service.ts +0 -138
  94. package/src/smoke.test.ts +0 -18
  95. package/src/templates/index.ts +0 -338
  96. package/src/types/fix-packet.ts +0 -32
  97. package/src/types/index.ts +0 -200
  98. package/src/utils/logger.ts +0 -43
  99. package/src/utils/scanner.test.ts +0 -37
  100. package/src/utils/scanner.ts +0 -43
  101. package/tsconfig.json +0 -10
  102. package/vitest.config.ts +0 -7
  103. package/vitest.setup.ts +0 -30
@@ -1,88 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { DiscoveryService } from './discovery.js';
3
- import fs from 'fs-extra';
4
- import path from 'path';
5
-
6
- vi.mock('fs-extra');
7
-
8
- describe('DiscoveryService', () => {
9
- beforeEach(() => {
10
- vi.resetAllMocks();
11
- });
12
-
13
- it('should discover project marker in root directory', async () => {
14
- const service = new DiscoveryService();
15
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('package.json'));
16
- vi.mocked(fs.readdir).mockResolvedValue(['package.json'] as any);
17
- vi.mocked(fs.readFile).mockResolvedValue('{}' as any);
18
-
19
- const result = await service.discover('/test');
20
- // If package.json doesn't match a specific role marker, it stays Universal.
21
- // Let's mock a specific one like 'express'
22
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('express'));
23
- const result2 = await service.discover('/test');
24
- expect(result2.matches.preset?.name).toBe('api');
25
- });
26
-
27
- it('should discover project marker in src/ directory (Deep Detection)', async () => {
28
- const service = new DiscoveryService();
29
- vi.mocked(fs.pathExists).mockImplementation((async (p: string) => {
30
- if (p.endsWith('src')) return true;
31
- if (p.includes('src/index.ts')) return true;
32
- return false;
33
- }) as any);
34
- vi.mocked(fs.readdir).mockImplementation((async (p: string) => {
35
- if (p.toString().endsWith('/test')) return ['src'] as any;
36
- if (p.toString().endsWith('src')) return ['index.ts'] as any;
37
- return [] as any;
38
- }) as any);
39
- vi.mocked(fs.readFile).mockResolvedValue('export const x = 1;' as any);
40
-
41
- const result = await service.discover('/test');
42
- // Since UNIVERSAL_CONFIG has a default, we check if it found something extra or matches expectation
43
- // Default is universal, but detecting .ts should tilt it towards node or similar if configured
44
- // In our current templates, package.json is the node marker.
45
- // Let's check for paradigm detection which uses content
46
- expect(result.config).toBeDefined();
47
- });
48
-
49
- it('should identify OOP paradigm from content in subfolder', async () => {
50
- const service = new DiscoveryService();
51
- vi.mocked(fs.pathExists).mockImplementation((async (p: string) => p.endsWith('src') || p.endsWith('src/Service.ts')) as any);
52
- vi.mocked(fs.readdir).mockImplementation((async (p: string) => {
53
- if (p.toString().endsWith('src')) return ['Service.ts'] as any;
54
- return ['src'] as any;
55
- }) as any);
56
- vi.mocked(fs.readFile).mockResolvedValue('class MyService {}' as any);
57
-
58
- const result = await service.discover('/test');
59
- expect(result.matches.paradigm?.name).toBe('oop');
60
- });
61
-
62
- it('should include project-type-aware ignore patterns for API preset', async () => {
63
- const service = new DiscoveryService();
64
- // Mock finding requirements.txt (Python API marker)
65
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('requirements.txt'));
66
- vi.mocked(fs.readdir).mockResolvedValue(['requirements.txt'] as any);
67
- vi.mocked(fs.readFile).mockResolvedValue('flask==2.0.0' as any);
68
-
69
- const result = await service.discover('/test');
70
- expect(result.matches.preset?.name).toBe('api');
71
- expect(result.config.ignore).toContain('venv/**');
72
- expect(result.config.ignore).toContain('__pycache__/**');
73
- expect(result.config.ignore).toContain('*.pyc');
74
- });
75
-
76
- it('should include project-type-aware ignore patterns for UI preset', async () => {
77
- const service = new DiscoveryService();
78
- // Mock finding next.config.js (UI marker)
79
- vi.mocked(fs.pathExists).mockImplementation(async (p: string) => p.includes('next.config.js'));
80
- vi.mocked(fs.readdir).mockResolvedValue(['next.config.js'] as any);
81
- vi.mocked(fs.readFile).mockResolvedValue('module.exports = {}' as any);
82
-
83
- const result = await service.discover('/test');
84
- expect(result.matches.preset?.name).toBe('ui');
85
- expect(result.config.ignore).toContain('node_modules/**');
86
- expect(result.config.ignore).toContain('.next/**');
87
- });
88
- });
package/src/discovery.ts DELETED
@@ -1,112 +0,0 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import { Config, Gates } from './types/index.js';
4
- import { TEMPLATES, PARADIGM_TEMPLATES, UNIVERSAL_CONFIG } from './templates/index.js';
5
-
6
- export interface DiscoveryResult {
7
- config: Config;
8
- matches: {
9
- preset?: { name: string; marker: string };
10
- paradigm?: { name: string; marker: string };
11
- };
12
- }
13
-
14
- export class DiscoveryService {
15
- async discover(cwd: string): Promise<DiscoveryResult> {
16
- let config = { ...UNIVERSAL_CONFIG };
17
- const matches: DiscoveryResult['matches'] = {};
18
-
19
- // 1. Detect Role (ui, api, infra, data)
20
- for (const template of TEMPLATES) {
21
- const marker = await this.findFirstMarker(cwd, template.markers, true); // Search content for roles too
22
- if (marker) {
23
- config = this.mergeConfig(config, template.config);
24
- matches.preset = { name: template.name, marker };
25
- break; // Only one role for now
26
- }
27
- }
28
-
29
- // 2. Detect Paradigm (oop, functional)
30
- for (const template of PARADIGM_TEMPLATES) {
31
- const marker = await this.findFirstMarker(cwd, template.markers, true); // Search content
32
- if (marker) {
33
- config = this.mergeConfig(config, template.config);
34
- matches.paradigm = { name: template.name, marker };
35
- break;
36
- }
37
- }
38
-
39
- return { config, matches };
40
- }
41
-
42
- private mergeConfig(base: Config, extension: any): Config {
43
- // Deep merge for gates to preserve defaults when overrides are partial
44
- const mergedGates = { ...base.gates };
45
- if (extension.gates) {
46
- for (const [key, value] of Object.entries(extension.gates)) {
47
- if (typeof value === 'object' && value !== null && !Array.isArray(value) && (mergedGates as any)[key]) {
48
- (mergedGates as any)[key] = { ...(mergedGates as any)[key], ...value };
49
- } else {
50
- (mergedGates as any)[key] = value;
51
- }
52
- }
53
- }
54
-
55
- return {
56
- ...base,
57
- preset: extension.preset || base.preset,
58
- paradigm: extension.paradigm || base.paradigm,
59
- commands: { ...base.commands, ...extension.commands },
60
- gates: mergedGates as Gates,
61
- ignore: [...new Set([...(base.ignore || []), ...(extension.ignore || [])])],
62
- };
63
- }
64
-
65
- private async findFirstMarker(cwd: string, markers: string[], searchContent: boolean = false): Promise<string | null> {
66
- for (const marker of markers) {
67
- const fullPath = path.join(cwd, marker);
68
-
69
- // File/Directory existence check
70
- if (await fs.pathExists(fullPath)) {
71
- return marker;
72
- }
73
-
74
- // Deep content check for paradigms
75
- if (searchContent) {
76
- const match = await this.existsInContent(cwd, marker);
77
- if (match) return `content:${marker}`;
78
- }
79
- }
80
- return null;
81
- }
82
-
83
- private async existsInContent(cwd: string, pattern: string): Promise<boolean> {
84
- // Simple heuristic: search in top 5 source files
85
- const files = await this.findSourceFiles(cwd);
86
- for (const file of files) {
87
- const content = await fs.readFile(file, 'utf-8');
88
- if (content.includes(pattern)) return true;
89
- }
90
- return false;
91
- }
92
-
93
- private async findSourceFiles(cwd: string): Promise<string[]> {
94
- const extensions = ['.ts', '.js', '.py', '.go', '.java', '.tf', 'package.json'];
95
- const samples: string[] = [];
96
- const commonDirs = ['.', 'src', 'app', 'lib', 'api', 'pkg'];
97
-
98
- for (const dir of commonDirs) {
99
- const fullDir = path.join(cwd, dir);
100
- if (!(await fs.pathExists(fullDir))) continue;
101
-
102
- const files = await fs.readdir(fullDir);
103
- for (const file of files) {
104
- if (extensions.some(ext => file.endsWith(ext))) {
105
- samples.push(path.join(fullDir, file));
106
- if (samples.length >= 5) return samples;
107
- }
108
- }
109
- }
110
- return samples;
111
- }
112
- }
@@ -1,115 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { GateRunner } from './gates/runner.js';
3
- import { Config, RawConfig, ConfigSchema } from './types/index.js';
4
- import fs from 'fs-extra';
5
- import path from 'path';
6
-
7
- describe('Environment Alignment Gate', () => {
8
- const testDir = path.join(process.cwd(), 'temp-test-env');
9
-
10
- beforeEach(async () => {
11
- await fs.ensureDir(testDir);
12
- });
13
-
14
- afterEach(async () => {
15
- await fs.remove(testDir);
16
- });
17
-
18
- it('should detect tool version mismatch (Explicit)', async () => {
19
- const rawConfig: RawConfig = {
20
- version: 1,
21
- gates: {
22
- environment: {
23
- enabled: true,
24
- enforce_contracts: false,
25
- tools: {
26
- node: ">=99.0.0" // Guaranteed to fail
27
- }
28
- }
29
- }
30
- };
31
-
32
- const config = ConfigSchema.parse(rawConfig);
33
- const runner = new GateRunner(config);
34
- const report = await runner.run(testDir);
35
-
36
- expect(report.status).toBe('FAIL');
37
- const envFailure = report.failures.find(f => f.id === 'environment-alignment');
38
- expect(envFailure).toBeDefined();
39
- expect(envFailure?.details).toContain('node');
40
- expect(envFailure?.details).toContain('version mismatch');
41
- });
42
-
43
- it('should detect missing environment variables', async () => {
44
- const rawConfig: RawConfig = {
45
- version: 1,
46
- gates: {
47
- environment: {
48
- enabled: true,
49
- required_env: ["RIGOUR_TEST_VAR_MISSING"]
50
- }
51
- }
52
- };
53
-
54
- const config = ConfigSchema.parse(rawConfig);
55
- const runner = new GateRunner(config);
56
- const report = await runner.run(testDir);
57
-
58
- expect(report.status).toBe('FAIL');
59
- expect(report.failures[0].details).toContain('RIGOUR_TEST_VAR_MISSING');
60
- });
61
-
62
- it('should discover contracts from pyproject.toml', async () => {
63
- // Create mock pyproject.toml with a version that will surely fail
64
- await fs.writeFile(path.join(testDir, 'pyproject.toml'), `
65
- [tool.ruff]
66
- ruff = ">=99.14.0"
67
- `);
68
-
69
- const rawConfig: RawConfig = {
70
- version: 1,
71
- gates: {
72
- environment: {
73
- enabled: true,
74
- enforce_contracts: true,
75
- tools: {} // Should discover ruff from file
76
- }
77
- }
78
- };
79
-
80
- const config = ConfigSchema.parse(rawConfig);
81
- const runner = new GateRunner(config);
82
- const report = await runner.run(testDir);
83
-
84
- // This might pass or fail depending on the local ruff version,
85
- // but we want to check if the gate attempted to check ruff.
86
- // If ruff is missing, it will fail with "is missing".
87
- const ruffFailure = report.failures.find(f => f.details.includes('ruff'));
88
- expect(ruffFailure).toBeDefined();
89
- });
90
-
91
- it('should prioritize environment gate and run it first', async () => {
92
- const rawConfig: RawConfig = {
93
- version: 1,
94
- gates: {
95
- max_file_lines: 1,
96
- environment: {
97
- enabled: true,
98
- required_env: ["MANDATORY_SECRET_MISSING"]
99
- }
100
- }
101
- };
102
-
103
- const config = ConfigSchema.parse(rawConfig);
104
-
105
- // Create a file that would fail max_file_lines
106
- await fs.writeFile(path.join(testDir, 'large.js'), 'line1\nline2\nline3');
107
-
108
- const runner = new GateRunner(config);
109
- const report = await runner.run(testDir);
110
-
111
- // In a real priority system, we might want to stop after environment failure.
112
- // Currently GateRunner runs all, but environment-alignment has been unshifted.
113
- expect(Object.keys(report.summary)[0]).toBe('environment-alignment');
114
- });
115
- });
@@ -1,134 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { AgentTeamGate, registerAgent, getSession, clearSession } from './agent-team.js';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
-
7
- describe('AgentTeamGate', () => {
8
- let testDir: string;
9
-
10
- beforeEach(() => {
11
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-team-test-'));
12
- });
13
-
14
- afterEach(() => {
15
- clearSession(testDir);
16
- fs.rmSync(testDir, { recursive: true, force: true });
17
- });
18
-
19
- describe('gate initialization', () => {
20
- it('should create gate with default config', () => {
21
- const gate = new AgentTeamGate();
22
- expect(gate.id).toBe('agent-team');
23
- expect(gate.title).toBe('Agent Team Governance');
24
- });
25
-
26
- it('should skip when not enabled', async () => {
27
- const gate = new AgentTeamGate({ enabled: false });
28
- const failures = await gate.run({ cwd: testDir });
29
- expect(failures).toEqual([]);
30
- });
31
- });
32
-
33
- describe('session management', () => {
34
- it('should register an agent', () => {
35
- const session = registerAgent(testDir, 'agent-a', ['src/api/**']);
36
- expect(session.agents).toHaveLength(1);
37
- expect(session.agents[0].agentId).toBe('agent-a');
38
- expect(session.agents[0].taskScope).toEqual(['src/api/**']);
39
- });
40
-
41
- it('should update existing agent registration', () => {
42
- registerAgent(testDir, 'agent-a', ['src/api/**']);
43
- const session = registerAgent(testDir, 'agent-a', ['src/api/**', 'src/utils/**']);
44
- expect(session.agents).toHaveLength(1);
45
- expect(session.agents[0].taskScope).toEqual(['src/api/**', 'src/utils/**']);
46
- });
47
-
48
- it('should persist session to disk', () => {
49
- registerAgent(testDir, 'agent-a', ['src/**']);
50
- const sessionPath = path.join(testDir, '.rigour', 'agent-session.json');
51
- expect(fs.existsSync(sessionPath)).toBe(true);
52
- });
53
-
54
- it('should load session from disk', () => {
55
- registerAgent(testDir, 'agent-a', ['src/**']);
56
- // Clear in-memory cache
57
- clearSession(testDir);
58
- // Re-register to re-create session file
59
- registerAgent(testDir, 'agent-b', ['tests/**']);
60
-
61
- const session = getSession(testDir);
62
- expect(session).not.toBeNull();
63
- expect(session!.agents).toHaveLength(1); // Only agent-b after clearSession
64
- });
65
-
66
- it('should clear session', () => {
67
- registerAgent(testDir, 'agent-a', ['src/**']);
68
- clearSession(testDir);
69
- const session = getSession(testDir);
70
- expect(session).toBeNull();
71
- });
72
- });
73
-
74
- describe('max concurrent agents check', () => {
75
- it('should pass when under limit', async () => {
76
- const gate = new AgentTeamGate({ enabled: true, max_concurrent_agents: 3 });
77
- registerAgent(testDir, 'agent-a', ['src/a/**']);
78
- registerAgent(testDir, 'agent-b', ['src/b/**']);
79
-
80
- const failures = await gate.run({ cwd: testDir });
81
- expect(failures).toEqual([]);
82
- });
83
-
84
- it('should fail when over limit', async () => {
85
- const gate = new AgentTeamGate({ enabled: true, max_concurrent_agents: 2 });
86
- registerAgent(testDir, 'agent-a', ['src/a/**']);
87
- registerAgent(testDir, 'agent-b', ['src/b/**']);
88
- registerAgent(testDir, 'agent-c', ['src/c/**']);
89
-
90
- const failures = await gate.run({ cwd: testDir });
91
- expect(failures).toHaveLength(1);
92
- expect(failures[0].title).toBe('Too Many Concurrent Agents');
93
- });
94
- });
95
-
96
- describe('task scope conflicts (strict mode)', () => {
97
- it('should pass when scopes are disjoint', async () => {
98
- const gate = new AgentTeamGate({
99
- enabled: true,
100
- task_ownership: 'strict'
101
- });
102
- registerAgent(testDir, 'agent-a', ['src/api/**']);
103
- registerAgent(testDir, 'agent-b', ['src/ui/**']);
104
-
105
- const failures = await gate.run({ cwd: testDir });
106
- expect(failures).toEqual([]);
107
- });
108
-
109
- it('should fail when scopes overlap', async () => {
110
- const gate = new AgentTeamGate({
111
- enabled: true,
112
- task_ownership: 'strict'
113
- });
114
- registerAgent(testDir, 'agent-a', ['src/api/**']);
115
- registerAgent(testDir, 'agent-b', ['src/api/**']); // Same scope!
116
-
117
- const failures = await gate.run({ cwd: testDir });
118
- expect(failures).toHaveLength(1);
119
- expect(failures[0].title).toBe('Task Scope Conflict');
120
- });
121
-
122
- it('should allow overlapping scopes in collaborative mode', async () => {
123
- const gate = new AgentTeamGate({
124
- enabled: true,
125
- task_ownership: 'collaborative'
126
- });
127
- registerAgent(testDir, 'agent-a', ['src/api/**']);
128
- registerAgent(testDir, 'agent-b', ['src/api/**']); // Same scope - OK in collaborative
129
-
130
- const failures = await gate.run({ cwd: testDir });
131
- expect(failures).toEqual([]);
132
- });
133
- });
134
- });
@@ -1,210 +0,0 @@
1
- /**
2
- * Agent Team Governance Gate
3
- *
4
- * Supervises multi-agent coordination for frontier models like
5
- * Opus 4.6 (agent teams) and GPT-5.3-Codex (coworking mode).
6
- *
7
- * Detects:
8
- * - Cross-agent pattern conflicts
9
- * - Task scope violations
10
- * - Handoff context loss
11
- *
12
- * @since v2.14.0
13
- */
14
-
15
- import { Gate, GateContext } from './base.js';
16
- import { Failure } from '../types/index.js';
17
- import { Logger } from '../utils/logger.js';
18
- import * as fs from 'fs';
19
- import * as path from 'path';
20
-
21
- export interface AgentRegistration {
22
- agentId: string;
23
- taskScope: string[]; // Glob patterns for files this agent owns
24
- registeredAt: Date;
25
- lastActivity?: Date;
26
- }
27
-
28
- export interface AgentTeamSession {
29
- sessionId: string;
30
- agents: AgentRegistration[];
31
- startedAt: Date;
32
- }
33
-
34
- export interface AgentTeamConfig {
35
- enabled?: boolean;
36
- max_concurrent_agents?: number;
37
- cross_agent_pattern_check?: boolean;
38
- handoff_verification?: boolean;
39
- task_ownership?: 'strict' | 'collaborative';
40
- }
41
-
42
- // In-memory session store (persisted to .rigour/agent-session.json)
43
- let currentSession: AgentTeamSession | null = null;
44
-
45
- /**
46
- * Register an agent in the current session
47
- */
48
- export function registerAgent(cwd: string, agentId: string, taskScope: string[]): AgentTeamSession {
49
- if (!currentSession) {
50
- currentSession = {
51
- sessionId: `session-${Date.now()}`,
52
- agents: [],
53
- startedAt: new Date(),
54
- };
55
- }
56
-
57
- // Check if agent already registered
58
- const existing = currentSession.agents.find(a => a.agentId === agentId);
59
- if (existing) {
60
- existing.taskScope = taskScope;
61
- existing.lastActivity = new Date();
62
- } else {
63
- currentSession.agents.push({
64
- agentId,
65
- taskScope,
66
- registeredAt: new Date(),
67
- });
68
- }
69
-
70
- // Persist session
71
- persistSession(cwd);
72
- return currentSession;
73
- }
74
-
75
- /**
76
- * Get current session status
77
- */
78
- export function getSession(cwd: string): AgentTeamSession | null {
79
- if (!currentSession) {
80
- loadSession(cwd);
81
- }
82
- return currentSession;
83
- }
84
-
85
- /**
86
- * Clear current session
87
- */
88
- export function clearSession(cwd: string): void {
89
- currentSession = null;
90
- const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
91
- if (fs.existsSync(sessionPath)) {
92
- fs.unlinkSync(sessionPath);
93
- }
94
- }
95
-
96
- function persistSession(cwd: string): void {
97
- const rigourDir = path.join(cwd, '.rigour');
98
- if (!fs.existsSync(rigourDir)) {
99
- fs.mkdirSync(rigourDir, { recursive: true });
100
- }
101
- const sessionPath = path.join(rigourDir, 'agent-session.json');
102
- fs.writeFileSync(sessionPath, JSON.stringify(currentSession, null, 2));
103
- }
104
-
105
- function loadSession(cwd: string): void {
106
- const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
107
- if (fs.existsSync(sessionPath)) {
108
- try {
109
- const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
110
- currentSession = {
111
- ...data,
112
- startedAt: new Date(data.startedAt),
113
- agents: data.agents.map((a: any) => ({
114
- ...a,
115
- registeredAt: new Date(a.registeredAt),
116
- lastActivity: a.lastActivity ? new Date(a.lastActivity) : undefined,
117
- })),
118
- };
119
- } catch (err) {
120
- Logger.warn('Failed to load agent session, starting fresh');
121
- currentSession = null;
122
- }
123
- }
124
- }
125
-
126
- /**
127
- * Check if two glob patterns might overlap
128
- */
129
- function scopesOverlap(scope1: string[], scope2: string[]): string[] {
130
- const overlapping: string[] = [];
131
- for (const s1 of scope1) {
132
- for (const s2 of scope2) {
133
- // Simple overlap detection - same path or one is prefix of other
134
- if (s1 === s2 || s1.startsWith(s2.replace('**', '')) || s2.startsWith(s1.replace('**', ''))) {
135
- overlapping.push(`${s1} ↔ ${s2}`);
136
- }
137
- }
138
- }
139
- return overlapping;
140
- }
141
-
142
- export class AgentTeamGate extends Gate {
143
- private config: AgentTeamConfig;
144
-
145
- constructor(config: AgentTeamConfig = {}) {
146
- super('agent-team', 'Agent Team Governance');
147
- this.config = {
148
- enabled: config.enabled ?? false,
149
- max_concurrent_agents: config.max_concurrent_agents ?? 3,
150
- cross_agent_pattern_check: config.cross_agent_pattern_check ?? true,
151
- handoff_verification: config.handoff_verification ?? true,
152
- task_ownership: config.task_ownership ?? 'strict',
153
- };
154
- }
155
-
156
- async run(context: GateContext): Promise<Failure[]> {
157
- if (!this.config.enabled) {
158
- return [];
159
- }
160
-
161
- const failures: Failure[] = [];
162
- const session = getSession(context.cwd);
163
-
164
- if (!session || session.agents.length === 0) {
165
- // No multi-agent session active, skip
166
- return [];
167
- }
168
-
169
- Logger.info(`Agent Team Gate: ${session.agents.length} agents in session`);
170
-
171
- // Check 1: Max concurrent agents
172
- if (session.agents.length > (this.config.max_concurrent_agents ?? 3)) {
173
- failures.push(this.createFailure(
174
- `Too many concurrent agents: ${session.agents.length} (max: ${this.config.max_concurrent_agents})`,
175
- undefined,
176
- 'Reduce the number of concurrent agents or increase max_concurrent_agents in rigour.yml',
177
- 'Too Many Concurrent Agents'
178
- ));
179
- }
180
-
181
- // Check 2: Task scope conflicts (strict mode only)
182
- if (this.config.task_ownership === 'strict') {
183
- for (let i = 0; i < session.agents.length; i++) {
184
- for (let j = i + 1; j < session.agents.length; j++) {
185
- const agent1 = session.agents[i];
186
- const agent2 = session.agents[j];
187
- const overlaps = scopesOverlap(agent1.taskScope, agent2.taskScope);
188
-
189
- if (overlaps.length > 0) {
190
- failures.push(this.createFailure(
191
- `Task scope conflict between ${agent1.agentId} and ${agent2.agentId}: ${overlaps.join(', ')}`,
192
- undefined,
193
- 'In strict mode, each agent must have exclusive task scope. Either adjust scopes or set task_ownership: collaborative',
194
- 'Task Scope Conflict'
195
- ));
196
- }
197
- }
198
- }
199
- }
200
-
201
- // Check 3: Cross-agent pattern detection (if enabled)
202
- if (this.config.cross_agent_pattern_check && context.record) {
203
- // This would integrate with the Pattern Index to detect conflicting patterns
204
- // For now, we log that we would do this check
205
- Logger.debug('Cross-agent pattern check: would analyze patterns across agent scopes');
206
- }
207
-
208
- return failures;
209
- }
210
- }
@@ -1,13 +0,0 @@
1
- import { Failure, Gates } from '../../types/index.js';
2
-
3
- export interface ASTHandlerContext {
4
- cwd: string;
5
- file: string;
6
- content: string;
7
- }
8
-
9
- export abstract class ASTHandler {
10
- constructor(protected config: Gates) { }
11
- abstract supports(file: string): boolean;
12
- abstract run(context: ASTHandlerContext): Promise<Failure[]>;
13
- }