@olegkoval/agent-skills 1.51.3 → 1.52.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 +24 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/artifact-page.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/fun.json +222 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/github-post.md +5 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/output-format.md +4 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/pick-closer.mjs +99 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +60 -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 +24 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/artifact-page.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/fun.json +222 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/github-post.md +5 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/output-format.md +4 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/pick-closer.mjs +99 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +60 -0
- 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
|
@@ -280,6 +280,9 @@ in Step 4. Use it for a read-only pre-push look.
|
|
|
280
280
|
**`--no-artifact` flag:** parse and store as `ARTIFACT=false` (default true).
|
|
281
281
|
Skips Step 3.5 (living review artifact) silently.
|
|
282
282
|
|
|
283
|
+
**`--no-fun` flag:** parse and store as `FUN=false` (default true). Skips the
|
|
284
|
+
closer (Step 3) everywhere: terminal, saved file, artifact, GitHub body.
|
|
285
|
+
|
|
283
286
|
**`--engine revmux|workflow|codex` flag:** parse and store as `ENGINE`. Default
|
|
284
287
|
to `workflow` for `HOST=claude` and `codex` for `HOST=codex`. Reject an explicit
|
|
285
288
|
engine that does not exist on the current host (`workflow` requires Claude's
|
|
@@ -725,6 +728,20 @@ codebase. Never on taste alone. These land in their own section, not in
|
|
|
725
728
|
Critical/Important. A finding without a cited precedent from the codebase is
|
|
726
729
|
dropped.
|
|
727
730
|
|
|
731
|
+
**Pick the closer** (skip when `FUN=false`): once the verdict is decided, run
|
|
732
|
+
the picker ONCE and keep its JSON as `CLOSER` for the rest of the run, so the
|
|
733
|
+
terminal, saved file, artifact and GitHub body all show the same joke:
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
# verdict key: ✅ -> lgtm, ⚠️ -> lgtm-with-changes, 🚫 -> needs-work
|
|
737
|
+
node ${SKILL_ROOT}/scripts/pick-closer.mjs --verdict <key>
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Empty output means no closer (pool missing or broken): leave `CLOSER` empty and
|
|
741
|
+
carry on. Never invent an xkcd number or GIF URL in its place; the pool in
|
|
742
|
+
`references/fun.json` is URL-verified, a guessed link is not. Unattended runs
|
|
743
|
+
include it too.
|
|
744
|
+
|
|
728
745
|
**Format** the review per `references/output-format.md` (read it now). Key
|
|
729
746
|
requirements:
|
|
730
747
|
|
|
@@ -799,6 +816,12 @@ that is about to be pushed, and a marker for a superseded sha would be a lie.
|
|
|
799
816
|
|
|
800
817
|
Then print the full review as the response.
|
|
801
818
|
|
|
819
|
+
**Closer goes last.** The run's FINAL message (this one, or the later summary
|
|
820
|
+
after Step 4 / 4.6 / 5 when those run) ends with `CLOSER.md_terminal` as its
|
|
821
|
+
last line. Print it once per run, on whichever message is last, not on every
|
|
822
|
+
step. Pass `CLOSER` to the Step 3.5 artifact agent and to the Step 5 GitHub
|
|
823
|
+
post.
|
|
824
|
+
|
|
802
825
|
---
|
|
803
826
|
|
|
804
827
|
## Step 3.5 - Living review artifact (skip when ARTIFACT=false)
|
|
@@ -977,6 +1000,7 @@ Follow `references/github-post.md`:
|
|
|
977
1000
|
- Verify post-condition: fetch review list, confirm PENDING state exists.
|
|
978
1001
|
- Print count + link.
|
|
979
1002
|
- Observation and Idiomatic findings go in the review body, never inline.
|
|
1003
|
+
- When `CLOSER` is set, its `md_github` is the last thing in the review body.
|
|
980
1004
|
- Never submit the review programmatically.
|
|
981
1005
|
- When fix mode applied and committed a finding, EXCLUDE it from the inline
|
|
982
1006
|
comments - do not ask for a change you already made. Mention the applied
|
|
@@ -156,6 +156,9 @@ Hard requirements for the HTML page:
|
|
|
156
156
|
- **Test Quality + Review Cost sections**, mirrored from the review file,
|
|
157
157
|
kept concise (verdict + gaps + cost table; no need to reproduce every
|
|
158
158
|
sentence).
|
|
159
|
+
- **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`. Link only, no `<img>`:
|
|
161
|
+
the CSP blocks external images. Skip the card when `CLOSER` is empty.
|
|
159
162
|
- **Layout:** every code block (`badCode`, `fix`, `redOutput`, `testCode`)
|
|
160
163
|
horizontally scrollable within its own container; the page body itself must
|
|
161
164
|
never scroll horizontally. Keep total page weight sane -- plain `<pre>`
|
|
@@ -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
|
+
}
|
|
@@ -95,6 +95,11 @@ Rules:
|
|
|
95
95
|
|
|
96
96
|
Keep the body itself under ~2,500 characters. If it is longer, the asks are
|
|
97
97
|
buried: cut prose, never cut the numbered asks.
|
|
98
|
+
|
|
99
|
+
**Closer:** when `CLOSER` is set (SKILL.md Step 3), append a blank line and
|
|
100
|
+
`CLOSER.md_github` as the very last lines of the body. It does not count
|
|
101
|
+
toward the ~2,500-character budget and is exempt from the no-praise rule
|
|
102
|
+
below, but the em-dash payload check still applies.
|
|
98
103
|
- Do NOT prefix the body with a banner like "## Lekker review". Do NOT praise the
|
|
99
104
|
PR or describe what it does well: no "solid", "handled well", "clean", "nice".
|
|
100
105
|
State the risk and the asks only.
|
|
@@ -250,6 +250,10 @@ The hard rule against `<N>` placeholders still applies. When usage is exposed,
|
|
|
250
250
|
report the real values and use an official or operator-configured pricing source,
|
|
251
251
|
naming its date in the block.
|
|
252
252
|
|
|
253
|
+
## 🎲 Closer
|
|
254
|
+
|
|
255
|
+
<CLOSER.md_github verbatim. Omit this whole section when CLOSER is empty (--no-fun, or the picker printed nothing).>
|
|
256
|
+
|
|
253
257
|
---
|
|
254
258
|
|
|
255
259
|
### Finding format rules
|
|
@@ -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: `\n[${label}](${item.url})`,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...item,
|
|
47
|
+
md_terminal: `🎬 ${item.alt} - ${item.url}`,
|
|
48
|
+
md_github: ``,
|
|
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
|
+
}
|
|
@@ -259,5 +259,65 @@ console.log('\nrevmux adapter: fixture mapping')
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
console.log('\ncloser picker: pool, exact rendering, fail-soft and --check')
|
|
263
|
+
{
|
|
264
|
+
const { spawnSync } = await import('node:child_process')
|
|
265
|
+
const { writeFileSync, mkdtempSync } = await import('node:fs')
|
|
266
|
+
const { tmpdir } = await import('node:os')
|
|
267
|
+
const pickerPath = join(SKILL, 'scripts', 'pick-closer.mjs')
|
|
268
|
+
const { VERDICTS, render, loadPool } = await import(pickerPath)
|
|
269
|
+
const run = (args, pool) => spawnSync('node', [pickerPath, ...args], {
|
|
270
|
+
encoding: 'utf8', env: { ...process.env, ...(pool ? { LEKKER_FUN_FILE: pool } : {}) },
|
|
271
|
+
})
|
|
272
|
+
const tmp = mkdtempSync(join(tmpdir(), 'lekker-fun-'))
|
|
273
|
+
const writePool = (name, body) => {
|
|
274
|
+
const p = join(tmp, name)
|
|
275
|
+
writeFileSync(p, typeof body === 'string' ? body : JSON.stringify(body))
|
|
276
|
+
return p
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
check('fun.json passes --check', run(['--check']).status, 0)
|
|
280
|
+
for (const item of loadPool()) {
|
|
281
|
+
const r = render(item)
|
|
282
|
+
check(`renders ${item.url}`, r.md_github.includes(item.kind === 'xkcd' ? item.img : item.url) && r.md_terminal.endsWith(item.url), true)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const xkcd = { num: 1, title: 'T', url: 'https://xkcd.com/1/', img: 'https://imgs.xkcd.com/1.png', verdicts: ['lgtm'] }
|
|
286
|
+
const gif = { url: 'https://media.giphy.com/media/x/giphy.gif', alt: 'Nod', verdicts: ['lgtm-with-changes', 'needs-work'] }
|
|
287
|
+
check('xkcd md_terminal', render({ kind: 'xkcd', ...xkcd }).md_terminal, '🎲 xkcd #1 "T" - https://xkcd.com/1/')
|
|
288
|
+
check('xkcd md_github', render({ kind: 'xkcd', ...xkcd }).md_github, '\n[xkcd #1 "T"](https://xkcd.com/1/)')
|
|
289
|
+
check('gif md_terminal', render({ kind: 'gif', ...gif }).md_terminal, '🎬 Nod - https://media.giphy.com/media/x/giphy.gif')
|
|
290
|
+
check('gif md_github', render({ kind: 'gif', ...gif }).md_github, '')
|
|
291
|
+
|
|
292
|
+
// One entry per verdict makes the random pick deterministic.
|
|
293
|
+
const fixture = writePool('fixture.json', { xkcd: [xkcd], gifs: [gif] })
|
|
294
|
+
for (const verdict of VERDICTS) {
|
|
295
|
+
const out = run(['--verdict', verdict], fixture)
|
|
296
|
+
const expected = render(verdict === 'lgtm' ? { kind: 'xkcd', ...xkcd } : { kind: 'gif', ...gif })
|
|
297
|
+
check(`${verdict}: picks the matching fixture entry`, out.stdout ? JSON.parse(out.stdout).md_github : null, expected.md_github)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const [label, pool] of [['missing pool', join(tmp, 'missing.json')], ['malformed pool', writePool('bad.json', '{not json')]]) {
|
|
301
|
+
const out = run(['--verdict', 'lgtm'], pool)
|
|
302
|
+
check(`${label} fails soft (exit 0, no output)`, [out.status, out.stdout], [0, ''])
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const [label, pool] of [
|
|
306
|
+
['unknown verdict', { xkcd: [xkcd], gifs: [{ ...gif, verdicts: ['lgtm-with-changes', 'needs-work', 'meh'] }] }],
|
|
307
|
+
['xkcd without img', { xkcd: [{ ...xkcd, img: '' }], gifs: [gif] }],
|
|
308
|
+
['bracket in title', { xkcd: [{ ...xkcd, title: 'a]b' }], gifs: [gif] }],
|
|
309
|
+
['uncovered verdict', { xkcd: [xkcd], gifs: [{ ...gif, verdicts: ['needs-work'] }] }],
|
|
310
|
+
['em dash in alt', { xkcd: [xkcd], gifs: [{ ...gif, alt: 'a \u2014 b' }] }],
|
|
311
|
+
]) {
|
|
312
|
+
check(`--check rejects ${label}`, run(['--check'], writePool(`${label.replace(/ /g, '-')}.json`, pool)).status, 1)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const read = f => readFileSync(join(SKILL, f), 'utf8')
|
|
316
|
+
check('SKILL.md documents --no-fun', read('SKILL.md').includes('--no-fun') && read('SKILL.md').includes('FUN=false'), true)
|
|
317
|
+
const fmt = read('references/output-format.md')
|
|
318
|
+
check('Closer section follows the Review Cost pricing notes', fmt.indexOf('## 🎲 Closer') > fmt.indexOf('Per-MTok pricing'), true)
|
|
319
|
+
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
|
+
|
|
262
322
|
console.log(failed === 0 ? '\nall checks passed\n' : `\n${failed} check(s) FAILED\n`)
|
|
263
323
|
process.exit(failed === 0 ? 0 : 1)
|
package/package.json
CHANGED
|
@@ -279,6 +279,9 @@ in Step 4. Use it for a read-only pre-push look.
|
|
|
279
279
|
**`--no-artifact` flag:** parse and store as `ARTIFACT=false` (default true).
|
|
280
280
|
Skips Step 3.5 (living review artifact) silently.
|
|
281
281
|
|
|
282
|
+
**`--no-fun` flag:** parse and store as `FUN=false` (default true). Skips the
|
|
283
|
+
closer (Step 3) everywhere: terminal, saved file, artifact, GitHub body.
|
|
284
|
+
|
|
282
285
|
**`--engine revmux|workflow|codex` flag:** parse and store as `ENGINE`. Default
|
|
283
286
|
to `workflow` for `HOST=claude` and `codex` for `HOST=codex`. Reject an explicit
|
|
284
287
|
engine that does not exist on the current host (`workflow` requires Claude's
|
|
@@ -724,6 +727,20 @@ codebase. Never on taste alone. These land in their own section, not in
|
|
|
724
727
|
Critical/Important. A finding without a cited precedent from the codebase is
|
|
725
728
|
dropped.
|
|
726
729
|
|
|
730
|
+
**Pick the closer** (skip when `FUN=false`): once the verdict is decided, run
|
|
731
|
+
the picker ONCE and keep its JSON as `CLOSER` for the rest of the run, so the
|
|
732
|
+
terminal, saved file, artifact and GitHub body all show the same joke:
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
# verdict key: ✅ -> lgtm, ⚠️ -> lgtm-with-changes, 🚫 -> needs-work
|
|
736
|
+
node ${SKILL_ROOT}/scripts/pick-closer.mjs --verdict <key>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Empty output means no closer (pool missing or broken): leave `CLOSER` empty and
|
|
740
|
+
carry on. Never invent an xkcd number or GIF URL in its place; the pool in
|
|
741
|
+
`references/fun.json` is URL-verified, a guessed link is not. Unattended runs
|
|
742
|
+
include it too.
|
|
743
|
+
|
|
727
744
|
**Format** the review per `references/output-format.md` (read it now). Key
|
|
728
745
|
requirements:
|
|
729
746
|
|
|
@@ -798,6 +815,12 @@ that is about to be pushed, and a marker for a superseded sha would be a lie.
|
|
|
798
815
|
|
|
799
816
|
Then print the full review as the response.
|
|
800
817
|
|
|
818
|
+
**Closer goes last.** The run's FINAL message (this one, or the later summary
|
|
819
|
+
after Step 4 / 4.6 / 5 when those run) ends with `CLOSER.md_terminal` as its
|
|
820
|
+
last line. Print it once per run, on whichever message is last, not on every
|
|
821
|
+
step. Pass `CLOSER` to the Step 3.5 artifact agent and to the Step 5 GitHub
|
|
822
|
+
post.
|
|
823
|
+
|
|
801
824
|
---
|
|
802
825
|
|
|
803
826
|
## Step 3.5 - Living review artifact (skip when ARTIFACT=false)
|
|
@@ -976,6 +999,7 @@ Follow `references/github-post.md`:
|
|
|
976
999
|
- Verify post-condition: fetch review list, confirm PENDING state exists.
|
|
977
1000
|
- Print count + link.
|
|
978
1001
|
- Observation and Idiomatic findings go in the review body, never inline.
|
|
1002
|
+
- When `CLOSER` is set, its `md_github` is the last thing in the review body.
|
|
979
1003
|
- Never submit the review programmatically.
|
|
980
1004
|
- When fix mode applied and committed a finding, EXCLUDE it from the inline
|
|
981
1005
|
comments - do not ask for a change you already made. Mention the applied
|
|
@@ -156,6 +156,9 @@ Hard requirements for the HTML page:
|
|
|
156
156
|
- **Test Quality + Review Cost sections**, mirrored from the review file,
|
|
157
157
|
kept concise (verdict + gaps + cost table; no need to reproduce every
|
|
158
158
|
sentence).
|
|
159
|
+
- **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`. Link only, no `<img>`:
|
|
161
|
+
the CSP blocks external images. Skip the card when `CLOSER` is empty.
|
|
159
162
|
- **Layout:** every code block (`badCode`, `fix`, `redOutput`, `testCode`)
|
|
160
163
|
horizontally scrollable within its own container; the page body itself must
|
|
161
164
|
never scroll horizontally. Keep total page weight sane -- plain `<pre>`
|
|
@@ -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
|
+
}
|
|
@@ -95,6 +95,11 @@ Rules:
|
|
|
95
95
|
|
|
96
96
|
Keep the body itself under ~2,500 characters. If it is longer, the asks are
|
|
97
97
|
buried: cut prose, never cut the numbered asks.
|
|
98
|
+
|
|
99
|
+
**Closer:** when `CLOSER` is set (SKILL.md Step 3), append a blank line and
|
|
100
|
+
`CLOSER.md_github` as the very last lines of the body. It does not count
|
|
101
|
+
toward the ~2,500-character budget and is exempt from the no-praise rule
|
|
102
|
+
below, but the em-dash payload check still applies.
|
|
98
103
|
- Do NOT prefix the body with a banner like "## Lekker review". Do NOT praise the
|
|
99
104
|
PR or describe what it does well: no "solid", "handled well", "clean", "nice".
|
|
100
105
|
State the risk and the asks only.
|
|
@@ -250,6 +250,10 @@ The hard rule against `<N>` placeholders still applies. When usage is exposed,
|
|
|
250
250
|
report the real values and use an official or operator-configured pricing source,
|
|
251
251
|
naming its date in the block.
|
|
252
252
|
|
|
253
|
+
## 🎲 Closer
|
|
254
|
+
|
|
255
|
+
<CLOSER.md_github verbatim. Omit this whole section when CLOSER is empty (--no-fun, or the picker printed nothing).>
|
|
256
|
+
|
|
253
257
|
---
|
|
254
258
|
|
|
255
259
|
### Finding format rules
|
|
@@ -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: `\n[${label}](${item.url})`,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...item,
|
|
47
|
+
md_terminal: `🎬 ${item.alt} - ${item.url}`,
|
|
48
|
+
md_github: ``,
|
|
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
|
+
}
|
|
@@ -259,5 +259,65 @@ console.log('\nrevmux adapter: fixture mapping')
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
console.log('\ncloser picker: pool, exact rendering, fail-soft and --check')
|
|
263
|
+
{
|
|
264
|
+
const { spawnSync } = await import('node:child_process')
|
|
265
|
+
const { writeFileSync, mkdtempSync } = await import('node:fs')
|
|
266
|
+
const { tmpdir } = await import('node:os')
|
|
267
|
+
const pickerPath = join(SKILL, 'scripts', 'pick-closer.mjs')
|
|
268
|
+
const { VERDICTS, render, loadPool } = await import(pickerPath)
|
|
269
|
+
const run = (args, pool) => spawnSync('node', [pickerPath, ...args], {
|
|
270
|
+
encoding: 'utf8', env: { ...process.env, ...(pool ? { LEKKER_FUN_FILE: pool } : {}) },
|
|
271
|
+
})
|
|
272
|
+
const tmp = mkdtempSync(join(tmpdir(), 'lekker-fun-'))
|
|
273
|
+
const writePool = (name, body) => {
|
|
274
|
+
const p = join(tmp, name)
|
|
275
|
+
writeFileSync(p, typeof body === 'string' ? body : JSON.stringify(body))
|
|
276
|
+
return p
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
check('fun.json passes --check', run(['--check']).status, 0)
|
|
280
|
+
for (const item of loadPool()) {
|
|
281
|
+
const r = render(item)
|
|
282
|
+
check(`renders ${item.url}`, r.md_github.includes(item.kind === 'xkcd' ? item.img : item.url) && r.md_terminal.endsWith(item.url), true)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const xkcd = { num: 1, title: 'T', url: 'https://xkcd.com/1/', img: 'https://imgs.xkcd.com/1.png', verdicts: ['lgtm'] }
|
|
286
|
+
const gif = { url: 'https://media.giphy.com/media/x/giphy.gif', alt: 'Nod', verdicts: ['lgtm-with-changes', 'needs-work'] }
|
|
287
|
+
check('xkcd md_terminal', render({ kind: 'xkcd', ...xkcd }).md_terminal, '🎲 xkcd #1 "T" - https://xkcd.com/1/')
|
|
288
|
+
check('xkcd md_github', render({ kind: 'xkcd', ...xkcd }).md_github, '\n[xkcd #1 "T"](https://xkcd.com/1/)')
|
|
289
|
+
check('gif md_terminal', render({ kind: 'gif', ...gif }).md_terminal, '🎬 Nod - https://media.giphy.com/media/x/giphy.gif')
|
|
290
|
+
check('gif md_github', render({ kind: 'gif', ...gif }).md_github, '')
|
|
291
|
+
|
|
292
|
+
// One entry per verdict makes the random pick deterministic.
|
|
293
|
+
const fixture = writePool('fixture.json', { xkcd: [xkcd], gifs: [gif] })
|
|
294
|
+
for (const verdict of VERDICTS) {
|
|
295
|
+
const out = run(['--verdict', verdict], fixture)
|
|
296
|
+
const expected = render(verdict === 'lgtm' ? { kind: 'xkcd', ...xkcd } : { kind: 'gif', ...gif })
|
|
297
|
+
check(`${verdict}: picks the matching fixture entry`, out.stdout ? JSON.parse(out.stdout).md_github : null, expected.md_github)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const [label, pool] of [['missing pool', join(tmp, 'missing.json')], ['malformed pool', writePool('bad.json', '{not json')]]) {
|
|
301
|
+
const out = run(['--verdict', 'lgtm'], pool)
|
|
302
|
+
check(`${label} fails soft (exit 0, no output)`, [out.status, out.stdout], [0, ''])
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const [label, pool] of [
|
|
306
|
+
['unknown verdict', { xkcd: [xkcd], gifs: [{ ...gif, verdicts: ['lgtm-with-changes', 'needs-work', 'meh'] }] }],
|
|
307
|
+
['xkcd without img', { xkcd: [{ ...xkcd, img: '' }], gifs: [gif] }],
|
|
308
|
+
['bracket in title', { xkcd: [{ ...xkcd, title: 'a]b' }], gifs: [gif] }],
|
|
309
|
+
['uncovered verdict', { xkcd: [xkcd], gifs: [{ ...gif, verdicts: ['needs-work'] }] }],
|
|
310
|
+
['em dash in alt', { xkcd: [xkcd], gifs: [{ ...gif, alt: 'a \u2014 b' }] }],
|
|
311
|
+
]) {
|
|
312
|
+
check(`--check rejects ${label}`, run(['--check'], writePool(`${label.replace(/ /g, '-')}.json`, pool)).status, 1)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const read = f => readFileSync(join(SKILL, f), 'utf8')
|
|
316
|
+
check('SKILL.md documents --no-fun', read('SKILL.md').includes('--no-fun') && read('SKILL.md').includes('FUN=false'), true)
|
|
317
|
+
const fmt = read('references/output-format.md')
|
|
318
|
+
check('Closer section follows the Review Cost pricing notes', fmt.indexOf('## 🎲 Closer') > fmt.indexOf('Per-MTok pricing'), true)
|
|
319
|
+
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
|
+
|
|
262
322
|
console.log(failed === 0 ? '\nall checks passed\n' : `\n${failed} check(s) FAILED\n`)
|
|
263
323
|
process.exit(failed === 0 ? 0 : 1)
|