@olegkoval/agent-skills 1.43.0 → 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.
- package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +85 -2
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/pricing.json +9 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-conventions.md +158 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-implementation.md +76 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-quality.md +87 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-simplification.md +63 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-test-quality.md +172 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/phase4-comparison.md +42 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/profile.md +347 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-deep.md +82 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-medium.md +81 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/context.json +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/house-rules.md +11 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/revmux-report.json +179 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/install-revmux-prompts.sh +66 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/revmux-adapter.mjs +261 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/revmux-engine.sh +156 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +76 -0
- 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/SKILL.md +85 -2
- package/plugins/olko-github-pr/skills/lekker-review/references/pricing.json +9 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-conventions.md +158 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-implementation.md +76 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-quality.md +87 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-simplification.md +63 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/lenses/lekker-test-quality.md +172 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/phase4-comparison.md +42 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/profile.md +347 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-deep.md +82 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/revmux/profiles/lekker-medium.md +81 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/context.json +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/house-rules.md +11 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/revmux-report.json +179 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/install-revmux-prompts.sh +66 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/revmux-adapter.mjs +261 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/revmux-engine.sh +156 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +76 -0
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +43 -6
- 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
|
@@ -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')
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# revmux-engine.sh -- lekker-review's revmux caller (Phase 2 of the revmux spike).
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# revmux-engine.sh --task SLUG --run NAME --depth medium|deep --workdir DIR \
|
|
6
|
+
# --diff-file FILE --context-file FILE --profile-file FILE --out FILE \
|
|
7
|
+
# [--tasks-dir DIR] [--config-dir DIR]
|
|
8
|
+
#
|
|
9
|
+
# Builds one revmux round (scope.md, goal.md, input/context/, input/profile.md),
|
|
10
|
+
# runs revmux --no-tui, writes its stdout JSON to --out, and exits with revmux's
|
|
11
|
+
# own code (0 = no findings, 1 = findings, 2 = tool error). A round name that
|
|
12
|
+
# already exists is revmux's own error -- this script never deletes anything to
|
|
13
|
+
# work around it.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
TASK=""
|
|
17
|
+
RUN=""
|
|
18
|
+
DEPTH=""
|
|
19
|
+
WORKDIR=""
|
|
20
|
+
DIFF_FILE=""
|
|
21
|
+
CONTEXT_FILE=""
|
|
22
|
+
PROFILE_FILE=""
|
|
23
|
+
OUT=""
|
|
24
|
+
TASKS_DIR="${LEKKER_REVMUX_TASKS_DIR:-$HOME/code-reviews/revmux-tasks}"
|
|
25
|
+
CONFIG_DIR="${REVMUX_CONFIG_DIR:-$HOME/.config/revmux}"
|
|
26
|
+
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--task) TASK="$2"; shift 2 ;;
|
|
30
|
+
--run) RUN="$2"; shift 2 ;;
|
|
31
|
+
--depth) DEPTH="$2"; shift 2 ;;
|
|
32
|
+
--workdir) WORKDIR="$2"; shift 2 ;;
|
|
33
|
+
--diff-file) DIFF_FILE="$2"; shift 2 ;;
|
|
34
|
+
--context-file) CONTEXT_FILE="$2"; shift 2 ;;
|
|
35
|
+
--profile-file) PROFILE_FILE="$2"; shift 2 ;;
|
|
36
|
+
--out) OUT="$2"; shift 2 ;;
|
|
37
|
+
--tasks-dir) TASKS_DIR="$2"; shift 2 ;;
|
|
38
|
+
--config-dir) CONFIG_DIR="$2"; shift 2 ;;
|
|
39
|
+
*) printf 'revmux-engine: unknown arg %s\n' "$1" >&2; exit 2 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
for req in TASK RUN DEPTH WORKDIR DIFF_FILE CONTEXT_FILE PROFILE_FILE OUT; do
|
|
44
|
+
if [[ -z "${!req}" ]]; then
|
|
45
|
+
printf 'revmux-engine: missing required --%s\n' "$(printf '%s' "$req" | tr 'A-Z' 'a-z' | tr '_' '-')" >&2
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
for input in DIFF_FILE CONTEXT_FILE PROFILE_FILE; do
|
|
51
|
+
if [[ ! -r "${!input}" ]]; then
|
|
52
|
+
printf 'revmux-engine: --%s must be a readable file: %s\n' \
|
|
53
|
+
"$(printf '%s' "$input" | tr 'A-Z' 'a-z' | tr '_' '-')" "${!input}" >&2
|
|
54
|
+
exit 2
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
WORKTREE_JSON="$(dirname "$CONTEXT_FILE")/worktree.json"
|
|
59
|
+
if [[ ! -r "$WORKTREE_JSON" ]]; then
|
|
60
|
+
printf 'revmux-engine: worktree configuration must be readable: %s\n' "$WORKTREE_JSON" >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
case "$DEPTH" in
|
|
65
|
+
medium|deep) ;;
|
|
66
|
+
*)
|
|
67
|
+
printf 'revmux-engine: depth "%s" is not supported by this engine (only medium|deep -- scan stays on the workflow engine, it has no worktree)\n' "$DEPTH" >&2
|
|
68
|
+
exit 2
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
PROFILE_NAME="lekker-${DEPTH}"
|
|
73
|
+
|
|
74
|
+
command -v revmux >/dev/null 2>&1 || { printf 'revmux-engine: revmux binary not found on PATH\n' >&2; exit 2; }
|
|
75
|
+
command -v jq >/dev/null 2>&1 || { printf 'revmux-engine: jq not found on PATH\n' >&2; exit 2; }
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Codex guard: refuse to run a profile whose resolved text names a codex/
|
|
79
|
+
# runner. Company does not support the codex CLI (revmux is claude-only here).
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
PROFILE_TEXT_FILE="$CONFIG_DIR/prompts/profiles/${PROFILE_NAME}.md"
|
|
82
|
+
if [[ ! -f "$PROFILE_TEXT_FILE" ]]; then
|
|
83
|
+
printf 'revmux-engine: cannot resolve profile text at %s to run the codex guard -- refusing to proceed\n' "$PROFILE_TEXT_FILE" >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
if grep -q 'codex/' "$PROFILE_TEXT_FILE"; then
|
|
87
|
+
printf 'revmux-engine: profile %s names a codex/ runner -- codex is not supported here, refusing to run\n' "$PROFILE_TEXT_FILE" >&2
|
|
88
|
+
exit 2
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
mkdir -p "$TASKS_DIR"
|
|
92
|
+
|
|
93
|
+
NEW_JSON="$(revmux --tasks-dir "$TASKS_DIR" --config-dir "$CONFIG_DIR" new --task "$TASK" --run "$RUN")"
|
|
94
|
+
|
|
95
|
+
resolve_new_path() {
|
|
96
|
+
local key="$1"
|
|
97
|
+
local value
|
|
98
|
+
if ! value="$(printf '%s' "$NEW_JSON" | jq -er --arg key "$key" \
|
|
99
|
+
'.[$key] | select(type == "string" and length > 0)')" \
|
|
100
|
+
|| [[ -z "$value" || "$value" == "null" ]]; then
|
|
101
|
+
printf 'revmux-engine: revmux new returned an invalid %s path\n' "$key" >&2
|
|
102
|
+
return 2
|
|
103
|
+
fi
|
|
104
|
+
printf '%s' "$value"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
SCOPE_FILE="$(resolve_new_path scope)"
|
|
108
|
+
GOAL_FILE="$(resolve_new_path goal)"
|
|
109
|
+
PROFILE_INPUT_FILE="$(resolve_new_path profile)"
|
|
110
|
+
CONTEXT_DIR="$(resolve_new_path context)"
|
|
111
|
+
|
|
112
|
+
mkdir -p "$CONTEXT_DIR"
|
|
113
|
+
|
|
114
|
+
TARGET_LABEL="$(jq -r '.reviewTarget // "unknown target"' "$CONTEXT_FILE" 2>/dev/null || printf 'unknown target')"
|
|
115
|
+
|
|
116
|
+
BASE_REF=""
|
|
117
|
+
BASE_REF="$(jq -r '.mergeBase // .baseRef // empty' "$WORKTREE_JSON" 2>/dev/null || true)"
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
printf '# Scope\n\n'
|
|
121
|
+
printf 'Under review: %s\n\n' "$TARGET_LABEL"
|
|
122
|
+
printf 'Diff file (authoritative): %s\n\n' "$DIFF_FILE"
|
|
123
|
+
if [[ -n "$BASE_REF" ]]; then
|
|
124
|
+
printf 'Reproduce inside WORKDIR with:\n\n git -C %s diff %s...HEAD\n\n' "$WORKDIR" "$BASE_REF"
|
|
125
|
+
else
|
|
126
|
+
printf 'No base ref was recorded for this worktree; the diff file above is authoritative, do not attempt to reconstruct it from git.\n\n'
|
|
127
|
+
fi
|
|
128
|
+
} > "$SCOPE_FILE"
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
printf '# Goal\n\n'
|
|
132
|
+
printf '## PR title\n\n%s\n\n' "$(jq -r '.reviewTarget // "none provided"' "$CONTEXT_FILE" 2>/dev/null || printf 'none provided')"
|
|
133
|
+
printf '## Ticket / acceptance criteria\n\n%s\n\n' "$(jq -r '.acList // "none provided"' "$CONTEXT_FILE" 2>/dev/null || printf 'none provided')"
|
|
134
|
+
printf '## Scoping decisions\n\n%s\n\n' "$(jq -r '.scopingDecisions // "none provided"' "$CONTEXT_FILE" 2>/dev/null || printf 'none provided')"
|
|
135
|
+
} > "$GOAL_FILE"
|
|
136
|
+
|
|
137
|
+
cp "$CONTEXT_FILE" "$CONTEXT_DIR/context.json"
|
|
138
|
+
cp "$DIFF_FILE" "$CONTEXT_DIR/pr.diff"
|
|
139
|
+
cp "$WORKTREE_JSON" "$CONTEXT_DIR/worktree.json"
|
|
140
|
+
cp "$PROFILE_FILE" "$PROFILE_INPUT_FILE"
|
|
141
|
+
|
|
142
|
+
REVMUX_OUT="$OUT"
|
|
143
|
+
set +e
|
|
144
|
+
revmux --task "$TASK" --run "$RUN" --profile "$PROFILE_NAME" \
|
|
145
|
+
--workdir "$WORKDIR" --tasks-dir "$TASKS_DIR" --config-dir "$CONFIG_DIR" \
|
|
146
|
+
--no-tui > "$REVMUX_OUT"
|
|
147
|
+
CODE=$?
|
|
148
|
+
set -e
|
|
149
|
+
|
|
150
|
+
case "$CODE" in
|
|
151
|
+
0) printf 'revmux exit 0 (no findings)\n' >&2 ;;
|
|
152
|
+
1) printf 'revmux exit 1 (findings reported)\n' >&2 ;;
|
|
153
|
+
*) printf 'revmux exit %s (tool error)\n' "$CODE" >&2 ;;
|
|
154
|
+
esac
|
|
155
|
+
|
|
156
|
+
exit "$CODE"
|