agent-gauntlet 0.1.9 → 0.1.10
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/package.json +1 -1
- package/src/commands/init.ts +1 -0
- package/src/core/runner.ts +17 -8
- package/src/gates/result.ts +1 -0
- package/src/gates/review.test.ts +152 -0
- package/src/gates/review.ts +67 -11
- package/src/output/console.ts +26 -10
- package/src/output/logger.ts +4 -2
- package/src/utils/log-parser.ts +2 -2
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -25,6 +25,7 @@ Execute the autonomous verification suite.
|
|
|
25
25
|
- All gates pass
|
|
26
26
|
- You disagree with remaining failures (ask the human how to proceed)
|
|
27
27
|
- Still failing after 3 rerun attempts
|
|
28
|
+
8. Once all gates pass, do NOT commit or push your changes—await the human's review and explicit instruction to commit.
|
|
28
29
|
`;
|
|
29
30
|
|
|
30
31
|
type InstallLevel = 'none' | 'project' | 'user';
|
package/src/core/runner.ts
CHANGED
|
@@ -66,18 +66,19 @@ export class Runner {
|
|
|
66
66
|
if (this.shouldStop) return;
|
|
67
67
|
|
|
68
68
|
this.reporter.onJobStart(job);
|
|
69
|
-
const logPath = this.logger.getLogPath(job.id);
|
|
70
|
-
const jobLogger = await this.logger.createJobLogger(job.id);
|
|
71
69
|
|
|
72
70
|
let result: GateResult;
|
|
73
71
|
|
|
74
72
|
if (job.type === 'check') {
|
|
73
|
+
const logPath = this.logger.getLogPath(job.id);
|
|
74
|
+
const jobLogger = await this.logger.createJobLogger(job.id);
|
|
75
75
|
result = await this.checkExecutor.execute(
|
|
76
76
|
job.id,
|
|
77
77
|
job.gateConfig as any,
|
|
78
78
|
job.workingDirectory,
|
|
79
79
|
jobLogger
|
|
80
80
|
);
|
|
81
|
+
result.logPath = logPath;
|
|
81
82
|
} else {
|
|
82
83
|
// Use sanitized Job ID for lookup because that's what log-parser uses (based on filenames)
|
|
83
84
|
const safeJobId = sanitizeJobId(job.id);
|
|
@@ -95,7 +96,6 @@ export class Runner {
|
|
|
95
96
|
);
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
result.logPath = logPath;
|
|
99
99
|
this.results.push(result);
|
|
100
100
|
this.reporter.onJobComplete(job, result);
|
|
101
101
|
|
|
@@ -159,15 +159,24 @@ export class Runner {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
private async recordPreflightFailure(job: Job, message: string): Promise<GateResult> {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
if (job.type === 'check') {
|
|
163
|
+
const logPath = this.logger.getLogPath(job.id);
|
|
164
|
+
const jobLogger = await this.logger.createJobLogger(job.id);
|
|
165
|
+
await jobLogger(`[${new Date().toISOString()}] Health check failed\n${message}\n`);
|
|
166
|
+
return {
|
|
167
|
+
jobId: job.id,
|
|
168
|
+
status: 'error',
|
|
169
|
+
duration: 0,
|
|
170
|
+
message,
|
|
171
|
+
logPath
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
return {
|
|
166
176
|
jobId: job.id,
|
|
167
177
|
status: 'error',
|
|
168
178
|
duration: 0,
|
|
169
|
-
message
|
|
170
|
-
logPath
|
|
179
|
+
message
|
|
171
180
|
};
|
|
172
181
|
}
|
|
173
182
|
|
package/src/gates/result.ts
CHANGED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ReviewGateExecutor } from './review.js';
|
|
5
|
+
import { Logger } from '../output/logger.js';
|
|
6
|
+
import * as cliAdapters from '../cli-adapters/index.js';
|
|
7
|
+
import type { CLIAdapter } from '../cli-adapters/index.js';
|
|
8
|
+
|
|
9
|
+
const TEST_DIR = path.join(process.cwd(), 'test-review-logs-' + Date.now());
|
|
10
|
+
const LOG_DIR = path.join(TEST_DIR, 'logs');
|
|
11
|
+
|
|
12
|
+
describe('ReviewGateExecutor Logging', () => {
|
|
13
|
+
let logger: Logger;
|
|
14
|
+
let executor: ReviewGateExecutor;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await fs.mkdir(TEST_DIR, { recursive: true });
|
|
18
|
+
await fs.mkdir(LOG_DIR, { recursive: true });
|
|
19
|
+
logger = new Logger(LOG_DIR);
|
|
20
|
+
executor = new ReviewGateExecutor();
|
|
21
|
+
|
|
22
|
+
// Mock getAdapter
|
|
23
|
+
mock.module('../cli-adapters/index.js', () => ({
|
|
24
|
+
getAdapter: (name: string) => ({
|
|
25
|
+
name,
|
|
26
|
+
isAvailable: async () => true,
|
|
27
|
+
checkHealth: async () => ({ status: 'healthy' }),
|
|
28
|
+
// execute returns the raw string output from the LLM, which is then parsed by the executor.
|
|
29
|
+
// The real adapter returns a string. In this test, we return a JSON string to simulate
|
|
30
|
+
// the LLM returning structured data. This IS intentional and matches the expected contract
|
|
31
|
+
// where execute() -> Promise<string>.
|
|
32
|
+
execute: async () => {
|
|
33
|
+
await new Promise(r => setTimeout(r, 1)); // Simulate async work
|
|
34
|
+
return JSON.stringify({ status: 'pass', message: 'OK' });
|
|
35
|
+
},
|
|
36
|
+
getProjectCommandDir: () => null,
|
|
37
|
+
getUserCommandDir: () => null,
|
|
38
|
+
getCommandExtension: () => 'md',
|
|
39
|
+
canUseSymlink: () => false,
|
|
40
|
+
transformCommand: (c: string) => c
|
|
41
|
+
} as unknown as CLIAdapter)
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mock git commands via util.promisify(exec)
|
|
45
|
+
mock.module('node:util', () => ({
|
|
46
|
+
promisify: (fn: Function) => {
|
|
47
|
+
// Only mock exec, let others pass (though in this test env we likely only use exec)
|
|
48
|
+
if (fn.name === 'exec') {
|
|
49
|
+
return async (cmd: string) => {
|
|
50
|
+
if (/^git diff/.test(cmd)) return 'diff content';
|
|
51
|
+
if (/^git ls-files/.test(cmd)) return 'file.ts';
|
|
52
|
+
return { stdout: '', stderr: '' };
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Fallback for other functions if needed
|
|
56
|
+
return async () => {};
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(async () => {
|
|
62
|
+
await fs.rm(TEST_DIR, { recursive: true, force: true });
|
|
63
|
+
mock.restore();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should only create adapter-specific logs and no generic log', async () => {
|
|
67
|
+
const jobId = 'review:src:code-quality';
|
|
68
|
+
const config = {
|
|
69
|
+
name: 'code-quality',
|
|
70
|
+
cli_preference: ['codex', 'claude'],
|
|
71
|
+
num_reviews: 2
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const loggerFactory = logger.createLoggerFactory(jobId);
|
|
75
|
+
|
|
76
|
+
// We need to mock getDiff since it uses execAsync which we mocked
|
|
77
|
+
// Actually ReviewGateExecutor is a class, we can mock its private method if needed
|
|
78
|
+
// or just let it run if the mock promisify works.
|
|
79
|
+
|
|
80
|
+
const result = await executor.execute(
|
|
81
|
+
jobId,
|
|
82
|
+
config as any,
|
|
83
|
+
'src/',
|
|
84
|
+
loggerFactory,
|
|
85
|
+
'main'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(result.status).toBe('pass');
|
|
89
|
+
expect(result.logPaths).toBeDefined();
|
|
90
|
+
expect(result.logPaths).toHaveLength(2);
|
|
91
|
+
expect(result.logPaths?.[0]).toContain('review_src_code-quality_codex.log');
|
|
92
|
+
expect(result.logPaths?.[1]).toContain('review_src_code-quality_claude.log');
|
|
93
|
+
|
|
94
|
+
const files = await fs.readdir(LOG_DIR);
|
|
95
|
+
expect(files).toContain('review_src_code-quality_codex.log');
|
|
96
|
+
expect(files).toContain('review_src_code-quality_claude.log');
|
|
97
|
+
expect(files).not.toContain('review_src_code-quality.log');
|
|
98
|
+
|
|
99
|
+
// Verify multiplexed content
|
|
100
|
+
const codexLog = await fs.readFile(path.join(LOG_DIR, 'review_src_code-quality_codex.log'), 'utf-8');
|
|
101
|
+
expect(codexLog).toContain('Starting review: code-quality');
|
|
102
|
+
expect(codexLog).toContain('Review result (codex): pass');
|
|
103
|
+
|
|
104
|
+
const claudeLog = await fs.readFile(path.join(LOG_DIR, 'review_src_code-quality_claude.log'), 'utf-8');
|
|
105
|
+
expect(claudeLog).toContain('Starting review: code-quality');
|
|
106
|
+
expect(claudeLog).toContain('Review result (claude): pass');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should be handled correctly by ConsoleReporter', async () => {
|
|
110
|
+
const jobId = 'review:src:code-quality';
|
|
111
|
+
const codexPath = path.join(LOG_DIR, 'review_src_code-quality_codex.log');
|
|
112
|
+
const claudePath = path.join(LOG_DIR, 'review_src_code-quality_claude.log');
|
|
113
|
+
|
|
114
|
+
await fs.writeFile(codexPath, `
|
|
115
|
+
[2026-01-14T10:00:00.000Z] Starting review: code-quality
|
|
116
|
+
--- Parsed Result (codex) ---
|
|
117
|
+
Status: FAIL
|
|
118
|
+
Violations:
|
|
119
|
+
1. src/index.ts:10 - Security risk
|
|
120
|
+
Fix: Use a safer method
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
await fs.writeFile(claudePath, `
|
|
124
|
+
[2026-01-14T10:00:00.000Z] Starting review: code-quality
|
|
125
|
+
--- Parsed Result (claude) ---
|
|
126
|
+
Status: FAIL
|
|
127
|
+
Violations:
|
|
128
|
+
1. src/main.ts:20 - Style issue
|
|
129
|
+
Fix: Rename variable
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
const result = {
|
|
133
|
+
jobId,
|
|
134
|
+
status: 'fail' as const,
|
|
135
|
+
duration: 1000,
|
|
136
|
+
message: 'Found violations',
|
|
137
|
+
logPaths: [codexPath, claudePath]
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const { ConsoleReporter } = await import('../output/console.js');
|
|
141
|
+
const reporter = new ConsoleReporter();
|
|
142
|
+
|
|
143
|
+
// We can access extractFailureDetails directly as it is public
|
|
144
|
+
const details = await reporter.extractFailureDetails(result);
|
|
145
|
+
|
|
146
|
+
// Check for presence of key information rather than exact counts
|
|
147
|
+
expect(details.some((d: string) => d.includes('src/index.ts') && d.includes('10') && d.includes('Security risk'))).toBe(true);
|
|
148
|
+
expect(details.some((d: string) => d.includes('Use a safer method'))).toBe(true);
|
|
149
|
+
expect(details.some((d: string) => d.includes('src/main.ts') && d.includes('20') && d.includes('Style issue'))).toBe(true);
|
|
150
|
+
expect(details.some((d: string) => d.includes('Rename variable'))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/gates/review.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type PreviousViolation } from '../utils/log-parser.js';
|
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
11
|
|
|
12
12
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
13
|
+
const MAX_LOG_BUFFER_SIZE = 10000;
|
|
13
14
|
|
|
14
15
|
const JSON_SYSTEM_INSTRUCTION = `
|
|
15
16
|
You are in a read-only mode. You may read files in the repository to gather context.
|
|
@@ -69,14 +70,64 @@ export class ReviewGateExecutor {
|
|
|
69
70
|
jobId: string,
|
|
70
71
|
config: ReviewConfig,
|
|
71
72
|
entryPointPath: string,
|
|
72
|
-
loggerFactory: (adapterName?: string) => Promise<(output: string) => Promise<void
|
|
73
|
+
loggerFactory: (adapterName?: string) => Promise<{ logger: (output: string) => Promise<void>; logPath: string }>,
|
|
73
74
|
baseBranch: string,
|
|
74
75
|
previousFailures?: Map<string, PreviousViolation[]>,
|
|
75
76
|
changeOptions?: { commit?: string; uncommitted?: boolean },
|
|
76
77
|
checkUsageLimit: boolean = false
|
|
77
78
|
): Promise<GateResult> {
|
|
78
79
|
const startTime = Date.now();
|
|
79
|
-
const
|
|
80
|
+
const logBuffer: string[] = [];
|
|
81
|
+
let logSequence = 0; // Monotonic counter for dedup
|
|
82
|
+
const activeLoggers: Array<(output: string, index: number) => Promise<void>> = [];
|
|
83
|
+
const logPaths: string[] = [];
|
|
84
|
+
const logPathsSet = new Set<string>(); // O(1) lookup
|
|
85
|
+
|
|
86
|
+
const mainLogger = async (output: string) => {
|
|
87
|
+
const seq = logSequence++;
|
|
88
|
+
// Atomic length check and push
|
|
89
|
+
// We check length directly on the array property to ensure we use the current value.
|
|
90
|
+
// Even if we exceed the limit slightly due to concurrency (impossible in single-threaded JS),
|
|
91
|
+
// it's a soft limit.
|
|
92
|
+
if (logBuffer.length < MAX_LOG_BUFFER_SIZE) {
|
|
93
|
+
logBuffer.push(output);
|
|
94
|
+
}
|
|
95
|
+
// Use allSettled to prevent failures from stopping the main logger
|
|
96
|
+
await Promise.allSettled(activeLoggers.map(l => l(output, seq)));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getAdapterLogger = async (adapterName: string) => {
|
|
100
|
+
const { logger, logPath } = await loggerFactory(adapterName);
|
|
101
|
+
if (!logPathsSet.has(logPath)) {
|
|
102
|
+
logPathsSet.add(logPath);
|
|
103
|
+
logPaths.push(logPath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Robust synchronization using index tracking.
|
|
107
|
+
// We add the logger to activeLoggers FIRST to catch all future messages.
|
|
108
|
+
// We also flush the buffer.
|
|
109
|
+
// We use 'seenIndices' to prevent duplicates if a message arrives via both paths
|
|
110
|
+
// (e.g. added to buffer and sent to activeLoggers simultaneously).
|
|
111
|
+
// This acts as the atomic counter mechanism requested to safely handle race conditions.
|
|
112
|
+
// Even if mainLogger pushes to buffer and calls activeLoggers during the snapshot flush,
|
|
113
|
+
// seenIndices will prevent double logging.
|
|
114
|
+
const seenIndices = new Set<number>();
|
|
115
|
+
|
|
116
|
+
const safeLogger = async (msg: string, index: number) => {
|
|
117
|
+
if (seenIndices.has(index)) return;
|
|
118
|
+
seenIndices.add(index);
|
|
119
|
+
await logger(msg);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
activeLoggers.push(safeLogger);
|
|
123
|
+
|
|
124
|
+
// Flush existing buffer
|
|
125
|
+
const snapshot = [...logBuffer];
|
|
126
|
+
// We pass the loop index 'i' which corresponds to the buffer index
|
|
127
|
+
await Promise.all(snapshot.map((msg, i) => safeLogger(msg, i)));
|
|
128
|
+
|
|
129
|
+
return logger;
|
|
130
|
+
};
|
|
80
131
|
|
|
81
132
|
try {
|
|
82
133
|
await mainLogger(`Starting review: ${config.name}\n`);
|
|
@@ -91,7 +142,8 @@ export class ReviewGateExecutor {
|
|
|
91
142
|
jobId,
|
|
92
143
|
status: 'pass',
|
|
93
144
|
duration: Date.now() - startTime,
|
|
94
|
-
message: 'No changes to review'
|
|
145
|
+
message: 'No changes to review',
|
|
146
|
+
logPaths
|
|
95
147
|
};
|
|
96
148
|
}
|
|
97
149
|
|
|
@@ -138,7 +190,8 @@ export class ReviewGateExecutor {
|
|
|
138
190
|
jobId,
|
|
139
191
|
status: 'error',
|
|
140
192
|
duration: Date.now() - startTime,
|
|
141
|
-
message: msg
|
|
193
|
+
message: msg,
|
|
194
|
+
logPaths
|
|
142
195
|
};
|
|
143
196
|
}
|
|
144
197
|
|
|
@@ -148,7 +201,7 @@ export class ReviewGateExecutor {
|
|
|
148
201
|
|
|
149
202
|
const results = await Promise.all(
|
|
150
203
|
selectedAdapters.map((toolName) =>
|
|
151
|
-
this.runSingleReview(toolName, config, diff,
|
|
204
|
+
this.runSingleReview(toolName, config, diff, getAdapterLogger, mainLogger, previousFailures, true, checkUsageLimit)
|
|
152
205
|
)
|
|
153
206
|
);
|
|
154
207
|
|
|
@@ -162,7 +215,7 @@ export class ReviewGateExecutor {
|
|
|
162
215
|
// Sequential Execution Logic
|
|
163
216
|
for (const toolName of preferences) {
|
|
164
217
|
if (usedAdapters.size >= required) break;
|
|
165
|
-
const res = await this.runSingleReview(toolName, config, diff,
|
|
218
|
+
const res = await this.runSingleReview(toolName, config, diff, getAdapterLogger, mainLogger, previousFailures, false, checkUsageLimit);
|
|
166
219
|
if (res) {
|
|
167
220
|
outputs.push({ adapter: res.adapter, ...res.evaluation });
|
|
168
221
|
usedAdapters.add(res.adapter);
|
|
@@ -177,7 +230,8 @@ export class ReviewGateExecutor {
|
|
|
177
230
|
jobId,
|
|
178
231
|
status: 'error',
|
|
179
232
|
duration: Date.now() - startTime,
|
|
180
|
-
message: msg
|
|
233
|
+
message: msg,
|
|
234
|
+
logPaths
|
|
181
235
|
};
|
|
182
236
|
}
|
|
183
237
|
|
|
@@ -201,7 +255,8 @@ export class ReviewGateExecutor {
|
|
|
201
255
|
jobId,
|
|
202
256
|
status,
|
|
203
257
|
duration: Date.now() - startTime,
|
|
204
|
-
message
|
|
258
|
+
message,
|
|
259
|
+
logPaths
|
|
205
260
|
};
|
|
206
261
|
} catch (error: any) {
|
|
207
262
|
await mainLogger(`Critical Error: ${error.message}\n`);
|
|
@@ -210,7 +265,8 @@ export class ReviewGateExecutor {
|
|
|
210
265
|
jobId,
|
|
211
266
|
status: 'error',
|
|
212
267
|
duration: Date.now() - startTime,
|
|
213
|
-
message: error.message
|
|
268
|
+
message: error.message,
|
|
269
|
+
logPaths
|
|
214
270
|
};
|
|
215
271
|
}
|
|
216
272
|
}
|
|
@@ -219,7 +275,7 @@ export class ReviewGateExecutor {
|
|
|
219
275
|
toolName: string,
|
|
220
276
|
config: ReviewConfig,
|
|
221
277
|
diff: string,
|
|
222
|
-
|
|
278
|
+
getAdapterLogger: (adapterName: string) => Promise<(output: string) => Promise<void>>,
|
|
223
279
|
mainLogger: (output: string) => Promise<void>,
|
|
224
280
|
previousFailures?: Map<string, PreviousViolation[]>,
|
|
225
281
|
skipHealthCheck: boolean = false,
|
|
@@ -238,7 +294,7 @@ export class ReviewGateExecutor {
|
|
|
238
294
|
}
|
|
239
295
|
|
|
240
296
|
// Create per-adapter logger
|
|
241
|
-
const adapterLogger = await
|
|
297
|
+
const adapterLogger = await getAdapterLogger(adapter.name);
|
|
242
298
|
|
|
243
299
|
try {
|
|
244
300
|
const startMsg = `[START] review:.:${config.name} (${adapter.name})`;
|
package/src/output/console.ts
CHANGED
|
@@ -43,17 +43,26 @@ export class ConsoleReporter {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
/** @internal Public for testing */
|
|
47
|
+
async extractFailureDetails(result: GateResult): Promise<string[]> {
|
|
48
|
+
const logPaths = result.logPaths || (result.logPath ? [result.logPath] : []);
|
|
49
|
+
|
|
50
|
+
if (logPaths.length === 0) {
|
|
48
51
|
return [result.message ?? 'Unknown error'];
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const allDetails: string[] = [];
|
|
55
|
+
for (const logPath of logPaths) {
|
|
56
|
+
try {
|
|
57
|
+
const logContent = await fs.readFile(logPath, 'utf-8');
|
|
58
|
+
const details = this.parseLogContent(logContent, result.jobId);
|
|
59
|
+
allDetails.push(...details);
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
allDetails.push(`(Could not read log file: ${logPath})`);
|
|
62
|
+
}
|
|
56
63
|
}
|
|
64
|
+
|
|
65
|
+
return allDetails.length > 0 ? allDetails : [result.message ?? 'Unknown error'];
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
private parseLogContent(logContent: string, jobId: string): string[] {
|
|
@@ -63,8 +72,13 @@ export class ConsoleReporter {
|
|
|
63
72
|
// Check if this is a review log
|
|
64
73
|
if (jobId.startsWith('review:')) {
|
|
65
74
|
// Look for parsed violations section (formatted output)
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
// Use regex to be flexible about adapter name in parentheses
|
|
76
|
+
// Matches: "--- Parsed Result ---" or "--- Parsed Result (adapter) ---"
|
|
77
|
+
const parsedResultRegex = /---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---/;
|
|
78
|
+
const match = logContent.match(parsedResultRegex);
|
|
79
|
+
|
|
80
|
+
if (match && match.index !== undefined) {
|
|
81
|
+
const violationsStart = match.index;
|
|
68
82
|
const violationsSection = logContent.substring(violationsStart);
|
|
69
83
|
const sectionLines = violationsSection.split('\n');
|
|
70
84
|
|
|
@@ -192,7 +206,9 @@ export class ConsoleReporter {
|
|
|
192
206
|
details.forEach(detail => console.log(detail));
|
|
193
207
|
}
|
|
194
208
|
|
|
195
|
-
if (result.
|
|
209
|
+
if (result.logPaths && result.logPaths.length > 0) {
|
|
210
|
+
result.logPaths.forEach(p => console.log(chalk.dim(` Log: ${p}`)));
|
|
211
|
+
} else if (result.logPath) {
|
|
196
212
|
console.log(chalk.dim(` Log: ${result.logPath}`));
|
|
197
213
|
}
|
|
198
214
|
|
package/src/output/logger.ts
CHANGED
|
@@ -48,12 +48,12 @@ export class Logger {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
createLoggerFactory(jobId: string): (adapterName?: string) => Promise<(text: string) => Promise<void
|
|
51
|
+
createLoggerFactory(jobId: string): (adapterName?: string) => Promise<{ logger: (text: string) => Promise<void>; logPath: string }> {
|
|
52
52
|
return async (adapterName?: string) => {
|
|
53
53
|
const logPath = this.getLogPath(jobId, adapterName);
|
|
54
54
|
await this.initFile(logPath);
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
const logger = async (text: string) => {
|
|
57
57
|
const timestamp = formatTimestamp();
|
|
58
58
|
const lines = text.split('\n');
|
|
59
59
|
if (lines.length > 0) {
|
|
@@ -61,6 +61,8 @@ export class Logger {
|
|
|
61
61
|
}
|
|
62
62
|
await fs.appendFile(logPath, lines.join('\n') + (text.endsWith('\n') ? '' : '\n'));
|
|
63
63
|
};
|
|
64
|
+
|
|
65
|
+
return { logger, logPath };
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
}
|
package/src/utils/log-parser.ts
CHANGED
|
@@ -71,10 +71,10 @@ export async function parseLogFile(logPath: string): Promise<GateFailures | null
|
|
|
71
71
|
const violations: PreviousViolation[] = [];
|
|
72
72
|
|
|
73
73
|
// 1. Look for "--- Parsed Result ---"
|
|
74
|
-
const parsedResultMatch = sectionContent.match(
|
|
74
|
+
const parsedResultMatch = sectionContent.match(/---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---([\s\S]*?)(?:$|---)/);
|
|
75
75
|
|
|
76
76
|
if (parsedResultMatch) {
|
|
77
|
-
const parsedContent = parsedResultMatch[
|
|
77
|
+
const parsedContent = parsedResultMatch[2];
|
|
78
78
|
|
|
79
79
|
// Check status
|
|
80
80
|
if (parsedContent.includes('Status: PASS')) {
|