@olegkoval/agent-skills 1.72.0 → 1.74.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/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +9 -6
- package/adapters/claude/olko-github-pr/skills/lekker-review/evals/manifest.json +1 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/project-rules.md +48 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/codex-workflow.md +2 -2
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/eval-runner.mjs +54 -16
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +4 -3
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/README.md +2 -2
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +9 -6
- package/plugins/olko-github-pr/skills/lekker-review/evals/manifest.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/project-rules.md +48 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/codex-workflow.md +2 -2
- package/plugins/olko-github-pr/skills/lekker-review/scripts/eval-runner.mjs +54 -16
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +4 -3
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +28 -8
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
|
@@ -434,7 +434,7 @@ skip the review because the log failed, just say `run log unavailable`.
|
|
|
434
434
|
| Context: issue-tracker/CI/diff/existing-reviews | always | always | always | always |
|
|
435
435
|
| Context: chat/docs/framework-docs/monitoring/prior-review-memory (optional, MCP-dependent) | skip | skip | included | included + broader recall |
|
|
436
436
|
| Worktree + static checks | included (`npm ci` per changed lockfile) | skip (WORKTREE_PATH=null) unless `--fix` | included | included |
|
|
437
|
-
| Review agents | 1 deps checklist | 2 triage (haiku)
|
|
437
|
+
| Review agents | 1 deps checklist | 2 triage (haiku) + project-rules (sonnet) | 5 specialists + project-rules (sonnet) | 5 specialists + project-rules (sonnet) |
|
|
438
438
|
| Per-finding verification | none | Criticals + Importants | Criticals + Importants | Criticals + Importants |
|
|
439
439
|
| Completeness critic | skip | skip | skip | included |
|
|
440
440
|
| Proof-of-bug (failing test per Critical or composed-boundary Important) | skip | skip | included (max 5) | included (max 5) |
|
|
@@ -767,8 +767,8 @@ must pass it, or the workflow throws on its missing-args guard.
|
|
|
767
767
|
|
|
768
768
|
Two optional args tune concurrency; omit both unless a run needs it:
|
|
769
769
|
|
|
770
|
-
- `reviewBatchPlan` - batch sizes for the review dimensions, default `[
|
|
771
|
-
(all
|
|
770
|
+
- `reviewBatchPlan` - batch sizes for the review dimensions, default `[6]`
|
|
771
|
+
(all six at once). The last entry repeats to cover any remainder, so
|
|
772
772
|
`[2]` would mean two at a time.
|
|
773
773
|
- `maxConcurrent` - cap for the per-finding fan-outs (verify, critic, prove),
|
|
774
774
|
default `5`.
|
|
@@ -780,8 +780,9 @@ The workflow runs three phases:
|
|
|
780
780
|
|
|
781
781
|
- **Review:** deps uses the single `deps` checklist agent; scan uses `[triage-quality, triage-logic]` on `haiku`; medium/deep
|
|
782
782
|
use 5 specialists (quality, implementation, simplification, conventions,
|
|
783
|
-
test-quality) on `sonnet`.
|
|
784
|
-
|
|
783
|
+
test-quality) on `sonnet`. Scan, medium and deep also run `project-rules`
|
|
784
|
+
(sonnet), which only checks the diff against each project rule. Agents receive DIFF_FILE + CONTEXT_FILE by path.
|
|
785
|
+
All run concurrently: the stage is self-limiting at one agent per
|
|
785
786
|
dimension. All reviewers run to completion before verification starts.
|
|
786
787
|
Override per run with the `reviewBatchPlan` arg (e.g. `[1]` runs them
|
|
787
788
|
strictly one at a time on a constrained machine).
|
|
@@ -1429,7 +1430,9 @@ threads remain in the total but do not count as dismissed. Only comments ending
|
|
|
1429
1430
|
counted, so PRs posted before the marker existed score zero comments.
|
|
1430
1431
|
**Eval harness.** `evals/manifest.json` lists PRs with a known outcome (`expect:
|
|
1431
1432
|
"buggy"` with `knownBug: "file:line description"`, or `"clean"`). Run lekker on
|
|
1432
|
-
each, save the `findings` arrays
|
|
1433
|
+
each, save the `findings` arrays under each case's key (`id`, else
|
|
1434
|
+
`<repo>#<pr>@<sha>` when the entry has `sha`, else `<repo>#<pr>`), for example
|
|
1435
|
+
`{"<repo>#<pr>": [...]}`; a value may be an array of runs to get a hit rate, then
|
|
1433
1436
|
`node "${SKILL_ROOT}/scripts/eval-runner.mjs" --manifest "${SKILL_ROOT}/evals/manifest.json" --results <results.json>`
|
|
1434
1437
|
prints recall on buggy PRs and the false-positive count (critical or important
|
|
1435
1438
|
findings) on clean ones. The shipped manifest holds two placeholders; choosing
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"note": "PLACEHOLDERS ONLY. Replace with real PRs chosen by a human: known-buggy PRs with the bug located, and clean PRs that merged without follow-up fixes.",
|
|
2
|
+
"note": "PLACEHOLDERS ONLY. Replace with real PRs chosen by a human: known-buggy PRs with the bug located, and clean PRs that merged without follow-up fixes. Add id or sha to an entry to list the same PR at two commits (buggy head, merged head); a results value may be an array of runs.",
|
|
3
3
|
"prs": [
|
|
4
4
|
{
|
|
5
5
|
"repo": "example-org/example-repo",
|
package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/project-rules.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# project-rules: lekker-review agent prompt
|
|
2
|
+
You will receive in your task message: REPO_SLUG, PR_NUMBER, PR_URL, DIFF_FILE, CONTEXT_FILE (JSON), WORKTREE_PATH (may be null).
|
|
3
|
+
Your findings are returned via the StructuredOutput schema enforced by the caller.
|
|
4
|
+
|
|
5
|
+
You are the project-rules reviewer. Your only job is to find changed lines that
|
|
6
|
+
break the repository's own written rules. Report nothing else: no general bugs,
|
|
7
|
+
no style, no suggestions. Other agents cover those.
|
|
8
|
+
|
|
9
|
+
Procedure:
|
|
10
|
+
1. Read CONTEXT_FILE. The authoritative rules are "baseProjectRules" when it is a
|
|
11
|
+
string (the base branch's copy, which the PR author cannot edit), otherwise
|
|
12
|
+
"projectRules". When the chosen source is missing or empty, return
|
|
13
|
+
`{"findings": []}` and stop.
|
|
14
|
+
2. List every rule written as a prohibition or an obligation (NEVER, MUST,
|
|
15
|
+
MUST NOT, ALWAYS, DO NOT, or a plain-English equivalent). Work through the
|
|
16
|
+
whole source: a rule buried in a long file counts exactly as much as the
|
|
17
|
+
first one. Preferences and suggestions are not rules.
|
|
18
|
+
3. Read DIFF_FILE. For EACH listed rule, check every `+` line against it. Do not
|
|
19
|
+
rely on skimming: go rule by rule.
|
|
20
|
+
4. When a rule has a mechanical signature (a literal string, API or function
|
|
21
|
+
name, import, file pattern, naming pattern, template-literal shape), derive
|
|
22
|
+
the pattern and search the diff for it (grep or Bash is allowed) instead of
|
|
23
|
+
relying on reading. Check added file paths in the diff headers too.
|
|
24
|
+
5. A rule is broken by a `+` line (or an added file). Unchanged code is out of
|
|
25
|
+
scope.
|
|
26
|
+
6. Separately, when "baseProjectRules" is a string, check every `-` line in
|
|
27
|
+
DIFF_FILE against its rule lines (including the rules files themselves, such
|
|
28
|
+
as CLAUDE.md or AGENTS.md). A rule line that is removed, or reworded so it
|
|
29
|
+
forbids or requires less (a NEVER softened to a preference, a dropped
|
|
30
|
+
exception list, a narrowed scope), is a breach even when no `+` line violates
|
|
31
|
+
anything. The PR's own copy cannot clear it: compare against the base copy.
|
|
32
|
+
|
|
33
|
+
Reporting contract (same as the PROJECT RULES AS HARD RULES instruction below):
|
|
34
|
+
- One finding per broken rule, Critical, `rule` set to `PROJECT:<short-kebab-slug>`,
|
|
35
|
+
`ruleQuote` set to the exact sentence from the authoritative source, file:line on
|
|
36
|
+
the first violating line, every other violating site in `otherInstances`.
|
|
37
|
+
- When the diff deletes or weakens a rule line that is in "baseProjectRules"
|
|
38
|
+
(procedure step 6), report an Important finding with `rule`
|
|
39
|
+
"weakens-project-rule", `ruleQuote` set to the exact base-branch sentence,
|
|
40
|
+
file:line on the removed or reworded line, and `badCode` set to the verbatim
|
|
41
|
+
`-` line (plus the replacing `+` line when there is one). `fix` is to restore
|
|
42
|
+
the rule or to state why the project dropped it.
|
|
43
|
+
- Never quote a rule that is not in the authoritative source.
|
|
44
|
+
- `badCode` is REQUIRED: the verbatim violating line copied from the diff. `fix`
|
|
45
|
+
is REQUIRED: a concrete drop-in replacement that follows the rule. If you cannot
|
|
46
|
+
quote and fix it, drop it.
|
|
47
|
+
|
|
48
|
+
CI status, acceptance criteria and existing reviews are not your concern.
|
|
@@ -66,9 +66,9 @@ unavailable; do not infer what it meant from prose.
|
|
|
66
66
|
Select dimensions exactly as the Claude workflow does:
|
|
67
67
|
|
|
68
68
|
- deps: `deps` (single checklist agent; no verifier, no prover)
|
|
69
|
-
- scan: `triage-quality`, `triage-logic`
|
|
69
|
+
- scan: `triage-quality`, `triage-logic`, `project-rules`
|
|
70
70
|
- medium/deep: `quality`, `implementation`, `simplification`, `conventions`,
|
|
71
|
-
`test-quality`
|
|
71
|
+
`test-quality`, `project-rules`
|
|
72
72
|
|
|
73
73
|
For each dimension, spawn a `default` read-only agent with the following
|
|
74
74
|
bounded task:
|
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
// Usage:
|
|
5
5
|
// node eval-runner.mjs --manifest evals/manifest.json --results <results.json> [--json]
|
|
6
6
|
//
|
|
7
|
-
// manifest.json: { prs: [{ repo, pr, expect: "buggy"|"clean", knownBug?: "file:line + description" }] }
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// manifest.json: { prs: [{ repo, pr, expect: "buggy"|"clean", knownBug?: "file:line + description", id?, sha? }] }
|
|
8
|
+
// The case key is `id` when present, else "<repo>#<pr>@<sha>" when `sha` is
|
|
9
|
+
// present, else "<repo>#<pr>". Use id or sha to list one PR at two commits.
|
|
10
|
+
// results.json: { "<case key>": [ <finding>, ... ] } the findings of one lekker run per case
|
|
11
|
+
// (the `findings` array the workflow returns), or an array of such arrays for
|
|
12
|
+
// repeated runs, which adds per-case hitRate and per-run counts to separate
|
|
13
|
+
// model noise from a real difference. A case missing from results is
|
|
10
14
|
// reported as unscored, never as zero findings.
|
|
11
15
|
//
|
|
12
16
|
// A buggy PR is HIT when some finding is on the knownBug file within LINE_WINDOW lines
|
|
13
17
|
// of the knownBug line. Known limitation: the match is by location only, so a
|
|
14
18
|
// same-file finding near the line counts even if it describes a different problem;
|
|
15
|
-
// judging "same bug" needs an LLM or a description match and is not attempted. Recall = hits /
|
|
19
|
+
// judging "same bug" needs an LLM or a description match and is not attempted. Recall = hits /
|
|
20
|
+
// scored buggy runs (one run per case unless a case has repeated runs). On a clean PR every
|
|
16
21
|
// verdict-affecting finding (critical or important) is a false positive.
|
|
17
22
|
|
|
18
23
|
import { readFileSync } from 'node:fs';
|
|
@@ -38,6 +43,15 @@ export function isHit(findings, knownBug) {
|
|
|
38
43
|
&& Math.abs(f.line - spot.line) <= LINE_WINDOW);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
export function caseKey(e) {
|
|
47
|
+
if (typeof e.id === 'string' && e.id) return e.id;
|
|
48
|
+
return typeof e.sha === 'string' && e.sha ? `${e.repo}#${e.pr}@${e.sha}` : `${e.repo}#${e.pr}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SHA = /^[0-9a-f]{7,40}$/i;
|
|
52
|
+
|
|
53
|
+
const isMultiRun = (v) => Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
|
|
54
|
+
|
|
41
55
|
export function validateManifest(manifest) {
|
|
42
56
|
const prs = manifest && Array.isArray(manifest.prs) ? manifest.prs : null;
|
|
43
57
|
if (!prs) throw new Error('manifest needs a "prs" array');
|
|
@@ -45,7 +59,9 @@ export function validateManifest(manifest) {
|
|
|
45
59
|
prs.forEach((e, i) => {
|
|
46
60
|
if (!e.repo || !Number.isInteger(e.pr)) throw new Error(`prs[${i}]: repo and integer pr are required`);
|
|
47
61
|
if (e.expect !== 'buggy' && e.expect !== 'clean') throw new Error(`prs[${i}]: expect must be "buggy" or "clean"`);
|
|
48
|
-
|
|
62
|
+
if ('id' in e && (typeof e.id !== 'string' || !e.id)) throw new Error(`prs[${i}]: id must be a non-empty string`);
|
|
63
|
+
if ('sha' in e && (typeof e.sha !== 'string' || !SHA.test(e.sha))) throw new Error(`prs[${i}]: sha must be 7 to 40 hex characters`);
|
|
64
|
+
const key = caseKey(e);
|
|
49
65
|
if (seen.has(key)) throw new Error(`prs[${i}]: duplicate entry ${key}`);
|
|
50
66
|
seen.add(key);
|
|
51
67
|
if (e.expect === 'buggy' && !parseKnownBug(e.knownBug)) {
|
|
@@ -57,25 +73,42 @@ export function validateManifest(manifest) {
|
|
|
57
73
|
|
|
58
74
|
export function score(manifest, results) {
|
|
59
75
|
const rows = validateManifest(manifest).map((e) => {
|
|
60
|
-
const key =
|
|
61
|
-
const
|
|
62
|
-
if (!Array.isArray(
|
|
63
|
-
|
|
76
|
+
const key = caseKey(e);
|
|
77
|
+
const value = results[key];
|
|
78
|
+
if (!Array.isArray(value)) return { key, expect: e.expect, scored: false };
|
|
79
|
+
const runs = isMultiRun(value) ? value : null;
|
|
80
|
+
if (e.expect === 'buggy') {
|
|
81
|
+
if (!runs) return { key, expect: 'buggy', scored: true, hit: isHit(value, e.knownBug) };
|
|
82
|
+
const hits = runs.filter((r) => isHit(r, e.knownBug)).length;
|
|
83
|
+
return { key, expect: 'buggy', scored: true, hit: hits > 0, runs: runs.length, hits, hitRate: hits / runs.length };
|
|
84
|
+
}
|
|
85
|
+
const fp = (r) => r.filter((f) => BLOCKING.has(f.severity)).length;
|
|
86
|
+
if (!runs) return { key, expect: 'clean', scored: true, falsePositives: fp(value) };
|
|
87
|
+
const perRun = runs.map(fp);
|
|
64
88
|
return {
|
|
65
|
-
key, expect: 'clean', scored: true,
|
|
66
|
-
|
|
89
|
+
key, expect: 'clean', scored: true, falsePositives: perRun.reduce((n, x) => n + x, 0),
|
|
90
|
+
runs: runs.length, falsePositivesPerRun: perRun,
|
|
67
91
|
};
|
|
68
92
|
});
|
|
69
93
|
const buggy = rows.filter((r) => r.expect === 'buggy' && r.scored);
|
|
70
94
|
const clean = rows.filter((r) => r.expect === 'clean' && r.scored);
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
const buggyRuns = buggy.reduce((n, r) => n + (r.runs || 1), 0);
|
|
96
|
+
const cleanRuns = clean.reduce((n, r) => n + (r.runs || 1), 0);
|
|
97
|
+
const falsePositives = clean.reduce((n, r) => n + r.falsePositives, 0);
|
|
98
|
+
const out = {
|
|
99
|
+
recall: buggyRuns === 0 ? null : buggy.reduce((n, r) => n + (r.runs ? r.hits : Number(r.hit)), 0) / buggyRuns,
|
|
73
100
|
buggyScored: buggy.length,
|
|
74
|
-
falsePositives
|
|
101
|
+
falsePositives,
|
|
75
102
|
cleanScored: clean.length,
|
|
76
103
|
unscored: rows.filter((r) => !r.scored).map((r) => r.key),
|
|
77
104
|
rows,
|
|
78
105
|
};
|
|
106
|
+
if (rows.some((r) => r.runs)) {
|
|
107
|
+
Object.assign(out, {
|
|
108
|
+
buggyRuns, cleanRuns, falsePositivesPerCleanRun: cleanRuns === 0 ? null : falsePositives / cleanRuns,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
function main(argv) {
|
|
@@ -86,8 +119,13 @@ function main(argv) {
|
|
|
86
119
|
}
|
|
87
120
|
const out = score(JSON.parse(readFileSync(flag('manifest'), 'utf8')), JSON.parse(readFileSync(flag('results'), 'utf8')));
|
|
88
121
|
if (argv.includes('--json')) { console.log(JSON.stringify(out)); return; }
|
|
89
|
-
const pct =
|
|
90
|
-
|
|
122
|
+
const pct = (x) => (x === null ? 'n/a' : `${Math.round(x * 100)}%`);
|
|
123
|
+
if (out.buggyRuns === undefined) {
|
|
124
|
+
console.log(`recall ${pct(out.recall)} (${out.buggyScored} buggy scored), false positives ${out.falsePositives} (${out.cleanScored} clean scored), unscored ${out.unscored.length}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const perRun = out.falsePositivesPerCleanRun === null ? 'n/a' : out.falsePositivesPerCleanRun.toFixed(2);
|
|
128
|
+
console.log(`recall ${pct(out.recall)} per run (${out.buggyRuns} runs over ${out.buggyScored} buggy cases), false positives ${out.falsePositives} (${out.cleanRuns} runs over ${out.cleanScored} clean cases, ${perRun} per run), unscored ${out.unscored.length}`);
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -335,8 +335,8 @@ console.log('\ndeps depth: routing, classifier, line validation')
|
|
|
335
335
|
const dimensionsFor = new Function(dimsSrc + '\nreturn dimensionsFor')()
|
|
336
336
|
const M = { review: 'r', triage: 't' }
|
|
337
337
|
check('deps runs exactly one agent', dimensionsFor('deps', M).map(d => d.file), ['deps.md'])
|
|
338
|
-
check('scan
|
|
339
|
-
check('medium
|
|
338
|
+
check('scan runs the triage pair plus project-rules', dimensionsFor('scan', M).map(d => d.key), ['triage-quality', 'triage-logic', 'project-rules'])
|
|
339
|
+
check('medium runs five specialists plus project-rules', dimensionsFor('medium', M).length, 6)
|
|
340
340
|
check('every dimension prompt file exists',
|
|
341
341
|
['deps', 'scan', 'medium'].flatMap(d => dimensionsFor(d, M)).every(d => existsSync(join(SKILL, 'references', 'agents', d.file))), true)
|
|
342
342
|
}
|
|
@@ -871,7 +871,8 @@ function liftBlock(name) {
|
|
|
871
871
|
const liftBlockOr = name => { try { return liftBlock(name) } catch { return `const ${name} = ''\n` } }
|
|
872
872
|
const buildPrompt = new Function(
|
|
873
873
|
'dim', 'SUPPRESSED', 'prevSha',
|
|
874
|
-
[liftBlockOr('PROJECT_RULE_DIMENSIONS'), liftBlockOr('
|
|
874
|
+
[liftBlockOr('PROJECT_RULE_DIMENSIONS'), liftBlockOr('PROJECT_RULE_CORE'), liftBlockOr('IGNORE_ENTRY_NOTE'),
|
|
875
|
+
liftBlockOr('PROJECT_RULE_INSTRUCTION'), liftBlockOr('PROJECT_RULE_ONLY_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
|
|
875
876
|
`const targetMetadata = '{}', promptDir = 'p', repoSlug = 'o/r', prNumber = 1, prUrl = 'u',
|
|
876
877
|
diffFile = 'd', contextFile = 'c', wtDisplay = 'w'`,
|
|
877
878
|
lift('reviewPrompt'), 'return reviewPrompt(dim)'].join('\n'),
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
FAANG-quality PR code review for GitHub. It gathers the context a human reviewer
|
|
4
4
|
would gather and checks the branch out into an isolated worktree when the depth
|
|
5
|
-
requires one. `scan` runs two triage agents. `medium` and
|
|
6
|
-
`deep` run five specialists and attempt proofs for at most five non-rule
|
|
5
|
+
requires one. `scan` runs two triage agents plus project-rules. `medium` and
|
|
6
|
+
`deep` run five specialists plus project-rules and attempt proofs for at most five non-rule
|
|
7
7
|
Critical findings.
|
|
8
8
|
|
|
9
9
|

|
|
@@ -433,7 +433,7 @@ skip the review because the log failed, just say `run log unavailable`.
|
|
|
433
433
|
| Context: issue-tracker/CI/diff/existing-reviews | always | always | always | always |
|
|
434
434
|
| Context: chat/docs/framework-docs/monitoring/prior-review-memory (optional, MCP-dependent) | skip | skip | included | included + broader recall |
|
|
435
435
|
| Worktree + static checks | included (`npm ci` per changed lockfile) | skip (WORKTREE_PATH=null) unless `--fix` | included | included |
|
|
436
|
-
| Review agents | 1 deps checklist | 2 triage (haiku)
|
|
436
|
+
| Review agents | 1 deps checklist | 2 triage (haiku) + project-rules (sonnet) | 5 specialists + project-rules (sonnet) | 5 specialists + project-rules (sonnet) |
|
|
437
437
|
| Per-finding verification | none | Criticals + Importants | Criticals + Importants | Criticals + Importants |
|
|
438
438
|
| Completeness critic | skip | skip | skip | included |
|
|
439
439
|
| Proof-of-bug (failing test per Critical or composed-boundary Important) | skip | skip | included (max 5) | included (max 5) |
|
|
@@ -766,8 +766,8 @@ must pass it, or the workflow throws on its missing-args guard.
|
|
|
766
766
|
|
|
767
767
|
Two optional args tune concurrency; omit both unless a run needs it:
|
|
768
768
|
|
|
769
|
-
- `reviewBatchPlan` - batch sizes for the review dimensions, default `[
|
|
770
|
-
(all
|
|
769
|
+
- `reviewBatchPlan` - batch sizes for the review dimensions, default `[6]`
|
|
770
|
+
(all six at once). The last entry repeats to cover any remainder, so
|
|
771
771
|
`[2]` would mean two at a time.
|
|
772
772
|
- `maxConcurrent` - cap for the per-finding fan-outs (verify, critic, prove),
|
|
773
773
|
default `5`.
|
|
@@ -779,8 +779,9 @@ The workflow runs three phases:
|
|
|
779
779
|
|
|
780
780
|
- **Review:** deps uses the single `deps` checklist agent; scan uses `[triage-quality, triage-logic]` on `haiku`; medium/deep
|
|
781
781
|
use 5 specialists (quality, implementation, simplification, conventions,
|
|
782
|
-
test-quality) on `sonnet`.
|
|
783
|
-
|
|
782
|
+
test-quality) on `sonnet`. Scan, medium and deep also run `project-rules`
|
|
783
|
+
(sonnet), which only checks the diff against each project rule. Agents receive DIFF_FILE + CONTEXT_FILE by path.
|
|
784
|
+
All run concurrently: the stage is self-limiting at one agent per
|
|
784
785
|
dimension. All reviewers run to completion before verification starts.
|
|
785
786
|
Override per run with the `reviewBatchPlan` arg (e.g. `[1]` runs them
|
|
786
787
|
strictly one at a time on a constrained machine).
|
|
@@ -1428,7 +1429,9 @@ threads remain in the total but do not count as dismissed. Only comments ending
|
|
|
1428
1429
|
counted, so PRs posted before the marker existed score zero comments.
|
|
1429
1430
|
**Eval harness.** `evals/manifest.json` lists PRs with a known outcome (`expect:
|
|
1430
1431
|
"buggy"` with `knownBug: "file:line description"`, or `"clean"`). Run lekker on
|
|
1431
|
-
each, save the `findings` arrays
|
|
1432
|
+
each, save the `findings` arrays under each case's key (`id`, else
|
|
1433
|
+
`<repo>#<pr>@<sha>` when the entry has `sha`, else `<repo>#<pr>`), for example
|
|
1434
|
+
`{"<repo>#<pr>": [...]}`; a value may be an array of runs to get a hit rate, then
|
|
1432
1435
|
`node "${SKILL_ROOT}/scripts/eval-runner.mjs" --manifest "${SKILL_ROOT}/evals/manifest.json" --results <results.json>`
|
|
1433
1436
|
prints recall on buggy PRs and the false-positive count (critical or important
|
|
1434
1437
|
findings) on clean ones. The shipped manifest holds two placeholders; choosing
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"note": "PLACEHOLDERS ONLY. Replace with real PRs chosen by a human: known-buggy PRs with the bug located, and clean PRs that merged without follow-up fixes.",
|
|
2
|
+
"note": "PLACEHOLDERS ONLY. Replace with real PRs chosen by a human: known-buggy PRs with the bug located, and clean PRs that merged without follow-up fixes. Add id or sha to an entry to list the same PR at two commits (buggy head, merged head); a results value may be an array of runs.",
|
|
3
3
|
"prs": [
|
|
4
4
|
{
|
|
5
5
|
"repo": "example-org/example-repo",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# project-rules: lekker-review agent prompt
|
|
2
|
+
You will receive in your task message: REPO_SLUG, PR_NUMBER, PR_URL, DIFF_FILE, CONTEXT_FILE (JSON), WORKTREE_PATH (may be null).
|
|
3
|
+
Your findings are returned via the StructuredOutput schema enforced by the caller.
|
|
4
|
+
|
|
5
|
+
You are the project-rules reviewer. Your only job is to find changed lines that
|
|
6
|
+
break the repository's own written rules. Report nothing else: no general bugs,
|
|
7
|
+
no style, no suggestions. Other agents cover those.
|
|
8
|
+
|
|
9
|
+
Procedure:
|
|
10
|
+
1. Read CONTEXT_FILE. The authoritative rules are "baseProjectRules" when it is a
|
|
11
|
+
string (the base branch's copy, which the PR author cannot edit), otherwise
|
|
12
|
+
"projectRules". When the chosen source is missing or empty, return
|
|
13
|
+
`{"findings": []}` and stop.
|
|
14
|
+
2. List every rule written as a prohibition or an obligation (NEVER, MUST,
|
|
15
|
+
MUST NOT, ALWAYS, DO NOT, or a plain-English equivalent). Work through the
|
|
16
|
+
whole source: a rule buried in a long file counts exactly as much as the
|
|
17
|
+
first one. Preferences and suggestions are not rules.
|
|
18
|
+
3. Read DIFF_FILE. For EACH listed rule, check every `+` line against it. Do not
|
|
19
|
+
rely on skimming: go rule by rule.
|
|
20
|
+
4. When a rule has a mechanical signature (a literal string, API or function
|
|
21
|
+
name, import, file pattern, naming pattern, template-literal shape), derive
|
|
22
|
+
the pattern and search the diff for it (grep or Bash is allowed) instead of
|
|
23
|
+
relying on reading. Check added file paths in the diff headers too.
|
|
24
|
+
5. A rule is broken by a `+` line (or an added file). Unchanged code is out of
|
|
25
|
+
scope.
|
|
26
|
+
6. Separately, when "baseProjectRules" is a string, check every `-` line in
|
|
27
|
+
DIFF_FILE against its rule lines (including the rules files themselves, such
|
|
28
|
+
as CLAUDE.md or AGENTS.md). A rule line that is removed, or reworded so it
|
|
29
|
+
forbids or requires less (a NEVER softened to a preference, a dropped
|
|
30
|
+
exception list, a narrowed scope), is a breach even when no `+` line violates
|
|
31
|
+
anything. The PR's own copy cannot clear it: compare against the base copy.
|
|
32
|
+
|
|
33
|
+
Reporting contract (same as the PROJECT RULES AS HARD RULES instruction below):
|
|
34
|
+
- One finding per broken rule, Critical, `rule` set to `PROJECT:<short-kebab-slug>`,
|
|
35
|
+
`ruleQuote` set to the exact sentence from the authoritative source, file:line on
|
|
36
|
+
the first violating line, every other violating site in `otherInstances`.
|
|
37
|
+
- When the diff deletes or weakens a rule line that is in "baseProjectRules"
|
|
38
|
+
(procedure step 6), report an Important finding with `rule`
|
|
39
|
+
"weakens-project-rule", `ruleQuote` set to the exact base-branch sentence,
|
|
40
|
+
file:line on the removed or reworded line, and `badCode` set to the verbatim
|
|
41
|
+
`-` line (plus the replacing `+` line when there is one). `fix` is to restore
|
|
42
|
+
the rule or to state why the project dropped it.
|
|
43
|
+
- Never quote a rule that is not in the authoritative source.
|
|
44
|
+
- `badCode` is REQUIRED: the verbatim violating line copied from the diff. `fix`
|
|
45
|
+
is REQUIRED: a concrete drop-in replacement that follows the rule. If you cannot
|
|
46
|
+
quote and fix it, drop it.
|
|
47
|
+
|
|
48
|
+
CI status, acceptance criteria and existing reviews are not your concern.
|
|
@@ -66,9 +66,9 @@ unavailable; do not infer what it meant from prose.
|
|
|
66
66
|
Select dimensions exactly as the Claude workflow does:
|
|
67
67
|
|
|
68
68
|
- deps: `deps` (single checklist agent; no verifier, no prover)
|
|
69
|
-
- scan: `triage-quality`, `triage-logic`
|
|
69
|
+
- scan: `triage-quality`, `triage-logic`, `project-rules`
|
|
70
70
|
- medium/deep: `quality`, `implementation`, `simplification`, `conventions`,
|
|
71
|
-
`test-quality`
|
|
71
|
+
`test-quality`, `project-rules`
|
|
72
72
|
|
|
73
73
|
For each dimension, spawn a `default` read-only agent with the following
|
|
74
74
|
bounded task:
|
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
// Usage:
|
|
5
5
|
// node eval-runner.mjs --manifest evals/manifest.json --results <results.json> [--json]
|
|
6
6
|
//
|
|
7
|
-
// manifest.json: { prs: [{ repo, pr, expect: "buggy"|"clean", knownBug?: "file:line + description" }] }
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// manifest.json: { prs: [{ repo, pr, expect: "buggy"|"clean", knownBug?: "file:line + description", id?, sha? }] }
|
|
8
|
+
// The case key is `id` when present, else "<repo>#<pr>@<sha>" when `sha` is
|
|
9
|
+
// present, else "<repo>#<pr>". Use id or sha to list one PR at two commits.
|
|
10
|
+
// results.json: { "<case key>": [ <finding>, ... ] } the findings of one lekker run per case
|
|
11
|
+
// (the `findings` array the workflow returns), or an array of such arrays for
|
|
12
|
+
// repeated runs, which adds per-case hitRate and per-run counts to separate
|
|
13
|
+
// model noise from a real difference. A case missing from results is
|
|
10
14
|
// reported as unscored, never as zero findings.
|
|
11
15
|
//
|
|
12
16
|
// A buggy PR is HIT when some finding is on the knownBug file within LINE_WINDOW lines
|
|
13
17
|
// of the knownBug line. Known limitation: the match is by location only, so a
|
|
14
18
|
// same-file finding near the line counts even if it describes a different problem;
|
|
15
|
-
// judging "same bug" needs an LLM or a description match and is not attempted. Recall = hits /
|
|
19
|
+
// judging "same bug" needs an LLM or a description match and is not attempted. Recall = hits /
|
|
20
|
+
// scored buggy runs (one run per case unless a case has repeated runs). On a clean PR every
|
|
16
21
|
// verdict-affecting finding (critical or important) is a false positive.
|
|
17
22
|
|
|
18
23
|
import { readFileSync } from 'node:fs';
|
|
@@ -38,6 +43,15 @@ export function isHit(findings, knownBug) {
|
|
|
38
43
|
&& Math.abs(f.line - spot.line) <= LINE_WINDOW);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
export function caseKey(e) {
|
|
47
|
+
if (typeof e.id === 'string' && e.id) return e.id;
|
|
48
|
+
return typeof e.sha === 'string' && e.sha ? `${e.repo}#${e.pr}@${e.sha}` : `${e.repo}#${e.pr}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SHA = /^[0-9a-f]{7,40}$/i;
|
|
52
|
+
|
|
53
|
+
const isMultiRun = (v) => Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
|
|
54
|
+
|
|
41
55
|
export function validateManifest(manifest) {
|
|
42
56
|
const prs = manifest && Array.isArray(manifest.prs) ? manifest.prs : null;
|
|
43
57
|
if (!prs) throw new Error('manifest needs a "prs" array');
|
|
@@ -45,7 +59,9 @@ export function validateManifest(manifest) {
|
|
|
45
59
|
prs.forEach((e, i) => {
|
|
46
60
|
if (!e.repo || !Number.isInteger(e.pr)) throw new Error(`prs[${i}]: repo and integer pr are required`);
|
|
47
61
|
if (e.expect !== 'buggy' && e.expect !== 'clean') throw new Error(`prs[${i}]: expect must be "buggy" or "clean"`);
|
|
48
|
-
|
|
62
|
+
if ('id' in e && (typeof e.id !== 'string' || !e.id)) throw new Error(`prs[${i}]: id must be a non-empty string`);
|
|
63
|
+
if ('sha' in e && (typeof e.sha !== 'string' || !SHA.test(e.sha))) throw new Error(`prs[${i}]: sha must be 7 to 40 hex characters`);
|
|
64
|
+
const key = caseKey(e);
|
|
49
65
|
if (seen.has(key)) throw new Error(`prs[${i}]: duplicate entry ${key}`);
|
|
50
66
|
seen.add(key);
|
|
51
67
|
if (e.expect === 'buggy' && !parseKnownBug(e.knownBug)) {
|
|
@@ -57,25 +73,42 @@ export function validateManifest(manifest) {
|
|
|
57
73
|
|
|
58
74
|
export function score(manifest, results) {
|
|
59
75
|
const rows = validateManifest(manifest).map((e) => {
|
|
60
|
-
const key =
|
|
61
|
-
const
|
|
62
|
-
if (!Array.isArray(
|
|
63
|
-
|
|
76
|
+
const key = caseKey(e);
|
|
77
|
+
const value = results[key];
|
|
78
|
+
if (!Array.isArray(value)) return { key, expect: e.expect, scored: false };
|
|
79
|
+
const runs = isMultiRun(value) ? value : null;
|
|
80
|
+
if (e.expect === 'buggy') {
|
|
81
|
+
if (!runs) return { key, expect: 'buggy', scored: true, hit: isHit(value, e.knownBug) };
|
|
82
|
+
const hits = runs.filter((r) => isHit(r, e.knownBug)).length;
|
|
83
|
+
return { key, expect: 'buggy', scored: true, hit: hits > 0, runs: runs.length, hits, hitRate: hits / runs.length };
|
|
84
|
+
}
|
|
85
|
+
const fp = (r) => r.filter((f) => BLOCKING.has(f.severity)).length;
|
|
86
|
+
if (!runs) return { key, expect: 'clean', scored: true, falsePositives: fp(value) };
|
|
87
|
+
const perRun = runs.map(fp);
|
|
64
88
|
return {
|
|
65
|
-
key, expect: 'clean', scored: true,
|
|
66
|
-
|
|
89
|
+
key, expect: 'clean', scored: true, falsePositives: perRun.reduce((n, x) => n + x, 0),
|
|
90
|
+
runs: runs.length, falsePositivesPerRun: perRun,
|
|
67
91
|
};
|
|
68
92
|
});
|
|
69
93
|
const buggy = rows.filter((r) => r.expect === 'buggy' && r.scored);
|
|
70
94
|
const clean = rows.filter((r) => r.expect === 'clean' && r.scored);
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
const buggyRuns = buggy.reduce((n, r) => n + (r.runs || 1), 0);
|
|
96
|
+
const cleanRuns = clean.reduce((n, r) => n + (r.runs || 1), 0);
|
|
97
|
+
const falsePositives = clean.reduce((n, r) => n + r.falsePositives, 0);
|
|
98
|
+
const out = {
|
|
99
|
+
recall: buggyRuns === 0 ? null : buggy.reduce((n, r) => n + (r.runs ? r.hits : Number(r.hit)), 0) / buggyRuns,
|
|
73
100
|
buggyScored: buggy.length,
|
|
74
|
-
falsePositives
|
|
101
|
+
falsePositives,
|
|
75
102
|
cleanScored: clean.length,
|
|
76
103
|
unscored: rows.filter((r) => !r.scored).map((r) => r.key),
|
|
77
104
|
rows,
|
|
78
105
|
};
|
|
106
|
+
if (rows.some((r) => r.runs)) {
|
|
107
|
+
Object.assign(out, {
|
|
108
|
+
buggyRuns, cleanRuns, falsePositivesPerCleanRun: cleanRuns === 0 ? null : falsePositives / cleanRuns,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
function main(argv) {
|
|
@@ -86,8 +119,13 @@ function main(argv) {
|
|
|
86
119
|
}
|
|
87
120
|
const out = score(JSON.parse(readFileSync(flag('manifest'), 'utf8')), JSON.parse(readFileSync(flag('results'), 'utf8')));
|
|
88
121
|
if (argv.includes('--json')) { console.log(JSON.stringify(out)); return; }
|
|
89
|
-
const pct =
|
|
90
|
-
|
|
122
|
+
const pct = (x) => (x === null ? 'n/a' : `${Math.round(x * 100)}%`);
|
|
123
|
+
if (out.buggyRuns === undefined) {
|
|
124
|
+
console.log(`recall ${pct(out.recall)} (${out.buggyScored} buggy scored), false positives ${out.falsePositives} (${out.cleanScored} clean scored), unscored ${out.unscored.length}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const perRun = out.falsePositivesPerCleanRun === null ? 'n/a' : out.falsePositivesPerCleanRun.toFixed(2);
|
|
128
|
+
console.log(`recall ${pct(out.recall)} per run (${out.buggyRuns} runs over ${out.buggyScored} buggy cases), false positives ${out.falsePositives} (${out.cleanRuns} runs over ${out.cleanScored} clean cases, ${perRun} per run), unscored ${out.unscored.length}`);
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -335,8 +335,8 @@ console.log('\ndeps depth: routing, classifier, line validation')
|
|
|
335
335
|
const dimensionsFor = new Function(dimsSrc + '\nreturn dimensionsFor')()
|
|
336
336
|
const M = { review: 'r', triage: 't' }
|
|
337
337
|
check('deps runs exactly one agent', dimensionsFor('deps', M).map(d => d.file), ['deps.md'])
|
|
338
|
-
check('scan
|
|
339
|
-
check('medium
|
|
338
|
+
check('scan runs the triage pair plus project-rules', dimensionsFor('scan', M).map(d => d.key), ['triage-quality', 'triage-logic', 'project-rules'])
|
|
339
|
+
check('medium runs five specialists plus project-rules', dimensionsFor('medium', M).length, 6)
|
|
340
340
|
check('every dimension prompt file exists',
|
|
341
341
|
['deps', 'scan', 'medium'].flatMap(d => dimensionsFor(d, M)).every(d => existsSync(join(SKILL, 'references', 'agents', d.file))), true)
|
|
342
342
|
}
|
|
@@ -871,7 +871,8 @@ function liftBlock(name) {
|
|
|
871
871
|
const liftBlockOr = name => { try { return liftBlock(name) } catch { return `const ${name} = ''\n` } }
|
|
872
872
|
const buildPrompt = new Function(
|
|
873
873
|
'dim', 'SUPPRESSED', 'prevSha',
|
|
874
|
-
[liftBlockOr('PROJECT_RULE_DIMENSIONS'), liftBlockOr('
|
|
874
|
+
[liftBlockOr('PROJECT_RULE_DIMENSIONS'), liftBlockOr('PROJECT_RULE_CORE'), liftBlockOr('IGNORE_ENTRY_NOTE'),
|
|
875
|
+
liftBlockOr('PROJECT_RULE_INSTRUCTION'), liftBlockOr('PROJECT_RULE_ONLY_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
|
|
875
876
|
`const targetMetadata = '{}', promptDir = 'p', repoSlug = 'o/r', prNumber = 1, prUrl = 'u',
|
|
876
877
|
diffFile = 'd', contextFile = 'c', wtDisplay = 'w'`,
|
|
877
878
|
lift('reviewPrompt'), 'return reviewPrompt(dim)'].join('\n'),
|
|
@@ -201,6 +201,11 @@ function sameIssue(a, b) {
|
|
|
201
201
|
&& qa !== '' && qb !== '' && qa !== qb) {
|
|
202
202
|
return false
|
|
203
203
|
}
|
|
204
|
+
// One authoritative rule quote is one breach, whatever each reviewer titled it or
|
|
205
|
+
// classed it. Location is still gated by the caller's file bucket and line window.
|
|
206
|
+
if (sameProjectQuote) {
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
204
209
|
// Merging two classes would send one class's instances to the other's fix.
|
|
205
210
|
if (a.bugClass && b.bugClass && a.bugClass !== b.bugClass) {
|
|
206
211
|
return false
|
|
@@ -222,9 +227,9 @@ const BUG_CLASS_INSTRUCTION = `BUG CLASS: give every finding a "bugClass" (short
|
|
|
222
227
|
+ ` "file:line" (empty array when there is none). Report the class, not just the first hit.`
|
|
223
228
|
|
|
224
229
|
const PROJECT_RULE_DIMENSIONS = ['quality', 'implementation', 'simplification', 'conventions', 'test-quality',
|
|
225
|
-
'triage-quality', 'triage-logic']
|
|
230
|
+
'triage-quality', 'triage-logic', 'project-rules']
|
|
226
231
|
|
|
227
|
-
const
|
|
232
|
+
const PROJECT_RULE_CORE = `PROJECT RULES AS HARD RULES: read "projectRules" from CONTEXT_FILE.`
|
|
228
233
|
+ ` When a changed line violates a project rule written as a prohibition or an obligation`
|
|
229
234
|
+ ` (NEVER, MUST, MUST NOT, ALWAYS, DO NOT, or a plain-English equivalent), raise a Critical`
|
|
230
235
|
+ ` finding with "rule" set to "PROJECT:<short-kebab-slug>", "ruleQuote" set to the exact`
|
|
@@ -234,8 +239,18 @@ const PROJECT_RULE_INSTRUCTION = `PROJECT RULES AS HARD RULES: read "projectRule
|
|
|
234
239
|
+ ` baseProjectRules, raise an Important finding with rule "weakens-project-rule" quoting`
|
|
235
240
|
+ ` the removed line. One finding per rule,`
|
|
236
241
|
+ ` with every other violating site in "otherInstances". Never quote a rule that is not in`
|
|
237
|
-
+ ` the authoritative source ("baseProjectRules" when it is a string, otherwise "projectRules")
|
|
238
|
-
|
|
242
|
+
+ ` the authoritative source ("baseProjectRules" when it is a string, otherwise "projectRules").`
|
|
243
|
+
|
|
244
|
+
const IGNORE_ENTRY_NOTE = ` A REVIEW.md ignore entry narrows where you look, it never disables a rule.`
|
|
245
|
+
|
|
246
|
+
const PROJECT_RULE_INSTRUCTION = PROJECT_RULE_CORE
|
|
247
|
+
+ ` Preferences and suggestions are not hard rules: report them as ordinary findings.`
|
|
248
|
+
+ IGNORE_ENTRY_NOTE
|
|
249
|
+
|
|
250
|
+
// The dedicated reviewer owns hard rules only, so it must not be told to emit preference findings.
|
|
251
|
+
const PROJECT_RULE_ONLY_INSTRUCTION = PROJECT_RULE_CORE
|
|
252
|
+
+ ` Preferences and suggestions are out of scope for you: do not report them.`
|
|
253
|
+
+ IGNORE_ENTRY_NOTE
|
|
239
254
|
|
|
240
255
|
const BUG_CLASS_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
241
256
|
const INSTANCE_PATTERN = new RegExp(INSTANCE_PATTERN_SOURCE)
|
|
@@ -397,7 +412,10 @@ function shouldVerify(finding, depth) {
|
|
|
397
412
|
// Which reviewers run at each depth. `deps` is a PR whose only changes are
|
|
398
413
|
// lockfiles and the dependency keys of package.json: five specialists would
|
|
399
414
|
// produce a couple of notes that a single checklist agent covers, so it gets
|
|
400
|
-
// exactly one.
|
|
415
|
+
// exactly one. `project-rules` runs at every other depth: a rule sentence inside
|
|
416
|
+
// a large rules file was missed when the check was only a side duty of generalist
|
|
417
|
+
// agents. It always runs because the script cannot cheaply tell whether the
|
|
418
|
+
// context holds rules; its prompt returns no findings when they are absent.
|
|
401
419
|
function dimensionsFor(depth, MODEL) {
|
|
402
420
|
if (depth === 'deps') {
|
|
403
421
|
return [{ key: 'deps', file: 'deps.md', model: MODEL.review }]
|
|
@@ -406,6 +424,7 @@ function dimensionsFor(depth, MODEL) {
|
|
|
406
424
|
return [
|
|
407
425
|
{ key: 'triage-quality', file: 'triage-quality.md', model: MODEL.triage },
|
|
408
426
|
{ key: 'triage-logic', file: 'triage-logic.md', model: MODEL.triage },
|
|
427
|
+
{ key: 'project-rules', file: 'project-rules.md', model: MODEL.review },
|
|
409
428
|
]
|
|
410
429
|
}
|
|
411
430
|
return [
|
|
@@ -414,6 +433,7 @@ function dimensionsFor(depth, MODEL) {
|
|
|
414
433
|
{ key: 'simplification', file: 'simplification.md', model: MODEL.review },
|
|
415
434
|
{ key: 'conventions', file: 'conventions.md', model: MODEL.review },
|
|
416
435
|
{ key: 'test-quality', file: 'test-quality.md', model: MODEL.review },
|
|
436
|
+
{ key: 'project-rules', file: 'project-rules.md', model: MODEL.review },
|
|
417
437
|
]
|
|
418
438
|
}
|
|
419
439
|
|
|
@@ -586,14 +606,14 @@ if (ENGINE === 'revmux' && !findingsFile) {
|
|
|
586
606
|
throw new Error('lekker-review workflow: engine "revmux" requires findingsFile (adapter output)')
|
|
587
607
|
}
|
|
588
608
|
|
|
589
|
-
// Concurrency. The review stage is self-limiting:
|
|
609
|
+
// Concurrency. The review stage is self-limiting: six dimensions means six
|
|
590
610
|
// agents, so it runs them all at once. The other stages are not. Verify,
|
|
591
611
|
// critic and prove spawn one agent PER FINDING, so a finding-heavy PR would
|
|
592
612
|
// fan out far wider than the review that produced it, with no ceiling. Those
|
|
593
613
|
// are capped at the review stage's own width.
|
|
594
614
|
const REVIEW_PLAN = (Array.isArray(reviewBatchPlan) && reviewBatchPlan.length > 0)
|
|
595
615
|
? reviewBatchPlan
|
|
596
|
-
: [
|
|
616
|
+
: [6]
|
|
597
617
|
const FANOUT_PLAN = [Math.max(1, maxConcurrent || 5)]
|
|
598
618
|
const targetMetadata = JSON.stringify({ targetLabel })
|
|
599
619
|
|
|
@@ -678,7 +698,7 @@ const budgetAtStart = budget.spent()
|
|
|
678
698
|
prompt += ' ' + BUG_CLASS_INSTRUCTION
|
|
679
699
|
|
|
680
700
|
if (PROJECT_RULE_DIMENSIONS.includes(dim.key)) {
|
|
681
|
-
prompt += ' ' + PROJECT_RULE_INSTRUCTION
|
|
701
|
+
prompt += ' ' + (dim.key === 'project-rules' ? PROJECT_RULE_ONLY_INSTRUCTION : PROJECT_RULE_INSTRUCTION)
|
|
682
702
|
}
|
|
683
703
|
|
|
684
704
|
// The deterministic lint pre-flight has already collected every rule the
|