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/gates/review.ts
DELETED
|
@@ -1,1333 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
import { getAdapter, isUsageLimit } from "../cli-adapters/index.js";
|
|
6
|
-
import type { AdapterConfig, LoadedReviewGateConfig } from "../config/types.js";
|
|
7
|
-
import { getCategoryLogger } from "../output/app-logger.js";
|
|
8
|
-
import {
|
|
9
|
-
type DiffFileRange,
|
|
10
|
-
isValidViolationLocation,
|
|
11
|
-
parseDiff,
|
|
12
|
-
} from "../utils/diff-parser.js";
|
|
13
|
-
import {
|
|
14
|
-
getUnhealthyAdapters,
|
|
15
|
-
isAdapterCoolingDown,
|
|
16
|
-
markAdapterHealthy,
|
|
17
|
-
markAdapterUnhealthy,
|
|
18
|
-
} from "../utils/execution-state.js";
|
|
19
|
-
import type {
|
|
20
|
-
GateResult,
|
|
21
|
-
PreviousViolation,
|
|
22
|
-
ReviewFullJsonOutput,
|
|
23
|
-
} from "./result.js";
|
|
24
|
-
|
|
25
|
-
const log = getCategoryLogger("gate", "review");
|
|
26
|
-
|
|
27
|
-
const execAsync = promisify(exec);
|
|
28
|
-
|
|
29
|
-
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
30
|
-
const MAX_LOG_BUFFER_SIZE = 10000;
|
|
31
|
-
const REVIEW_ADAPTER_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
32
|
-
|
|
33
|
-
/** Chars-per-token approximation for rough token estimates. */
|
|
34
|
-
const CHARS_PER_TOKEN = 4;
|
|
35
|
-
|
|
36
|
-
export const JSON_SYSTEM_INSTRUCTION = `
|
|
37
|
-
You are in a read-only mode. You may read files in the repository to gather context.
|
|
38
|
-
Do NOT attempt to modify files or run shell commands that change system state.
|
|
39
|
-
Do NOT access files outside the repository root.
|
|
40
|
-
Do NOT access the .git/ directory or read git history/commit information.
|
|
41
|
-
Use your available file-reading and search tools to find information.
|
|
42
|
-
If the diff is insufficient or ambiguous, use your tools to read the full file content or related files.
|
|
43
|
-
|
|
44
|
-
CRITICAL SCOPE RESTRICTIONS:
|
|
45
|
-
- ONLY review the code changes shown in the diff below
|
|
46
|
-
- DO NOT review commit history or existing code outside the diff
|
|
47
|
-
- All violations MUST reference file paths and line numbers that appear IN THE DIFF
|
|
48
|
-
- The "file" field must match a file from the diff
|
|
49
|
-
- The "line" field must be within a changed region (lines starting with + in the diff)
|
|
50
|
-
|
|
51
|
-
IMPORTANT: You must output ONLY a valid JSON object. Do not output any markdown text, explanations, or code blocks outside of the JSON.
|
|
52
|
-
Each violation MUST include a "priority" field with one of: "critical", "high", "medium", "low".
|
|
53
|
-
Each violation MUST include a "status" field set to "new".
|
|
54
|
-
|
|
55
|
-
If violations are found:
|
|
56
|
-
{
|
|
57
|
-
"status": "fail",
|
|
58
|
-
"violations": [
|
|
59
|
-
{
|
|
60
|
-
"file": "path/to/file.rb",
|
|
61
|
-
"line": 10,
|
|
62
|
-
"issue": "Description of the violation",
|
|
63
|
-
"fix": "Suggestion on how to fix it",
|
|
64
|
-
"priority": "high",
|
|
65
|
-
"status": "new"
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
If NO violations are found:
|
|
71
|
-
{
|
|
72
|
-
"status": "pass",
|
|
73
|
-
"message": "No problems found"
|
|
74
|
-
}
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
type ReviewConfig = LoadedReviewGateConfig;
|
|
78
|
-
|
|
79
|
-
interface ReviewJsonOutput {
|
|
80
|
-
status: "pass" | "fail";
|
|
81
|
-
message?: string;
|
|
82
|
-
violations?: Array<{
|
|
83
|
-
file: string;
|
|
84
|
-
line: number | string;
|
|
85
|
-
issue: string;
|
|
86
|
-
fix?: string;
|
|
87
|
-
priority: "critical" | "high" | "medium" | "low";
|
|
88
|
-
status: "new" | "fixed" | "skipped";
|
|
89
|
-
result?: string | null;
|
|
90
|
-
}>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class ReviewGateExecutor {
|
|
94
|
-
private constructPrompt(
|
|
95
|
-
config: ReviewConfig,
|
|
96
|
-
previousViolations: PreviousViolation[] = [],
|
|
97
|
-
): string {
|
|
98
|
-
const baseContent = config.promptContent || "";
|
|
99
|
-
|
|
100
|
-
if (previousViolations.length > 0) {
|
|
101
|
-
return (
|
|
102
|
-
baseContent +
|
|
103
|
-
"\n\n" +
|
|
104
|
-
this.buildPreviousFailuresSection(previousViolations) +
|
|
105
|
-
"\n" +
|
|
106
|
-
JSON_SYSTEM_INSTRUCTION
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return `${baseContent}\n${JSON_SYSTEM_INSTRUCTION}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async execute(
|
|
114
|
-
jobId: string,
|
|
115
|
-
config: ReviewConfig,
|
|
116
|
-
entryPointPath: string,
|
|
117
|
-
loggerFactory: (
|
|
118
|
-
adapterName?: string,
|
|
119
|
-
reviewIndex?: number,
|
|
120
|
-
) => Promise<{
|
|
121
|
-
logger: (output: string) => Promise<void>;
|
|
122
|
-
logPath: string;
|
|
123
|
-
}>,
|
|
124
|
-
baseBranch: string,
|
|
125
|
-
previousFailures?: Map<string, PreviousViolation[]>,
|
|
126
|
-
changeOptions?: {
|
|
127
|
-
commit?: string;
|
|
128
|
-
uncommitted?: boolean;
|
|
129
|
-
fixBase?: string;
|
|
130
|
-
},
|
|
131
|
-
rerunThreshold: "critical" | "high" | "medium" | "low" = "high",
|
|
132
|
-
passedSlots?: Map<number, { adapter: string; passIteration: number }>,
|
|
133
|
-
logDir?: string,
|
|
134
|
-
adapterConfigs?: Record<string, AdapterConfig>,
|
|
135
|
-
): Promise<GateResult> {
|
|
136
|
-
const startTime = Date.now();
|
|
137
|
-
const logBuffer: string[] = [];
|
|
138
|
-
let logSequence = 0;
|
|
139
|
-
const activeLoggers: Array<
|
|
140
|
-
(output: string, index: number) => Promise<void>
|
|
141
|
-
> = [];
|
|
142
|
-
const logPaths: string[] = [];
|
|
143
|
-
const logPathsSet = new Set<string>();
|
|
144
|
-
|
|
145
|
-
const mainLogger = async (output: string) => {
|
|
146
|
-
const seq = logSequence++;
|
|
147
|
-
if (logBuffer.length < MAX_LOG_BUFFER_SIZE) {
|
|
148
|
-
logBuffer.push(output);
|
|
149
|
-
}
|
|
150
|
-
await Promise.allSettled(activeLoggers.map((l) => l(output, seq)));
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const getAdapterLogger = async (
|
|
154
|
-
adapterName: string,
|
|
155
|
-
reviewIndex: number,
|
|
156
|
-
) => {
|
|
157
|
-
const { logger, logPath } = await loggerFactory(adapterName, reviewIndex);
|
|
158
|
-
if (!logPathsSet.has(logPath)) {
|
|
159
|
-
logPathsSet.add(logPath);
|
|
160
|
-
logPaths.push(logPath);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const seenIndices = new Set<number>();
|
|
164
|
-
|
|
165
|
-
const safeLogger = async (msg: string, index: number) => {
|
|
166
|
-
if (seenIndices.has(index)) return;
|
|
167
|
-
seenIndices.add(index);
|
|
168
|
-
await logger(msg);
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
activeLoggers.push(safeLogger);
|
|
172
|
-
|
|
173
|
-
const snapshot = [...logBuffer];
|
|
174
|
-
await Promise.all(snapshot.map((msg, i) => safeLogger(msg, i)));
|
|
175
|
-
|
|
176
|
-
return logger;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
log.debug(`Starting review: ${config.name} | entry=${entryPointPath}`);
|
|
181
|
-
await mainLogger(`Starting review: ${config.name}\n`);
|
|
182
|
-
await mainLogger(`Entry point: ${entryPointPath}\n`);
|
|
183
|
-
await mainLogger(`Base branch: ${baseBranch}\n`);
|
|
184
|
-
|
|
185
|
-
const diff = await this.getDiff(
|
|
186
|
-
entryPointPath,
|
|
187
|
-
baseBranch,
|
|
188
|
-
changeOptions,
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
// Compute and log diff size stats
|
|
192
|
-
const diffLines = diff.split("\n").length;
|
|
193
|
-
const diffChars = diff.length;
|
|
194
|
-
const diffEstTokens = Math.ceil(diffChars / CHARS_PER_TOKEN);
|
|
195
|
-
const diffFileRanges = parseDiff(diff);
|
|
196
|
-
const diffFiles = diffFileRanges.size;
|
|
197
|
-
const diffSizeMsg = `[diff-stats] files=${diffFiles} lines=${diffLines} chars=${diffChars} est_tokens=${diffEstTokens}`;
|
|
198
|
-
log.debug(diffSizeMsg);
|
|
199
|
-
await mainLogger(`${diffSizeMsg}\n`);
|
|
200
|
-
|
|
201
|
-
if (!diff.trim()) {
|
|
202
|
-
log.debug(`Empty diff after trim, returning pass`);
|
|
203
|
-
await mainLogger("No changes found in entry point, skipping review.\n");
|
|
204
|
-
await mainLogger("Result: pass - No changes to review\n");
|
|
205
|
-
return {
|
|
206
|
-
jobId,
|
|
207
|
-
status: "pass",
|
|
208
|
-
duration: Date.now() - startTime,
|
|
209
|
-
message: "No changes to review",
|
|
210
|
-
logPaths,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const required = config.num_reviews ?? 1;
|
|
215
|
-
const outputs: Array<{
|
|
216
|
-
adapter: string;
|
|
217
|
-
reviewIndex: number;
|
|
218
|
-
duration?: number;
|
|
219
|
-
status: "pass" | "fail" | "error";
|
|
220
|
-
message: string;
|
|
221
|
-
json?: ReviewJsonOutput;
|
|
222
|
-
skipped?: Array<{
|
|
223
|
-
file: string;
|
|
224
|
-
line: number | string;
|
|
225
|
-
issue: string;
|
|
226
|
-
result?: string | null;
|
|
227
|
-
}>;
|
|
228
|
-
}> = [];
|
|
229
|
-
|
|
230
|
-
const preferences = config.cli_preference || [];
|
|
231
|
-
const parallel = config.parallel ?? false;
|
|
232
|
-
log.debug(
|
|
233
|
-
`Checking adapters: ${preferences.join(", ") || "(none configured)"}`,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
// Determine healthy adapters using cooldown-based filtering
|
|
237
|
-
const healthyAdapters: string[] = [];
|
|
238
|
-
const unhealthyMap = logDir ? await getUnhealthyAdapters(logDir) : {};
|
|
239
|
-
|
|
240
|
-
for (const toolName of preferences) {
|
|
241
|
-
const adapter = getAdapter(toolName);
|
|
242
|
-
if (!adapter) {
|
|
243
|
-
log.debug(`Adapter ${toolName}: not found`);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Check cooldown status
|
|
248
|
-
const unhealthyEntry = unhealthyMap[toolName];
|
|
249
|
-
if (unhealthyEntry) {
|
|
250
|
-
if (isAdapterCoolingDown(unhealthyEntry)) {
|
|
251
|
-
log.debug(`Adapter ${toolName}: cooling down`);
|
|
252
|
-
await mainLogger(
|
|
253
|
-
`Skipping ${toolName}: cooling down (${unhealthyEntry.reason})\n`,
|
|
254
|
-
);
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Cooldown expired - probe binary availability
|
|
259
|
-
const health = await adapter.checkHealth();
|
|
260
|
-
if (health.status === "healthy") {
|
|
261
|
-
log.debug(
|
|
262
|
-
`Adapter ${toolName}: cooldown expired, binary available, clearing unhealthy flag`,
|
|
263
|
-
);
|
|
264
|
-
if (logDir) {
|
|
265
|
-
await markAdapterHealthy(logDir, toolName);
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
log.debug(
|
|
269
|
-
`Adapter ${toolName}: cooldown expired but binary missing`,
|
|
270
|
-
);
|
|
271
|
-
await mainLogger(
|
|
272
|
-
`Skipping ${toolName}: ${health.message || "Missing"}\n`,
|
|
273
|
-
);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
} else {
|
|
277
|
-
// Not in unhealthy list - check binary availability
|
|
278
|
-
const health = await adapter.checkHealth();
|
|
279
|
-
if (health.status !== "healthy") {
|
|
280
|
-
log.debug(
|
|
281
|
-
`Adapter ${toolName}: ${health.status}${health.message ? ` - ${health.message}` : ""}`,
|
|
282
|
-
);
|
|
283
|
-
await mainLogger(
|
|
284
|
-
`Skipping ${toolName}: ${health.message || "Unhealthy"}\n`,
|
|
285
|
-
);
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
healthyAdapters.push(toolName);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (healthyAdapters.length === 0) {
|
|
294
|
-
const msg = "Review dispatch failed: no healthy adapters available";
|
|
295
|
-
log.error(`ERROR: ${msg}`);
|
|
296
|
-
await mainLogger(`Result: error - ${msg}\n`);
|
|
297
|
-
return {
|
|
298
|
-
jobId,
|
|
299
|
-
status: "error",
|
|
300
|
-
duration: Date.now() - startTime,
|
|
301
|
-
message: msg,
|
|
302
|
-
logPaths,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
log.debug(`Healthy adapters: ${healthyAdapters.join(", ")}`);
|
|
306
|
-
|
|
307
|
-
// Round-robin assignment over healthy adapters
|
|
308
|
-
const assignments: Array<{
|
|
309
|
-
adapter: string;
|
|
310
|
-
reviewIndex: number;
|
|
311
|
-
skip?: boolean;
|
|
312
|
-
skipReason?: string;
|
|
313
|
-
passIteration?: number;
|
|
314
|
-
}> = [];
|
|
315
|
-
for (let i = 0; i < required; i++) {
|
|
316
|
-
const adapter = healthyAdapters[i % healthyAdapters.length];
|
|
317
|
-
if (!adapter) continue;
|
|
318
|
-
assignments.push({
|
|
319
|
-
adapter,
|
|
320
|
-
reviewIndex: i + 1,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Skip logic for passed slots (only when num_reviews > 1 and in rerun mode)
|
|
325
|
-
if (required > 1 && passedSlots && passedSlots.size > 0) {
|
|
326
|
-
// Identify which slots passed (with same adapter) and which failed
|
|
327
|
-
const passedIndexes: number[] = [];
|
|
328
|
-
const failedIndexes: number[] = [];
|
|
329
|
-
|
|
330
|
-
for (const assignment of assignments) {
|
|
331
|
-
const passed = passedSlots.get(assignment.reviewIndex);
|
|
332
|
-
// Only consider as passed if same adapter is assigned
|
|
333
|
-
if (passed && passed.adapter === assignment.adapter) {
|
|
334
|
-
passedIndexes.push(assignment.reviewIndex);
|
|
335
|
-
assignment.passIteration = passed.passIteration;
|
|
336
|
-
} else {
|
|
337
|
-
failedIndexes.push(assignment.reviewIndex);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (failedIndexes.length > 0) {
|
|
342
|
-
// Some slots failed: run failed slots, skip passed slots
|
|
343
|
-
for (const assignment of assignments) {
|
|
344
|
-
if (assignment.passIteration !== undefined) {
|
|
345
|
-
assignment.skip = true;
|
|
346
|
-
assignment.skipReason = `previously passed in iteration ${assignment.passIteration} (num_reviews > 1)`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
} else if (passedIndexes.length === assignments.length) {
|
|
350
|
-
// All slots passed: safety latch - run slot 1, skip rest
|
|
351
|
-
for (const assignment of assignments) {
|
|
352
|
-
if (assignment.reviewIndex === 1) {
|
|
353
|
-
assignment.skip = false;
|
|
354
|
-
// Log safety latch message
|
|
355
|
-
await mainLogger(
|
|
356
|
-
`Running @1: safety latch (all slots previously passed)\n`,
|
|
357
|
-
);
|
|
358
|
-
} else {
|
|
359
|
-
assignment.skip = true;
|
|
360
|
-
assignment.skipReason = `previously passed in iteration ${assignment.passIteration} (num_reviews > 1)`;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Log skip messages
|
|
367
|
-
for (const assignment of assignments) {
|
|
368
|
-
if (assignment.skip && assignment.skipReason) {
|
|
369
|
-
await mainLogger(
|
|
370
|
-
`Skipping @${assignment.reviewIndex}: ${assignment.skipReason}\n`,
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const dispatchMsg = `Dispatching ${required} review(s) via round-robin: ${assignments.map((a) => `${a.adapter}@${a.reviewIndex}`).join(", ")}`;
|
|
376
|
-
log.debug(dispatchMsg);
|
|
377
|
-
await mainLogger(`${dispatchMsg}\n`);
|
|
378
|
-
|
|
379
|
-
// Separate assignments into running and skipped
|
|
380
|
-
const runningAssignments = assignments.filter((a) => !a.skip);
|
|
381
|
-
const skippedAssignments = assignments.filter((a) => a.skip);
|
|
382
|
-
log.debug(
|
|
383
|
-
`Running: ${runningAssignments.length}, Skipped: ${skippedAssignments.length}`,
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
// Track skipped slots for output
|
|
387
|
-
const skippedSlotOutputs: Array<{
|
|
388
|
-
adapter: string;
|
|
389
|
-
reviewIndex: number;
|
|
390
|
-
status: "skipped_prior_pass";
|
|
391
|
-
message: string;
|
|
392
|
-
passIteration: number;
|
|
393
|
-
}> = [];
|
|
394
|
-
|
|
395
|
-
// Handle skipped slots: write JSON log with status "skipped_prior_pass"
|
|
396
|
-
for (const assignment of skippedAssignments) {
|
|
397
|
-
const { logger, logPath } = await loggerFactory(
|
|
398
|
-
assignment.adapter,
|
|
399
|
-
assignment.reviewIndex,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
// Write to log file explaining the skip
|
|
403
|
-
const skipMessage = `[${new Date().toISOString()}] Review skipped: previously passed in iteration ${assignment.passIteration}\n`;
|
|
404
|
-
await logger(skipMessage);
|
|
405
|
-
await logger(`Adapter: ${assignment.adapter}\n`);
|
|
406
|
-
await logger(`Review index: @${assignment.reviewIndex}\n`);
|
|
407
|
-
await logger(`Status: skipped_prior_pass\n`);
|
|
408
|
-
|
|
409
|
-
const jsonPath = logPath.replace(/\.log$/, ".json");
|
|
410
|
-
const skippedOutput: ReviewFullJsonOutput = {
|
|
411
|
-
adapter: assignment.adapter,
|
|
412
|
-
timestamp: new Date().toISOString(),
|
|
413
|
-
status: "skipped_prior_pass",
|
|
414
|
-
rawOutput: "",
|
|
415
|
-
violations: [],
|
|
416
|
-
passIteration: assignment.passIteration,
|
|
417
|
-
};
|
|
418
|
-
await fs.writeFile(jsonPath, JSON.stringify(skippedOutput, null, 2));
|
|
419
|
-
|
|
420
|
-
if (!logPathsSet.has(logPath)) {
|
|
421
|
-
logPathsSet.add(logPath);
|
|
422
|
-
logPaths.push(logPath);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
skippedSlotOutputs.push({
|
|
426
|
-
adapter: assignment.adapter,
|
|
427
|
-
reviewIndex: assignment.reviewIndex,
|
|
428
|
-
status: "skipped_prior_pass",
|
|
429
|
-
message: `Skipped: previously passed in iteration ${assignment.passIteration}`,
|
|
430
|
-
passIteration: assignment.passIteration ?? 0,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (parallel && runningAssignments.length > 1) {
|
|
435
|
-
// Parallel execution
|
|
436
|
-
const results = await Promise.all(
|
|
437
|
-
runningAssignments.map((assignment) =>
|
|
438
|
-
this.runSingleReview(
|
|
439
|
-
assignment.adapter,
|
|
440
|
-
assignment.reviewIndex,
|
|
441
|
-
config,
|
|
442
|
-
diff,
|
|
443
|
-
getAdapterLogger,
|
|
444
|
-
mainLogger,
|
|
445
|
-
loggerFactory,
|
|
446
|
-
previousFailures,
|
|
447
|
-
rerunThreshold,
|
|
448
|
-
logDir,
|
|
449
|
-
adapterConfigs,
|
|
450
|
-
),
|
|
451
|
-
),
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
for (const res of results) {
|
|
455
|
-
if (res) {
|
|
456
|
-
outputs.push({
|
|
457
|
-
adapter: res.adapter,
|
|
458
|
-
reviewIndex: res.reviewIndex,
|
|
459
|
-
duration: res.duration,
|
|
460
|
-
...res.evaluation,
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
} else {
|
|
465
|
-
// Sequential execution
|
|
466
|
-
for (const assignment of runningAssignments) {
|
|
467
|
-
const res = await this.runSingleReview(
|
|
468
|
-
assignment.adapter,
|
|
469
|
-
assignment.reviewIndex,
|
|
470
|
-
config,
|
|
471
|
-
diff,
|
|
472
|
-
getAdapterLogger,
|
|
473
|
-
mainLogger,
|
|
474
|
-
loggerFactory,
|
|
475
|
-
previousFailures,
|
|
476
|
-
rerunThreshold,
|
|
477
|
-
logDir,
|
|
478
|
-
adapterConfigs,
|
|
479
|
-
);
|
|
480
|
-
if (res) {
|
|
481
|
-
outputs.push({
|
|
482
|
-
adapter: res.adapter,
|
|
483
|
-
reviewIndex: res.reviewIndex,
|
|
484
|
-
duration: res.duration,
|
|
485
|
-
...res.evaluation,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Check if all running reviews completed (skipped ones don't count)
|
|
492
|
-
if (outputs.length < runningAssignments.length) {
|
|
493
|
-
const msg = `Failed to complete reviews. Expected: ${runningAssignments.length}, Completed: ${outputs.length}. See logs for details.`;
|
|
494
|
-
await mainLogger(`Result: error - ${msg}\n`);
|
|
495
|
-
return {
|
|
496
|
-
jobId,
|
|
497
|
-
status: "error",
|
|
498
|
-
duration: Date.now() - startTime,
|
|
499
|
-
message: msg,
|
|
500
|
-
logPaths,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const failed = outputs.filter((result) => result.status === "fail");
|
|
505
|
-
const errored = outputs.filter((result) => result.status === "error");
|
|
506
|
-
const allSkipped = outputs.flatMap((result) => result.skipped || []);
|
|
507
|
-
|
|
508
|
-
let status: "pass" | "fail" | "error" = "pass";
|
|
509
|
-
let message = "Passed";
|
|
510
|
-
|
|
511
|
-
if (errored.length > 0) {
|
|
512
|
-
status = "error";
|
|
513
|
-
message = `Error in ${errored.length} adapter(s)`;
|
|
514
|
-
} else if (failed.length > 0) {
|
|
515
|
-
status = "fail";
|
|
516
|
-
message = `Failed by ${failed.length} adapter(s)`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Add skipped slot count to message if any
|
|
520
|
-
if (skippedSlotOutputs.length > 0) {
|
|
521
|
-
message += ` (${skippedSlotOutputs.length} skipped due to prior pass)`;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const subResults = outputs.map((out) => {
|
|
525
|
-
const specificLog = logPaths.find((p) => {
|
|
526
|
-
const filename = path.basename(p);
|
|
527
|
-
return (
|
|
528
|
-
filename.includes(`_${out.adapter}@${out.reviewIndex}.`) &&
|
|
529
|
-
filename.endsWith(".log")
|
|
530
|
-
);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
let logPath = specificLog;
|
|
534
|
-
if (specificLog && out.json && out.status === "fail") {
|
|
535
|
-
logPath = specificLog.replace(/\.log$/, ".json");
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const errorCount =
|
|
539
|
-
out.json && Array.isArray(out.json.violations)
|
|
540
|
-
? out.json.violations.filter((v) => !v.status || v.status === "new")
|
|
541
|
-
.length
|
|
542
|
-
: out.status === "fail" || out.status === "error"
|
|
543
|
-
? 1
|
|
544
|
-
: 0;
|
|
545
|
-
|
|
546
|
-
const fixedCount =
|
|
547
|
-
out.json && Array.isArray(out.json.violations)
|
|
548
|
-
? out.json.violations.filter((v) => v.status === "fixed").length
|
|
549
|
-
: 0;
|
|
550
|
-
|
|
551
|
-
return {
|
|
552
|
-
nameSuffix: `(${out.adapter}@${out.reviewIndex})`,
|
|
553
|
-
status: out.status,
|
|
554
|
-
duration: out.duration,
|
|
555
|
-
message: out.message,
|
|
556
|
-
logPath,
|
|
557
|
-
errorCount,
|
|
558
|
-
fixedCount,
|
|
559
|
-
skipped: out.skipped,
|
|
560
|
-
};
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// Add skipped slot subResults (they don't affect gate status)
|
|
564
|
-
for (const skipped of skippedSlotOutputs) {
|
|
565
|
-
const specificLog = logPaths.find((p) => {
|
|
566
|
-
const filename = path.basename(p);
|
|
567
|
-
return (
|
|
568
|
-
filename.includes(`_${skipped.adapter}@${skipped.reviewIndex}.`) &&
|
|
569
|
-
filename.endsWith(".log")
|
|
570
|
-
);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
subResults.push({
|
|
574
|
-
nameSuffix: `(${skipped.adapter}@${skipped.reviewIndex})`,
|
|
575
|
-
status: "pass" as const, // Show as pass since it previously passed
|
|
576
|
-
duration: undefined,
|
|
577
|
-
message: skipped.message,
|
|
578
|
-
logPath: specificLog?.replace(/\.log$/, ".json"),
|
|
579
|
-
errorCount: 0,
|
|
580
|
-
fixedCount: 0,
|
|
581
|
-
skipped: undefined,
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Sort subResults by review index for consistent ordering
|
|
586
|
-
subResults.sort((a, b) => {
|
|
587
|
-
const aIndex = parseInt(a.nameSuffix.match(/@(\d+)/)?.[1] || "0", 10);
|
|
588
|
-
const bIndex = parseInt(b.nameSuffix.match(/@(\d+)/)?.[1] || "0", 10);
|
|
589
|
-
return aIndex - bIndex;
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
log.debug(`Complete: ${status} - ${message}`);
|
|
593
|
-
await mainLogger(`Result: ${status} - ${message}\n`);
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
jobId,
|
|
597
|
-
status,
|
|
598
|
-
duration: Date.now() - startTime,
|
|
599
|
-
message,
|
|
600
|
-
logPaths,
|
|
601
|
-
subResults,
|
|
602
|
-
skipped: allSkipped,
|
|
603
|
-
};
|
|
604
|
-
} catch (error: unknown) {
|
|
605
|
-
const err = error as { message?: string; stack?: string };
|
|
606
|
-
const errMsg = err.message || "Unknown error";
|
|
607
|
-
const errStack = err.stack || "";
|
|
608
|
-
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring
|
|
609
|
-
log.error(`CRITICAL ERROR: ${errMsg} ${errStack}`);
|
|
610
|
-
await mainLogger(`Critical Error: ${errMsg}\n`);
|
|
611
|
-
await mainLogger("Result: error\n");
|
|
612
|
-
return {
|
|
613
|
-
jobId,
|
|
614
|
-
status: "error",
|
|
615
|
-
duration: Date.now() - startTime,
|
|
616
|
-
message: errMsg,
|
|
617
|
-
logPaths,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
private async runSingleReview(
|
|
623
|
-
toolName: string,
|
|
624
|
-
reviewIndex: number,
|
|
625
|
-
config: ReviewConfig,
|
|
626
|
-
diff: string,
|
|
627
|
-
getAdapterLogger: (
|
|
628
|
-
adapterName: string,
|
|
629
|
-
reviewIndex: number,
|
|
630
|
-
) => Promise<(output: string) => Promise<void>>,
|
|
631
|
-
mainLogger: (output: string) => Promise<void>,
|
|
632
|
-
loggerFactory: (
|
|
633
|
-
adapterName?: string,
|
|
634
|
-
reviewIndex?: number,
|
|
635
|
-
) => Promise<{
|
|
636
|
-
logger: (output: string) => Promise<void>;
|
|
637
|
-
logPath: string;
|
|
638
|
-
}>,
|
|
639
|
-
previousFailures?: Map<string, PreviousViolation[]>,
|
|
640
|
-
rerunThreshold: "critical" | "high" | "medium" | "low" = "high",
|
|
641
|
-
logDir?: string,
|
|
642
|
-
adapterConfigs?: Record<string, AdapterConfig>,
|
|
643
|
-
): Promise<{
|
|
644
|
-
adapter: string;
|
|
645
|
-
reviewIndex: number;
|
|
646
|
-
duration: number;
|
|
647
|
-
evaluation: {
|
|
648
|
-
status: "pass" | "fail" | "error";
|
|
649
|
-
message: string;
|
|
650
|
-
json?: ReviewJsonOutput;
|
|
651
|
-
skipped?: Array<{
|
|
652
|
-
file: string;
|
|
653
|
-
line: number | string;
|
|
654
|
-
issue: string;
|
|
655
|
-
result?: string | null;
|
|
656
|
-
}>;
|
|
657
|
-
};
|
|
658
|
-
} | null> {
|
|
659
|
-
const reviewStartTime = Date.now();
|
|
660
|
-
const adapter = getAdapter(toolName);
|
|
661
|
-
if (!adapter) return null;
|
|
662
|
-
|
|
663
|
-
if (!adapter.name || typeof adapter.name !== "string") {
|
|
664
|
-
await mainLogger(
|
|
665
|
-
`Error: Invalid adapter name: ${JSON.stringify(adapter.name)}\n`,
|
|
666
|
-
);
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
669
|
-
const adapterLogger = await getAdapterLogger(adapter.name, reviewIndex);
|
|
670
|
-
const { logPath } = await loggerFactory(adapter.name, reviewIndex);
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
const startMsg = `[START] review:.:${config.name} (${adapter.name}@${reviewIndex})`;
|
|
674
|
-
await adapterLogger(`${startMsg}\n`);
|
|
675
|
-
|
|
676
|
-
// Look up previous violations by review index key, falling back to adapter name for legacy logs
|
|
677
|
-
const indexKey = String(reviewIndex);
|
|
678
|
-
const adapterPreviousViolations =
|
|
679
|
-
previousFailures?.get(indexKey) ??
|
|
680
|
-
previousFailures?.get(adapter.name) ??
|
|
681
|
-
[];
|
|
682
|
-
const finalPrompt = this.constructPrompt(
|
|
683
|
-
config,
|
|
684
|
-
adapterPreviousViolations,
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
// Log prompt + diff size so we can compare against actual token usage from telemetry
|
|
688
|
-
const promptChars = finalPrompt.length;
|
|
689
|
-
const diffChars = diff.length;
|
|
690
|
-
const totalInputChars = promptChars + diffChars;
|
|
691
|
-
const promptEstTokens = Math.ceil(promptChars / CHARS_PER_TOKEN);
|
|
692
|
-
const diffEstTokens = Math.ceil(diffChars / CHARS_PER_TOKEN);
|
|
693
|
-
const totalEstTokens = promptEstTokens + diffEstTokens;
|
|
694
|
-
const inputSizeMsg = `[input-stats] prompt_chars=${promptChars} diff_chars=${diffChars} total_chars=${totalInputChars} prompt_est_tokens=${promptEstTokens} diff_est_tokens=${diffEstTokens} total_est_tokens=${totalEstTokens}`;
|
|
695
|
-
await adapterLogger(`${inputSizeMsg}\n`);
|
|
696
|
-
|
|
697
|
-
const adapterCfg = adapterConfigs?.[toolName];
|
|
698
|
-
const output = await adapter.execute({
|
|
699
|
-
prompt: finalPrompt,
|
|
700
|
-
diff,
|
|
701
|
-
model: config.model,
|
|
702
|
-
timeoutMs: config.timeout
|
|
703
|
-
? config.timeout * 1000
|
|
704
|
-
: REVIEW_ADAPTER_TIMEOUT_MS,
|
|
705
|
-
onOutput: (chunk: string) => {
|
|
706
|
-
// Stream output to log file in real-time
|
|
707
|
-
adapterLogger(chunk);
|
|
708
|
-
},
|
|
709
|
-
allowToolUse: adapterCfg?.allow_tool_use,
|
|
710
|
-
thinkingBudget: adapterCfg?.thinking_budget,
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
await adapterLogger(
|
|
714
|
-
`\n--- Review Output (${adapter.name}) ---\n${output}\n`,
|
|
715
|
-
);
|
|
716
|
-
|
|
717
|
-
const evaluation = this.evaluateOutput(output, diff);
|
|
718
|
-
|
|
719
|
-
// Check for usage limit only when output failed to parse as valid JSON.
|
|
720
|
-
// This avoids false positives when a review legitimately mentions "usage limit".
|
|
721
|
-
if (evaluation.status === "error" && isUsageLimit(output)) {
|
|
722
|
-
const reason = "Usage limit exceeded";
|
|
723
|
-
if (logDir) {
|
|
724
|
-
await markAdapterUnhealthy(logDir, adapter.name, reason);
|
|
725
|
-
log.debug(
|
|
726
|
-
`Adapter ${adapter.name} marked unhealthy for 1 hour: ${reason}`,
|
|
727
|
-
);
|
|
728
|
-
await mainLogger(
|
|
729
|
-
`${adapter.name} marked unhealthy for 1 hour: ${reason}\n`,
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
return {
|
|
733
|
-
adapter: adapter.name,
|
|
734
|
-
reviewIndex,
|
|
735
|
-
duration: Date.now() - reviewStartTime,
|
|
736
|
-
evaluation: {
|
|
737
|
-
status: "error",
|
|
738
|
-
message: reason,
|
|
739
|
-
},
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Rerun Filtering: If we have previous failures, filter new violations by threshold
|
|
744
|
-
if (
|
|
745
|
-
adapterPreviousViolations.length > 0 &&
|
|
746
|
-
evaluation.json?.violations &&
|
|
747
|
-
evaluation.status === "fail"
|
|
748
|
-
) {
|
|
749
|
-
const priorities = ["critical", "high", "medium", "low"];
|
|
750
|
-
const thresholdIndex = priorities.indexOf(rerunThreshold);
|
|
751
|
-
|
|
752
|
-
const originalCount = evaluation.json.violations.length;
|
|
753
|
-
|
|
754
|
-
evaluation.json.violations = evaluation.json.violations.filter((v) => {
|
|
755
|
-
const priority = v.priority || "low";
|
|
756
|
-
const priorityIndex = priorities.indexOf(priority);
|
|
757
|
-
|
|
758
|
-
if (priorityIndex === -1) return true;
|
|
759
|
-
|
|
760
|
-
return priorityIndex <= thresholdIndex;
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const filteredByThreshold =
|
|
764
|
-
originalCount - evaluation.json.violations.length;
|
|
765
|
-
|
|
766
|
-
if (filteredByThreshold > 0) {
|
|
767
|
-
await adapterLogger(
|
|
768
|
-
`Note: ${filteredByThreshold} new violations filtered due to rerun threshold (${rerunThreshold})\n`,
|
|
769
|
-
);
|
|
770
|
-
evaluation.filteredCount =
|
|
771
|
-
(evaluation.filteredCount || 0) + filteredByThreshold;
|
|
772
|
-
|
|
773
|
-
if (evaluation.json.violations.length === 0) {
|
|
774
|
-
evaluation.status = "pass";
|
|
775
|
-
evaluation.message = `Passed (${filteredByThreshold} below-threshold violations filtered)`;
|
|
776
|
-
evaluation.json.status = "pass";
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (evaluation.status === "error") {
|
|
782
|
-
await adapterLogger(`Error: ${evaluation.message}\n`);
|
|
783
|
-
await mainLogger(
|
|
784
|
-
`Error parsing review from ${adapter.name}: ${evaluation.message}\n`,
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (evaluation.filteredCount && evaluation.filteredCount > 0) {
|
|
789
|
-
await adapterLogger(
|
|
790
|
-
`Note: ${evaluation.filteredCount} out-of-scope violations filtered\n`,
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
let skipped: Array<{
|
|
795
|
-
file: string;
|
|
796
|
-
line: number | string;
|
|
797
|
-
issue: string;
|
|
798
|
-
result?: string | null;
|
|
799
|
-
}> = [];
|
|
800
|
-
|
|
801
|
-
if (evaluation.json) {
|
|
802
|
-
if (evaluation.json.status === "fail") {
|
|
803
|
-
if (!Array.isArray(evaluation.json.violations)) {
|
|
804
|
-
await adapterLogger(
|
|
805
|
-
"Warning: Missing 'violations' array in failure response\n",
|
|
806
|
-
);
|
|
807
|
-
} else {
|
|
808
|
-
for (const v of evaluation.json.violations) {
|
|
809
|
-
if (
|
|
810
|
-
!v.file ||
|
|
811
|
-
v.line === undefined ||
|
|
812
|
-
v.line === null ||
|
|
813
|
-
!v.issue ||
|
|
814
|
-
!v.priority ||
|
|
815
|
-
!v.status
|
|
816
|
-
) {
|
|
817
|
-
await adapterLogger(
|
|
818
|
-
`Warning: Violation missing required fields: ${JSON.stringify(v)}\n`,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const jsonPath = await this.writeJsonResult(
|
|
826
|
-
logPath,
|
|
827
|
-
adapter.name,
|
|
828
|
-
evaluation.status,
|
|
829
|
-
output,
|
|
830
|
-
evaluation.json,
|
|
831
|
-
);
|
|
832
|
-
|
|
833
|
-
skipped = (evaluation.json.violations || [])
|
|
834
|
-
.filter((v) => v.status === "skipped")
|
|
835
|
-
.map((v) => ({
|
|
836
|
-
file: v.file,
|
|
837
|
-
line: v.line,
|
|
838
|
-
issue: v.issue,
|
|
839
|
-
result: v.result,
|
|
840
|
-
}));
|
|
841
|
-
|
|
842
|
-
await adapterLogger(`\n--- Parsed Result (${adapter.name}) ---\n`);
|
|
843
|
-
if (
|
|
844
|
-
evaluation.json.status === "fail" &&
|
|
845
|
-
Array.isArray(evaluation.json.violations)
|
|
846
|
-
) {
|
|
847
|
-
await adapterLogger(`Status: FAIL\n`);
|
|
848
|
-
await adapterLogger(`Review: ${jsonPath}\n`);
|
|
849
|
-
await adapterLogger(`Violations:\n`);
|
|
850
|
-
for (const [i, v] of evaluation.json.violations.entries()) {
|
|
851
|
-
await adapterLogger(
|
|
852
|
-
`${i + 1}. ${v.file}:${v.line || "?"} - ${v.issue}\n`,
|
|
853
|
-
);
|
|
854
|
-
if (v.fix) await adapterLogger(` Fix: ${v.fix}\n`);
|
|
855
|
-
}
|
|
856
|
-
} else if (evaluation.json.status === "pass") {
|
|
857
|
-
await adapterLogger(`Status: PASS\n`);
|
|
858
|
-
if (evaluation.json.message)
|
|
859
|
-
await adapterLogger(`Message: ${evaluation.json.message}\n`);
|
|
860
|
-
} else {
|
|
861
|
-
await adapterLogger(`Status: ${evaluation.json.status}\n`);
|
|
862
|
-
await adapterLogger(
|
|
863
|
-
`Raw: ${JSON.stringify(evaluation.json, null, 2)}\n`,
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
await adapterLogger(`---------------------\n`);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const resultMsg = `Review result (${adapter.name}@${reviewIndex}): ${evaluation.status} - ${evaluation.message}`;
|
|
870
|
-
await adapterLogger(`${resultMsg}\n`);
|
|
871
|
-
|
|
872
|
-
return {
|
|
873
|
-
adapter: adapter.name,
|
|
874
|
-
reviewIndex,
|
|
875
|
-
duration: Date.now() - reviewStartTime,
|
|
876
|
-
evaluation: {
|
|
877
|
-
status: evaluation.status,
|
|
878
|
-
message: evaluation.message,
|
|
879
|
-
json: evaluation.json,
|
|
880
|
-
skipped,
|
|
881
|
-
},
|
|
882
|
-
};
|
|
883
|
-
} catch (error: unknown) {
|
|
884
|
-
const err = error as { message?: string };
|
|
885
|
-
const errorMsg = `Error running ${adapter.name}@${reviewIndex}: ${err.message}`;
|
|
886
|
-
log.error(errorMsg);
|
|
887
|
-
await adapterLogger(`${errorMsg}\n`);
|
|
888
|
-
await mainLogger(`${errorMsg}\n`);
|
|
889
|
-
|
|
890
|
-
// Check if the error is a usage limit
|
|
891
|
-
if (err.message && isUsageLimit(err.message)) {
|
|
892
|
-
const reason = "Usage limit exceeded";
|
|
893
|
-
if (logDir) {
|
|
894
|
-
await markAdapterUnhealthy(logDir, adapter.name, reason);
|
|
895
|
-
log.debug(
|
|
896
|
-
`Adapter ${adapter.name} marked unhealthy for 1 hour: ${reason}`,
|
|
897
|
-
);
|
|
898
|
-
await mainLogger(
|
|
899
|
-
`${adapter.name} marked unhealthy for 1 hour: ${reason}\n`,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
return {
|
|
903
|
-
adapter: adapter.name,
|
|
904
|
-
reviewIndex,
|
|
905
|
-
duration: Date.now() - reviewStartTime,
|
|
906
|
-
evaluation: {
|
|
907
|
-
status: "error",
|
|
908
|
-
message: reason,
|
|
909
|
-
},
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
private async getDiff(
|
|
918
|
-
entryPointPath: string,
|
|
919
|
-
baseBranch: string,
|
|
920
|
-
options?: { commit?: string; uncommitted?: boolean; fixBase?: string },
|
|
921
|
-
): Promise<string> {
|
|
922
|
-
// Debug: log which diff mode is active
|
|
923
|
-
log.debug(
|
|
924
|
-
`getDiff: entryPoint=${entryPointPath}, fixBase=${options?.fixBase ?? "none"}, uncommitted=${options?.uncommitted ?? false}, commit=${options?.commit ?? "none"}`,
|
|
925
|
-
);
|
|
926
|
-
|
|
927
|
-
// If fixBase is provided (rerun mode)
|
|
928
|
-
if (options?.fixBase) {
|
|
929
|
-
// Validate fixBase to prevent command injection
|
|
930
|
-
if (!/^[a-f0-9]+$/.test(options.fixBase)) {
|
|
931
|
-
throw new Error(`Invalid session ref: ${options.fixBase}`);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const pathArg = this.pathArg(entryPointPath);
|
|
935
|
-
try {
|
|
936
|
-
const diff = await this.execDiff(
|
|
937
|
-
`git diff ${options.fixBase}${pathArg}`,
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
const { stdout: untrackedStdout } = await execAsync(
|
|
941
|
-
`git ls-files --others --exclude-standard${pathArg}`,
|
|
942
|
-
{ maxBuffer: MAX_BUFFER_BYTES },
|
|
943
|
-
);
|
|
944
|
-
const currentUntracked = new Set(this.parseLines(untrackedStdout));
|
|
945
|
-
|
|
946
|
-
const { stdout: snapshotFilesStdout } = await execAsync(
|
|
947
|
-
`git ls-tree -r --name-only ${options.fixBase}${pathArg}`,
|
|
948
|
-
{ maxBuffer: MAX_BUFFER_BYTES },
|
|
949
|
-
);
|
|
950
|
-
const snapshotFiles = new Set(this.parseLines(snapshotFilesStdout));
|
|
951
|
-
|
|
952
|
-
const newUntracked = [...currentUntracked].filter(
|
|
953
|
-
(f) => !snapshotFiles.has(f),
|
|
954
|
-
);
|
|
955
|
-
const newUntrackedDiffs: string[] = [];
|
|
956
|
-
|
|
957
|
-
for (const file of newUntracked) {
|
|
958
|
-
try {
|
|
959
|
-
const d = await this.execDiff(
|
|
960
|
-
`git diff --no-index -- /dev/null ${this.quoteArg(file)}`,
|
|
961
|
-
);
|
|
962
|
-
if (d.trim()) newUntrackedDiffs.push(d);
|
|
963
|
-
} catch (error: unknown) {
|
|
964
|
-
const err = error as { message?: string; stderr?: string };
|
|
965
|
-
const msg = [err.message, err.stderr].filter(Boolean).join("\n");
|
|
966
|
-
if (
|
|
967
|
-
!msg.includes("Could not access") &&
|
|
968
|
-
!msg.includes("ENOENT") &&
|
|
969
|
-
!msg.includes("No such file")
|
|
970
|
-
) {
|
|
971
|
-
throw error;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const scopedDiff = [diff, ...newUntrackedDiffs]
|
|
977
|
-
.filter(Boolean)
|
|
978
|
-
.join("\n");
|
|
979
|
-
log.debug(
|
|
980
|
-
`Scoped diff via fixBase: ${scopedDiff.split("\n").length} lines`,
|
|
981
|
-
);
|
|
982
|
-
return scopedDiff;
|
|
983
|
-
} catch (error) {
|
|
984
|
-
log.warn(
|
|
985
|
-
`Failed to compute diff against fixBase ${options.fixBase}, falling back to full uncommitted diff. ${error instanceof Error ? error.message : error}`,
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (options?.uncommitted) {
|
|
991
|
-
log.debug(`Using full uncommitted diff (no fixBase)`);
|
|
992
|
-
const pathArg = this.pathArg(entryPointPath);
|
|
993
|
-
const staged = await this.execDiff(`git diff --cached${pathArg}`);
|
|
994
|
-
const unstaged = await this.execDiff(`git diff${pathArg}`);
|
|
995
|
-
const untracked = await this.untrackedDiff(entryPointPath);
|
|
996
|
-
return [staged, unstaged, untracked].filter(Boolean).join("\n");
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
if (options?.commit) {
|
|
1000
|
-
const pathArg = this.pathArg(entryPointPath);
|
|
1001
|
-
try {
|
|
1002
|
-
return await this.execDiff(
|
|
1003
|
-
`git diff ${options.commit}^..${options.commit}${pathArg}`,
|
|
1004
|
-
);
|
|
1005
|
-
} catch (error: unknown) {
|
|
1006
|
-
const err = error as { message?: string; stderr?: string };
|
|
1007
|
-
if (
|
|
1008
|
-
err.message?.includes("unknown revision") ||
|
|
1009
|
-
err.stderr?.includes("unknown revision")
|
|
1010
|
-
) {
|
|
1011
|
-
return await this.execDiff(
|
|
1012
|
-
`git diff --root ${options.commit}${pathArg}`,
|
|
1013
|
-
);
|
|
1014
|
-
}
|
|
1015
|
-
throw error;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
const isCI =
|
|
1020
|
-
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
1021
|
-
return isCI
|
|
1022
|
-
? this.getCIDiff(entryPointPath, baseBranch)
|
|
1023
|
-
: this.getLocalDiff(entryPointPath, baseBranch);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
private async getCIDiff(
|
|
1027
|
-
entryPointPath: string,
|
|
1028
|
-
baseBranch: string,
|
|
1029
|
-
): Promise<string> {
|
|
1030
|
-
const baseRef = baseBranch;
|
|
1031
|
-
const headRef = process.env.GITHUB_SHA || "HEAD";
|
|
1032
|
-
const pathArg = this.pathArg(entryPointPath);
|
|
1033
|
-
|
|
1034
|
-
try {
|
|
1035
|
-
return await this.execDiff(`git diff ${baseRef}...${headRef}${pathArg}`);
|
|
1036
|
-
} catch (_error) {
|
|
1037
|
-
const fallback = await this.execDiff(`git diff HEAD^...HEAD${pathArg}`);
|
|
1038
|
-
return fallback;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
private async getLocalDiff(
|
|
1043
|
-
entryPointPath: string,
|
|
1044
|
-
baseBranch: string,
|
|
1045
|
-
): Promise<string> {
|
|
1046
|
-
const pathArg = this.pathArg(entryPointPath);
|
|
1047
|
-
const committed = await this.execDiff(
|
|
1048
|
-
`git diff ${baseBranch}...HEAD${pathArg}`,
|
|
1049
|
-
);
|
|
1050
|
-
const uncommitted = await this.execDiff(`git diff HEAD${pathArg}`);
|
|
1051
|
-
const untracked = await this.untrackedDiff(entryPointPath);
|
|
1052
|
-
|
|
1053
|
-
return [committed, uncommitted, untracked].filter(Boolean).join("\n");
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
private async untrackedDiff(entryPointPath: string): Promise<string> {
|
|
1057
|
-
const pathArg = this.pathArg(entryPointPath);
|
|
1058
|
-
const { stdout } = await execAsync(
|
|
1059
|
-
`git ls-files --others --exclude-standard${pathArg}`,
|
|
1060
|
-
{
|
|
1061
|
-
maxBuffer: MAX_BUFFER_BYTES,
|
|
1062
|
-
},
|
|
1063
|
-
);
|
|
1064
|
-
const files = this.parseLines(stdout);
|
|
1065
|
-
const diffs: string[] = [];
|
|
1066
|
-
|
|
1067
|
-
for (const file of files) {
|
|
1068
|
-
try {
|
|
1069
|
-
const diff = await this.execDiff(
|
|
1070
|
-
`git diff --no-index -- /dev/null ${this.quoteArg(file)}`,
|
|
1071
|
-
);
|
|
1072
|
-
if (diff.trim()) diffs.push(diff);
|
|
1073
|
-
} catch (error: unknown) {
|
|
1074
|
-
const err = error as { message?: string; stderr?: string };
|
|
1075
|
-
const msg = [err.message, err.stderr].filter(Boolean).join("\n");
|
|
1076
|
-
if (
|
|
1077
|
-
msg.includes("Could not access") ||
|
|
1078
|
-
msg.includes("ENOENT") ||
|
|
1079
|
-
msg.includes("No such file")
|
|
1080
|
-
) {
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
throw error;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
return diffs.join("\n");
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
private async execDiff(command: string): Promise<string> {
|
|
1091
|
-
try {
|
|
1092
|
-
const { stdout } = await execAsync(command, {
|
|
1093
|
-
maxBuffer: MAX_BUFFER_BYTES,
|
|
1094
|
-
});
|
|
1095
|
-
return stdout;
|
|
1096
|
-
} catch (error: unknown) {
|
|
1097
|
-
const err = error as { code?: number; stdout?: string };
|
|
1098
|
-
if (typeof err.code === "number" && err.stdout) {
|
|
1099
|
-
return err.stdout;
|
|
1100
|
-
}
|
|
1101
|
-
throw error;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
private buildPreviousFailuresSection(
|
|
1106
|
-
violations: PreviousViolation[],
|
|
1107
|
-
): string {
|
|
1108
|
-
const toVerify = violations.filter((v) => v.status === "fixed");
|
|
1109
|
-
const unaddressed = violations.filter(
|
|
1110
|
-
(v) => v.status === "new" || !v.status,
|
|
1111
|
-
);
|
|
1112
|
-
|
|
1113
|
-
const affectedFiles = [...new Set(violations.map((v) => v.file))];
|
|
1114
|
-
|
|
1115
|
-
const lines: string[] = [];
|
|
1116
|
-
|
|
1117
|
-
lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1118
|
-
RERUN MODE: VERIFY PREVIOUS FIXES ONLY
|
|
1119
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1120
|
-
|
|
1121
|
-
This is a RERUN review. The agent attempted to fix some of the violations listed below.
|
|
1122
|
-
Your task is STRICTLY LIMITED to verifying the fixes for violations marked as FIXED.
|
|
1123
|
-
|
|
1124
|
-
PREVIOUS VIOLATIONS TO VERIFY:
|
|
1125
|
-
`);
|
|
1126
|
-
|
|
1127
|
-
if (toVerify.length === 0) {
|
|
1128
|
-
lines.push("(No violations were marked as FIXED for verification)\n");
|
|
1129
|
-
} else {
|
|
1130
|
-
toVerify.forEach((v, i) => {
|
|
1131
|
-
lines.push(`${i + 1}. ${v.file}:${v.line} - ${v.issue}`);
|
|
1132
|
-
if (v.fix) {
|
|
1133
|
-
lines.push(` Suggested fix: ${v.fix}`);
|
|
1134
|
-
}
|
|
1135
|
-
if (v.result) {
|
|
1136
|
-
lines.push(` Agent result: ${v.result}`);
|
|
1137
|
-
}
|
|
1138
|
-
lines.push("");
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
if (unaddressed.length > 0) {
|
|
1143
|
-
lines.push(`UNADDRESSED VIOLATIONS (STILL FAILING):
|
|
1144
|
-
The following violations were NOT marked as fixed or skipped and are still active failures:
|
|
1145
|
-
`);
|
|
1146
|
-
unaddressed.forEach((v, i) => {
|
|
1147
|
-
lines.push(`${i + 1}. ${v.file}:${v.line} - ${v.issue}`);
|
|
1148
|
-
});
|
|
1149
|
-
lines.push("");
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
lines.push(`STRICT INSTRUCTIONS FOR RERUN MODE:
|
|
1153
|
-
|
|
1154
|
-
1. VERIFY FIXES: Check if each violation marked as FIXED above has been addressed
|
|
1155
|
-
- For violations that are fixed, confirm they no longer appear
|
|
1156
|
-
- For violations that remain unfixed, include them in your violations array (status: "new")
|
|
1157
|
-
|
|
1158
|
-
2. UNADDRESSED VIOLATIONS: You MUST include all UNADDRESSED violations listed above in your output array if they still exist.
|
|
1159
|
-
|
|
1160
|
-
3. CHECK FOR REGRESSIONS ONLY: You may ONLY report NEW violations if they:
|
|
1161
|
-
- Are in FILES that were modified to fix the above violations: ${affectedFiles.join(", ")}
|
|
1162
|
-
- Are DIRECTLY caused by the fix changes (e.g., a fix introduced a new bug)
|
|
1163
|
-
- Are in the same function/region that was modified to address a previous violation
|
|
1164
|
-
|
|
1165
|
-
4. Return status "pass" ONLY if ALL previous violations (including unaddressed ones) are now fixed AND no regressions were introduced.
|
|
1166
|
-
Otherwise, return status "fail" and list all remaining violations.
|
|
1167
|
-
|
|
1168
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
1169
|
-
|
|
1170
|
-
return lines.join("\n");
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
public evaluateOutput(
|
|
1174
|
-
output: string,
|
|
1175
|
-
diff?: string,
|
|
1176
|
-
): {
|
|
1177
|
-
status: "pass" | "fail" | "error";
|
|
1178
|
-
message: string;
|
|
1179
|
-
json?: ReviewJsonOutput;
|
|
1180
|
-
filteredCount?: number;
|
|
1181
|
-
} {
|
|
1182
|
-
const diffRanges = diff ? parseDiff(diff) : undefined;
|
|
1183
|
-
|
|
1184
|
-
try {
|
|
1185
|
-
const jsonBlockMatch = output.match(/```json\s*([\s\S]*?)\s*```/);
|
|
1186
|
-
if (jsonBlockMatch?.[1]) {
|
|
1187
|
-
try {
|
|
1188
|
-
const json = JSON.parse(jsonBlockMatch[1]);
|
|
1189
|
-
return this.validateAndReturn(json, diffRanges);
|
|
1190
|
-
} catch {
|
|
1191
|
-
// Fall through
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const end = output.lastIndexOf("}");
|
|
1196
|
-
if (end !== -1) {
|
|
1197
|
-
let start = output.lastIndexOf("{", end);
|
|
1198
|
-
while (start !== -1) {
|
|
1199
|
-
const candidate = output.substring(start, end + 1);
|
|
1200
|
-
try {
|
|
1201
|
-
const json = JSON.parse(candidate);
|
|
1202
|
-
if (json.status) {
|
|
1203
|
-
return this.validateAndReturn(json, diffRanges);
|
|
1204
|
-
}
|
|
1205
|
-
} catch {
|
|
1206
|
-
// Not valid JSON, keep searching
|
|
1207
|
-
}
|
|
1208
|
-
start = output.lastIndexOf("{", start - 1);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const firstStart = output.indexOf("{");
|
|
1213
|
-
if (firstStart !== -1 && end !== -1 && end > firstStart) {
|
|
1214
|
-
try {
|
|
1215
|
-
const candidate = output.substring(firstStart, end + 1);
|
|
1216
|
-
const json = JSON.parse(candidate);
|
|
1217
|
-
return this.validateAndReturn(json, diffRanges);
|
|
1218
|
-
} catch {
|
|
1219
|
-
// Ignore
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
return {
|
|
1224
|
-
status: "error",
|
|
1225
|
-
message: "No valid JSON object found in output",
|
|
1226
|
-
};
|
|
1227
|
-
} catch (error: unknown) {
|
|
1228
|
-
const err = error as { message?: string };
|
|
1229
|
-
return {
|
|
1230
|
-
status: "error",
|
|
1231
|
-
message: `Failed to parse JSON output: ${err.message}`,
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
private validateAndReturn(
|
|
1237
|
-
json: ReviewJsonOutput,
|
|
1238
|
-
diffRanges?: Map<string, DiffFileRange>,
|
|
1239
|
-
): {
|
|
1240
|
-
status: "pass" | "fail" | "error";
|
|
1241
|
-
message: string;
|
|
1242
|
-
json?: ReviewJsonOutput;
|
|
1243
|
-
filteredCount?: number;
|
|
1244
|
-
} {
|
|
1245
|
-
if (!json.status || (json.status !== "pass" && json.status !== "fail")) {
|
|
1246
|
-
return {
|
|
1247
|
-
status: "error",
|
|
1248
|
-
message: 'Invalid JSON: missing or invalid "status" field',
|
|
1249
|
-
json,
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
if (json.status === "pass") {
|
|
1254
|
-
return { status: "pass", message: json.message || "Passed", json };
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
let filteredCount = 0;
|
|
1258
|
-
|
|
1259
|
-
if (Array.isArray(json.violations) && diffRanges?.size) {
|
|
1260
|
-
const originalCount = json.violations.length;
|
|
1261
|
-
|
|
1262
|
-
json.violations = json.violations.filter(
|
|
1263
|
-
(v: { file: string; line: number | string }) => {
|
|
1264
|
-
// Coerce string line numbers to numbers for validation
|
|
1265
|
-
const lineStr =
|
|
1266
|
-
typeof v.line === "string" ? v.line.trim() : undefined;
|
|
1267
|
-
const lineNum =
|
|
1268
|
-
typeof v.line === "number"
|
|
1269
|
-
? v.line
|
|
1270
|
-
: lineStr && /^\d+$/.test(lineStr)
|
|
1271
|
-
? Number(lineStr)
|
|
1272
|
-
: undefined;
|
|
1273
|
-
const isValid = isValidViolationLocation(v.file, lineNum, diffRanges);
|
|
1274
|
-
return isValid;
|
|
1275
|
-
},
|
|
1276
|
-
);
|
|
1277
|
-
|
|
1278
|
-
filteredCount = originalCount - json.violations.length;
|
|
1279
|
-
|
|
1280
|
-
if (json.violations.length === 0) {
|
|
1281
|
-
return {
|
|
1282
|
-
status: "pass",
|
|
1283
|
-
message: `Passed (${filteredCount} out-of-scope violations filtered)`,
|
|
1284
|
-
json: { status: "pass" },
|
|
1285
|
-
filteredCount,
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
const violationCount = Array.isArray(json.violations)
|
|
1291
|
-
? json.violations.length
|
|
1292
|
-
: "some";
|
|
1293
|
-
|
|
1294
|
-
const msg = `Found ${violationCount} violations`;
|
|
1295
|
-
|
|
1296
|
-
return { status: "fail", message: msg, json, filteredCount };
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
private async writeJsonResult(
|
|
1300
|
-
logPath: string,
|
|
1301
|
-
adapter: string,
|
|
1302
|
-
status: "pass" | "fail" | "error",
|
|
1303
|
-
rawOutput: string,
|
|
1304
|
-
json: ReviewJsonOutput,
|
|
1305
|
-
): Promise<string> {
|
|
1306
|
-
const jsonPath = logPath.replace(/\.log$/, ".json");
|
|
1307
|
-
const fullOutput: ReviewFullJsonOutput = {
|
|
1308
|
-
adapter,
|
|
1309
|
-
timestamp: new Date().toISOString(),
|
|
1310
|
-
status,
|
|
1311
|
-
rawOutput,
|
|
1312
|
-
violations: json.violations || [],
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
await fs.writeFile(jsonPath, JSON.stringify(fullOutput, null, 2));
|
|
1316
|
-
return jsonPath;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
private parseLines(stdout: string): string[] {
|
|
1320
|
-
return stdout
|
|
1321
|
-
.split("\n")
|
|
1322
|
-
.map((line) => line.trim())
|
|
1323
|
-
.filter((line) => line.length > 0);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
private pathArg(entryPointPath: string): string {
|
|
1327
|
-
return ` -- ${this.quoteArg(entryPointPath)}`;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
private quoteArg(value: string): string {
|
|
1331
|
-
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
1332
|
-
}
|
|
1333
|
-
}
|