agent-gauntlet 0.1.10 → 0.1.12
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 +55 -87
- package/package.json +4 -2
- package/src/bun-plugins.d.ts +4 -0
- package/src/cli-adapters/claude.ts +139 -108
- package/src/cli-adapters/codex.ts +141 -117
- package/src/cli-adapters/cursor.ts +152 -0
- package/src/cli-adapters/gemini.ts +171 -139
- package/src/cli-adapters/github-copilot.ts +153 -0
- package/src/cli-adapters/index.ts +77 -48
- package/src/commands/check.test.ts +24 -20
- package/src/commands/check.ts +86 -59
- package/src/commands/ci/index.ts +15 -0
- package/src/commands/ci/init.ts +96 -0
- package/src/commands/ci/list-jobs.ts +78 -0
- package/src/commands/detect.test.ts +38 -32
- package/src/commands/detect.ts +89 -61
- package/src/commands/health.test.ts +67 -53
- package/src/commands/health.ts +167 -145
- package/src/commands/help.test.ts +37 -37
- package/src/commands/help.ts +31 -22
- package/src/commands/index.ts +10 -9
- package/src/commands/init.test.ts +120 -107
- package/src/commands/init.ts +514 -417
- package/src/commands/list.test.ts +87 -70
- package/src/commands/list.ts +28 -24
- package/src/commands/rerun.ts +157 -119
- package/src/commands/review.test.ts +26 -20
- package/src/commands/review.ts +86 -59
- package/src/commands/run.test.ts +22 -20
- package/src/commands/run.ts +85 -58
- package/src/commands/shared.ts +44 -35
- package/src/config/ci-loader.ts +33 -0
- package/src/config/ci-schema.ts +52 -0
- package/src/config/loader.test.ts +112 -90
- package/src/config/loader.ts +132 -123
- package/src/config/schema.ts +48 -47
- package/src/config/types.ts +28 -13
- package/src/config/validator.ts +521 -454
- package/src/core/change-detector.ts +122 -104
- package/src/core/entry-point.test.ts +60 -62
- package/src/core/entry-point.ts +120 -74
- package/src/core/job.ts +69 -59
- package/src/core/runner.ts +264 -230
- package/src/gates/check.ts +78 -69
- package/src/gates/result.ts +7 -7
- package/src/gates/review.test.ts +277 -138
- package/src/gates/review.ts +724 -561
- package/src/index.ts +18 -15
- package/src/output/console.ts +253 -214
- package/src/output/logger.ts +66 -52
- package/src/templates/run_gauntlet.template.md +18 -0
- package/src/templates/workflow.yml +77 -0
- package/src/utils/diff-parser.ts +64 -62
- package/src/utils/log-parser.ts +227 -206
- package/src/utils/sanitizer.ts +1 -1
package/src/gates/review.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { getAdapter } from "../cli-adapters/index.js";
|
|
4
|
+
import type {
|
|
5
|
+
ReviewGateConfig,
|
|
6
|
+
ReviewPromptFrontmatter,
|
|
7
|
+
} from "../config/types.js";
|
|
8
|
+
import {
|
|
9
|
+
type DiffFileRange,
|
|
10
|
+
isValidViolationLocation,
|
|
11
|
+
parseDiff,
|
|
12
|
+
} from "../utils/diff-parser.js";
|
|
13
|
+
import type { PreviousViolation } from "../utils/log-parser.js";
|
|
14
|
+
import type { GateResult } from "./result.js";
|
|
9
15
|
|
|
10
16
|
const execAsync = promisify(exec);
|
|
11
17
|
|
|
@@ -47,562 +53,719 @@ If violations are found:
|
|
|
47
53
|
If NO violations are found:
|
|
48
54
|
{
|
|
49
55
|
"status": "pass",
|
|
50
|
-
"message": "No
|
|
56
|
+
"message": "No problems found"
|
|
51
57
|
}
|
|
52
58
|
`;
|
|
53
59
|
|
|
54
|
-
type ReviewConfig = ReviewGateConfig &
|
|
60
|
+
type ReviewConfig = ReviewGateConfig &
|
|
61
|
+
ReviewPromptFrontmatter & { promptContent?: string };
|
|
62
|
+
|
|
63
|
+
interface ReviewJsonOutput {
|
|
64
|
+
status: "pass" | "fail";
|
|
65
|
+
message?: string;
|
|
66
|
+
violations?: Array<{
|
|
67
|
+
file: string;
|
|
68
|
+
line: number | string;
|
|
69
|
+
issue: string;
|
|
70
|
+
fix?: string;
|
|
71
|
+
priority: "critical" | "high" | "medium" | "low";
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
55
74
|
|
|
56
75
|
export class ReviewGateExecutor {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
76
|
+
private constructPrompt(
|
|
77
|
+
config: ReviewConfig,
|
|
78
|
+
previousViolations: PreviousViolation[] = [],
|
|
79
|
+
): string {
|
|
80
|
+
const baseContent = config.promptContent || "";
|
|
81
|
+
|
|
82
|
+
if (previousViolations.length > 0) {
|
|
83
|
+
return (
|
|
84
|
+
baseContent +
|
|
85
|
+
"\n\n" +
|
|
86
|
+
this.buildPreviousFailuresSection(previousViolations) +
|
|
87
|
+
"\n" +
|
|
88
|
+
JSON_SYSTEM_INSTRUCTION
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `${baseContent}\n${JSON_SYSTEM_INSTRUCTION}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async execute(
|
|
96
|
+
jobId: string,
|
|
97
|
+
config: ReviewConfig,
|
|
98
|
+
entryPointPath: string,
|
|
99
|
+
loggerFactory: (adapterName?: string) => Promise<{
|
|
100
|
+
logger: (output: string) => Promise<void>;
|
|
101
|
+
logPath: string;
|
|
102
|
+
}>,
|
|
103
|
+
baseBranch: string,
|
|
104
|
+
previousFailures?: Map<string, PreviousViolation[]>,
|
|
105
|
+
changeOptions?: { commit?: string; uncommitted?: boolean },
|
|
106
|
+
checkUsageLimit: boolean = false,
|
|
107
|
+
): Promise<GateResult> {
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
const logBuffer: string[] = [];
|
|
110
|
+
let logSequence = 0; // Monotonic counter for dedup
|
|
111
|
+
const activeLoggers: Array<
|
|
112
|
+
(output: string, index: number) => Promise<void>
|
|
113
|
+
> = [];
|
|
114
|
+
const logPaths: string[] = [];
|
|
115
|
+
const logPathsSet = new Set<string>(); // O(1) lookup
|
|
116
|
+
|
|
117
|
+
const mainLogger = async (output: string) => {
|
|
118
|
+
const seq = logSequence++;
|
|
119
|
+
// Atomic length check and push
|
|
120
|
+
// We check length directly on the array property to ensure we use the current value.
|
|
121
|
+
// Even if we exceed the limit slightly due to concurrency (impossible in single-threaded JS),
|
|
122
|
+
// it's a soft limit.
|
|
123
|
+
if (logBuffer.length < MAX_LOG_BUFFER_SIZE) {
|
|
124
|
+
logBuffer.push(output);
|
|
125
|
+
}
|
|
126
|
+
// Use allSettled to prevent failures from stopping the main logger
|
|
127
|
+
await Promise.allSettled(activeLoggers.map((l) => l(output, seq)));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const getAdapterLogger = async (adapterName: string) => {
|
|
131
|
+
const { logger, logPath } = await loggerFactory(adapterName);
|
|
132
|
+
if (!logPathsSet.has(logPath)) {
|
|
133
|
+
logPathsSet.add(logPath);
|
|
134
|
+
logPaths.push(logPath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Robust synchronization using index tracking.
|
|
138
|
+
// We add the logger to activeLoggers FIRST to catch all future messages.
|
|
139
|
+
// We also flush the buffer.
|
|
140
|
+
// We use 'seenIndices' to prevent duplicates if a message arrives via both paths
|
|
141
|
+
// (e.g. added to buffer and sent to activeLoggers simultaneously).
|
|
142
|
+
// This acts as the atomic counter mechanism requested to safely handle race conditions.
|
|
143
|
+
// Even if mainLogger pushes to buffer and calls activeLoggers during the snapshot flush,
|
|
144
|
+
// seenIndices will prevent double logging.
|
|
145
|
+
const seenIndices = new Set<number>();
|
|
146
|
+
|
|
147
|
+
const safeLogger = async (msg: string, index: number) => {
|
|
148
|
+
if (seenIndices.has(index)) return;
|
|
149
|
+
seenIndices.add(index);
|
|
150
|
+
await logger(msg);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
activeLoggers.push(safeLogger);
|
|
154
|
+
|
|
155
|
+
// Flush existing buffer
|
|
156
|
+
const snapshot = [...logBuffer];
|
|
157
|
+
// We pass the loop index 'i' which corresponds to the buffer index
|
|
158
|
+
await Promise.all(snapshot.map((msg, i) => safeLogger(msg, i)));
|
|
159
|
+
|
|
160
|
+
return logger;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await mainLogger(`Starting review: ${config.name}\n`);
|
|
165
|
+
await mainLogger(`Entry point: ${entryPointPath}\n`);
|
|
166
|
+
await mainLogger(`Base branch: ${baseBranch}\n`);
|
|
167
|
+
|
|
168
|
+
const diff = await this.getDiff(
|
|
169
|
+
entryPointPath,
|
|
170
|
+
baseBranch,
|
|
171
|
+
changeOptions,
|
|
172
|
+
);
|
|
173
|
+
if (!diff.trim()) {
|
|
174
|
+
await mainLogger("No changes found in entry point, skipping review.\n");
|
|
175
|
+
await mainLogger("Result: pass - No changes to review\n");
|
|
176
|
+
return {
|
|
177
|
+
jobId,
|
|
178
|
+
status: "pass",
|
|
179
|
+
duration: Date.now() - startTime,
|
|
180
|
+
message: "No changes to review",
|
|
181
|
+
logPaths,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const required = config.num_reviews ?? 1;
|
|
186
|
+
const outputs: Array<{
|
|
187
|
+
adapter: string;
|
|
188
|
+
status: "pass" | "fail" | "error";
|
|
189
|
+
message: string;
|
|
190
|
+
}> = [];
|
|
191
|
+
const usedAdapters = new Set<string>();
|
|
192
|
+
|
|
193
|
+
const preferences = config.cli_preference || [];
|
|
194
|
+
const parallel = config.parallel ?? false;
|
|
195
|
+
|
|
196
|
+
if (parallel && required > 1) {
|
|
197
|
+
// Parallel Execution Logic
|
|
198
|
+
// Check health of adapters in parallel, but only as many as needed
|
|
199
|
+
const healthyAdapters: string[] = [];
|
|
200
|
+
let prefIndex = 0;
|
|
201
|
+
|
|
202
|
+
while (
|
|
203
|
+
healthyAdapters.length < required &&
|
|
204
|
+
prefIndex < preferences.length
|
|
205
|
+
) {
|
|
206
|
+
const batchSize = required - healthyAdapters.length;
|
|
207
|
+
const batch = preferences.slice(prefIndex, prefIndex + batchSize);
|
|
208
|
+
prefIndex += batchSize;
|
|
209
|
+
|
|
210
|
+
const batchResults = await Promise.all(
|
|
211
|
+
batch.map(async (toolName) => {
|
|
212
|
+
const adapter = getAdapter(toolName);
|
|
213
|
+
if (!adapter) return { toolName, status: "missing" as const };
|
|
214
|
+
const health = await adapter.checkHealth({ checkUsageLimit });
|
|
215
|
+
return { toolName, ...health };
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
for (const res of batchResults) {
|
|
220
|
+
if (res.status === "healthy") {
|
|
221
|
+
healthyAdapters.push(res.toolName);
|
|
222
|
+
} else if (res.status === "unhealthy") {
|
|
223
|
+
await mainLogger(
|
|
224
|
+
`Skipping ${res.toolName}: ${res.message || "Unhealthy"}\n`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (healthyAdapters.length < required) {
|
|
231
|
+
const msg = `Not enough healthy adapters. Need ${required}, found ${healthyAdapters.length}.`;
|
|
232
|
+
await mainLogger(`Result: error - ${msg}\n`);
|
|
233
|
+
return {
|
|
234
|
+
jobId,
|
|
235
|
+
status: "error",
|
|
236
|
+
duration: Date.now() - startTime,
|
|
237
|
+
message: msg,
|
|
238
|
+
logPaths,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Launch exactly 'required' reviews in parallel
|
|
243
|
+
const selectedAdapters = healthyAdapters.slice(0, required);
|
|
244
|
+
await mainLogger(
|
|
245
|
+
`Starting parallel reviews with: ${selectedAdapters.join(", ")}\n`,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const results = await Promise.all(
|
|
249
|
+
selectedAdapters.map((toolName) =>
|
|
250
|
+
this.runSingleReview(
|
|
251
|
+
toolName,
|
|
252
|
+
config,
|
|
253
|
+
diff,
|
|
254
|
+
getAdapterLogger,
|
|
255
|
+
mainLogger,
|
|
256
|
+
previousFailures,
|
|
257
|
+
true,
|
|
258
|
+
checkUsageLimit,
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
for (const res of results) {
|
|
264
|
+
if (res) {
|
|
265
|
+
outputs.push({ adapter: res.adapter, ...res.evaluation });
|
|
266
|
+
usedAdapters.add(res.adapter);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
// Sequential Execution Logic
|
|
271
|
+
for (const toolName of preferences) {
|
|
272
|
+
if (usedAdapters.size >= required) break;
|
|
273
|
+
const res = await this.runSingleReview(
|
|
274
|
+
toolName,
|
|
275
|
+
config,
|
|
276
|
+
diff,
|
|
277
|
+
getAdapterLogger,
|
|
278
|
+
mainLogger,
|
|
279
|
+
previousFailures,
|
|
280
|
+
false,
|
|
281
|
+
checkUsageLimit,
|
|
282
|
+
);
|
|
283
|
+
if (res) {
|
|
284
|
+
outputs.push({ adapter: res.adapter, ...res.evaluation });
|
|
285
|
+
usedAdapters.add(res.adapter);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (usedAdapters.size < required) {
|
|
291
|
+
const msg = `Failed to complete ${required} reviews. Completed: ${usedAdapters.size}. See logs for details.`;
|
|
292
|
+
await mainLogger(`Result: error - ${msg}\n`);
|
|
293
|
+
return {
|
|
294
|
+
jobId,
|
|
295
|
+
status: "error",
|
|
296
|
+
duration: Date.now() - startTime,
|
|
297
|
+
message: msg,
|
|
298
|
+
logPaths,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const failed = outputs.find((result) => result.status === "fail");
|
|
303
|
+
const error = outputs.find((result) => result.status === "error");
|
|
304
|
+
|
|
305
|
+
let status: "pass" | "fail" | "error" = "pass";
|
|
306
|
+
let message = "Passed";
|
|
307
|
+
|
|
308
|
+
if (error) {
|
|
309
|
+
status = "error";
|
|
310
|
+
message = `Error (${error.adapter}): ${error.message}`;
|
|
311
|
+
} else if (failed) {
|
|
312
|
+
status = "fail";
|
|
313
|
+
message = `Failed (${failed.adapter}): ${failed.message}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await mainLogger(`Result: ${status} - ${message}\n`);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
jobId,
|
|
320
|
+
status,
|
|
321
|
+
duration: Date.now() - startTime,
|
|
322
|
+
message,
|
|
323
|
+
logPaths,
|
|
324
|
+
};
|
|
325
|
+
} catch (error: unknown) {
|
|
326
|
+
const err = error as { message?: string };
|
|
327
|
+
await mainLogger(`Critical Error: ${err.message}\n`);
|
|
328
|
+
await mainLogger("Result: error\n");
|
|
329
|
+
return {
|
|
330
|
+
jobId,
|
|
331
|
+
status: "error",
|
|
332
|
+
duration: Date.now() - startTime,
|
|
333
|
+
message: err.message,
|
|
334
|
+
logPaths,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private async runSingleReview(
|
|
340
|
+
toolName: string,
|
|
341
|
+
config: ReviewConfig,
|
|
342
|
+
diff: string,
|
|
343
|
+
getAdapterLogger: (
|
|
344
|
+
adapterName: string,
|
|
345
|
+
) => Promise<(output: string) => Promise<void>>,
|
|
346
|
+
mainLogger: (output: string) => Promise<void>,
|
|
347
|
+
previousFailures?: Map<string, PreviousViolation[]>,
|
|
348
|
+
skipHealthCheck: boolean = false,
|
|
349
|
+
checkUsageLimit: boolean = false,
|
|
350
|
+
): Promise<{
|
|
351
|
+
adapter: string;
|
|
352
|
+
evaluation: {
|
|
353
|
+
status: "pass" | "fail" | "error";
|
|
354
|
+
message: string;
|
|
355
|
+
json?: ReviewJsonOutput;
|
|
356
|
+
};
|
|
357
|
+
} | null> {
|
|
358
|
+
const adapter = getAdapter(toolName);
|
|
359
|
+
if (!adapter) return null;
|
|
360
|
+
|
|
361
|
+
if (!skipHealthCheck) {
|
|
362
|
+
const health = await adapter.checkHealth({ checkUsageLimit });
|
|
363
|
+
if (health.status === "missing") return null;
|
|
364
|
+
if (health.status === "unhealthy") {
|
|
365
|
+
await mainLogger(
|
|
366
|
+
`Skipping ${adapter.name}: ${health.message || "Unhealthy"}\n`,
|
|
367
|
+
);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Create per-adapter logger
|
|
373
|
+
// Defensive check: ensure adapter name is valid
|
|
374
|
+
if (!adapter.name || typeof adapter.name !== "string") {
|
|
375
|
+
await mainLogger(
|
|
376
|
+
`Error: Invalid adapter name: ${JSON.stringify(adapter.name)}\n`,
|
|
377
|
+
);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const adapterLogger = await getAdapterLogger(adapter.name);
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const startMsg = `[START] review:.:${config.name} (${adapter.name})`;
|
|
384
|
+
await adapterLogger(`${startMsg}\n`);
|
|
385
|
+
|
|
386
|
+
const adapterPreviousViolations =
|
|
387
|
+
previousFailures?.get(adapter.name) || [];
|
|
388
|
+
const finalPrompt = this.constructPrompt(
|
|
389
|
+
config,
|
|
390
|
+
adapterPreviousViolations,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const output = await adapter.execute({
|
|
394
|
+
prompt: finalPrompt,
|
|
395
|
+
diff,
|
|
396
|
+
model: config.model,
|
|
397
|
+
timeoutMs: config.timeout ? config.timeout * 1000 : undefined,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await adapterLogger(
|
|
401
|
+
`\n--- Review Output (${adapter.name}) ---\n${output}\n`,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const evaluation = this.evaluateOutput(output, diff);
|
|
405
|
+
|
|
406
|
+
if (evaluation.filteredCount && evaluation.filteredCount > 0) {
|
|
407
|
+
await adapterLogger(
|
|
408
|
+
`Note: ${evaluation.filteredCount} out-of-scope violations filtered\n`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Log formatted summary
|
|
413
|
+
if (evaluation.json) {
|
|
414
|
+
await adapterLogger(`\n--- Parsed Result (${adapter.name}) ---\n`);
|
|
415
|
+
if (
|
|
416
|
+
evaluation.json.status === "fail" &&
|
|
417
|
+
Array.isArray(evaluation.json.violations)
|
|
418
|
+
) {
|
|
419
|
+
await adapterLogger(`Status: FAIL\n`);
|
|
420
|
+
await adapterLogger(`Violations:\n`);
|
|
421
|
+
for (const [i, v] of evaluation.json.violations.entries()) {
|
|
422
|
+
await adapterLogger(
|
|
423
|
+
`${i + 1}. ${v.file}:${v.line || "?"} - ${v.issue}\n`,
|
|
424
|
+
);
|
|
425
|
+
if (v.fix) await adapterLogger(` Fix: ${v.fix}\n`);
|
|
426
|
+
}
|
|
427
|
+
} else if (evaluation.json.status === "pass") {
|
|
428
|
+
await adapterLogger(`Status: PASS\n`);
|
|
429
|
+
if (evaluation.json.message)
|
|
430
|
+
await adapterLogger(`Message: ${evaluation.json.message}\n`);
|
|
431
|
+
} else {
|
|
432
|
+
await adapterLogger(`Status: ${evaluation.json.status}\n`);
|
|
433
|
+
await adapterLogger(
|
|
434
|
+
`Raw: ${JSON.stringify(evaluation.json, null, 2)}\n`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
await adapterLogger(`---------------------\n`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const resultMsg = `Review result (${adapter.name}): ${evaluation.status} - ${evaluation.message}`;
|
|
441
|
+
await adapterLogger(`${resultMsg}\n`);
|
|
442
|
+
await mainLogger(`${resultMsg}\n`);
|
|
443
|
+
|
|
444
|
+
return { adapter: adapter.name, evaluation };
|
|
445
|
+
} catch (error: unknown) {
|
|
446
|
+
const err = error as { message?: string };
|
|
447
|
+
const errorMsg = `Error running ${adapter.name}: ${err.message}`;
|
|
448
|
+
await adapterLogger(`${errorMsg}\n`);
|
|
449
|
+
await mainLogger(`${errorMsg}\n`);
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private async getDiff(
|
|
455
|
+
entryPointPath: string,
|
|
456
|
+
baseBranch: string,
|
|
457
|
+
options?: { commit?: string; uncommitted?: boolean },
|
|
458
|
+
): Promise<string> {
|
|
459
|
+
// If uncommitted mode is explicitly requested
|
|
460
|
+
if (options?.uncommitted) {
|
|
461
|
+
const pathArg = this.pathArg(entryPointPath);
|
|
462
|
+
// Match ChangeDetector.getUncommittedChangedFiles() behavior
|
|
463
|
+
const staged = await this.execDiff(`git diff --cached${pathArg}`);
|
|
464
|
+
const unstaged = await this.execDiff(`git diff${pathArg}`);
|
|
465
|
+
const untracked = await this.untrackedDiff(entryPointPath);
|
|
466
|
+
return [staged, unstaged, untracked].filter(Boolean).join("\n");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// If a specific commit is requested
|
|
470
|
+
if (options?.commit) {
|
|
471
|
+
const pathArg = this.pathArg(entryPointPath);
|
|
472
|
+
// Match ChangeDetector.getCommitChangedFiles() behavior
|
|
473
|
+
try {
|
|
474
|
+
return await this.execDiff(
|
|
475
|
+
`git diff ${options.commit}^..${options.commit}${pathArg}`,
|
|
476
|
+
);
|
|
477
|
+
} catch (error: unknown) {
|
|
478
|
+
// Handle initial commit case
|
|
479
|
+
const err = error as { message?: string; stderr?: string };
|
|
480
|
+
if (
|
|
481
|
+
err.message?.includes("unknown revision") ||
|
|
482
|
+
err.stderr?.includes("unknown revision")
|
|
483
|
+
) {
|
|
484
|
+
return await this.execDiff(
|
|
485
|
+
`git diff --root ${options.commit}${pathArg}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const isCI =
|
|
493
|
+
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
494
|
+
return isCI
|
|
495
|
+
? this.getCIDiff(entryPointPath, baseBranch)
|
|
496
|
+
: this.getLocalDiff(entryPointPath, baseBranch);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private async getCIDiff(
|
|
500
|
+
entryPointPath: string,
|
|
501
|
+
baseBranch: string,
|
|
502
|
+
): Promise<string> {
|
|
503
|
+
// Base branch priority is already resolved by caller
|
|
504
|
+
const baseRef = baseBranch;
|
|
505
|
+
const headRef = process.env.GITHUB_SHA || "HEAD";
|
|
506
|
+
const pathArg = this.pathArg(entryPointPath);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
return await this.execDiff(`git diff ${baseRef}...${headRef}${pathArg}`);
|
|
510
|
+
} catch (_error) {
|
|
511
|
+
const fallback = await this.execDiff(`git diff HEAD^...HEAD${pathArg}`);
|
|
512
|
+
return fallback;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private async getLocalDiff(
|
|
517
|
+
entryPointPath: string,
|
|
518
|
+
baseBranch: string,
|
|
519
|
+
): Promise<string> {
|
|
520
|
+
const pathArg = this.pathArg(entryPointPath);
|
|
521
|
+
const committed = await this.execDiff(
|
|
522
|
+
`git diff ${baseBranch}...HEAD${pathArg}`,
|
|
523
|
+
);
|
|
524
|
+
const uncommitted = await this.execDiff(`git diff HEAD${pathArg}`);
|
|
525
|
+
const untracked = await this.untrackedDiff(entryPointPath);
|
|
526
|
+
|
|
527
|
+
return [committed, uncommitted, untracked].filter(Boolean).join("\n");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private async untrackedDiff(entryPointPath: string): Promise<string> {
|
|
531
|
+
const pathArg = this.pathArg(entryPointPath);
|
|
532
|
+
const { stdout } = await execAsync(
|
|
533
|
+
`git ls-files --others --exclude-standard${pathArg}`,
|
|
534
|
+
{
|
|
535
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
const files = this.parseLines(stdout);
|
|
539
|
+
const diffs: string[] = [];
|
|
540
|
+
|
|
541
|
+
for (const file of files) {
|
|
542
|
+
try {
|
|
543
|
+
const diff = await this.execDiff(
|
|
544
|
+
`git diff --no-index -- /dev/null ${this.quoteArg(file)}`,
|
|
545
|
+
);
|
|
546
|
+
if (diff.trim()) diffs.push(diff);
|
|
547
|
+
} catch (error: unknown) {
|
|
548
|
+
// Only suppress errors for missing/deleted files (ENOENT or "Could not access")
|
|
549
|
+
// Re-throw other errors (permissions, git issues) so they surface properly
|
|
550
|
+
const err = error as { message?: string; stderr?: string };
|
|
551
|
+
const msg = [err.message, err.stderr].filter(Boolean).join("\n");
|
|
552
|
+
if (
|
|
553
|
+
msg.includes("Could not access") ||
|
|
554
|
+
msg.includes("ENOENT") ||
|
|
555
|
+
msg.includes("No such file")
|
|
556
|
+
) {
|
|
557
|
+
// File was deleted/moved between listing and diff; skip it
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return diffs.join("\n");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private async execDiff(command: string): Promise<string> {
|
|
568
|
+
try {
|
|
569
|
+
const { stdout } = await execAsync(command, {
|
|
570
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
571
|
+
});
|
|
572
|
+
return stdout;
|
|
573
|
+
} catch (error: unknown) {
|
|
574
|
+
const err = error as { code?: number; stdout?: string };
|
|
575
|
+
if (typeof err.code === "number" && err.stdout) {
|
|
576
|
+
return err.stdout;
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private buildPreviousFailuresSection(
|
|
583
|
+
violations: PreviousViolation[],
|
|
584
|
+
): string {
|
|
585
|
+
const lines = [
|
|
586
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
587
|
+
"PREVIOUS FAILURES TO VERIFY (from last run)",
|
|
588
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
589
|
+
"",
|
|
590
|
+
"The following violations were identified in the previous review. Your PRIMARY TASK is to verify whether these specific issues have been fixed in the current changes:",
|
|
591
|
+
"",
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
violations.forEach((v, i) => {
|
|
595
|
+
lines.push(`${i + 1}. ${v.file}:${v.line} - ${v.issue}`);
|
|
596
|
+
if (v.fix) {
|
|
597
|
+
lines.push(` Suggested fix: ${v.fix}`);
|
|
598
|
+
}
|
|
599
|
+
lines.push("");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
lines.push("INSTRUCTIONS:");
|
|
603
|
+
lines.push(
|
|
604
|
+
"- Check if each violation listed above has been addressed in the diff",
|
|
605
|
+
);
|
|
606
|
+
lines.push(
|
|
607
|
+
"- For violations that are fixed, confirm they no longer appear",
|
|
608
|
+
);
|
|
609
|
+
lines.push(
|
|
610
|
+
"- For violations that remain unfixed, include them in your violations array",
|
|
611
|
+
);
|
|
612
|
+
lines.push("- Also check for any NEW violations in the changed code");
|
|
613
|
+
lines.push(
|
|
614
|
+
'- Return status "pass" only if ALL previous violations are fixed AND no new violations exist',
|
|
615
|
+
);
|
|
616
|
+
lines.push("");
|
|
617
|
+
lines.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
618
|
+
|
|
619
|
+
return lines.join("\n");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
public evaluateOutput(
|
|
623
|
+
output: string,
|
|
624
|
+
diff?: string,
|
|
625
|
+
): {
|
|
626
|
+
status: "pass" | "fail" | "error";
|
|
627
|
+
message: string;
|
|
628
|
+
json?: ReviewJsonOutput;
|
|
629
|
+
filteredCount?: number;
|
|
630
|
+
} {
|
|
631
|
+
const diffRanges = diff ? parseDiff(diff) : undefined;
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
// 1. Try to extract from markdown code block first (most reliable)
|
|
635
|
+
const jsonBlockMatch = output.match(/```json\s*([\s\S]*?)\s*```/);
|
|
636
|
+
if (jsonBlockMatch) {
|
|
637
|
+
try {
|
|
638
|
+
const json = JSON.parse(jsonBlockMatch[1]);
|
|
639
|
+
return this.validateAndReturn(json, diffRanges);
|
|
640
|
+
} catch {
|
|
641
|
+
// If code block parse fails, fall back to other methods
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 2. Fallback: Find the last valid JSON object
|
|
646
|
+
// This helps when there are braces in the explanation text before the actual JSON
|
|
647
|
+
// We start from the last '}' and search backwards for a matching '{' that creates valid JSON
|
|
648
|
+
const end = output.lastIndexOf("}");
|
|
649
|
+
if (end !== -1) {
|
|
650
|
+
let start = output.lastIndexOf("{", end);
|
|
651
|
+
while (start !== -1) {
|
|
652
|
+
const candidate = output.substring(start, end + 1);
|
|
653
|
+
try {
|
|
654
|
+
const json = JSON.parse(candidate);
|
|
655
|
+
// If we successfully parsed an object with 'status', it's likely our result
|
|
656
|
+
if (json.status) {
|
|
657
|
+
return this.validateAndReturn(json, diffRanges);
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
// Not valid JSON, keep searching backwards
|
|
661
|
+
}
|
|
662
|
+
start = output.lastIndexOf("{", start - 1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 3. Last resort: simplistic extraction (original behavior)
|
|
667
|
+
const firstStart = output.indexOf("{");
|
|
668
|
+
if (firstStart !== -1 && end !== -1 && end > firstStart) {
|
|
669
|
+
try {
|
|
670
|
+
const candidate = output.substring(firstStart, end + 1);
|
|
671
|
+
const json = JSON.parse(candidate);
|
|
672
|
+
return this.validateAndReturn(json, diffRanges);
|
|
673
|
+
} catch {
|
|
674
|
+
// Ignore
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
status: "error",
|
|
680
|
+
message: "No valid JSON object found in output",
|
|
681
|
+
};
|
|
682
|
+
} catch (error: unknown) {
|
|
683
|
+
const err = error as { message?: string };
|
|
684
|
+
return {
|
|
685
|
+
status: "error",
|
|
686
|
+
message: `Failed to parse JSON output: ${err.message}`,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private validateAndReturn(
|
|
692
|
+
json: ReviewJsonOutput,
|
|
693
|
+
diffRanges?: Map<string, DiffFileRange>,
|
|
694
|
+
): {
|
|
695
|
+
status: "pass" | "fail" | "error";
|
|
696
|
+
message: string;
|
|
697
|
+
json?: ReviewJsonOutput;
|
|
698
|
+
filteredCount?: number;
|
|
699
|
+
} {
|
|
700
|
+
// Validate Schema
|
|
701
|
+
if (!json.status || (json.status !== "pass" && json.status !== "fail")) {
|
|
702
|
+
return {
|
|
703
|
+
status: "error",
|
|
704
|
+
message: 'Invalid JSON: missing or invalid "status" field',
|
|
705
|
+
json,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (json.status === "pass") {
|
|
710
|
+
return { status: "pass", message: json.message || "Passed", json };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// json.status === 'fail'
|
|
714
|
+
let filteredCount = 0;
|
|
715
|
+
|
|
716
|
+
if (Array.isArray(json.violations) && diffRanges?.size) {
|
|
717
|
+
const originalCount = json.violations.length;
|
|
718
|
+
|
|
719
|
+
json.violations = json.violations.filter(
|
|
720
|
+
(v: { file: string; line: number | string }) => {
|
|
721
|
+
const isValid = isValidViolationLocation(v.file, v.line, diffRanges);
|
|
722
|
+
if (!isValid) {
|
|
723
|
+
// Can't easily access logger here, but could return warning info
|
|
724
|
+
// console.warn(`[WARNING] Filtered violation: ${v.file}:${v.line ?? '?'} (not in diff)`);
|
|
725
|
+
}
|
|
726
|
+
return isValid;
|
|
727
|
+
},
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
filteredCount = originalCount - json.violations.length;
|
|
731
|
+
|
|
732
|
+
// If all filtered out, change to pass
|
|
733
|
+
if (json.violations.length === 0) {
|
|
734
|
+
return {
|
|
735
|
+
status: "pass",
|
|
736
|
+
message: `Passed (${filteredCount} out-of-scope violations filtered)`,
|
|
737
|
+
json: { status: "pass" },
|
|
738
|
+
filteredCount,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const violationCount = Array.isArray(json.violations)
|
|
744
|
+
? json.violations.length
|
|
745
|
+
: "some";
|
|
746
|
+
|
|
747
|
+
// Construct a summary message
|
|
748
|
+
let msg = `Found ${violationCount} violations`;
|
|
749
|
+
if (Array.isArray(json.violations) && json.violations.length > 0) {
|
|
750
|
+
const first = json.violations[0];
|
|
751
|
+
msg += `. Example: ${first.issue} in ${first.file}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return { status: "fail", message: msg, json, filteredCount };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private parseLines(stdout: string): string[] {
|
|
758
|
+
return stdout
|
|
759
|
+
.split("\n")
|
|
760
|
+
.map((line) => line.trim())
|
|
761
|
+
.filter((line) => line.length > 0);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private pathArg(entryPointPath: string): string {
|
|
765
|
+
return ` -- ${this.quoteArg(entryPointPath)}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private quoteArg(value: string): string {
|
|
769
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
770
|
+
}
|
|
608
771
|
}
|