@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.
Files changed (26) hide show
  1. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +9 -6
  2. package/adapters/claude/olko-github-pr/skills/lekker-review/evals/manifest.json +1 -1
  3. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/project-rules.md +48 -0
  4. package/adapters/claude/olko-github-pr/skills/lekker-review/references/codex-workflow.md +2 -2
  5. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/eval-runner.mjs +54 -16
  6. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +4 -3
  7. package/package.json +1 -1
  8. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  9. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  10. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  11. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  12. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  13. package/plugins/olko-github-pr/skills/lekker-review/README.md +2 -2
  14. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +9 -6
  15. package/plugins/olko-github-pr/skills/lekker-review/evals/manifest.json +1 -1
  16. package/plugins/olko-github-pr/skills/lekker-review/references/agents/project-rules.md +48 -0
  17. package/plugins/olko-github-pr/skills/lekker-review/references/codex-workflow.md +2 -2
  18. package/plugins/olko-github-pr/skills/lekker-review/scripts/eval-runner.mjs +54 -16
  19. package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +4 -3
  20. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +28 -8
  21. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  22. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  23. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  24. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  25. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  26. 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) | 5 specialists (sonnet) | 5 specialists (sonnet) |
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 `[5]`
771
- (all five at once). The last entry repeats to cover any remainder, so
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`. Agents receive DIFF_FILE + CONTEXT_FILE by path.
784
- All five run concurrently: the stage is self-limiting at one agent per
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 as `{"<repo>#<pr>": [...]}`, then
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",
@@ -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
- // results.json: { "<repo>#<pr>": [ <finding>, ... ] } the findings of one lekker run per PR
9
- // (the `findings` array the workflow returns). A PR missing from results is
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 / scored buggy PRs. On a clean PR every
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
- const key = `${e.repo}#${e.pr}`;
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 = `${e.repo}#${e.pr}`;
61
- const findings = results[key];
62
- if (!Array.isArray(findings)) return { key, expect: e.expect, scored: false };
63
- if (e.expect === 'buggy') return { key, expect: 'buggy', scored: true, hit: isHit(findings, e.knownBug) };
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
- falsePositives: findings.filter((f) => BLOCKING.has(f.severity)).length,
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
- return {
72
- recall: buggy.length === 0 ? null : buggy.filter((r) => r.hit).length / buggy.length,
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: clean.reduce((n, r) => n + r.falsePositives, 0),
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 = out.recall === null ? 'n/a' : `${Math.round(out.recall * 100)}%`;
90
- console.log(`recall ${pct} (${out.buggyScored} buggy scored), false positives ${out.falsePositives} (${out.cleanScored} clean scored), unscored ${out.unscored.length}`);
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 still runs the triage pair', dimensionsFor('scan', M).length, 2)
339
- check('medium still runs five specialists', dimensionsFor('medium', M).length, 5)
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('PROJECT_RULE_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olegkoval/agent-skills",
3
- "version": "1.72.0",
3
+ "version": "1.74.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-apple-kit",
3
3
  "description": "Build and ship Apple platform apps: macOS menubar apps, App Store submissions.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-creative",
3
3
  "description": "Creative and personal projects: photo galleries, music players, listings, wiki editing.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-garmin-kit",
3
3
  "description": "Build, test and publish Garmin Connect IQ watch faces.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-git-tools",
3
3
  "description": "Everyday git and GitHub CLI operations: conventional commits, branch hygiene.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-github-pr",
3
3
  "description": "Drive GitHub pull requests to merge-ready: review-bot loops, CI fixes, descriptions, dependency triage.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -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
  ![lekker-review at a glance](assets/poster-overview.png)
@@ -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) | 5 specialists (sonnet) | 5 specialists (sonnet) |
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 `[5]`
770
- (all five at once). The last entry repeats to cover any remainder, so
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`. Agents receive DIFF_FILE + CONTEXT_FILE by path.
783
- All five run concurrently: the stage is self-limiting at one agent per
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 as `{"<repo>#<pr>": [...]}`, then
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
- // results.json: { "<repo>#<pr>": [ <finding>, ... ] } the findings of one lekker run per PR
9
- // (the `findings` array the workflow returns). A PR missing from results is
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 / scored buggy PRs. On a clean PR every
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
- const key = `${e.repo}#${e.pr}`;
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 = `${e.repo}#${e.pr}`;
61
- const findings = results[key];
62
- if (!Array.isArray(findings)) return { key, expect: e.expect, scored: false };
63
- if (e.expect === 'buggy') return { key, expect: 'buggy', scored: true, hit: isHit(findings, e.knownBug) };
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
- falsePositives: findings.filter((f) => BLOCKING.has(f.severity)).length,
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
- return {
72
- recall: buggy.length === 0 ? null : buggy.filter((r) => r.hit).length / buggy.length,
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: clean.reduce((n, r) => n + r.falsePositives, 0),
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 = out.recall === null ? 'n/a' : `${Math.round(out.recall * 100)}%`;
90
- console.log(`recall ${pct} (${out.buggyScored} buggy scored), false positives ${out.falsePositives} (${out.cleanScored} clean scored), unscored ${out.unscored.length}`);
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 still runs the triage pair', dimensionsFor('scan', M).length, 2)
339
- check('medium still runs five specialists', dimensionsFor('medium', M).length, 5)
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('PROJECT_RULE_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
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 PROJECT_RULE_INSTRUCTION = `PROJECT RULES AS HARD RULES: read "projectRules" from CONTEXT_FILE.`
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"). Preferences and suggestions are not hard rules: report them as ordinary`
238
- + ` findings. A REVIEW.md ignore entry narrows where you look, it never disables a rule.`
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: five dimensions means five
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
- : [5]
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-obsidian",
3
3
  "description": "Keep an Obsidian vault in sync with work: PR sync, task rollover, morning routine.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-product",
3
3
  "description": "Take a product idea to a shippable build: MVP passes, full-stack scaffolds, UX/UI quality loops, launch plans.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-reflection",
3
3
  "description": "Look back and improve: self-critique, retrospectives, performance review, rapid learning.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-release",
3
3
  "description": "Ship a release: semantic-release setup, changelogs, store listing copy, release-day routine.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-skill-meta",
3
3
  "description": "Author and maintain agent skills and the AI toolchain itself.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-web-ops",
3
3
  "description": "Operate a website: WAF rules, search console audits, analytics bootstrap, docs indexes.",
4
- "version": "1.72.0",
4
+ "version": "1.74.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },