@olegkoval/agent-skills 1.52.0 → 1.53.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/.github/prompts/codexloop.prompt.md +31 -7
- package/.github/prompts/pr-description-writer.prompt.md +25 -2
- package/.kiro/steering/codexloop.md +31 -7
- package/.windsurf/rules/codexloop.md +31 -7
- package/adapters/claude/olko-github-pr/skills/codexloop/SKILL.md +31 -7
- package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +97 -29
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/deps.md +54 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/artifact-page.md +39 -14
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/codex-workflow.md +1 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/github-post.md +3 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/output-format.md +11 -10
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/pricing.json +12 -11
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/deps-only.mjs +95 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +70 -6
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +136 -16
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/validate-lines.mjs +122 -0
- package/adapters/claude/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
- package/adapters/cursor/olko-github-pr/skills/codexloop/SKILL.md +31 -7
- package/adapters/cursor/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
- package/adapters/grok/olko-github-pr/skills/codexloop/SKILL.md +31 -7
- package/adapters/grok/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/codexloop/SKILL.md +31 -7
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +97 -29
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/deps.md +54 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/artifact-page.md +39 -14
- package/plugins/olko-github-pr/skills/lekker-review/references/codex-workflow.md +1 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/github-post.md +3 -1
- package/plugins/olko-github-pr/skills/lekker-review/references/output-format.md +11 -10
- package/plugins/olko-github-pr/skills/lekker-review/references/pricing.json +12 -11
- package/plugins/olko-github-pr/skills/lekker-review/scripts/deps-only.mjs +95 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +70 -6
- package/plugins/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +136 -16
- package/plugins/olko-github-pr/skills/lekker-review/scripts/validate-lines.mjs +122 -0
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +43 -23
- package/plugins/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
- 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
|
@@ -38,6 +38,25 @@ All of the following are already in hand after Step 3 of SKILL.md:
|
|
|
38
38
|
|
|
39
39
|
### Claude Code
|
|
40
40
|
|
|
41
|
+
The subagent RENDERS; the main loop PUBLISHES. Leaf agents run without the
|
|
42
|
+
Artifact tool (a renderer can write the page and then be unable to publish
|
|
43
|
+
it), so never ask the subagent to publish.
|
|
44
|
+
|
|
45
|
+
**Closer image (main loop, before launching):** when `CLOSER` is set, fetch its
|
|
46
|
+
image so the page can show it inline. The page CSP blocks external images but
|
|
47
|
+
not `data:` URIs.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# xkcd: the strip image (`img`); gif: the gif itself (`url`). Cap 500 KB, 10 s.
|
|
51
|
+
curl -sfL --max-time 10 --max-filesize 512000 -o <scratchpad>/closer-img <image url> \
|
|
52
|
+
&& [ "$(wc -c < <scratchpad>/closer-img)" -le 512000 ] \
|
|
53
|
+
&& echo "data:<image/png or image/gif>;base64,$(base64 < <scratchpad>/closer-img | tr -d '\n')" \
|
|
54
|
+
> <scratchpad>/closer-datauri.txt
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pass `CLOSER_DATAURI_FILE=<scratchpad>/closer-datauri.txt` when that file
|
|
58
|
+
exists; otherwise omit it and the card stays link-only.
|
|
59
|
+
|
|
41
60
|
Launch exactly ONE `general-purpose` agent, `model: sonnet`, in the
|
|
42
61
|
background. Do not block printing the review on it (see Hard rules).
|
|
43
62
|
|
|
@@ -53,17 +72,12 @@ Read the findings at <findings.json path> and the saved review at
|
|
|
53
72
|
Write ONE self-contained HTML file to the session scratchpad:
|
|
54
73
|
review-pr-<PR_NUMBER>-<repo-short>.html
|
|
55
74
|
|
|
56
|
-
Follow the page specification below exactly.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- description: <one sentence, e.g. "Living code review for PR #<N> --
|
|
61
|
-
findings update as commits land">
|
|
62
|
-
- url: <PREV_ARTIFACT_TARGET> (include ONLY when the target is a validated
|
|
63
|
-
http or https URL, so the SAME published artifact updates in place; omit
|
|
64
|
-
the `url` parameter for null or local-path targets)
|
|
75
|
+
Follow the page specification below exactly. The <title> is
|
|
76
|
+
"Review: <repo-short> #<PR_NUMBER>". CLOSER=<closer JSON, or empty>.
|
|
77
|
+
CLOSER_DATAURI_FILE=<path, or empty>. Do NOT publish the page and do not look
|
|
78
|
+
for an Artifact tool: the caller publishes it.
|
|
65
79
|
|
|
66
|
-
Return ONLY the
|
|
80
|
+
Return ONLY the absolute path of the HTML file as your final text.
|
|
67
81
|
|
|
68
82
|
--- PAGE SPECIFICATION ---
|
|
69
83
|
<paste Step 3 verbatim here>
|
|
@@ -157,8 +171,11 @@ Hard requirements for the HTML page:
|
|
|
157
171
|
kept concise (verdict + gaps + cost table; no need to reproduce every
|
|
158
172
|
sentence).
|
|
159
173
|
- **Closer footer** (when the caller passes `CLOSER`): a small last card with
|
|
160
|
-
the closer's title (or `alt`) as a link to its `url`.
|
|
161
|
-
|
|
174
|
+
the closer's title (or `alt`) as a link to its `url`. When
|
|
175
|
+
`CLOSER_DATAURI_FILE` is set, read it and render
|
|
176
|
+
`<img src="<its contents>" alt="<title or alt>">` above the link, at
|
|
177
|
+
`max-width:100%; height:auto`. Never point `<img>` at an external URL: the
|
|
178
|
+
CSP blocks it. Skip the card when `CLOSER` is empty.
|
|
162
179
|
- **Layout:** every code block (`badCode`, `fix`, `redOutput`, `testCode`)
|
|
163
180
|
horizontally scrollable within its own container; the page body itself must
|
|
164
181
|
never scroll horizontally. Keep total page weight sane -- plain `<pre>`
|
|
@@ -170,7 +187,15 @@ Hard requirements for the HTML page:
|
|
|
170
187
|
|
|
171
188
|
## Step 4 -- Record the artifact target (fresh post-condition)
|
|
172
189
|
|
|
173
|
-
When the renderer completes:
|
|
190
|
+
When the renderer completes (Claude Code: the subagent returns the HTML path):
|
|
191
|
+
|
|
192
|
+
1. Read the HTML file in full (the Artifact tool publishes only files that were
|
|
193
|
+
read), and check that it has a `<title>` and no external `<img src="http`.
|
|
194
|
+
2. Publish with the Artifact tool: `file_path` = that path, `description` = one
|
|
195
|
+
sentence ("Living code review for PR #<N>; findings update as commits
|
|
196
|
+
land"), and `url: <PREV_ARTIFACT_TARGET>` ONLY when it is a validated `http`
|
|
197
|
+
or `https` URL, so the same artifact updates in place. On a first publish
|
|
198
|
+
add `icon: "review"`; omit `icon` on a republish so the page keeps its icon.
|
|
174
199
|
|
|
175
200
|
**On success (URL or stable local HTML path returned):**
|
|
176
201
|
1. Append or refresh a `**Artifact:** <target>` line in the saved review file's
|
|
@@ -203,5 +228,5 @@ When the renderer completes:
|
|
|
203
228
|
- Never include secrets or tokens from context. The page contains only what
|
|
204
229
|
the saved review file already contains -- nothing pulled fresh from
|
|
205
230
|
scratchpad context that isn't already in the review.
|
|
206
|
-
-
|
|
231
|
+
- Set the artifact icon only on the first publish; never change it on
|
|
207
232
|
republish.
|
|
@@ -60,6 +60,7 @@ unavailable; do not infer what it meant from prose.
|
|
|
60
60
|
|
|
61
61
|
Select dimensions exactly as the Claude workflow does:
|
|
62
62
|
|
|
63
|
+
- deps: `deps` (single checklist agent; no verifier, no prover)
|
|
63
64
|
- scan: `triage-quality`, `triage-logic`
|
|
64
65
|
- medium/deep: `quality`, `implementation`, `simplification`, `conventions`,
|
|
65
66
|
`test-quality`
|
|
@@ -30,7 +30,9 @@ Rules:
|
|
|
30
30
|
review programmatically.
|
|
31
31
|
- Include one comment entry for every Critical and Important finding whose
|
|
32
32
|
`file:line` falls within the diff hunks of this PR (see Step 2 for how to
|
|
33
|
-
determine commentability).
|
|
33
|
+
determine commentability). A finding with `line: null` (set by
|
|
34
|
+
`scripts/validate-lines.mjs` when its quoted code is not in the file) is
|
|
35
|
+
never an inline comment: list it in the review body instead.
|
|
34
36
|
- **Keep every comment body to 2-4 sentences and under ~700 characters**
|
|
35
37
|
(excluding any suggestion block). Inline comments are read one at a time in a
|
|
36
38
|
narrow column; a long one does not get read, so length actively costs you the
|
|
@@ -23,7 +23,7 @@ Output the review as a markdown response (not via printf). No ANSI escapes.
|
|
|
23
23
|
**Findings:** <X Critical / Y Important / Z Observations / W Idiomatic>
|
|
24
24
|
**Proofs:** <M proven / N attempted> *(include only when N > 0)*
|
|
25
25
|
**CI:** <✅ All passing | ⚠️ N failing: check-name | ⏳ Pending | N/A>
|
|
26
|
-
**Depth:**
|
|
26
|
+
**Depth:** <📦 deps | ⚡ scan | 🔍 medium | 🔬 deep>
|
|
27
27
|
**Engine:** <workflow | codex | revmux>
|
|
28
28
|
|
|
29
29
|
---
|
|
@@ -215,7 +215,7 @@ if empty.)*
|
|
|
215
215
|
## 💰 Review Cost
|
|
216
216
|
|
|
217
217
|
```
|
|
218
|
-
Depth:
|
|
218
|
+
Depth: <📦 deps | ⚡ scan | 🔍 medium | 🔬 deep>
|
|
219
219
|
Diff size: ~<N> lines (~<N> tokens)
|
|
220
220
|
Agents run: <N total>: <breakdown, e.g. "5 reviewers (sonnet) + 6 verifiers (sonnet) + 2 provers (sonnet) + 1 critic (sonnet)">
|
|
221
221
|
Rule checks: <N> hard-rule finding(s) validated with rule-specific checks (omit the line when 0)
|
|
@@ -228,16 +228,17 @@ Input tokens: ~<N> ← estimated: diff tokens × agent passes + context fil
|
|
|
228
228
|
Cost: ~$<X.XX> (output actual, input estimated; blended across model tiers below)
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
Per-MTok pricing (input/output)
|
|
232
|
-
- claude-fable-5-1: $10 / $50
|
|
233
|
-
- claude-opus-5-5: $
|
|
234
|
-
- claude-
|
|
235
|
-
- claude-
|
|
236
|
-
- claude-
|
|
237
|
-
- claude-
|
|
238
|
-
- claude-sonnet-4-6 / claude-sonnet-5: $3 / $15
|
|
231
|
+
Per-MTok pricing (input/output) - verified 2026-10-15 from the claude-api reference (`shared/models.md`); re-check there if models changed:
|
|
232
|
+
- claude-fable-5-1 / claude-fable-5: $10 / $50
|
|
233
|
+
- claude-opus-5-5: $4 / $20
|
|
234
|
+
- claude-opus-5 / claude-opus-4-8: $5 / $25
|
|
235
|
+
- claude-sonnet-5-5 / claude-sonnet-5: $2 / $10
|
|
236
|
+
- claude-sonnet-4-6: $3 / $15
|
|
237
|
+
- claude-haiku-5-5: $0.10 / $0.50 (prompt <=100K tokens; $0.50 / $2.50 above)
|
|
239
238
|
- claude-haiku-4-5: $1 / $5
|
|
240
239
|
|
|
240
|
+
The main-loop line uses the session model's rate from this list. When the session model is not listed, say so in the cost block rather than leaving the main loop out silently.
|
|
241
|
+
|
|
241
242
|
Reviewer agents, verifiers, provers, and the critic all run on sonnet; triage and housekeeping on haiku. Only the main loop (context gathering + this synthesis) runs on the session model: use the session model's actual ID for that tier.
|
|
242
243
|
|
|
243
244
|
For the Codex engine, use the model and usage metadata actually exposed by the
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"note": "USD per million tokens, keyed by revmux's actual_model. revmux runs `claude --print` under subscription (Max plan) auth, so nothing here is a real API bill; this is an API-list-price ESTIMATE for the cost block only. Where a model exposes separate input/output prices, usd = tokens/1e6 * output price (output-only, not blended), because revmux reports a single combined `tokens` figure per agent with no input/output split to weight a blend. verified
|
|
3
|
-
"claude-fable-5-1": { "input": 10,
|
|
4
|
-
"claude-
|
|
5
|
-
"claude-
|
|
6
|
-
"claude-
|
|
7
|
-
"claude-
|
|
8
|
-
"claude-sonnet-5":
|
|
9
|
-
"claude-haiku-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
2
|
+
"note": "USD per million tokens, keyed by revmux's actual_model. revmux runs `claude --print` under subscription (Max plan) auth, so nothing here is a real API bill; this is an API-list-price ESTIMATE for the cost block only. Where a model exposes separate input/output prices, usd = tokens/1e6 * output price (output-only, not blended), because revmux reports a single combined `tokens` figure per agent with no input/output split to weight a blend. Rates verified 2026-10-15 against the claude-api skill reference (shared/models.md, shared/model-migration.md). Haiku 5.5 has two rate cards; the entry is the <=100K-prompt card. The bare tier aliases (opus/sonnet/haiku) follow the current model of each tier. Re-check there when a model changes.",
|
|
3
|
+
"claude-fable-5-1": { "input": 10, "output": 50, "blended": 30, "verified": "2026-10-15" },
|
|
4
|
+
"claude-fable-5": { "input": 10, "output": 50, "blended": 30, "verified": "2026-10-15" },
|
|
5
|
+
"claude-opus-5-5": { "input": 4, "output": 20, "blended": 12, "verified": "2026-10-15" },
|
|
6
|
+
"claude-opus-5": { "input": 5, "output": 25, "blended": 15, "verified": "2026-10-15" },
|
|
7
|
+
"claude-sonnet-5-5": { "input": 2, "output": 10, "blended": 6, "verified": "2026-10-15" },
|
|
8
|
+
"claude-sonnet-5": { "input": 2, "output": 10, "blended": 6, "verified": "2026-10-15" },
|
|
9
|
+
"claude-haiku-5-5": { "input": 0.1, "output": 0.5, "blended": 0.3, "verified": "2026-10-15" },
|
|
10
|
+
"claude-haiku-4-5-20251001": { "input": 1, "output": 5, "blended": 3, "verified": "2026-10-15" },
|
|
11
|
+
"opus": { "input": 4, "output": 20, "blended": 12, "verified": "2026-10-15" },
|
|
12
|
+
"sonnet": { "input": 2, "output": 10, "blended": 6, "verified": "2026-10-15" },
|
|
13
|
+
"haiku": { "input": 0.1, "output": 0.5, "blended": 0.3, "verified": "2026-10-15" }
|
|
13
14
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Decides whether a PR is a pure dependency change: every changed file is a
|
|
3
|
+
// lockfile or a package.json, and each package.json differs only in its
|
|
4
|
+
// dependency keys. Such a PR gets the one-agent `deps` depth; anything else is
|
|
5
|
+
// `mixed` and falls back to the normal depth rules.
|
|
6
|
+
//
|
|
7
|
+
// node deps-only.mjs <worktreePath> <mergeBase> <changed file>...
|
|
8
|
+
//
|
|
9
|
+
// Prints JSON: { kind: "deps"|"mixed", packageJsons, lockDirs, otherFiles }.
|
|
10
|
+
// Exit 0 in both cases; exit 2 only on bad arguments.
|
|
11
|
+
import { execFileSync } from 'node:child_process'
|
|
12
|
+
import { realpathSync } from 'node:fs'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { basename, dirname } from 'node:path'
|
|
15
|
+
|
|
16
|
+
export const LOCKFILES = new Set([
|
|
17
|
+
'package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb',
|
|
18
|
+
])
|
|
19
|
+
// `engines` is here because a dependency bump that needs a newer runtime
|
|
20
|
+
// raises it in the same PR; the deps agent checks it against the runtime image.
|
|
21
|
+
export const DEP_KEYS = new Set([
|
|
22
|
+
'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies',
|
|
23
|
+
'overrides', 'resolutions', 'engines',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
function changedKeys(before, after) {
|
|
27
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
28
|
+
return [...keys].filter(k => JSON.stringify(before[k]) !== JSON.stringify(after[k])).sort()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// readAt(rev, path) returns the file text at that revision, or null when the
|
|
32
|
+
// file does not exist there. Injected so the selftest can drive it with fixtures.
|
|
33
|
+
export function classify(files, readAt, base = 'BASE', head = 'HEAD') {
|
|
34
|
+
const packageJsons = []
|
|
35
|
+
const lockDirs = []
|
|
36
|
+
const otherFiles = []
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const name = basename(file)
|
|
39
|
+
if (LOCKFILES.has(name)) {
|
|
40
|
+
lockDirs.push(dirname(file))
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
if (name !== 'package.json') {
|
|
44
|
+
otherFiles.push(file)
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
let before, after
|
|
48
|
+
try {
|
|
49
|
+
const b = readAt(base, file), a = readAt(head, file)
|
|
50
|
+
if (b == null || a == null) throw new Error('missing on one side')
|
|
51
|
+
before = JSON.parse(b)
|
|
52
|
+
after = JSON.parse(a)
|
|
53
|
+
if (!before || !after || typeof before !== 'object' || typeof after !== 'object') throw new Error('not an object')
|
|
54
|
+
} catch {
|
|
55
|
+
// Added, deleted or unparseable: a new or removed workspace is not a bump.
|
|
56
|
+
packageJsons.push({ file, depKeysChanged: [], otherKeysChanged: ['<added, removed or unparseable>'] })
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
const keys = changedKeys(before, after)
|
|
60
|
+
packageJsons.push({
|
|
61
|
+
file,
|
|
62
|
+
depKeysChanged: keys.filter(k => DEP_KEYS.has(k)),
|
|
63
|
+
otherKeysChanged: keys.filter(k => !DEP_KEYS.has(k)),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
const pure = otherFiles.length === 0
|
|
67
|
+
&& packageJsons.every(p => p.otherKeysChanged.length === 0)
|
|
68
|
+
&& (lockDirs.length > 0 || packageJsons.length > 0)
|
|
69
|
+
return { kind: pure ? 'deps' : 'mixed', packageJsons, lockDirs: [...new Set(lockDirs)].sort(), otherFiles }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function gitReader(worktree) {
|
|
73
|
+
return (rev, path) => {
|
|
74
|
+
try {
|
|
75
|
+
return execFileSync('git', ['-C', worktree, 'show', `${rev}:${path}`], {
|
|
76
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024,
|
|
77
|
+
})
|
|
78
|
+
} catch {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isMain() {
|
|
85
|
+
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isMain()) {
|
|
89
|
+
const [worktree, mergeBase, ...files] = process.argv.slice(2)
|
|
90
|
+
if (!worktree || !mergeBase || files.length === 0) {
|
|
91
|
+
console.error('usage: deps-only.mjs <worktreePath> <mergeBase> <changed file>...')
|
|
92
|
+
process.exit(2)
|
|
93
|
+
}
|
|
94
|
+
console.log(JSON.stringify(classify(files, gitReader(worktree), mergeBase, 'HEAD'), null, 2))
|
|
95
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// It lifts the real functions out of workflow.js by source extraction rather
|
|
13
13
|
// than importing, because workflow.js is written for the Workflow harness (top
|
|
14
14
|
// level `return`, an injected `args` global) and is not importable as a module.
|
|
15
|
-
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
16
16
|
import { fileURLToPath } from 'node:url'
|
|
17
17
|
import { dirname, join } from 'node:path'
|
|
18
18
|
|
|
@@ -142,11 +142,12 @@ check('a custom house rule is a hard rule',
|
|
|
142
142
|
|
|
143
143
|
console.log('\nverification scope: depth controls breadth, never the trust bar')
|
|
144
144
|
const crit = { severity: 'critical' }, imp = { severity: 'important' }, obs = { severity: 'observation' }
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
check('critical and important are verified, observation is not',
|
|
149
|
-
[crit, imp, obs].map(f => shouldVerify(f)),
|
|
145
|
+
// Depth used to be able to switch verification off entirely, which meant a
|
|
146
|
+
// scan-depth review shipped unverified Criticals. Only `deps` opts out now (its
|
|
147
|
+
// single checklist agent reports advisory facts), see the deps depth section.
|
|
148
|
+
check('critical and important are verified at every non-deps depth, observation is not',
|
|
149
|
+
['scan', 'medium', 'deep', undefined].map(d => [crit, imp, obs].map(f => shouldVerify(f, d))),
|
|
150
|
+
Array(4).fill([true, true, false]))
|
|
150
151
|
check('a hard rule is NOT exempt from verification',
|
|
151
152
|
shouldVerify({ severity: 'critical', rule: 'TS-2', file: 'x.js', badCode: '', description: '', title: '' }), true)
|
|
152
153
|
check('a custom hard rule is NOT exempt either',
|
|
@@ -319,5 +320,68 @@ console.log('\ncloser picker: pool, exact rendering, fail-soft and --check')
|
|
|
319
320
|
check('github-post and artifact-page both use CLOSER', read('references/github-post.md').includes('CLOSER') && read('references/artifact-page.md').includes('CLOSER'), true)
|
|
320
321
|
}
|
|
321
322
|
|
|
323
|
+
console.log('\ndeps depth: routing, classifier, line validation')
|
|
324
|
+
{
|
|
325
|
+
const dimsSrc = (() => { try { return lift('dimensionsFor') } catch { return null } })()
|
|
326
|
+
check('workflow.js defines dimensionsFor', Boolean(dimsSrc), true)
|
|
327
|
+
if (dimsSrc) {
|
|
328
|
+
const dimensionsFor = new Function(dimsSrc + '\nreturn dimensionsFor')()
|
|
329
|
+
const M = { review: 'r', triage: 't' }
|
|
330
|
+
check('deps runs exactly one agent', dimensionsFor('deps', M).map(d => d.file), ['deps.md'])
|
|
331
|
+
check('scan still runs the triage pair', dimensionsFor('scan', M).length, 2)
|
|
332
|
+
check('medium still runs five specialists', dimensionsFor('medium', M).length, 5)
|
|
333
|
+
check('every dimension prompt file exists',
|
|
334
|
+
['deps', 'scan', 'medium'].flatMap(d => dimensionsFor(d, M)).every(d => existsSync(join(SKILL, 'references', 'agents', d.file))), true)
|
|
335
|
+
}
|
|
336
|
+
check('deps verifies nothing', [crit, imp, obs].map(f => shouldVerify(f, 'deps')), [false, false, false])
|
|
337
|
+
check('scan still verifies Criticals and Importants', [crit, imp, obs].map(f => shouldVerify(f, 'scan')), [true, true, false])
|
|
338
|
+
check('deps skips the prover', /depth !== 'scan' && depth !== 'deps'/.test(src), true)
|
|
339
|
+
|
|
340
|
+
const { classify } = await import(join(SKILL, 'scripts', 'deps-only.mjs'))
|
|
341
|
+
const pkg = o => JSON.stringify(o)
|
|
342
|
+
const files = {
|
|
343
|
+
'BASE:web/package.json': pkg({ name: 'web', scripts: { t: 'vitest' }, devDependencies: { vitest: '^3.2.0' }, engines: { node: '>=20' } }),
|
|
344
|
+
'HEAD:web/package.json': pkg({ name: 'web', scripts: { t: 'vitest' }, devDependencies: { vitest: '^3.2.4' }, overrides: { tinypool: '^2.1.2' }, engines: { node: '>=20' } }),
|
|
345
|
+
'BASE:eng/package.json': pkg({ name: 'eng', engines: { node: '>=20' } }),
|
|
346
|
+
'HEAD:eng/package.json': pkg({ name: 'eng', engines: { node: '>=24' } }),
|
|
347
|
+
'BASE:mix/package.json': pkg({ name: 'mix', scripts: { t: 'jest' }, dependencies: { a: '1' } }),
|
|
348
|
+
'HEAD:mix/package.json': pkg({ name: 'mix', scripts: { t: 'vitest' }, dependencies: { a: '2' } }),
|
|
349
|
+
}
|
|
350
|
+
const readAt = (rev, p) => files[`${rev}:${p}`] ?? null
|
|
351
|
+
const deps = classify(['package-lock.json', 'web/package.json', 'web/package-lock.json'], readAt)
|
|
352
|
+
check('lockfiles + dep keys -> deps', deps.kind, 'deps')
|
|
353
|
+
check('deps reports the changed dep keys', deps.packageJsons[0].depKeysChanged, ['devDependencies', 'overrides'])
|
|
354
|
+
check('deps reports each lockfile dir', deps.lockDirs, ['.', 'web'])
|
|
355
|
+
check('engines-only change -> deps', classify(['eng/package.json'], readAt).kind, 'deps')
|
|
356
|
+
check('a scripts change -> mixed', classify(['mix/package.json', 'package-lock.json'], readAt).kind, 'mixed')
|
|
357
|
+
check('a source file -> mixed', classify(['package-lock.json', 'web/src/a.ts'], readAt).kind, 'mixed')
|
|
358
|
+
check('an added package.json -> mixed', classify(['new/package.json'], readAt).kind, 'mixed')
|
|
359
|
+
|
|
360
|
+
const { validate, loadDedup } = await import(join(SKILL, 'scripts', 'validate-lines.mjs'))
|
|
361
|
+
const { mkdtempSync, writeFileSync: wf, mkdirSync } = await import('node:fs')
|
|
362
|
+
const { tmpdir } = await import('node:os')
|
|
363
|
+
const wt = mkdtempSync(join(tmpdir(), 'lekker-lines-'))
|
|
364
|
+
mkdirSync(join(wt, 'web'))
|
|
365
|
+
const body = Array.from({ length: 80 }, (_, i) => ` "pad${i}": "x",`)
|
|
366
|
+
body[69] = ' "tinypool": "^2.1.2"'
|
|
367
|
+
wf(join(wt, 'web', 'package.json'), body.join('\n'))
|
|
368
|
+
const dedupFn = loadDedup(target)
|
|
369
|
+
const f = (line, badCode, title = 'tinypool override forces a major') =>
|
|
370
|
+
({ file: 'web/package.json', line, severity: 'observation', title, description: '', badCode, fix: '' })
|
|
371
|
+
const r = validate([
|
|
372
|
+
f(398, '+ "tinypool": "^2.1.2"'),
|
|
373
|
+
f(70, '"tinypool": "^2.1.2"', 'tinypool override forces a major vitest does not declare'),
|
|
374
|
+
f(12, 'something that is not in the file at all', 'phantom'),
|
|
375
|
+
f(71, '"tinypool": "^2.1.2"', 'near enough'),
|
|
376
|
+
{ file: 'CI: checks / audit', line: 1, severity: 'observation', title: 'ci', description: '', badCode: 'x', fix: '' },
|
|
377
|
+
], wt, dedupFn)
|
|
378
|
+
check('diff line 398 is re-anchored to file line 70, then merged with its twin',
|
|
379
|
+
r.findings.filter(x => x.line === 70 && /tinypool/.test(x.title)).length, 1)
|
|
380
|
+
check('an unfindable badCode nulls the line', r.findings.find(x => x.title === 'phantom')?.line, null)
|
|
381
|
+
check('within +-3 lines is kept', r.findings.some(x => x.line === 71 || x.title === 'near enough'), true)
|
|
382
|
+
check('a non-file anchor is left alone', r.findings.find(x => x.title === 'ci')?.line, 1)
|
|
383
|
+
check('summary counts', [r.summary.remapped, r.summary.nulled, r.summary.unchecked], [1, 1, 1])
|
|
384
|
+
}
|
|
385
|
+
|
|
322
386
|
console.log(failed === 0 ? '\nall checks passed\n' : `\n${failed} check(s) FAILED\n`)
|
|
323
387
|
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -134,7 +134,23 @@ if [[ -n "$LOCAL_REPO" ]]; then
|
|
|
134
134
|
exit 1
|
|
135
135
|
fi
|
|
136
136
|
else
|
|
137
|
-
|
|
137
|
+
# Fetch the base branch too: without it origin/<base> is whatever this clone
|
|
138
|
+
# last saw, and the merge-base drifts back far enough to list files the PR
|
|
139
|
+
# never touched (65 instead of 9 on one review). Explicit refspecs, because
|
|
140
|
+
# a --depth clone is single-branch and would not update either ref.
|
|
141
|
+
# LEKKER_BASE_BRANCH names the branch to fetch when LEKKER_BASE_REF is not
|
|
142
|
+
# origin/<branch>: a merged PR is diffed against <mergeCommit>^1, because its
|
|
143
|
+
# head is already inside the base branch and the merge-base would be the head.
|
|
144
|
+
FETCH_SPECS=("+refs/heads/${PR_BRANCH}:refs/remotes/origin/${PR_BRANCH}")
|
|
145
|
+
BASE_BRANCH="${LEKKER_BASE_BRANCH:-}"
|
|
146
|
+
if [[ -z "$BASE_BRANCH" && "${LEKKER_BASE_REF:-}" == origin/* ]]; then
|
|
147
|
+
BASE_BRANCH="${LEKKER_BASE_REF#origin/}"
|
|
148
|
+
fi
|
|
149
|
+
if [[ -n "$BASE_BRANCH" ]]; then
|
|
150
|
+
FETCH_SPECS+=("+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}")
|
|
151
|
+
fi
|
|
152
|
+
git -C "$REPO_ROOT" fetch origin "${FETCH_SPECS[@]}" 2>/dev/null \
|
|
153
|
+
|| git -C "$REPO_ROOT" fetch origin "$PR_BRANCH" 2>/dev/null || true
|
|
138
154
|
|
|
139
155
|
if ! git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" "origin/$PR_BRANCH" 2>/dev/null; then
|
|
140
156
|
if ! git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" FETCH_HEAD 2>/dev/null; then
|
|
@@ -146,19 +162,9 @@ else
|
|
|
146
162
|
fi
|
|
147
163
|
|
|
148
164
|
# ---------------------------------------------------------------------------
|
|
149
|
-
# 3.
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
if [[ -d "$REPO_ROOT/node_modules" ]]; then
|
|
153
|
-
ln -s "$REPO_ROOT/node_modules" "$WORKTREE_PATH/node_modules"
|
|
154
|
-
NM_LINKED=true
|
|
155
|
-
else
|
|
156
|
-
NOTES="${NOTES}no-node_modules-static-checks-skipped;"
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
|
-
# ---------------------------------------------------------------------------
|
|
160
|
-
# 3b. Determine changed files (to scope tsc/eslint to this PR's diff instead
|
|
161
|
-
# of the whole repo -- see scripts/changed-files.sh).
|
|
165
|
+
# 3. Determine changed files (to scope tsc/eslint to this PR's diff instead
|
|
166
|
+
# of the whole repo -- see scripts/changed-files.sh). Runs before the
|
|
167
|
+
# node_modules step, which needs to know whether a lockfile changed.
|
|
162
168
|
# ---------------------------------------------------------------------------
|
|
163
169
|
CHANGED_FILES_FILE="/tmp/lekker-changed-$$.txt"
|
|
164
170
|
CF_ERR_FILE="$(mktemp "${TMPDIR:-/tmp}/lekker-changed-err.XXXXXX")"
|
|
@@ -188,6 +194,100 @@ else
|
|
|
188
194
|
NOTES="${NOTES}static-checks-repo-wide-no-base-ref;"
|
|
189
195
|
fi
|
|
190
196
|
|
|
197
|
+
# The caller passes the PR's own file count (gh pr view) so a merge-base that
|
|
198
|
+
# still drifted is reported instead of silently widening the static checks.
|
|
199
|
+
if [[ -n "${LEKKER_EXPECTED_FILES:-}" ]] && [[ -f "$CHANGED_FILES_FILE" ]]; then
|
|
200
|
+
GOT_FILES="$(grep -c . "$CHANGED_FILES_FILE" 2>/dev/null || true)"
|
|
201
|
+
if [[ "${GOT_FILES:-0}" != "$LEKKER_EXPECTED_FILES" ]]; then
|
|
202
|
+
NOTES="${NOTES}changed-files-mismatch:${GOT_FILES:-0}-vs-${LEKKER_EXPECTED_FILES};"
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# 3b. node_modules
|
|
208
|
+
# A PR that changes a lockfile gets a fresh `npm ci` in each changed
|
|
209
|
+
# lockfile's dir: the repo root's node_modules holds the OLD tree, so
|
|
210
|
+
# symlinking it would type-check and lint against dependencies the PR
|
|
211
|
+
# replaced. Every other PR symlinks the root's node_modules as before.
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
NM_LINKED=false
|
|
214
|
+
DEPS_INSTALL_FILE="/tmp/lekker-deps-install-$$.tsv"
|
|
215
|
+
printf '' > "$DEPS_INSTALL_FILE"
|
|
216
|
+
|
|
217
|
+
LOCK_DIRS=()
|
|
218
|
+
if [[ -s "$CHANGED_FILES_FILE" ]]; then
|
|
219
|
+
while IFS= read -r cf; do
|
|
220
|
+
case "$cf" in
|
|
221
|
+
package-lock.json|npm-shrinkwrap.json) LOCK_DIRS+=(".") ;;
|
|
222
|
+
*/package-lock.json|*/npm-shrinkwrap.json) LOCK_DIRS+=("${cf%/*}") ;;
|
|
223
|
+
yarn.lock|*/yarn.lock|pnpm-lock.yaml|*/pnpm-lock.yaml|bun.lock|*/bun.lock|bun.lockb|*/bun.lockb)
|
|
224
|
+
NOTES="${NOTES}deps-install-unsupported:${cf};" ;;
|
|
225
|
+
esac
|
|
226
|
+
done < "$CHANGED_FILES_FILE"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Portable timeout (macOS has no `timeout`). Exit 143 means it was killed.
|
|
230
|
+
run_with_timeout() {
|
|
231
|
+
local secs pid watcher rc
|
|
232
|
+
secs="$1"; shift
|
|
233
|
+
"$@" &
|
|
234
|
+
pid=$!
|
|
235
|
+
( sleep "$secs"; kill -TERM "$pid" 2>/dev/null ) &
|
|
236
|
+
watcher=$!
|
|
237
|
+
rc=0
|
|
238
|
+
wait "$pid" || rc=$?
|
|
239
|
+
kill "$watcher" 2>/dev/null || true
|
|
240
|
+
wait "$watcher" 2>/dev/null || true
|
|
241
|
+
return "$rc"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
ROOT_LOCK_CHANGED=false
|
|
245
|
+
for d in "${LOCK_DIRS[@]+"${LOCK_DIRS[@]}"}"; do
|
|
246
|
+
[[ "$d" == "." ]] && ROOT_LOCK_CHANGED=true
|
|
247
|
+
done
|
|
248
|
+
|
|
249
|
+
if [[ ${#LOCK_DIRS[@]} -gt 0 ]] && [[ -n "${LEKKER_NO_INSTALL:-}" ]]; then
|
|
250
|
+
NOTES="${NOTES}deps-install-disabled;"
|
|
251
|
+
elif [[ ${#LOCK_DIRS[@]} -gt 0 ]]; then
|
|
252
|
+
for d in "${LOCK_DIRS[@]}"; do
|
|
253
|
+
log_file="/tmp/lekker-npm-ci-$$-${d//\//_}.log"
|
|
254
|
+
# --ignore-scripts: a dependency bump under review must not get to run
|
|
255
|
+
# its postinstall on the reviewer's machine.
|
|
256
|
+
rc=0
|
|
257
|
+
(cd "$WORKTREE_PATH/$d" && run_with_timeout 300 npm ci --ignore-scripts \
|
|
258
|
+
--no-audit --no-fund --prefer-offline) > "$log_file" 2>&1 || rc=$?
|
|
259
|
+
if [[ "$rc" == "0" ]]; then
|
|
260
|
+
status="ok"
|
|
261
|
+
[[ "$d" == "." ]] && NM_LINKED=true
|
|
262
|
+
elif [[ "$rc" == "143" ]]; then
|
|
263
|
+
status="timeout"
|
|
264
|
+
else
|
|
265
|
+
status="failed"
|
|
266
|
+
fi
|
|
267
|
+
printf '%s\t%s\t%s\n' "$d" "$status" "$log_file" >> "$DEPS_INSTALL_FILE"
|
|
268
|
+
[[ "$status" == "ok" ]] || NOTES="${NOTES}deps-install-${status}:${d};"
|
|
269
|
+
done
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
if [[ "$ROOT_LOCK_CHANGED" == "false" ]]; then
|
|
273
|
+
if [[ -d "$REPO_ROOT/node_modules" ]]; then
|
|
274
|
+
ln -s "$REPO_ROOT/node_modules" "$WORKTREE_PATH/node_modules"
|
|
275
|
+
NM_LINKED=true
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# Nested packages (web/, extensions/*) resolve imports from their own
|
|
280
|
+
# node_modules; without it tsc reports every import in them as TS2307.
|
|
281
|
+
while IFS= read -r pj; do
|
|
282
|
+
d="${pj#"$WORKTREE_PATH"/}"; d="${d%/package.json}"
|
|
283
|
+
[[ -e "$WORKTREE_PATH/$d/node_modules" ]] && continue
|
|
284
|
+
[[ -d "$REPO_ROOT/$d/node_modules" ]] || continue
|
|
285
|
+
case " ${LOCK_DIRS[*]+"${LOCK_DIRS[*]}"} " in *" $d "*) continue ;; esac
|
|
286
|
+
ln -s "$REPO_ROOT/$d/node_modules" "$WORKTREE_PATH/$d/node_modules"
|
|
287
|
+
done < <(find "$WORKTREE_PATH" -mindepth 2 -maxdepth 3 -name package.json \
|
|
288
|
+
-not -path '*/node_modules/*' 2>/dev/null)
|
|
289
|
+
[[ "$NM_LINKED" == "true" ]] || NOTES="${NOTES}no-node_modules-static-checks-skipped;"
|
|
290
|
+
|
|
191
291
|
# Lintable, still-on-disk changed files (a deleted file must not go to eslint).
|
|
192
292
|
ESLINT_TARGETS=()
|
|
193
293
|
if [[ -s "$CHANGED_FILES_FILE" ]]; then
|
|
@@ -327,6 +427,7 @@ python3 - \
|
|
|
327
427
|
"$TSC_CHANGED_FILE" \
|
|
328
428
|
"$TSC_COUNT_FILE" \
|
|
329
429
|
"$MERGE_BASE_VAL" \
|
|
430
|
+
"$DEPS_INSTALL_FILE" \
|
|
330
431
|
"$OUT_JSON" <<'PYEOF'
|
|
331
432
|
import json, sys, os
|
|
332
433
|
|
|
@@ -334,7 +435,7 @@ import json, sys, os
|
|
|
334
435
|
tsc_file, eslint_file, rules_file, delta_file, notes,
|
|
335
436
|
changed_files_file, base_ref_val, eslint_scope_file,
|
|
336
437
|
tsc_changed_file, tsc_count_file, merge_base_val,
|
|
337
|
-
out_json) = sys.argv[1:
|
|
438
|
+
deps_install_file, out_json) = sys.argv[1:18]
|
|
338
439
|
|
|
339
440
|
def read_file(path):
|
|
340
441
|
if not os.path.exists(path):
|
|
@@ -342,6 +443,23 @@ def read_file(path):
|
|
|
342
443
|
with open(path, "r", errors="replace") as f:
|
|
343
444
|
return f.read()
|
|
344
445
|
|
|
446
|
+
# One row per changed-lockfile dir: dir<TAB>status<TAB>log path. Empty = no
|
|
447
|
+
# lockfile changed (or install disabled), so the root node_modules was linked.
|
|
448
|
+
deps_install = []
|
|
449
|
+
for row in read_file(deps_install_file).splitlines():
|
|
450
|
+
parts = row.split("\t")
|
|
451
|
+
if len(parts) != 3:
|
|
452
|
+
continue
|
|
453
|
+
d, status, log = parts
|
|
454
|
+
entry = {"dir": d, "status": status}
|
|
455
|
+
if status != "ok":
|
|
456
|
+
entry["tail"] = "\n".join(read_file(log).splitlines()[-20:])
|
|
457
|
+
deps_install.append(entry)
|
|
458
|
+
try:
|
|
459
|
+
os.remove(log)
|
|
460
|
+
except OSError:
|
|
461
|
+
pass
|
|
462
|
+
|
|
345
463
|
changed_files = [l for l in read_file(changed_files_file).splitlines() if l.strip()]
|
|
346
464
|
|
|
347
465
|
eslint_scope = read_file(eslint_scope_file).strip() or "skipped"
|
|
@@ -367,10 +485,12 @@ data = {
|
|
|
367
485
|
"eslintScope": eslint_scope,
|
|
368
486
|
"tscChangedTail": read_file(tsc_changed_file),
|
|
369
487
|
"tscErrorCount": tsc_error_count,
|
|
488
|
+
"depsInstall": deps_install,
|
|
370
489
|
}
|
|
371
490
|
|
|
372
491
|
for p in (tsc_file, eslint_file, rules_file, changed_files_file,
|
|
373
|
-
eslint_scope_file, tsc_changed_file, tsc_count_file
|
|
492
|
+
eslint_scope_file, tsc_changed_file, tsc_count_file,
|
|
493
|
+
deps_install_file):
|
|
374
494
|
try:
|
|
375
495
|
os.remove(p)
|
|
376
496
|
except OSError:
|