@olegkoval/agent-skills 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/README.md +7 -3
  3. package/catalog/skills.json +18 -0
  4. package/package.json +5 -3
  5. package/packages/software-development/lekker-review/SKILL.md +519 -0
  6. package/packages/software-development/lekker-review/adapters/claude/plugin.json +5 -0
  7. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/SKILL.md +520 -0
  8. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/completeness-critic.md +21 -0
  9. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/conventions.md +124 -0
  10. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fix-verifier.md +84 -0
  11. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fixer.md +120 -0
  12. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/implementation.md +53 -0
  13. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/prover.md +135 -0
  14. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/quality.md +72 -0
  15. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/simplification.md +45 -0
  16. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/test-quality.md +170 -0
  17. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-logic.md +27 -0
  18. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-quality.md +41 -0
  19. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/verifier.md +295 -0
  20. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/artifact-page.md +143 -0
  21. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/context-gathering.md +162 -0
  22. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/fix-mode.md +329 -0
  23. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/github-post.md +205 -0
  24. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/house-rules.md +76 -0
  25. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/output-format.md +232 -0
  26. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/changed-files.sh +77 -0
  27. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/setup-worktree.sh +337 -0
  28. package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/verify-fixes.sh +231 -0
  29. package/packages/software-development/lekker-review/fix-workflow.js +273 -0
  30. package/packages/software-development/lekker-review/references/agents/completeness-critic.md +21 -0
  31. package/packages/software-development/lekker-review/references/agents/conventions.md +124 -0
  32. package/packages/software-development/lekker-review/references/agents/fix-verifier.md +84 -0
  33. package/packages/software-development/lekker-review/references/agents/fixer.md +120 -0
  34. package/packages/software-development/lekker-review/references/agents/implementation.md +53 -0
  35. package/packages/software-development/lekker-review/references/agents/prover.md +135 -0
  36. package/packages/software-development/lekker-review/references/agents/quality.md +72 -0
  37. package/packages/software-development/lekker-review/references/agents/simplification.md +45 -0
  38. package/packages/software-development/lekker-review/references/agents/test-quality.md +170 -0
  39. package/packages/software-development/lekker-review/references/agents/triage-logic.md +27 -0
  40. package/packages/software-development/lekker-review/references/agents/triage-quality.md +41 -0
  41. package/packages/software-development/lekker-review/references/agents/verifier.md +295 -0
  42. package/packages/software-development/lekker-review/references/artifact-page.md +143 -0
  43. package/packages/software-development/lekker-review/references/context-gathering.md +162 -0
  44. package/packages/software-development/lekker-review/references/fix-mode.md +329 -0
  45. package/packages/software-development/lekker-review/references/github-post.md +205 -0
  46. package/packages/software-development/lekker-review/references/house-rules.md +76 -0
  47. package/packages/software-development/lekker-review/references/output-format.md +232 -0
  48. package/packages/software-development/lekker-review/scripts/changed-files.sh +77 -0
  49. package/packages/software-development/lekker-review/scripts/setup-worktree.sh +337 -0
  50. package/packages/software-development/lekker-review/scripts/verify-fixes.sh +231 -0
  51. package/packages/software-development/lekker-review/workflow.js +602 -0
  52. package/site/assets/paperbag.css +707 -0
  53. package/site/assets/paperbag.js +218 -0
  54. package/site/build.mjs +380 -0
