@olegkoval/agent-skills 1.51.3 → 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.
Files changed (51) hide show
  1. package/.github/prompts/codexloop.prompt.md +31 -7
  2. package/.github/prompts/pr-description-writer.prompt.md +25 -2
  3. package/.kiro/steering/codexloop.md +31 -7
  4. package/.windsurf/rules/codexloop.md +31 -7
  5. package/adapters/claude/olko-github-pr/skills/codexloop/SKILL.md +31 -7
  6. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +121 -29
  7. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/deps.md +54 -0
  8. package/adapters/claude/olko-github-pr/skills/lekker-review/references/artifact-page.md +40 -12
  9. package/adapters/claude/olko-github-pr/skills/lekker-review/references/codex-workflow.md +1 -0
  10. package/adapters/claude/olko-github-pr/skills/lekker-review/references/fun.json +222 -0
  11. package/adapters/claude/olko-github-pr/skills/lekker-review/references/github-post.md +8 -1
  12. package/adapters/claude/olko-github-pr/skills/lekker-review/references/output-format.md +15 -10
  13. package/adapters/claude/olko-github-pr/skills/lekker-review/references/pricing.json +12 -11
  14. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/deps-only.mjs +95 -0
  15. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/pick-closer.mjs +99 -0
  16. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +130 -6
  17. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +136 -16
  18. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/validate-lines.mjs +122 -0
  19. package/adapters/claude/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
  20. package/adapters/cursor/olko-github-pr/skills/codexloop/SKILL.md +31 -7
  21. package/adapters/cursor/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
  22. package/adapters/grok/olko-github-pr/skills/codexloop/SKILL.md +31 -7
  23. package/adapters/grok/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
  24. package/package.json +1 -1
  25. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  26. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  27. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  28. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  29. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  30. package/plugins/olko-github-pr/skills/codexloop/SKILL.md +31 -7
  31. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +121 -29
  32. package/plugins/olko-github-pr/skills/lekker-review/references/agents/deps.md +54 -0
  33. package/plugins/olko-github-pr/skills/lekker-review/references/artifact-page.md +40 -12
  34. package/plugins/olko-github-pr/skills/lekker-review/references/codex-workflow.md +1 -0
  35. package/plugins/olko-github-pr/skills/lekker-review/references/fun.json +222 -0
  36. package/plugins/olko-github-pr/skills/lekker-review/references/github-post.md +8 -1
  37. package/plugins/olko-github-pr/skills/lekker-review/references/output-format.md +15 -10
  38. package/plugins/olko-github-pr/skills/lekker-review/references/pricing.json +12 -11
  39. package/plugins/olko-github-pr/skills/lekker-review/scripts/deps-only.mjs +95 -0
  40. package/plugins/olko-github-pr/skills/lekker-review/scripts/pick-closer.mjs +99 -0
  41. package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +130 -6
  42. package/plugins/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +136 -16
  43. package/plugins/olko-github-pr/skills/lekker-review/scripts/validate-lines.mjs +122 -0
  44. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +43 -23
  45. package/plugins/olko-github-pr/skills/pr-description-writer/SKILL.md +26 -3
  46. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  47. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  48. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  49. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  50. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  51. 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. Then publish it with the
57
- Artifact tool using:
58
- - favicon: "🥩" (keep this IDENTICAL on every republish)
59
- - title: "Review: <repo-short> #<PR_NUMBER>"
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 resulting artifact URL as your final text. No other output.
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>
@@ -156,6 +170,12 @@ Hard requirements for the HTML page:
156
170
  - **Test Quality + Review Cost sections**, mirrored from the review file,
157
171
  kept concise (verdict + gaps + cost table; no need to reproduce every
158
172
  sentence).
173
+ - **Closer footer** (when the caller passes `CLOSER`): a small last card with
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.
159
179
  - **Layout:** every code block (`badCode`, `fix`, `redOutput`, `testCode`)
160
180
  horizontally scrollable within its own container; the page body itself must
161
181
  never scroll horizontally. Keep total page weight sane -- plain `<pre>`
@@ -167,7 +187,15 @@ Hard requirements for the HTML page:
167
187
 
168
188
  ## Step 4 -- Record the artifact target (fresh post-condition)
169
189
 
170
- 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.
171
199
 
