@olegkoval/agent-skills 1.43.1 → 1.44.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 (49) hide show
  1. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +85 -2
  2. package/adapters/claude/olko-github-pr/skills/lekker-review/references/pricing.json +9 -0
  3. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-conventions.md +158 -0
  4. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-implementation.md +76 -0
  5. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-quality.md +87 -0
  6. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-simplification.md +63 -0
  7. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-test-quality.md +172 -0
  8. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/phase4-comparison.md +42 -0
  9. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/profile.md +347 -0
  10. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-deep.md +82 -0
  11. package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-medium.md +81 -0
  12. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/context.json +3 -0
  13. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/house-rules.md +11 -0
  14. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/revmux-report.json +179 -0
  15. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/install-revmux-prompts.sh +66 -0
  16. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/revmux-adapter.mjs +261 -0
  17. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/revmux-engine.sh +156 -0
  18. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +76 -0
  19. package/package.json +1 -1
  20. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  21. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  22. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  23. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  24. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  25. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +85 -2
  26. package/plugins/olko-github-pr/skills/lekker-review/references/pricing.json +9 -0
  27. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-conventions.md +158 -0
  28. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-implementation.md +76 -0
  29. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-quality.md +87 -0
  30. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-simplification.md +63 -0
  31. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-test-quality.md +172 -0
  32. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/phase4-comparison.md +42 -0
  33. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/profile.md +347 -0
  34. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-deep.md +82 -0
  35. package/plugins/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-medium.md +81 -0
  36. package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/context.json +3 -0
  37. package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/house-rules.md +11 -0
  38. package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/revmux-report.json +179 -0
  39. package/plugins/olko-github-pr/skills/lekker-review/scripts/install-revmux-prompts.sh +66 -0
  40. package/plugins/olko-github-pr/skills/lekker-review/scripts/revmux-adapter.mjs +261 -0
  41. package/plugins/olko-github-pr/skills/lekker-review/scripts/revmux-engine.sh +156 -0
  42. package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +76 -0
  43. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +43 -6
  44. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  45. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  46. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  47. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  48. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  49. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