@@ -0,0 +1,602 @@
1
+ export const meta = {
2
+ name: 'lekker-review-core',
3
+ description: 'Parallel specialist PR review with per-finding adversarial verification',
4
+ phases: [ { title: 'Review' }, { title: 'Verify' }, { title: 'Critic' }, { title: 'Prove' } ],
5
+ }
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Schemas
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const FINDINGS_SCHEMA = {
12
+ type: 'object',
13
+ required: ['findings'],
14
+ properties: {
15
+ findings: {
16
+ type: 'array',
17
+ items: {
18
+ type: 'object',
19
+ required: ['file', 'line', 'severity', 'title', 'description', 'badCode', 'fix'],
20
+ properties: {
21
+ file: { type: 'string' },
22
+ line: { type: 'integer' },
23
+ severity: { enum: ['critical', 'important', 'observation', 'idiomatic'] },
24
+ title: { type: 'string' },
25
+ description: { type: 'string' },
26
+ badCode: { type: 'string' },
27
+ fix: { type: 'string' },
28
+ precedent: { type: 'string' },
29
+ rule: { enum: ['TS-1', 'TS-2', 'GQL-1', 'PR-1'] },
30
+ },
31
+ },
32
+ },
33
+ acCoverage: { type: 'string' },
34
+ mutationSlip: { type: 'string' },
35
+ coverageVerdict: { type: 'string' },
36
+ mockSmells: {
37
+ type: 'array',
38
+ items: {
39
+ type: 'object',
40
+ properties: {
41
+ file: { type: 'string' },
42
+ line: { type: 'integer' },
43
+ description: { type: 'string' },
44
+ fix: { type: 'string' },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ }
50
+
51
+ const VERDICT_SCHEMA = {
52
+ type: 'object',
53
+ required: ['verdict', 'reasoning'],
54
+ properties: {
55
+ verdict: { enum: ['confirmed', 'downgraded', 'dropped'] },
56
+ newSeverity: { enum: ['important', 'observation'] },
57
+ reasoning: { type: 'string' },
58
+ },
59
+ }
60
+
61
+ const CRITIC_SCHEMA = {
62
+ type: 'object',
63
+ required: ['angles'],
64
+ properties: {
65
+ angles: {
66
+ type: 'array',
67
+ items: {
68
+ type: 'object',
69
+ required: ['axis', 'file', 'line', 'reason'],
70
+ properties: {
71
+ axis: { type: 'string' },
72
+ file: { type: 'string' },
73
+ line: { type: 'integer' },
74
+ reason: { type: 'string' },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ }
80
+
81
+ const PROOF_SCHEMA = {
82
+ type: 'object',
83
+ required: ['attempted', 'proven', 'reason'],
84
+ properties: {
85
+ attempted: { type: 'boolean' },
86
+ proven: { type: 'boolean' },
87
+ reason: { type: 'string' },
88
+ testCode: { type: 'string' },
89
+ testCommand: { type: 'string' },
90
+ redOutput: { type: 'string' },
91
+ },
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Helpers
96
+ // ---------------------------------------------------------------------------
97
+
98
+ const HARD_RULES = ['TS-1', 'TS-2', 'GQL-1', 'PR-1']
99
+
100
+ function isHardRule(finding) {
101
+ return typeof finding.rule === 'string' && HARD_RULES.indexOf(finding.rule) !== -1
102
+ }
103
+
104
+ const SEVERITY_RANK = { critical: 3, important: 2, observation: 1, idiomatic: 0 }
105
+
106
+ function titleTokens(title) {
107
+ const words = String(title || '').toLowerCase().match(/[a-z0-9]+/g) || []
108
+ return words.filter(function(w) { return w.length > 2 })
109
+ }
110
+
111
+ function sameIssue(a, b) {
112
+ const ta = titleTokens(a.title)
113
+ const tb = titleTokens(b.title)
114
+ if (ta.length === 0 || tb.length === 0) {
115
+ return false
116
+ }
117
+ const setB = new Set(tb)
118
+ const shared = ta.filter(function(w) { return setB.has(w) }).length
119
+ const union = new Set(ta.concat(tb)).size
120
+ return (shared / union) >= 0.4
121
+ }
122
+
123
+ function longest(a, b) {
124
+ const sa = a || ''
125
+ const sb = b || ''
126
+ return sb.length > sa.length ? sb : sa
127
+ }
128
+
129
+ // Merge two findings that were independently flagged as the same issue at the
130
+ // same file:line. A critical found by one dimension must not be silently
131
+ // demoted to an observation found by another dimension at the same line, so
132
+ // merging always keeps the highest severity and the most detailed text
133
+ // across the merged set, and unions the set of dimensions that agreed.
134
+ function mergeFindings(a, b) {
135
+ const merged = Object.assign({}, a)
136
+
137
+ if (SEVERITY_RANK[b.severity] > SEVERITY_RANK[merged.severity]) {
138
+ merged.severity = b.severity
139
+ }
140
+
141
+ merged.description = longest(a.description, b.description)
142
+ merged.badCode = longest(a.badCode, b.badCode)
143
+ merged.fix = longest(a.fix, b.fix)
144
+ merged.precedent = longest(a.precedent, b.precedent)
145
+
146
+ if (!merged.rule && b.rule) {
147
+ merged.rule = b.rule
148
+ }
149
+
150
+ const agreedBy = []
151
+ const contributors = (a.agreedBy || (a._dimension ? [a._dimension] : []))
152
+ .concat(b.agreedBy || (b._dimension ? [b._dimension] : []))
153
+ for (const dim of contributors) {
154
+ if (dim && agreedBy.indexOf(dim) === -1) {
155
+ agreedBy.push(dim)
156
+ }
157
+ }
158
+ if (agreedBy.length > 0) {
159
+ merged.agreedBy = agreedBy
160
+ }
161
+
162
+ return merged
163
+ }
164
+
165
+ function dedup(allFindings) {
166
+ const byLocation = new Map()
167
+ for (const f of allFindings) {
168
+ const key = `${f.file}:${f.line}`
169
+ if (!byLocation.has(key)) {
170
+ byLocation.set(key, [])
171
+ }
172
+ byLocation.get(key).push(f)
173
+ }
174
+
175
+ const result = []
176
+ for (const candidates of byLocation.values()) {
177
+ const groups = []
178
+ for (const candidate of candidates) {
179
+ const match = groups.find(function(g) { return sameIssue(g[0], candidate) })
180
+ if (match) {
181
+ match.push(candidate)
182
+ } else {
183
+ groups.push([candidate])
184
+ }
185
+ }
186
+ for (const group of groups) {
187
+ result.push(group.reduce(mergeFindings))
188
+ }
189
+ }
190
+ return result
191
+ }
192
+
193
+ function shouldVerify(finding, depth) {
194
+ // house hard rules are policy violations, not runtime-failure claims. The
195
+ // five adversarial challenges cannot be answered for them, so verifying
196
+ // would systematically drop findings that are Critical by policy.
197
+ if (isHardRule(finding)) {
198
+ return false
199
+ }
200
+ if (depth === 'scan') {
201
+ return false
202
+ }
203
+ if (depth === 'medium') {
204
+ return finding.severity === 'critical'
205
+ }
206
+ // deep: critical + important
207
+ return finding.severity === 'critical' || finding.severity === 'important'
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Main
212
+ // ---------------------------------------------------------------------------
213
+
214
+ // The workflow harness executes this script body directly in an async context;
215
+ // `args` is a global provided by the runtime. It may arrive as a JSON string
216
+ // depending on how the caller encoded it, so normalize before destructuring.
217
+ const input = (typeof args === 'string') ? JSON.parse(args) : (args || {})
218
+ const {
219
+ repoSlug,
220
+ prNumber,
221
+ prUrl,
222
+ depth,
223
+ diffFile,
224
+ contextFile,
225
+ worktreePath,
226
+ promptDir,
227
+ prevSha,
228
+ } = input
229
+
230
+ if (!repoSlug || !prNumber || !depth || !diffFile || !contextFile || !promptDir) {
231
+ throw new Error(
232
+ 'lekker-review workflow: missing required args (got type ' + typeof args +
233
+ '): ' + JSON.stringify({ repoSlug, prNumber, depth, diffFile, contextFile, promptDir })
234
+ )
235
+ }
236
+ log(`args ok: PR #${prNumber} in ${repoSlug}, depth=${depth}`)
237
+
238
+ const budgetAtStart = budget.spent()
239
+
240
+ let agentCount = 0
241
+ let droppedCount = 0
242
+ let downgradedCount = 0
243
+ let hardRuleCount = 0
244
+ let provenCount = 0
245
+ let proveAttemptCount = 0
246
+
247
+ // -------------------------------------------------------------------------
248
+ // Dimensions by depth
249
+ // -------------------------------------------------------------------------
250
+
251
+ const isScan = depth === 'scan'
252
+
253
+ const dimensions = isScan
254
+ ? [
255
+ { key: 'triage-quality', file: 'triage-quality.md', model: 'haiku' },
256
+ { key: 'triage-logic', file: 'triage-logic.md', model: 'haiku' },
257
+ ]
258
+ : [
259
+ { key: 'quality', file: 'quality.md', model: 'sonnet' },
260
+ { key: 'implementation', file: 'implementation.md', model: 'sonnet' },
261
+ { key: 'simplification', file: 'simplification.md', model: 'sonnet' },
262
+ { key: 'conventions', file: 'conventions.md', model: 'sonnet' },
263
+ { key: 'test-quality', file: 'test-quality.md', model: 'sonnet' },
264
+ ]
265
+
266
+ const wtDisplay = worktreePath || 'null (scan mode: diff only)'
267
+
268
+ // -------------------------------------------------------------------------
269
+ // Build review agent envelope prompt
270
+ // -------------------------------------------------------------------------
271
+
272
+ function reviewPrompt(dim) {
273
+ let prompt = [
274
+ `You are the ${dim.key} review agent for PR #${prNumber} in ${repoSlug} (${prUrl}).`,
275
+ `First Read and follow the prompt file: ${promptDir}/${dim.file}.`,
276
+ `Parameters: REPO_SLUG=${repoSlug}, PR_NUMBER=${prNumber}, PR_URL=${prUrl},`,
277
+ `DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${wtDisplay}.`,
278
+ ].join(' ')
279
+
280
+ if (prevSha) {
281
+ prompt += ` RE-REVIEW MODE: a prior review covered up to commit ${prevSha}.`
282
+ + ` Focus findings on the delta (see key deltaFile in CONTEXT_FILE);`
283
+ + ` use the full diff for context only.`
284
+ }
285
+
286
+ return prompt
287
+ }
288
+
289
+ // -------------------------------------------------------------------------
290
+ // Build verifier agent prompt
291
+ // -------------------------------------------------------------------------
292
+
293
+ function verifierPrompt(finding) {
294
+ return [
295
+ `You are an adversarial finding verifier for PR #${prNumber} in ${repoSlug}.`,
296
+ `Read and follow ${promptDir}/verifier.md.`,
297
+ `FINDING (JSON): ${JSON.stringify(finding)}.`,
298
+ `DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${wtDisplay}.`,
299
+ `Default to dropping when uncertain.`,
300
+ ].join(' ')
301
+ }
302
+
303
+ // -------------------------------------------------------------------------
304
+ // Review stage: run all dimensions in parallel via pipeline
305
+ // -------------------------------------------------------------------------
306
+
307
+ async function reviewStage(dim) {
308
+ agentCount++
309
+ const result = await agent(reviewPrompt(dim), {
310
+ label: `review:${dim.key}`,
311
+ phase: 'Review',
312
+ schema: FINDINGS_SCHEMA,
313
+ model: dim.model,
314
+ })
315
+
316
+ if (!result) {
317
+ log(`${dim.key}: agent returned null, skipping`)
318
+ return []
319
+ }
320
+
321
+ const findings = (result.findings || []).map(function(f) {
322
+ const copy = Object.assign({}, f)
323
+ copy._dimension = dim.key
324
+ return copy
325
+ })
326
+
327
+ log(`${dim.key}: ${findings.length} findings, ${
328
+ findings.filter(function(f) { return shouldVerify(f, depth) }).length
329
+ } to verify`)
330
+
331
+ return findings
332
+ }
333
+
334
+ // -------------------------------------------------------------------------
335
+ // Verify stage: takes the deduped cross-dimension finding set, splits into
336
+ // in-scope (adversarially verified) and out-of-scope (kept as-is, including
337
+ // house hard rules which are exempt by policy), and runs verifiers in
338
+ // parallel over the in-scope set.
339
+ // -------------------------------------------------------------------------
340
+
341
+ async function verifyAll(findings) {
342
+ const inScope = findings.filter(function(f) {
343
+ return shouldVerify(f, depth)
344
+ })
345
+ const outOfScope = findings.filter(function(f) {
346
+ return !shouldVerify(f, depth)
347
+ })
348
+
349
+ const exempted = outOfScope.map(function(f) {
350
+ if (!isHardRule(f)) {
351
+ return f
352
+ }
353
+ hardRuleCount++
354
+ const exempt = Object.assign({}, f)
355
+ exempt.verifierReasoning = `hard rule ${f.rule}: exempt from adversarial verification (standards violation, not a runtime-failure claim)`
356
+ return exempt
357
+ })
358
+
359
+ if (inScope.length === 0) {
360
+ return exempted
361
+ }
362
+
363
+ const verified = await parallel(inScope.map(function(finding) {
364
+ return async function() {
365
+ agentCount++
366
+ const verdict = await agent(verifierPrompt(finding), {
367
+ label: `verify:${finding.file}:${finding.line}`,
368
+ model: 'sonnet',
369
+ phase: 'Verify',
370
+ schema: VERDICT_SCHEMA,
371
+ effort: 'high',
372
+ })
373
+
374
+ if (!verdict) {
375
+ // Null agent result: treat as confirmed-unverified
376
+ const kept = Object.assign({}, finding)
377
+ kept.verifierReasoning = 'verifier agent returned null; kept unverified'
378
+ return kept
379
+ }
380
+
381
+ if (verdict.verdict === 'dropped') {
382
+ droppedCount++
383
+ return null
384
+ }
385
+
386
+ if (verdict.verdict === 'downgraded') {
387
+ downgradedCount++
388
+ const downgraded = Object.assign({}, finding)
389
+ downgraded.severity = verdict.newSeverity || 'observation'
390
+ downgraded.verifierReasoning = verdict.reasoning
391
+ return downgraded
392
+ }
393
+
394
+ // confirmed
395
+ const confirmed = Object.assign({}, finding)
396
+ confirmed.verifierReasoning = verdict.reasoning
397
+ return confirmed
398
+ }
399
+ }))
400
+
401
+ return exempted.concat(verified.filter(Boolean))
402
+ }
403
+
404
+ // -------------------------------------------------------------------------
405
+ // Review -> dedup -> verify: all reviewers run to completion (barrier),
406
+ // then findings are deduped once across dimensions, then the unique set is
407
+ // verified. This avoids burning two verifier agents (and risking two
408
+ // contradictory verdicts) on the same issue found by two dimensions.
409
+ // -------------------------------------------------------------------------
410
+
411
+ phase('Review')
412
+
413
+ const reviewed = await parallel(dimensions.map(function(dim) {
414
+ return function() { return reviewStage(dim) }
415
+ }))
416
+
417
+ const deduped = dedup(reviewed.filter(Boolean).flat())
418
+
419
+ log(`${deduped.length} unique finding(s) after dedup; verifying ${
420
+ deduped.filter(function(f) { return shouldVerify(f, depth) }).length
421
+ }`)
422
+
423
+ phase('Verify')
424
+
425
+ const verifiedFindings = await verifyAll(deduped)
426
+
427
+ log(`Verify complete: ${verifiedFindings.length} finding(s) (dropped=${droppedCount}, downgraded=${downgradedCount}, hardRuleExempt=${hardRuleCount})`)
428
+
429
+ // -------------------------------------------------------------------------
430
+ // Critic (deep only)
431
+ // -------------------------------------------------------------------------
432
+
433
+ let finalFindings = verifiedFindings
434
+
435
+ if (depth === 'deep') {
436
+ phase('Critic')
437
+
438
+ const findingSummary = verifiedFindings.map(function(f) {
439
+ return { file: f.file, line: f.line, severity: f.severity, title: f.title }
440
+ })
441
+
442
+ agentCount++
443
+ const criticResult = await agent(
444
+ [
445
+ `You are the completeness critic for PR #${prNumber} in ${repoSlug}.`,
446
+ `Read and follow ${promptDir}/completeness-critic.md.`,
447
+ `Existing findings (JSON): ${JSON.stringify(findingSummary)}.`,
448
+ `DIFF_FILE=${diffFile}, WORKTREE_PATH=${wtDisplay}.`,
449
+ ].join(' '),
450
+ {
451
+ label: 'critic',
452
+ model: 'sonnet',
453
+ phase: 'Critic',
454
+ schema: CRITIC_SCHEMA,
455
+ }
456
+ )
457
+
458
+ const angles = (criticResult && criticResult.angles) ? criticResult.angles : []
459
+ log(`Critic: ${angles.length} angles to re-examine`)
460
+
461
+ if (angles.length > 0) {
462
+ const angleResults = await parallel(angles.map(function(angle) {
463
+ return async function() {
464
+ agentCount++
465
+ const reResult = await agent(
466
+ [
467
+ `You are re-examining a specific review angle for PR #${prNumber} in ${repoSlug}.`,
468
+ `Axis: ${angle.axis} at ${angle.file}:${angle.line}.`,
469
+ `Reason for re-examination: ${angle.reason}.`,
470
+ `Apply the same diff-anchor and verification rules from ${promptDir}/verifier.md.`,
471
+ `Return zero or one finding using DIFF_FILE=${diffFile} and WORKTREE_PATH=${wtDisplay}.`,
472
+ ].join(' '),
473
+ {
474
+ label: `critic-reexamine:${angle.file}:${angle.line}`,
475
+ model: 'sonnet',
476
+ phase: 'Critic',
477
+ schema: FINDINGS_SCHEMA,
478
+ effort: 'high',
479
+ }
480
+ )
481
+
482
+ if (!reResult || !reResult.findings || reResult.findings.length === 0) {
483
+ return []
484
+ }
485
+
486
+ // Verify the new finding through the same verifier path
487
+ const newFinding = Object.assign({}, reResult.findings[0])
488
+ newFinding._dimension = `critic:${angle.axis}`
489
+
490
+ agentCount++
491
+ const verdict = await agent(verifierPrompt(newFinding), {
492
+ label: `critic-verify:${newFinding.file}:${newFinding.line}`,
493
+ model: 'sonnet',
494
+ phase: 'Critic',
495
+ schema: VERDICT_SCHEMA,
496
+ effort: 'high',
497
+ })
498
+
499
+ if (!verdict || verdict.verdict === 'dropped') {
500
+ droppedCount++
501
+ return []
502
+ }
503
+
504
+ if (verdict.verdict === 'downgraded') {
505
+ downgradedCount++
506
+ newFinding.severity = verdict.newSeverity || 'observation'
507
+ newFinding.verifierReasoning = verdict.reasoning
508
+ } else {
509
+ newFinding.verifierReasoning = verdict.reasoning
510
+ }
511
+
512
+ return [newFinding]
513
+ }
514
+ }))
515
+
516
+ const newFindings = angleResults.flat().filter(Boolean)
517
+ if (newFindings.length > 0) {
518
+ log(`Critic promoted ${newFindings.length} additional finding(s)`)
519
+ finalFindings = dedup(verifiedFindings.concat(newFindings))
520
+ }
521
+ }
522
+ }
523
+
524
+ // -------------------------------------------------------------------------
525
+ // Prove: for each Critical finding (excluding hard rules, which are policy
526
+ // violations with no runtime failure to demonstrate), attempt to produce an
527
+ // executable failing test in the worktree. Only runs with a worktree and
528
+ // outside scan depth, and is capped so a pathological finding count can't
529
+ // blow the budget.
530
+ // -------------------------------------------------------------------------
531
+
532
+ function proverPrompt(finding) {
533
+ return [
534
+ `You are the proof-of-bug agent for PR #${prNumber} in ${repoSlug}.`,
535
+ `Read and follow ${promptDir}/prover.md.`,
536
+ `FINDING (JSON): ${JSON.stringify(finding)}.`,
537
+ `DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${worktreePath}.`,
538
+ `You get ONE attempt. Leave the worktree exactly as you found it.`,
539
+ ].join(' ')
540
+ }
541
+
542
+ if (worktreePath && depth !== 'scan') {
543
+ const proveCandidates = finalFindings.filter(function(f) {
544
+ return f.severity === 'critical' && !isHardRule(f)
545
+ })
546
+
547
+ if (proveCandidates.length > 0) {
548
+ phase('Prove')
549
+
550
+ const capped = proveCandidates.slice(0, 5)
551
+ const skipped = proveCandidates.length - capped.length
552
+ if (skipped > 0) {
553
+ log(`Prove: capping at 5 provers, skipping ${skipped} additional critical finding(s)`)
554
+ }
555
+
556
+ await parallel(capped.map(function(finding) {
557
+ return async function() {
558
+ agentCount++
559
+ proveAttemptCount++
560
+ const result = await agent(proverPrompt(finding), {
561
+ label: 'prove:' + finding.file + ':' + finding.line,
562
+ phase: 'Prove',
563
+ schema: PROOF_SCHEMA,
564
+ model: 'sonnet',
565
+ effort: 'high',
566
+ })
567
+
568
+ if (!result) {
569
+ finding.proof = null
570
+ log(`prove:${finding.file}:${finding.line}: agent returned null`)
571
+ return
572
+ }
573
+
574
+ finding.proof = result
575
+ if (result.proven === true) {
576
+ provenCount++
577
+ }
578
+ }
579
+ }))
580
+
581
+ log('Prove: ' + provenCount + '/' + proveAttemptCount + ' finding(s) demonstrated with a failing test')
582
+ }
583
+ }
584
+
585
+ // Strip internal _dimension field from output
586
+ const output = finalFindings.map(function(f) {
587
+ const clean = Object.assign({}, f)
588
+ delete clean._dimension
589
+ return clean
590
+ })
591
+
592
+ return {
593
+ findings: output,
594
+ droppedCount,
595
+ downgradedCount,
596
+ hardRuleCount,
597
+ agentCount,
598
+ proveAttemptCount,
599
+ provenCount,
600
+ outputTokens: budget.spent() - budgetAtStart,
601
+ turnTokensTotal: budget.spent(),
602
+ }