@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.
- package/.claude-plugin/plugin.json +2 -1
- package/README.md +7 -3
- package/catalog/skills.json +18 -0
- package/package.json +5 -3
- package/packages/software-development/lekker-review/SKILL.md +519 -0
- package/packages/software-development/lekker-review/adapters/claude/plugin.json +5 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/SKILL.md +520 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/completeness-critic.md +21 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/conventions.md +124 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fix-verifier.md +84 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/fixer.md +120 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/implementation.md +53 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/prover.md +135 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/quality.md +72 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/simplification.md +45 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/test-quality.md +170 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-logic.md +27 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/triage-quality.md +41 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/agents/verifier.md +295 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/artifact-page.md +143 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/context-gathering.md +162 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/fix-mode.md +329 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/github-post.md +205 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/house-rules.md +76 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/references/output-format.md +232 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/changed-files.sh +77 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/setup-worktree.sh +337 -0
- package/packages/software-development/lekker-review/adapters/claude/skills/lekker-review/scripts/verify-fixes.sh +231 -0
- package/packages/software-development/lekker-review/fix-workflow.js +273 -0
- package/packages/software-development/lekker-review/references/agents/completeness-critic.md +21 -0
- package/packages/software-development/lekker-review/references/agents/conventions.md +124 -0
- package/packages/software-development/lekker-review/references/agents/fix-verifier.md +84 -0
- package/packages/software-development/lekker-review/references/agents/fixer.md +120 -0
- package/packages/software-development/lekker-review/references/agents/implementation.md +53 -0
- package/packages/software-development/lekker-review/references/agents/prover.md +135 -0
- package/packages/software-development/lekker-review/references/agents/quality.md +72 -0
- package/packages/software-development/lekker-review/references/agents/simplification.md +45 -0
- package/packages/software-development/lekker-review/references/agents/test-quality.md +170 -0
- package/packages/software-development/lekker-review/references/agents/triage-logic.md +27 -0
- package/packages/software-development/lekker-review/references/agents/triage-quality.md +41 -0
- package/packages/software-development/lekker-review/references/agents/verifier.md +295 -0
- package/packages/software-development/lekker-review/references/artifact-page.md +143 -0
- package/packages/software-development/lekker-review/references/context-gathering.md +162 -0
- package/packages/software-development/lekker-review/references/fix-mode.md +329 -0
- package/packages/software-development/lekker-review/references/github-post.md +205 -0
- package/packages/software-development/lekker-review/references/house-rules.md +76 -0
- package/packages/software-development/lekker-review/references/output-format.md +232 -0
- package/packages/software-development/lekker-review/scripts/changed-files.sh +77 -0
- package/packages/software-development/lekker-review/scripts/setup-worktree.sh +337 -0
- package/packages/software-development/lekker-review/scripts/verify-fixes.sh +231 -0
- package/packages/software-development/lekker-review/workflow.js +602 -0
- package/site/assets/paperbag.css +707 -0
- package/site/assets/paperbag.js +218 -0
- 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
|
+
}
|