agent-gauntlet 0.10.0 → 0.11.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.
- package/README.md +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
package/src/output/console.ts
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import type { Job } from "../core/job.js";
|
|
4
|
-
import type { GateResult } from "../gates/result.js";
|
|
5
|
-
import { reconstructHistory } from "../utils/log-parser.js";
|
|
6
|
-
|
|
7
|
-
export class ConsoleReporter {
|
|
8
|
-
onJobStart(job: Job) {
|
|
9
|
-
console.error(chalk.blue(`[START] ${job.id}`));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
onJobComplete(job: Job, result: GateResult) {
|
|
13
|
-
const duration = `${(result.duration / 1000).toFixed(2)}s`;
|
|
14
|
-
|
|
15
|
-
const message = result.message ?? "";
|
|
16
|
-
|
|
17
|
-
if (result.subResults && result.subResults.length > 0) {
|
|
18
|
-
// Print split results
|
|
19
|
-
|
|
20
|
-
for (const sub of result.subResults) {
|
|
21
|
-
const statusColor =
|
|
22
|
-
sub.status === "pass"
|
|
23
|
-
? chalk.green
|
|
24
|
-
: sub.status === "fail"
|
|
25
|
-
? chalk.red
|
|
26
|
-
: chalk.magenta;
|
|
27
|
-
|
|
28
|
-
const label =
|
|
29
|
-
sub.status === "pass"
|
|
30
|
-
? "PASS"
|
|
31
|
-
: sub.status === "fail"
|
|
32
|
-
? "FAIL"
|
|
33
|
-
: "ERROR";
|
|
34
|
-
|
|
35
|
-
let logInfo = "";
|
|
36
|
-
|
|
37
|
-
if (sub.status !== "pass" && sub.logPath) {
|
|
38
|
-
// Prefer JSON if it exists for reviews
|
|
39
|
-
|
|
40
|
-
const displayLog = sub.logPath;
|
|
41
|
-
|
|
42
|
-
const logPrefix = displayLog.endsWith(".json") ? "Review:" : "Log:";
|
|
43
|
-
|
|
44
|
-
logInfo = `\n ${logPrefix} ${displayLog}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
console.error(
|
|
48
|
-
statusColor(
|
|
49
|
-
`[${label}] ${job.id} ${chalk.dim(sub.nameSuffix)} (${duration}) - ${sub.message}${logInfo}`,
|
|
50
|
-
),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
// Standard single result
|
|
55
|
-
let logInfo = "";
|
|
56
|
-
if (result.status !== "pass") {
|
|
57
|
-
// Try to find a relevant log path
|
|
58
|
-
const logPath = result.logPath || result.logPaths?.[0];
|
|
59
|
-
if (logPath) {
|
|
60
|
-
logInfo = `\n Log: ${logPath}`;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (result.status === "pass") {
|
|
65
|
-
console.error(chalk.green(`[PASS] ${job.id} (${duration})`));
|
|
66
|
-
} else if (result.status === "fail") {
|
|
67
|
-
console.error(
|
|
68
|
-
chalk.red(`[FAIL] ${job.id} (${duration}) - ${message}${logInfo}`),
|
|
69
|
-
);
|
|
70
|
-
} else {
|
|
71
|
-
console.error(
|
|
72
|
-
chalk.magenta(
|
|
73
|
-
`[ERROR] ${job.id} (${duration}) - ${message}${logInfo}`,
|
|
74
|
-
),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async printSummary(
|
|
81
|
-
results: GateResult[],
|
|
82
|
-
logDir?: string,
|
|
83
|
-
statusOverride?: string,
|
|
84
|
-
) {
|
|
85
|
-
console.error(
|
|
86
|
-
`\n${chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`,
|
|
87
|
-
);
|
|
88
|
-
console.error(chalk.bold("RESULTS SUMMARY"));
|
|
89
|
-
console.error(chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
90
|
-
|
|
91
|
-
if (logDir) {
|
|
92
|
-
try {
|
|
93
|
-
const history = await reconstructHistory(logDir);
|
|
94
|
-
for (const iter of history) {
|
|
95
|
-
if (iter.fixed.length === 0 && iter.skipped.length === 0) continue;
|
|
96
|
-
|
|
97
|
-
console.error(`\nIteration ${iter.iteration}:`);
|
|
98
|
-
for (const f of iter.fixed) {
|
|
99
|
-
const label = f.adapter ? `${f.jobId} (${f.adapter})` : f.jobId;
|
|
100
|
-
console.error(chalk.green(` ✓ Fixed: ${label} - ${f.details}`));
|
|
101
|
-
}
|
|
102
|
-
for (const s of iter.skipped) {
|
|
103
|
-
const label = s.adapter ? `${s.jobId} (${s.adapter})` : s.jobId;
|
|
104
|
-
console.error(
|
|
105
|
-
chalk.yellow(
|
|
106
|
-
` ⊘ Skipped: ${label} - ${s.file}:${s.line} ${s.issue}`,
|
|
107
|
-
),
|
|
108
|
-
);
|
|
109
|
-
if (s.result) {
|
|
110
|
-
console.error(chalk.dim(` Reason: ${s.result}`));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const totalFixed = history.reduce(
|
|
116
|
-
(sum, iter) => sum + iter.fixed.length,
|
|
117
|
-
0,
|
|
118
|
-
);
|
|
119
|
-
const totalSkipped = history.reduce(
|
|
120
|
-
(sum, iter) => sum + iter.skipped.length,
|
|
121
|
-
0,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
let totalFailed = 0;
|
|
125
|
-
for (const res of results) {
|
|
126
|
-
if (res.subResults && res.subResults.length > 0) {
|
|
127
|
-
for (const sub of res.subResults) {
|
|
128
|
-
if (sub.status === "fail" || sub.status === "error") {
|
|
129
|
-
totalFailed += sub.errorCount ?? 1;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} else if (res.status === "fail" || res.status === "error") {
|
|
133
|
-
totalFailed += res.errorCount ?? 1;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
console.error(
|
|
138
|
-
`\n${chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}`,
|
|
139
|
-
);
|
|
140
|
-
const iterationsText =
|
|
141
|
-
history.length > 1 ? ` after ${history.length} iterations` : "";
|
|
142
|
-
console.error(
|
|
143
|
-
`Total: ${totalFixed} fixed, ${totalSkipped} skipped, ${totalFailed} failed${iterationsText}`,
|
|
144
|
-
);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
console.warn(
|
|
147
|
-
chalk.yellow(`Warning: Failed to reconstruct history: ${err}`),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const failed = results.filter((r) => r.status === "fail");
|
|
153
|
-
const errored = results.filter((r) => r.status === "error");
|
|
154
|
-
const anySkipped = results.some((r) => r.skipped && r.skipped.length > 0);
|
|
155
|
-
|
|
156
|
-
let overallStatus = "Passed";
|
|
157
|
-
let statusColor = chalk.green;
|
|
158
|
-
|
|
159
|
-
if (statusOverride) {
|
|
160
|
-
overallStatus = statusOverride;
|
|
161
|
-
statusColor = chalk.red;
|
|
162
|
-
} else if (errored.length > 0) {
|
|
163
|
-
overallStatus = "Error";
|
|
164
|
-
statusColor = chalk.magenta;
|
|
165
|
-
} else if (failed.length > 0) {
|
|
166
|
-
overallStatus = "Failed";
|
|
167
|
-
statusColor = chalk.red;
|
|
168
|
-
} else if (anySkipped) {
|
|
169
|
-
overallStatus = "Passed with warnings";
|
|
170
|
-
statusColor = chalk.yellow;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
console.error(statusColor(`Status: ${overallStatus}`));
|
|
174
|
-
console.error(
|
|
175
|
-
chalk.bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"),
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** @internal Public for testing */
|
|
180
|
-
async extractFailureDetails(result: GateResult): Promise<string[]> {
|
|
181
|
-
const logPaths =
|
|
182
|
-
result.logPaths || (result.logPath ? [result.logPath] : []);
|
|
183
|
-
|
|
184
|
-
if (logPaths.length === 0) {
|
|
185
|
-
return [result.message ?? "Unknown error"];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const allDetails: string[] = [];
|
|
189
|
-
for (const logPath of logPaths) {
|
|
190
|
-
try {
|
|
191
|
-
const logContent = await fs.readFile(logPath, "utf-8");
|
|
192
|
-
const details = this.parseLogContent(logContent, result.jobId);
|
|
193
|
-
allDetails.push(...details);
|
|
194
|
-
} catch (_error: unknown) {
|
|
195
|
-
allDetails.push(`(Could not read log file: ${logPath})`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return allDetails.length > 0
|
|
200
|
-
? allDetails
|
|
201
|
-
: [result.message ?? "Unknown error"];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private parseLogContent(logContent: string, jobId: string): string[] {
|
|
205
|
-
const _lines = logContent.split("\n");
|
|
206
|
-
const details: string[] = [];
|
|
207
|
-
|
|
208
|
-
// Check if this is a review log
|
|
209
|
-
if (jobId.startsWith("review:")) {
|
|
210
|
-
// Look for parsed violations section (formatted output)
|
|
211
|
-
// Use regex to be flexible about adapter name in parentheses
|
|
212
|
-
// Matches: "--- Parsed Result ---" or "--- Parsed Result (adapter) ---"
|
|
213
|
-
const parsedResultRegex = /---\s*Parsed Result(?:\s+\(([^)]+)\))?\s*---/;
|
|
214
|
-
const match = logContent.match(parsedResultRegex);
|
|
215
|
-
|
|
216
|
-
if (match && match.index !== undefined) {
|
|
217
|
-
const violationsStart = match.index;
|
|
218
|
-
const violationsSection = logContent.substring(violationsStart);
|
|
219
|
-
const sectionLines = violationsSection.split("\n");
|
|
220
|
-
|
|
221
|
-
for (let i = 0; i < sectionLines.length; i++) {
|
|
222
|
-
const line = sectionLines[i]!;
|
|
223
|
-
// Match numbered violation lines: "1. file:line - issue" (line can be a number or '?')
|
|
224
|
-
const violationMatch = line.match(
|
|
225
|
-
/^\d+\.\s+(.+?):(\d+|\?)\s+-\s+(.+)$/,
|
|
226
|
-
);
|
|
227
|
-
if (violationMatch) {
|
|
228
|
-
const file = violationMatch[1];
|
|
229
|
-
const lineNum = violationMatch[2];
|
|
230
|
-
const issue = violationMatch[3];
|
|
231
|
-
details.push(
|
|
232
|
-
` ${chalk.cyan(file)}:${chalk.yellow(lineNum)} - ${issue}`,
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// Check next line for "Fix:" suggestion
|
|
236
|
-
if (i + 1 < sectionLines.length) {
|
|
237
|
-
const nextLine = sectionLines[i + 1]!.trim();
|
|
238
|
-
if (nextLine.startsWith("Fix:")) {
|
|
239
|
-
const fix = nextLine.substring(4).trim();
|
|
240
|
-
details.push(` ${chalk.dim("Fix:")} ${fix}`);
|
|
241
|
-
i++; // Skip the fix line
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// If no parsed violations, look for JSON violations (handles both minified and pretty-printed)
|
|
249
|
-
if (details.length === 0) {
|
|
250
|
-
// Find the first '{' and last '}' to extract JSON object
|
|
251
|
-
const jsonStart = logContent.indexOf("{");
|
|
252
|
-
const jsonEnd = logContent.lastIndexOf("}");
|
|
253
|
-
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
|
|
254
|
-
try {
|
|
255
|
-
const jsonStr = logContent.substring(jsonStart, jsonEnd + 1);
|
|
256
|
-
const json = JSON.parse(jsonStr);
|
|
257
|
-
if (
|
|
258
|
-
json.status === "fail" &&
|
|
259
|
-
json.violations &&
|
|
260
|
-
Array.isArray(json.violations)
|
|
261
|
-
) {
|
|
262
|
-
json.violations.forEach(
|
|
263
|
-
(v: {
|
|
264
|
-
file?: string;
|
|
265
|
-
line?: number | string;
|
|
266
|
-
issue?: string;
|
|
267
|
-
fix?: string;
|
|
268
|
-
}) => {
|
|
269
|
-
const file = v.file || "unknown";
|
|
270
|
-
const line = v.line || "?";
|
|
271
|
-
const issue = v.issue || "Unknown issue";
|
|
272
|
-
details.push(
|
|
273
|
-
` ${chalk.cyan(file)}:${chalk.yellow(line)} - ${issue}`,
|
|
274
|
-
);
|
|
275
|
-
if (v.fix) {
|
|
276
|
-
details.push(` ${chalk.dim("Fix:")} ${v.fix}`);
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
} catch {
|
|
282
|
-
// JSON parse failed, fall through to other parsing
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// If still no details, look for error messages
|
|
288
|
-
if (details.length === 0) {
|
|
289
|
-
// Try to find the actual error message (first non-empty line after "Error:")
|
|
290
|
-
const errorIndex = logContent.indexOf("Error:");
|
|
291
|
-
if (errorIndex !== -1) {
|
|
292
|
-
const afterError = logContent.substring(errorIndex + 6).trim();
|
|
293
|
-
const firstErrorLine = afterError.split("\n")[0]!.trim();
|
|
294
|
-
if (
|
|
295
|
-
firstErrorLine &&
|
|
296
|
-
!firstErrorLine.startsWith("Usage:") &&
|
|
297
|
-
!firstErrorLine.startsWith("Commands:")
|
|
298
|
-
) {
|
|
299
|
-
details.push(` ${firstErrorLine}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Also check for "Result: error" lines
|
|
304
|
-
if (details.length === 0) {
|
|
305
|
-
const resultMatch = logContent.match(
|
|
306
|
-
/Result:\s*error(?:\s*-\s*(.+?))?(?:\n|$)/,
|
|
307
|
-
);
|
|
308
|
-
if (resultMatch?.[1]) {
|
|
309
|
-
details.push(` ${resultMatch[1]}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
// This is a check log
|
|
315
|
-
// Look for STDERR section
|
|
316
|
-
const stderrStart = logContent.indexOf("STDERR:");
|
|
317
|
-
if (stderrStart !== -1) {
|
|
318
|
-
const stderrSection = logContent.substring(stderrStart + 7).trim();
|
|
319
|
-
const stderrLines = stderrSection.split("\n").filter((line) => {
|
|
320
|
-
// Skip empty lines and command output markers
|
|
321
|
-
return (
|
|
322
|
-
line.trim() &&
|
|
323
|
-
!line.includes("STDOUT:") &&
|
|
324
|
-
!line.includes("Command failed:") &&
|
|
325
|
-
!line.includes("Result:")
|
|
326
|
-
);
|
|
327
|
-
});
|
|
328
|
-
if (stderrLines.length > 0) {
|
|
329
|
-
details.push(
|
|
330
|
-
...stderrLines.slice(0, 10).map((line) => ` ${line.trim()}`),
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// If no STDERR, look for error messages
|
|
336
|
-
if (details.length === 0) {
|
|
337
|
-
const errorMatch = logContent.match(/Command failed:\s*(.+?)(?:\n|$)/);
|
|
338
|
-
if (errorMatch) {
|
|
339
|
-
details.push(` ${errorMatch[1]}`);
|
|
340
|
-
} else {
|
|
341
|
-
// Look for any line with "Result: fail" or "Result: error"
|
|
342
|
-
const resultMatch = logContent.match(
|
|
343
|
-
/Result:\s*(fail|error)\s*-\s*(.+?)(?:\n|$)/,
|
|
344
|
-
);
|
|
345
|
-
if (resultMatch) {
|
|
346
|
-
details.push(` ${resultMatch[2]}`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// If we still have no details, use the message from the result
|
|
353
|
-
if (details.length === 0) {
|
|
354
|
-
details.push(" (See log file for details)");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return details;
|
|
358
|
-
}
|
|
359
|
-
}
|
package/src/output/logger.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { sanitizeJobId } from "../utils/sanitizer.js";
|
|
4
|
-
|
|
5
|
-
function formatTimestamp(): string {
|
|
6
|
-
return new Date().toISOString();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Compute the global run number for the log directory.
|
|
11
|
-
* Finds the highest run-number suffix across ALL log files and returns max+1.
|
|
12
|
-
*/
|
|
13
|
-
async function computeGlobalRunNumber(logDir: string): Promise<number> {
|
|
14
|
-
try {
|
|
15
|
-
const files = await fs.readdir(logDir);
|
|
16
|
-
let max = 0;
|
|
17
|
-
for (const file of files) {
|
|
18
|
-
if (!file.endsWith(".log") && !file.endsWith(".json")) continue;
|
|
19
|
-
// Pattern: <anything>.<number>.(log|json)
|
|
20
|
-
const m = file.match(/\.(\d+)\.(log|json)$/);
|
|
21
|
-
if (m?.[1]) {
|
|
22
|
-
const n = parseInt(m[1], 10);
|
|
23
|
-
if (n > max) max = n;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return max + 1;
|
|
27
|
-
} catch {
|
|
28
|
-
return 1;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class Logger {
|
|
33
|
-
private initializedFiles: Set<string> = new Set();
|
|
34
|
-
private globalRunNumber: number | null = null;
|
|
35
|
-
|
|
36
|
-
constructor(private logDir: string) {}
|
|
37
|
-
|
|
38
|
-
async init() {
|
|
39
|
-
await fs.mkdir(this.logDir, { recursive: true });
|
|
40
|
-
this.globalRunNumber = await computeGlobalRunNumber(this.logDir);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async close() {
|
|
44
|
-
// No-op - using append mode
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
getRunNumber(): number {
|
|
48
|
-
return this.globalRunNumber ?? 1;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async getLogPath(
|
|
52
|
-
jobId: string,
|
|
53
|
-
adapterName?: string,
|
|
54
|
-
reviewIndex?: number,
|
|
55
|
-
): Promise<string> {
|
|
56
|
-
const safeName = sanitizeJobId(jobId);
|
|
57
|
-
const runNum = this.globalRunNumber ?? 1;
|
|
58
|
-
|
|
59
|
-
let filename: string;
|
|
60
|
-
if (adapterName && reviewIndex !== undefined) {
|
|
61
|
-
// Review gate with index: <jobId>_<adapter>@<index>.<runNum>.log
|
|
62
|
-
filename = `${safeName}_${adapterName}@${reviewIndex}.${runNum}.log`;
|
|
63
|
-
} else if (adapterName) {
|
|
64
|
-
// Review gate without explicit index (backwards compat for single review)
|
|
65
|
-
filename = `${safeName}_${adapterName}@1.${runNum}.log`;
|
|
66
|
-
} else {
|
|
67
|
-
// Check gate: <jobId>.<runNum>.log
|
|
68
|
-
filename = `${safeName}.${runNum}.log`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return path.join(this.logDir, filename);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private async initFile(logPath: string): Promise<void> {
|
|
75
|
-
if (this.initializedFiles.has(logPath)) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
this.initializedFiles.add(logPath);
|
|
79
|
-
await fs.writeFile(logPath, "");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async createJobLogger(
|
|
83
|
-
jobId: string,
|
|
84
|
-
): Promise<(text: string) => Promise<void>> {
|
|
85
|
-
const logPath = await this.getLogPath(jobId);
|
|
86
|
-
await this.initFile(logPath);
|
|
87
|
-
|
|
88
|
-
return async (text: string) => {
|
|
89
|
-
const timestamp = formatTimestamp();
|
|
90
|
-
const lines = text.split("\n");
|
|
91
|
-
if (lines.length > 0) {
|
|
92
|
-
lines[0] = `[${timestamp}] ${lines[0]}`;
|
|
93
|
-
}
|
|
94
|
-
await fs.appendFile(
|
|
95
|
-
logPath,
|
|
96
|
-
lines.join("\n") + (text.endsWith("\n") ? "" : "\n"),
|
|
97
|
-
);
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
createLoggerFactory(
|
|
102
|
-
jobId: string,
|
|
103
|
-
): (
|
|
104
|
-
adapterName?: string,
|
|
105
|
-
reviewIndex?: number,
|
|
106
|
-
) => Promise<{ logger: (text: string) => Promise<void>; logPath: string }> {
|
|
107
|
-
return async (adapterName?: string, reviewIndex?: number) => {
|
|
108
|
-
const logPath = await this.getLogPath(jobId, adapterName, reviewIndex);
|
|
109
|
-
await this.initFile(logPath);
|
|
110
|
-
|
|
111
|
-
const logger = async (text: string) => {
|
|
112
|
-
const timestamp = formatTimestamp();
|
|
113
|
-
const lines = text.split("\n");
|
|
114
|
-
if (lines.length > 0) {
|
|
115
|
-
lines[0] = `[${timestamp}] ${lines[0]}`;
|
|
116
|
-
}
|
|
117
|
-
await fs.appendFile(
|
|
118
|
-
logPath,
|
|
119
|
-
lines.join("\n") + (text.endsWith("\n") ? "" : "\n"),
|
|
120
|
-
);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
return { logger, logPath };
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { LogRecord, Sink } from "@logtape/logtape";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Safely serialize a value, handling circular references.
|
|
6
|
-
*/
|
|
7
|
-
function safeStringify(value: unknown): string {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.stringify(value);
|
|
10
|
-
} catch {
|
|
11
|
-
return "[Unserializable]";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Format a log record with chalk colors for console output.
|
|
17
|
-
* Level prefixes: [DEBUG] dim, [INFO] blue, [WARN] yellow, [ERROR] red
|
|
18
|
-
*/
|
|
19
|
-
function formatLogRecord(record: LogRecord): string {
|
|
20
|
-
const level = record.level.toUpperCase();
|
|
21
|
-
const category = record.category.join(".");
|
|
22
|
-
const message = record.message
|
|
23
|
-
.map((part) => (typeof part === "string" ? part : safeStringify(part)))
|
|
24
|
-
.join("");
|
|
25
|
-
|
|
26
|
-
let levelStr: string;
|
|
27
|
-
switch (record.level) {
|
|
28
|
-
case "debug":
|
|
29
|
-
levelStr = chalk.dim(`[${level}]`);
|
|
30
|
-
break;
|
|
31
|
-
case "info":
|
|
32
|
-
levelStr = chalk.blue(`[${level}]`);
|
|
33
|
-
break;
|
|
34
|
-
case "warning":
|
|
35
|
-
levelStr = chalk.yellow(`[${level}]`);
|
|
36
|
-
break;
|
|
37
|
-
case "error":
|
|
38
|
-
case "fatal":
|
|
39
|
-
levelStr = chalk.red(`[${level}]`);
|
|
40
|
-
break;
|
|
41
|
-
default:
|
|
42
|
-
levelStr = `[${level}]`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const categoryStr = category ? chalk.dim(`[${category}]`) : "";
|
|
46
|
-
|
|
47
|
-
return `${levelStr}${categoryStr} ${message}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Create a console sink that outputs to stderr with chalk formatting.
|
|
52
|
-
* Uses stderr to keep stdout clean for JSON protocol responses (stop-hook).
|
|
53
|
-
*/
|
|
54
|
-
export function createConsoleSink(): Sink {
|
|
55
|
-
return (record: LogRecord) => {
|
|
56
|
-
const formatted = formatLogRecord(record);
|
|
57
|
-
console.error(formatted);
|
|
58
|
-
};
|
|
59
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import type { LogRecord, Sink } from "@logtape/logtape";
|
|
3
|
-
|
|
4
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: Required for ANSI escape code stripping
|
|
5
|
-
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Strip ANSI escape codes from text.
|
|
9
|
-
*/
|
|
10
|
-
function stripAnsi(text: string): string {
|
|
11
|
-
return text.replace(ANSI_REGEX, "");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Format an ISO timestamp for log output.
|
|
16
|
-
*/
|
|
17
|
-
function formatTimestamp(timestamp: number): string {
|
|
18
|
-
return new Date(timestamp).toISOString();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Safely serialize a value, handling circular references.
|
|
23
|
-
*/
|
|
24
|
-
function safeStringify(value: unknown): string {
|
|
25
|
-
try {
|
|
26
|
-
return JSON.stringify(value);
|
|
27
|
-
} catch {
|
|
28
|
-
return "[Unserializable]";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Format a log record for file output with plain text (no ANSI).
|
|
34
|
-
* Format: [ISO_TIMESTAMP] LEVEL [category] message
|
|
35
|
-
*/
|
|
36
|
-
function formatLogRecord(record: LogRecord): string {
|
|
37
|
-
const timestamp = formatTimestamp(record.timestamp);
|
|
38
|
-
const level = record.level.toUpperCase().padEnd(7);
|
|
39
|
-
const category = record.category.join(".");
|
|
40
|
-
const message = record.message
|
|
41
|
-
.map((part) => (typeof part === "string" ? part : safeStringify(part)))
|
|
42
|
-
.join("");
|
|
43
|
-
|
|
44
|
-
// Strip any ANSI codes that might be in the message
|
|
45
|
-
const cleanMessage = stripAnsi(message);
|
|
46
|
-
|
|
47
|
-
const categoryStr = category ? `[${category}] ` : "";
|
|
48
|
-
return `[${timestamp}] ${level} ${categoryStr}${cleanMessage}\n`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Create a file sink that appends to the specified log file.
|
|
53
|
-
* Uses synchronous writes to ensure log ordering.
|
|
54
|
-
*
|
|
55
|
-
* @param logPath - Path to the log file (e.g., console.N.log)
|
|
56
|
-
*/
|
|
57
|
-
export function createFileSink(logPath: string): Sink {
|
|
58
|
-
// Ensure the file exists (create if not)
|
|
59
|
-
const fd = fs.openSync(
|
|
60
|
-
logPath,
|
|
61
|
-
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return (record: LogRecord) => {
|
|
65
|
-
const formatted = formatLogRecord(record);
|
|
66
|
-
try {
|
|
67
|
-
fs.writeSync(fd, formatted);
|
|
68
|
-
} catch {
|
|
69
|
-
// Suppress write errors to avoid crashing the application
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Create a closeable file sink that can be cleaned up.
|
|
76
|
-
* Returns both the sink and a close function.
|
|
77
|
-
*/
|
|
78
|
-
export function createCloseableFileSink(logPath: string): {
|
|
79
|
-
sink: Sink;
|
|
80
|
-
close: () => void;
|
|
81
|
-
} {
|
|
82
|
-
const fd = fs.openSync(
|
|
83
|
-
logPath,
|
|
84
|
-
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND,
|
|
85
|
-
);
|
|
86
|
-
let isClosed = false;
|
|
87
|
-
|
|
88
|
-
const sink: Sink = (record: LogRecord) => {
|
|
89
|
-
if (isClosed) return;
|
|
90
|
-
const formatted = formatLogRecord(record);
|
|
91
|
-
try {
|
|
92
|
-
fs.writeSync(fd, formatted);
|
|
93
|
-
} catch {
|
|
94
|
-
// Suppress write errors
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const close = () => {
|
|
99
|
-
if (!isClosed) {
|
|
100
|
-
isClosed = true;
|
|
101
|
-
try {
|
|
102
|
-
fs.closeSync(fd);
|
|
103
|
-
} catch {
|
|
104
|
-
// Ignore close errors
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
return { sink, close };
|
|
110
|
-
}
|