172
200
  **On success (URL or stable local HTML path returned):**
173
201
  1. Append or refresh a `**Artifact:** <target>` line in the saved review file's
@@ -200,5 +228,5 @@ When the renderer completes:
200
228
  - Never include secrets or tokens from context. The page contains only what
201
229
  the saved review file already contains -- nothing pulled fresh from
202
230
  scratchpad context that isn't already in the review.
203
- - Favicon stays `🥩` forever for this artifact -- never change it on
231
+ - Set the artifact icon only on the first publish; never change it on
204
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`
@@ -0,0 +1,222 @@
1
+ {
2
+ "xkcd": [
3
+ {
4
+ "num": 1513,
5
+ "title": "Code Quality",
6
+ "url": "https://xkcd.com/1513/",
7
+ "img": "https://imgs.xkcd.com/comics/code_quality.png",
8
+ "verdicts": [
9
+ "needs-work"
10
+ ]
11
+ },
12
+ {
13
+ "num": 1695,
14
+ "title": "Code Quality 2",
15
+ "url": "https://xkcd.com/1695/",
16
+ "img": "https://imgs.xkcd.com/comics/code_quality_2.png",
17
+ "verdicts": [
18
+ "needs-work"
19
+ ]
20
+ },
21
+ {
22
+ "num": 1833,
23
+ "title": "Code Quality 3",
24
+ "url": "https://xkcd.com/1833/",
25
+ "img": "https://imgs.xkcd.com/comics/code_quality_3.png",
26
+ "verdicts": [
27
+ "needs-work",
28
+ "lgtm-with-changes"
29
+ ]
30
+ },
31
+ {
32
+ "num": 2138,
33
+ "title": "Wanna See the Code?",
34
+ "url": "https://xkcd.com/2138/",
35
+ "img": "https://imgs.xkcd.com/comics/wanna_see_the_code.png",
36
+ "verdicts": [
37
+ "needs-work",
38
+ "lgtm-with-changes"
39
+ ]
40
+ },
41
+ {
42
+ "num": 1597,
43
+ "title": "Git",
44
+ "url": "https://xkcd.com/1597/",
45
+ "img": "https://imgs.xkcd.com/comics/git.png",
46
+ "verdicts": [
47
+ "lgtm-with-changes",
48
+ "needs-work"
49
+ ]
50
+ },
51
+ {
52
+ "num": 303,
53
+ "title": "Compiling",
54
+ "url": "https://xkcd.com/303/",
55
+ "img": "https://imgs.xkcd.com/comics/compiling.png",
56
+ "verdicts": [
57
+ "lgtm",
58
+ "lgtm-with-changes"
59
+ ]
60
+ },
61
+ {
62
+ "num": 1296,
63
+ "title": "Git Commit",
64
+ "url": "https://xkcd.com/1296/",
65
+ "img": "https://imgs.xkcd.com/comics/git_commit.png",
66
+ "verdicts": [
67
+ "lgtm-with-changes"
68
+ ]
69
+ },
70
+ {
71
+ "num": 844,
72
+ "title": "Good Code",
73
+ "url": "https://xkcd.com/844/",
74
+ "img": "https://imgs.xkcd.com/comics/good_code.png",
75
+ "verdicts": [
76
+ "lgtm",
77
+ "lgtm-with-changes"
78
+ ]
79
+ },
80
+ {
81
+ "num": 1739,
82
+ "title": "Fixing Problems",
83
+ "url": "https://xkcd.com/1739/",
84
+ "img": "https://imgs.xkcd.com/comics/fixing_problems.png",
85
+ "verdicts": [
86
+ "needs-work"
87
+ ]
88
+ },
89
+ {
90
+ "num": 2347,
91
+ "title": "Dependency",
92
+ "url": "https://xkcd.com/2347/",
93
+ "img": "https://imgs.xkcd.com/comics/dependency.png",
94
+ "verdicts": [
95
+ "lgtm",
96
+ "needs-work"
97
+ ]
98
+ },
99
+ {
100
+ "num": 979,
101
+ "title": "Wisdom of the Ancients",
102
+ "url": "https://xkcd.com/979/",
103
+ "img": "https://imgs.xkcd.com/comics/wisdom_of_the_ancients.png",
104
+ "verdicts": [
105
+ "lgtm",
106
+ "needs-work"
107
+ ]
108
+ },
109
+ {
110
+ "num": 1700,
111
+ "title": "New Bug",
112
+ "url": "https://xkcd.com/1700/",
113
+ "img": "https://imgs.xkcd.com/comics/new_bug.png",
114
+ "verdicts": [
115
+ "needs-work"
116
+ ]
117
+ },
118
+ {
119
+ "num": 2200,
120
+ "title": "Unreachable State",
121
+ "url": "https://xkcd.com/2200/",
122
+ "img": "https://imgs.xkcd.com/comics/unreachable_state.png",
123
+ "verdicts": [
124
+ "lgtm"
125
+ ]
126
+ }
127
+ ],
128
+ "gifs": [
129
+ {
130
+ "url": "https://media.giphy.com/media/13zeE9qQNC5IKk/giphy.gif",
131
+ "alt": "Seal of approval thumbs up",
132
+ "verdicts": [
133
+ "lgtm"
134
+ ]
135
+ },
136
+ {
137
+ "url": "https://media.giphy.com/media/SVDvVbkl359Xwzl3V4/giphy.gif",
138
+ "alt": "Approved",
139
+ "verdicts": [
140
+ "lgtm"
141
+ ]
142
+ },
143
+ {
144
+ "url": "https://media.giphy.com/media/ely3apij36BJhoZ234/giphy.gif",
145
+ "alt": "Great job otter",
146
+ "verdicts": [
147
+ "lgtm"
148
+ ]
149
+ },
150
+ {
151
+ "url": "https://media.giphy.com/media/rMEJyjch7L1tlRlCl3/giphy.gif",
152
+ "alt": "Cat says ok",
153
+ "verdicts": [
154
+ "lgtm"
155
+ ]
156
+ },
157
+ {
158
+ "url": "https://media.giphy.com/media/XdUMQuuiaAEkOUtezd/giphy.gif",
159
+ "alt": "Animated thumbs up",
160
+ "verdicts": [
161
+ "lgtm"
162
+ ]
163
+ },
164
+ {
165
+ "url": "https://media.giphy.com/media/Qaxd5XpgDWMso/giphy.gif",
166
+ "alt": "Golden retriever almost has the ball",
167
+ "verdicts": [
168
+ "lgtm-with-changes"
169
+ ]
170
+ },
171
+ {
172
+ "url": "https://media.giphy.com/media/YT95XJOLvY1t2SJgpR/giphy.gif",
173
+ "alt": "Don't give up, so close",
174
+ "verdicts": [
175
+ "lgtm-with-changes"
176
+ ]
177
+ },
178
+ {
179
+ "url": "https://media.giphy.com/media/NWHBEsCcpuAPiAPMBQ/giphy.gif",
180
+ "alt": "Almost there, on my way",
181
+ "verdicts": [
182
+ "lgtm-with-changes"
183
+ ]
184
+ },
185
+ {
186
+ "url": "https://media.giphy.com/media/0yqoE9MFpFDNjCY9lr/giphy.gif",
187
+ "alt": "Rough draft, still thinking",
188
+ "verdicts": [
189
+ "lgtm-with-changes",
190
+ "needs-work"
191
+ ]
192
+ },
193
+ {
194
+ "url": "https://media.giphy.com/media/yGREGYYecNd3pSrSU6/giphy.gif",
195
+ "alt": "Design thinking iteration loop",
196
+ "verdicts": [
197
+ "needs-work"
198
+ ]
199
+ },
200
+ {
201
+ "url": "https://media.giphy.com/media/hsbwJSHExeC5bVenv7/giphy.gif",
202
+ "alt": "Work mood, back to work",
203
+ "verdicts": [
204
+ "needs-work"
205
+ ]
206
+ },
207
+ {
208
+ "url": "https://media.giphy.com/media/CoND5j6Bn1QZUgm1xX/giphy.gif",
209
+ "alt": "Sorry, out of order",
210
+ "verdicts": [
211
+ "needs-work"
212
+ ]
213
+ },
214
+ {
215
+ "url": "https://media.giphy.com/media/2zdnl4CB3OygOHe1kX/giphy.gif",
216
+ "alt": "Technical difficulties, not working",
217
+ "verdicts": [
218
+ "needs-work"
219
+ ]
220
+ }
221
+ ]
222
+ }
@@ -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
@@ -95,6 +97,11 @@ Rules:
95
97
 
96
98
  Keep the body itself under ~2,500 characters. If it is longer, the asks are
97
99
  buried: cut prose, never cut the numbered asks.
100
+
101
+ **Closer:** when `CLOSER` is set (SKILL.md Step 3), append a blank line and
102
+ `CLOSER.md_github` as the very last lines of the body. It does not count
103
+ toward the ~2,500-character budget and is exempt from the no-praise rule
104
+ below, but the em-dash payload check still applies.
98
105
  - Do NOT prefix the body with a banner like "## Lekker review". Do NOT praise the
99
106
  PR or describe what it does well: no "solid", "handled well", "clean", "nice".
100
107
  State the risk and the asks only.
@@ -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:** <⚡ scan | 🔍 medium | 🔬 deep>
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: <⚡ scan | 🔍 medium | 🔬 deep>
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), verified 2026-07-07 from the claude-api reference; re-check there if models changed. The Claude 5.5 rows and fable-5-1 are not yet on that sheet: they reuse the predecessor-tier rates and must be re-checked:
232
- - claude-fable-5-1: $10 / $50 (unverified, as claude-fable-5)
233
- - claude-opus-5-5: $15 / $75 (unverified; copied from the opus-5 rate in pricing.json, not checked)
234
- - claude-sonnet-5-5: $3 / $15 (unverified)
235
- - claude-haiku-5-5: $1 / $5 (unverified)
236
- - claude-fable-5: $10 / $50
237
- - claude-opus-4-8: $5 / $25
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
@@ -250,6 +251,10 @@ The hard rule against `<N>` placeholders still applies. When usage is exposed,
250
251
  report the real values and use an official or operator-configured pricing source,
251
252
  naming its date in the block.
252
253
 
254
+ ## 🎲 Closer
255
+
256
+ <CLOSER.md_github verbatim. Omit this whole section when CLOSER is empty (--no-fun, or the picker printed nothing).>
257
+
253
258
  ---
254
259
 
255
260
  ### Finding format rules
@@ -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:false entries are not confirmed against a current price sheet (no local claude-api pricing doc found on this machine as of 2026-09-09) and must be checked before this cost block is trusted for a real decision. The claude-*-5-5 and claude-fable-5-1 rows carry the prices of their predecessor tier (fable: $10/$50 as listed in output-format.md; opus/sonnet/haiku: the 5-family rates above) until a price sheet confirms the new models.",
3
- "claude-fable-5-1": { "input": 10, "output": 50, "blended": 30, "verified": false },
4
- "claude-opus-5-5": { "input": 15, "output": 75, "blended": 45, "verified": false },
5
- "claude-sonnet-5-5": { "input": 3, "output": 15, "blended": 9, "verified": false },
6
- "claude-haiku-5-5": { "input": 1, "output": 5, "blended": 3, "verified": false },
7
- "claude-opus-5": { "input": 15, "output": 75, "blended": 45, "verified": false },
8
- "claude-sonnet-5": { "input": 3, "output": 15, "blended": 9, "verified": false },
9
- "claude-haiku-4-5-20251001": { "input": 1, "output": 5, "blended": 3, "verified": false },
10
- "opus": { "input": 15, "output": 75, "blended": 45, "verified": false },
11
- "sonnet": { "input": 3, "output": 15, "blended": 9, "verified": false },
12
- "haiku": { "input": 1, "output": 5, "blended": 3, "verified": false }
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
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // Picks the review's closing xkcd strip or GIF from references/fun.json,
3
+ // matched to the verdict. The pool is curated and URL-verified, so the model
4
+ // never invents a link at review time.
5
+ //
6
+ // node pick-closer.mjs --verdict lgtm|lgtm-with-changes|needs-work
7
+ // node pick-closer.mjs --check
8
+ //
9
+ // Fails soft: a missing or broken pool prints nothing and exits 0, because a
10
+ // joke must never break a review. Only --check exits 1.
11
+ import { readFileSync, realpathSync } from 'node:fs'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { dirname, join } from 'node:path'
14
+
15
+ export const VERDICTS = ['lgtm', 'lgtm-with-changes', 'needs-work']
16
+ const POOL_PATH = process.env.LEKKER_FUN_FILE ||
17
+ join(dirname(dirname(fileURLToPath(import.meta.url))), 'references', 'fun.json')
18
+
19
+ /**
20
+ * Loads and flattens the curated fun pool from fun.json.
21
+ * @returns {Array<object>} xkcd and gif entries tagged with a `kind`.
22
+ */
23
+ export function loadPool() {
24
+ const pool = JSON.parse(readFileSync(POOL_PATH, 'utf8'))
25
+ return [
26
+ ...(pool.xkcd || []).map(x => ({ kind: 'xkcd', ...x })),
27
+ ...(pool.gifs || []).map(g => ({ kind: 'gif', ...g })),
28
+ ]
29
+ }
30
+
31
+ /**
32
+ * Builds terminal and GitHub markdown renderings for a pool item.
33
+ * @param {object} item - A pool entry (xkcd or gif), as returned by loadPool.
34
+ * @returns {object} The item plus `md_terminal` and `md_github` strings.
35
+ */
36
+ export function render(item) {
37
+ if (item.kind === 'xkcd') {
38
+ const label = `xkcd #${item.num} "${item.title}"`
39
+ return {
40
+ ...item,
41
+ md_terminal: `🎲 ${label} - ${item.url}`,
42
+ md_github: `![${label}](${item.img})\n[${label}](${item.url})`,
43
+ }
44
+ }
45
+ return {
46
+ ...item,
47
+ md_terminal: `🎬 ${item.alt} - ${item.url}`,
48
+ md_github: `![${item.alt}](${item.url})`,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Validates fun.json: required fields, known verdicts, coverage of every
54
+ * verdict, and no em dashes. Prints a summary and exits 1 on problems.
55
+ * @returns {void}
56
+ */
57
+ function check() {
58
+ const items = loadPool()
59
+ const problems = []
60
+ for (const it of items) {
61
+ if (!it.url || !Array.isArray(it.verdicts) || it.verdicts.length === 0) problems.push(`incomplete: ${JSON.stringify(it)}`)
62
+ if (it.kind === 'xkcd' && (!it.img || !it.title || !it.num)) problems.push(`xkcd missing img/title/num: ${it.url}`)
63
+ if (it.kind === 'gif' && !it.alt) problems.push(`gif missing alt: ${it.url}`)
64
+ // title and alt become markdown link labels; these characters break them
65
+ if (/[[\]\n]/.test(it.title || it.alt || '')) problems.push(`"[", "]" or newline in title/alt: ${it.url}`)
66
+ for (const v of it.verdicts || []) if (!VERDICTS.includes(v)) problems.push(`unknown verdict "${v}": ${it.url}`)
67
+ }
68
+ for (const v of VERDICTS) if (!items.some(i => i.verdicts?.includes(v))) problems.push(`no entry for verdict ${v}`)
69
+ if (JSON.stringify(items).includes('\u2014')) problems.push('em dash in pool')
70
+ if (problems.length) { console.error(problems.join('\n')); process.exit(1) }
71
+ console.log(`fun.json ok: ${items.length} entries, ${VERDICTS.map(v => `${v}=${items.filter(i => i.verdicts.includes(v)).length}`).join(' ')}`)
72
+ }
73
+
74
+ /**
75
+ * Picks a random pool item matching the verdict and prints it as JSON.
76
+ * Fails soft: any error or empty match is silently swallowed.
77
+ * @param {string} verdict - One of VERDICTS.
78
+ * @returns {void}
79
+ */
80
+ function pick(verdict) {
81
+ try {
82
+ const matches = loadPool().filter(i => i.verdicts?.includes(verdict))
83
+ if (matches.length === 0) return
84
+ console.log(JSON.stringify(render(matches[Math.floor(Math.random() * matches.length)])))
85
+ } catch { /* fail soft */ }
86
+ }
87
+
88
+ function isMain() {
89
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
90
+ }
91
+
92
+ if (isMain()) {
93
+ const args = process.argv.slice(2)
94
+ if (args.includes('--check')) check()
95
+ else {
96
+ const i = args.indexOf('--verdict')
97
+ if (i !== -1) pick(args[i + 1])
98
+ }
99
+ }