@@ -0,0 +1,81 @@
1
+ ---
2
+ description: Teifi medium-depth review — four claude agents, adversarial second pass, claude-only
3
+ model: claude/sonnet:medium
4
+ agents:
5
+ - {name: quality+impl, lenses: [lekker-quality, lekker-implementation], color: cyan}
6
+ - {name: simpl+conventions, lenses: [lekker-simplification, lekker-conventions], color: magenta}
7
+ - {name: tests, lenses: [lekker-test-quality, tests], color: green}
8
+ - {name: adversarial, lenses: [adversarial], model: claude/sonnet:high, color: yellow}
9
+ stages: {synthesis: claude/sonnet:medium, verify: claude/sonnet:medium}
10
+ ---
11
+ You are one reviewer on a panel. Other reviewers are working the same change in parallel with
12
+ different lenses. You never see their findings and must not guess at them — report what your own
13
+ lenses find.
14
+
15
+ This review is **read-only**. You may read files and run read-only commands such as `git diff`,
16
+ `git log` and `rg`. Do not modify, delete, move, stage or commit anything, and do not write a file
17
+ through a shell redirect. Report what you find; changing it is the caller's job, never yours.
18
+ Do not run tests, builds or the linter - all of that was done before the review and passed.
19
+
20
+ ## Where the context lives
21
+
22
+ Every item below is a **path**, not the text it names. Read the file or directory before you start.
23
+
24
+ - `{{SCOPE}}` — what is under review and the command that produces the diff. Read this first and run
25
+ that command yourself.
26
+ - `{{GOAL}}` — what the change is trying to achieve.
27
+ - `{{PROFILE}}` — Teifi's own rules and conventions. Where they disagree with your general taste,
28
+ they win. This is also where the hard-rule text (TS-1, TS-2, GQL-1, PR-1) and the Teifi
29
+ conventions (naming matrix, comment policy, hygiene severities, test conventions) live in full.
30
+ - `{{CONTEXT}}` — a directory of supporting material: ticket text, design notes, spec excerpts, CI
31
+ status, Sentry signals, existing review comments.
32
+ - `{{WORKDIR}}` — run every command from here.
33
+
34
+ Any of these may read `none provided`. That is not an error and not something to work around: the
35
+ caller supplied nothing for it, so calibrate severity generically to that extent rather than
36
+ inventing the missing context.
37
+
38
+ ## Severity bar
39
+
40
+ No nitpicking. Critical and major findings are reserved for things that could cause bugs, outages,
41
+ data loss, security incidents, or real performance problems at scale.
42
+
43
+ - **critical** — a bug, an outage, data loss, a security hole, or a real performance problem at scale.
44
+ - **major** — wrong behavior, or a broken contract a caller executes against.
45
+ - **minor** — a real, contained defect.
46
+
47
+ Style preference and taste alone are never a finding. Anything you cannot place on that bar is not
48
+ a finding — leave it out.
49
+
50
+ ## Hard-rule findings are policy, not a runtime question
51
+
52
+ Findings titled `[TS-1]`, `[TS-2]`, `[GQL-1]`, or `[PR-1]` are Teifi's own policy violations,
53
+ defined in full in `{{PROFILE}}`. Confirm one when the quoted code shows the pattern the rule
54
+ names — a cast, an `any`, a `.js` file outside a theme repo, a missing `pageInfo`/pagination, a PR
55
+ title missing its ticket prefix. Never rate a hard-rule finding by its runtime impact and never mark
56
+ it immaterial for lack of one: the rule itself is the standard, and violating it is always critical,
57
+ independent of whether it happens to fail at runtime today.
58
+
59
+ ## Reporting
60
+
61
+ Apply every lens you carry, in full, and tag each finding with the lens that raised it.
62
+
63
+ - Point at a specific file and line. A finding with no location cannot be verified.
64
+ - State the failure concretely: the input or state, and what goes wrong because of it.
65
+ - Report the confidence you actually have, not the confidence that keeps the finding alive.
66
+ - Say when a problem is pre-existing rather than introduced by the change under review.
67
+ - Do not report one problem twice under two lenses. Report it once and name both lenses on it.
68
+
69
+ ## What not to report
70
+
71
+ Silence beats a finding the reader has to disprove. Do not report:
72
+
73
+ - a defect on a line this change did not touch, unless the change is what makes it reachable
74
+ - anything a linter, compiler or type checker catches. All of them ran before the review and passed
75
+ - a lint or vet rule the code silences deliberately, with the directive visible
76
+ - a missing test, missing doc or general-quality observation the project's own rules do not ask for
77
+ - a nitpick a senior engineer reading this diff would not raise
78
+ - a behaviour change that is plainly the point of the change
79
+
80
+ Pre-existing problems are the one exception: report them, and say so, so the reader can weigh them
81
+ separately from what the change introduced.
@@ -0,0 +1,3 @@
1
+ {
2
+ "houseRulesFile": "house-rules.md"
3
+ }
@@ -0,0 +1,11 @@
1
+ # Fixture house rules
2
+
3
+ ### TS-1 — Type safety
4
+
5
+ ### TS-2 — No JavaScript files
6
+
7
+ ### GQL-1 — Complete pagination
8
+
9
+ ### PR-1 — Ticket-prefixed titles
10
+
11
+ ### SEC-7 — Never log session tokens
@@ -0,0 +1,179 @@
1
+ {
2
+ "findings": [
3
+ {
4
+ "id": "f1",
5
+ "file": "web/services/order-sync.ts",
6
+ "line": 42,
7
+ "end_line": 44,
8
+ "severity": "critical",
9
+ "confidence": 0.9,
10
+ "title": "Race between webhook retry and manual sync writes duplicate fulfillment",
11
+ "body": "Two concurrent writers can both pass the guard and insert a fulfillment row.",
12
+ "fix": "Take a row lock on the order before the check-then-insert.",
13
+ "sources": ["quality+impl"],
14
+ "lenses": ["lekker-quality"],
15
+ "verdict": "confirmed"
16
+ },
17
+ {
18
+ "id": "f2",
19
+ "file": "web/services/pricing.ts",
20
+ "line": 88,
21
+ "end_line": 90,
22
+ "severity": "major",
23
+ "confidence": 0.75,
24
+ "title": "Discount stacking ignores currency rounding",
25
+ "body": "Two agents independently flagged the same rounding gap on stacked discounts.",
26
+ "fix": "Round once at the end, not after each discount.",
27
+ "sources": ["quality+impl", "simpl+conventions"],
28
+ "lenses": ["lekker-quality", "lekker-simplification"],
29
+ "verdict": "refined"
30
+ },
31
+ {
32
+ "id": "f3",
33
+ "file": "web/services/logger.ts",
34
+ "line": 12,
35
+ "end_line": 12,
36
+ "severity": "minor",
37
+ "confidence": 0.6,
38
+ "title": "Log helper name does not follow the naming matrix",
39
+ "body": "getLogger performs no I/O, per teifi-conventions section 1 it should not be prefixed get.",
40
+ "fix": "Rename to buildLogger.",
41
+ "sources": ["simpl+conventions"],
42
+ "lenses": ["lekker-conventions"],
43
+ "verdict": "confirmed"
44
+ },
45
+ {
46
+ "id": "f4",
47
+ "file": "web/services/webhook.ts",
48
+ "line": 5,
49
+ "end_line": 5,
50
+ "severity": "minor",
51
+ "confidence": 0.4,
52
+ "title": "Unused import left over from a refactor",
53
+ "body": "Import of `crypto` is unused after the signature check moved.",
54
+ "fix": "Remove the unused import.",
55
+ "sources": ["adversarial"],
56
+ "lenses": ["adversarial"],
57
+ "verdict": "immaterial"
58
+ },
59
+ {
60
+ "id": "f5",
61
+ "file": "web/services/refund.ts",
62
+ "line": 60,
63
+ "end_line": 61,
64
+ "severity": "major",
65
+ "confidence": 0.5,
66
+ "title": "Refund total may double-count a tip",
67
+ "body": "Speculative -- verifier could not reproduce a path where the tip line is summed twice.",
68
+ "fix": "n/a",
69
+ "sources": ["adversarial"],
70
+ "lenses": ["adversarial"],
71
+ "verdict": "rejected"
72
+ },
73
+ {
74
+ "id": "f6",
75
+ "file": "web/services/inventory.ts",
76
+ "line": 21,
77
+ "end_line": 21,
78
+ "severity": "minor",
79
+ "confidence": 0.7,
80
+ "title": "[TS-1] Unsafe cast on inventory adjustment payload",
81
+ "body": "const qty = payload as any; -- a bare `as any` cast on external webhook input, no runtime type check follows it.",
82
+ "fix": "Parse payload with the zod schema and drop the cast.",
83
+ "sources": ["quality+impl"],
84
+ "lenses": ["lekker-quality"],
85
+ "verdict": "rejected"
86
+ },
87
+ {
88
+ "id": "f7",
89
+ "file": "web/services/inventory.ts",
90
+ "line": 30,
91
+ "end_line": 30,
92
+ "severity": "minor",
93
+ "confidence": 0.3,
94
+ "title": "[TS-1] Type safety concern in inventory handler",
95
+ "body": "This function handles any inventory update and could fail for any malformed input; consider adding validation.",
96
+ "fix": "Add input validation.",
97
+ "sources": ["quality+impl"],
98
+ "lenses": ["lekker-quality"],
99
+ "verdict": "rejected"
100
+ },
101
+ {
102
+ "id": "f8",
103
+ "file": "web/services/session.ts",
104
+ "line": 14,
105
+ "end_line": 14,
106
+ "severity": "minor",
107
+ "confidence": 0.8,
108
+ "title": "[SEC-7] Session token is logged",
109
+ "body": "The configured security rule forbids `logger.info(sessionToken)`.",
110
+ "fix": "Remove the token from the log payload.",
111
+ "sources": ["quality+impl"],
112
+ "lenses": ["lekker-quality"],
113
+ "verdict": "rejected"
114
+ },
115
+ {
116
+ "id": "f9",
117
+ "file": "web/services/session.ts",
118
+ "line": 20,
119
+ "end_line": 20,
120
+ "severity": "minor",
121
+ "confidence": 0.3,
122
+ "title": "[SEC-7] Possible session concern",
123
+ "body": "The session handling might expose sensitive data in some circumstances.",
124
+ "fix": "Review the session handling.",
125
+ "sources": ["quality+impl"],
126
+ "lenses": ["lekker-quality"],
127
+ "verdict": "rejected"
128
+ },
129
+ {
130
+ "id": "f10",
131
+ "file": "web/services/order-sync.test.ts",
132
+ "line": 55,
133
+ "end_line": 55,
134
+ "severity": "major",
135
+ "confidence": 0.85,
136
+ "title": "Operator-flip mutation escapes the happy-path assertion",
137
+ "body": "Changing the retry guard from && to || still passes the only assertion.",
138
+ "fix": "Add a case that exercises one false operand.",
139
+ "sources": ["tests"],
140
+ "lenses": ["lekker-test-quality"],
141
+ "verdict": "confirmed"
142
+ },
143
+ {
144
+ "id": "f11",
145
+ "file": "web/services/order-sync.test.ts",
146
+ "line": 70,
147
+ "end_line": 70,
148
+ "severity": "minor",
149
+ "confidence": 0.8,
150
+ "title": "Mock assertion freezes the client call shape",
151
+ "body": "The test asserts toHaveBeenCalledWith instead of the observable result.",
152
+ "fix": "Assert the returned synchronization state.",
153
+ "sources": ["tests"],
154
+ "lenses": ["lekker-test-quality"],
155
+ "verdict": "confirmed"
156
+ }
157
+ ],
158
+ "pre_existing": [
159
+ {
160
+ "file": "web/services/order-sync.ts",
161
+ "line": 200,
162
+ "title": "Repo-wide: no retry backoff on the fulfillment API client",
163
+ "body": "Pre-existing since before this diff; noted for awareness only.",
164
+ "lenses": ["lekker-implementation"]
165
+ }
166
+ ],
167
+ "open_questions": [
168
+ "Is the webhook retry expected to be idempotent, or does the client dedupe upstream?"
169
+ ],
170
+ "sources": {
171
+ "agents": [
172
+ { "name": "quality+impl", "executor": "claude", "actual_model": "claude-sonnet-5", "effort": "medium", "tokens": 42000, "raised": 4, "degraded": false },
173
+ { "name": "simpl+conventions", "executor": "claude", "actual_model": "claude-sonnet-5", "effort": "medium", "tokens": 31000, "raised": 2, "degraded": false },
174
+ { "name": "adversarial", "executor": "claude", "actual_model": "claude-sonnet-5", "effort": "high", "tokens": 18000, "raised": 2, "degraded": true },
175
+ { "name": "malformed-cost", "executor": "claude", "actual_model": "claude-sonnet-5", "effort": "medium", "tokens": "unknown", "raised": 0, "degraded": false }
176
+ ]
177
+ },
178
+ "stats": { "tokens": 91000 }
179
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # Installs lekker's revmux lenses + profiles into ~/.config/revmux (layer 2),
3
+ # idempotently. --check verifies without writing.
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
8
+ SRC_LENSES="$REPO_ROOT/references/revmux/lenses"
9
+ SRC_PROFILES="$REPO_ROOT/references/revmux/profiles"
10
+
11
+ CONFIG_DIR="${REVMUX_CONFIG_DIR:-$HOME/.config/revmux}"
12
+ DST_LENSES="$CONFIG_DIR/lenses"
13
+ DST_PROFILES="$CONFIG_DIR/prompts/profiles"
14
+
15
+ CHECK_MODE=0
16
+ if [ "${1:-}" = "--check" ]; then
17
+ CHECK_MODE=1
18
+ fi
19
+
20
+ if grep -rln 'codex' "$SRC_LENSES" "$SRC_PROFILES" >/dev/null 2>&1; then
21
+ echo "refusing to install: codex reference found under references/revmux/" >&2
22
+ exit 1
23
+ fi
24
+
25
+ FAIL=0
26
+
27
+ sync_one() {
28
+ local src="$1" dst="$2"
29
+ local name
30
+ name="$(basename "$src")"
31
+
32
+ if [ "$CHECK_MODE" -eq 1 ]; then
33
+ if [ ! -f "$dst" ]; then
34
+ echo "missing: $name"
35
+ FAIL=1
36
+ elif ! diff -q "$src" "$dst" >/dev/null 2>&1; then
37
+ echo "differs: $name"
38
+ FAIL=1
39
+ fi
40
+ return
41
+ fi
42
+
43
+ mkdir -p "$(dirname "$dst")"
44
+ if [ ! -f "$dst" ]; then
45
+ cp "$src" "$dst"
46
+ echo "installed: $name"
47
+ elif diff -q "$src" "$dst" >/dev/null 2>&1; then
48
+ echo "unchanged: $name"
49
+ else
50
+ echo "updated: $name"
51
+ diff -u "$dst" "$src" || true
52
+ cp "$src" "$dst"
53
+ fi
54
+ }
55
+
56
+ for f in "$SRC_LENSES"/*.md; do
57
+ sync_one "$f" "$DST_LENSES/$(basename "$f")"
58
+ done
59
+
60
+ for f in "$SRC_PROFILES"/*.md; do
61
+ sync_one "$f" "$DST_PROFILES/$(basename "$f")"
62
+ done
63
+
64
+ if [ "$CHECK_MODE" -eq 1 ]; then
65
+ exit "$FAIL"
66
+ fi
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ // revmux-adapter.mjs -- turns a revmux report into lekker's findings JSON.
3
+ //
4
+ // Usage: node scripts/revmux-adapter.mjs <revmux.json> [--pricing FILE] [--context FILE] > findings.json
5
+ import { readFileSync } from 'node:fs'
6
+ import { dirname, resolve } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const args = process.argv.slice(2)
10
+ const reportPath = args[0]
11
+ if (!reportPath) {
12
+ console.error('usage: revmux-adapter.mjs <revmux.json> [--pricing FILE] [--context FILE]')
13
+ process.exit(2)
14
+ }
15
+ function flagValue(name) {
16
+ const i = args.indexOf(name)
17
+ if (i === -1) return null
18
+ const value = args[i + 1]
19
+ if (!value || value.startsWith('--')) {
20
+ console.error(`revmux-adapter: ${name} requires a file path`)
21
+ process.exit(2)
22
+ }
23
+ return value
24
+ }
25
+ const pricingPath = flagValue('--pricing')
26
+ const contextPath = flagValue('--context')
27
+
28
+ function loadJson(label, path) {
29
+ try {
30
+ return JSON.parse(readFileSync(path, 'utf8'))
31
+ } catch (error) {
32
+ console.error(`revmux-adapter: cannot read or parse ${label} JSON at ${path}: ${error.message}`)
33
+ process.exit(2)
34
+ }
35
+ }
36
+
37
+ const report = loadJson('report', reportPath)
38
+ const pricing = pricingPath ? loadJson('pricing', pricingPath) : {}
39
+ const contextJson = contextPath ? loadJson('context', contextPath) : null
40
+
41
+ function configuredHardRules() {
42
+ const defaultPath = fileURLToPath(new URL('../references/house-rules.md', import.meta.url))
43
+ const configuredPath = (contextJson && typeof contextJson.houseRulesFile === 'string')
44
+ ? contextJson.houseRulesFile
45
+ : defaultPath
46
+ const houseRulesPath = contextPath && !configuredPath.startsWith('/')
47
+ ? resolve(dirname(contextPath), configuredPath)
48
+ : configuredPath
49
+
50
+ let text
51
+ try {
52
+ text = readFileSync(houseRulesPath, 'utf8')
53
+ } catch (error) {
54
+ console.error(`revmux-adapter: cannot read configured house rules at ${houseRulesPath}: ${error.message}`)
55
+ process.exit(2)
56
+ }
57
+
58
+ const tags = new Set()
59
+ for (const match of text.matchAll(/^#{2,6}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\b/gm)) {
60
+ if (!match[1].startsWith('EXAMPLE-')) tags.add(match[1])
61
+ }
62
+ return tags
63
+ }
64
+
65
+ const HARD_RULES = configuredHardRules()
66
+
67
+ function hardRuleCorroborated(finding) {
68
+ const evidence = [finding.badCode, finding.description, finding.title]
69
+ .map(function(x) { return String(x || '') }).join('\n')
70
+ const file = String(finding.file || '')
71
+ const code = String(finding.badCode || '')
72
+
73
+ switch (finding.rule) {
74
+ case 'TS-1':
75
+ return /\bas\s+(?:[A-Z_$][\w$]*|string|number|boolean|bigint|symbol|object|unknown|never|any|const)\b/.test(code)
76
+ || /:\s*any\b|<\s*any[\s,>]|\bany\[\]/.test(code)
77
+ case 'TS-2':
78
+ return /\.js$/.test(file)
79
+ case 'GQL-1':
80
+ return /\bnodes\b|\bpageInfo\b|\bedges\b/.test(evidence)
81
+ case 'PR-1':
82
+ return /pr\s*(title|description)/i.test(file + '\n' + evidence)
83
+ default:
84
+ return code.trim().length > 0
85
+ }
86
+ }
87
+
88
+ function isHardRule(finding) {
89
+ if (typeof finding.rule !== 'string' || !HARD_RULES.has(finding.rule)) {
90
+ return false
91
+ }
92
+ return hardRuleCorroborated(finding)
93
+ }
94
+
95
+ function extractRule(title) {
96
+ const m = /^\[([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\]/.exec(String(title || ''))
97
+ return m && HARD_RULES.has(m[1]) ? m[1] : undefined
98
+ }
99
+
100
+ function mapSeverity(f) {
101
+ if (f.severity === 'critical') return 'critical'
102
+ if (f.severity === 'major') return 'important'
103
+ if (f.severity === 'minor') {
104
+ return (f.lenses || []).includes('lekker-conventions') ? 'idiomatic' : 'observation'
105
+ }
106
+ return 'observation'
107
+ }
108
+
109
+ // Pull an inline code snippet out of the body so hardRuleCorroborated has
110
+ // something to check against; revmux findings carry no separate badCode field.
111
+ function extractBadCode(body) {
112
+ const text = String(body || '')
113
+ const backtick = /`([^`]+)`/.exec(text)
114
+ if (backtick) return backtick[1]
115
+ const dashCode = /--\s*(.+)$/.exec(text.split('\n')[0])
116
+ return dashCode ? dashCode[1] : ''
117
+ }
118
+
119
+ const findings = []
120
+ let droppedCount = 0
121
+ let hardRuleCount = 0
122
+
123
+ for (const f of report.findings || []) {
124
+ const rule = extractRule(f.title)
125
+ const badCode = extractBadCode(f.body)
126
+ const base = {
127
+ file: f.file,
128
+ line: f.line,
129
+ severity: mapSeverity(f),
130
+ title: f.title,
131
+ description: f.body,
132
+ badCode,
133
+ fix: f.fix,
134
+ confidence: f.confidence,
135
+ rule,
136
+ dimension: f.lenses || [],
137
+ }
138
+ if ((f.sources || []).length >= 2) {
139
+ base.agreedBy = f.sources
140
+ }
141
+
142
+ const rejectedOrImmaterial = f.verdict === 'rejected' || f.verdict === 'immaterial'
143
+
144
+ if (!rejectedOrImmaterial) {
145
+ base.verifierReasoning = `revmux verify: ${f.verdict}`
146
+ findings.push(base)
147
+ continue
148
+ }
149
+
150
+ // A hard-rule tag whose own quoted code corroborates it is Critical
151
+ // regardless of revmux's verdict.
152
+ if (isHardRule({ rule, badCode, description: f.body, title: f.title, file: f.file })) {
153
+ hardRuleCount++
154
+ base.severity = 'critical'
155
+ base.verifierReasoning = `hard rule ${rule}: corroborated by quoted code, re-promoted despite revmux verdict "${f.verdict}"`
156
+ findings.push(base)
157
+ continue
158
+ }
159
+
160
+ droppedCount++
161
+ }
162
+
163
+ const reportFindings = findings.slice()
164
+
165
+ for (const pe of report.pre_existing || []) {
166
+ findings.push({
167
+ file: pe.file,
168
+ line: pe.line,
169
+ severity: 'observation',
170
+ title: pe.title,
171
+ description: `Pre-existing: ${pe.body}`,
172
+ badCode: extractBadCode(pe.body),
173
+ fix: '',
174
+ dimension: pe.lenses || [],
175
+ })
176
+ }
177
+
178
+ const questions = report.open_questions || []
179
+
180
+ const implementationFindings = reportFindings.filter(function(f) {
181
+ return f.dimension.includes('lekker-implementation')
182
+ })
183
+ const testQualityFindings = reportFindings.filter(function(f) {
184
+ return f.dimension.includes('lekker-test-quality')
185
+ })
186
+ const mutationFindings = testQualityFindings.filter(function(f) {
187
+ return /mutation|operator flip|off-by-one/i.test(`${f.title}\n${f.description}`)
188
+ })
189
+ const mockFindings = testQualityFindings.filter(function(f) {
190
+ return /\bmock(?:ing|ed|s)?\b|\bspy(?:ing)?\b|toHaveBeenCalled/i.test(`${f.title}\n${f.description}`)
191
+ })
192
+
193
+ const acCoverage = implementationFindings.length > 0
194
+ ? implementationFindings.map(function(f) { return `${f.title}: ${f.description}` }).join('\n')
195
+ : 'No acceptance-criteria gaps were reported by the revmux implementation lens.'
196
+ const coverageVerdict = testQualityFindings.length > 0
197
+ ? `${testQualityFindings.length} test-quality finding(s): ${testQualityFindings.map(function(f) { return f.title }).join('; ')}`
198
+ : 'No test-quality gaps were reported by the revmux test-quality lens.'
199
+ const mutationSlip = mutationFindings.length > 0
200
+ ? mutationFindings.map(function(f) { return f.description }).join('\n')
201
+ : 'No mutation-slip gap was reported by the revmux test-quality lens.'
202
+ const mockSmells = mockFindings.map(function(f) {
203
+ return { file: f.file, line: f.line, description: f.description, fix: f.fix }
204
+ })
205
+
206
+ const agentsIn = (report.sources && report.sources.agents) || []
207
+ const pricingMissing = []
208
+ let totalUsd = 0
209
+ let anyUsdKnown = false
210
+
211
+ const agents = agentsIn.map(function(a) {
212
+ const priceEntry = pricing[a.actual_model]
213
+ let usd = null
214
+ if (priceEntry) {
215
+ const rate = (typeof priceEntry.output === 'number') ? priceEntry.output : priceEntry.blended
216
+ if (typeof a.tokens === 'number' && Number.isFinite(a.tokens) && typeof rate === 'number' && Number.isFinite(rate)) {
217
+ usd = (a.tokens / 1e6) * rate
218
+ anyUsdKnown = true
219
+ totalUsd += usd
220
+ }
221
+ } else {
222
+ pricingMissing.push(a.actual_model)
223
+ }
224
+ return {
225
+ name: a.name,
226
+ model: a.actual_model,
227
+ effort: a.effort,
228
+ tokens: a.tokens,
229
+ usd,
230
+ raised: a.raised,
231
+ degraded: Boolean(a.degraded),
232
+ }
233
+ })
234
+
235
+ const degraded = agents.filter(function(a) { return a.degraded }).map(function(a) { return a.name })
236
+ const totalTokens = (report.stats && report.stats.tokens) || agentsIn.reduce(function(s, a) { return s + (a.tokens || 0) }, 0)
237
+
238
+ const output = {
239
+ engine: 'revmux',
240
+ findings,
241
+ questions,
242
+ droppedCount,
243
+ downgradedCount: 0,
244
+ hardRuleCount,
245
+ agentCount: agentsIn.length,
246
+ degraded,
247
+ agents,
248
+ totalTokens,
249
+ totalUsd: anyUsdKnown ? totalUsd : null,
250
+ acCoverage,
251
+ coverageVerdict,
252
+ mutationSlip,
253
+ mockSmells,
254
+ stats: { durationMs: (report.stats && report.stats.durationMs) || null },
255
+ }
256
+
257
+ if (pricingMissing.length > 0) {
258
+ output.pricingMissing = Array.from(new Set(pricingMissing))
259
+ }
260
+
261
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n')