@olegkoval/agent-skills 1.51.2 → 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 +35 -3
- 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 +35 -3
- 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
|
|
@@ -495,10 +498,16 @@ an independent adversarial verifier.
|
|
|
495
498
|
|
|
496
499
|
### ENGINE=workflow (Claude Code default)
|
|
497
500
|
|
|
501
|
+
First copy the script into the scratchpad. The Workflow tool rejects a
|
|
502
|
+
`scriptPath` outside the working directory or added dirs ("scriptPath must be a
|
|
503
|
+
script path this tool returned, or a file you can already read"), and the skill
|
|
504
|
+
dir is usually neither: `cp ${SKILL_ROOT}/workflow.js <scratchpad>/workflow.js`.
|
|
505
|
+
If it still rejects the path, pass the script inline via `script` instead.
|
|
506
|
+
|
|
498
507
|
Invoke the Workflow tool:
|
|
499
508
|
|
|
500
509
|
```
|
|
501
|
-
scriptPath:
|
|
510
|
+
scriptPath: <scratchpad>/workflow.js
|
|
502
511
|
args: {
|
|
503
512
|
repoSlug,
|
|
504
513
|
prNumber, // null in MODE=branch
|
|
@@ -719,6 +728,20 @@ codebase. Never on taste alone. These land in their own section, not in
|
|
|
719
728
|
Critical/Important. A finding without a cited precedent from the codebase is
|
|
720
729
|
dropped.
|
|
721
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
|
+
|
|
722
745
|
**Format** the review per `references/output-format.md` (read it now). Key
|
|
723
746
|
requirements:
|
|
724
747
|
|
|
@@ -793,6 +816,12 @@ that is about to be pushed, and a marker for a superseded sha would be a lie.
|
|
|
793
816
|
|
|
794
817
|
Then print the full review as the response.
|
|
795
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
|
+
|
|
796
825
|
---
|
|
797
826
|
|
|
798
827
|
## Step 3.5 - Living review artifact (skip when ARTIFACT=false)
|
|
@@ -845,9 +874,11 @@ Read `references/fix-mode.md` and follow it. Shape of the run:
|
|
|
845
874
|
stop - do not land onto a tip you did not review.
|
|
846
875
|
2. Select eligible findings (Critical/Important with a `fix`, real file,
|
|
847
876
|
non-generated). Never auto-fix Observation, Idiomatic, or a title/process rule.
|
|
848
|
-
3. Invoke the host's fix workflow. In Claude,
|
|
877
|
+
3. Invoke the host's fix workflow. In Claude, copy `fix-workflow.js` to the
|
|
878
|
+
scratchpad first (same reason as the review workflow; inline `script` if the
|
|
879
|
+
path is still rejected), then run:
|
|
849
880
|
```
|
|
850
|
-
scriptPath:
|
|
881
|
+
scriptPath: <scratchpad>/fix-workflow.js
|
|
851
882
|
args: { repoSlug, prNumber, targetLabel, worktreePath, diffFile, contextFile,
|
|
852
883
|
promptDir, findings: [<selected findings verbatim>], acList }
|
|
853
884
|
```
|
|
@@ -969,6 +1000,7 @@ Follow `references/github-post.md`:
|
|
|
969
1000
|
- Verify post-condition: fetch review list, confirm PENDING state exists.
|
|
970
1001
|
- Print count + link.
|
|
971
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.
|
|
972
1004
|
- Never submit the review programmatically.
|
|
973
1005
|
- When fix mode applied and committed a finding, EXCLUDE it from the inline
|
|
974
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
|
|
@@ -494,10 +497,16 @@ an independent adversarial verifier.
|
|
|
494
497
|
|
|
495
498
|
### ENGINE=workflow (Claude Code default)
|
|
496
499
|
|
|
500
|
+
First copy the script into the scratchpad. The Workflow tool rejects a
|
|
501
|
+
`scriptPath` outside the working directory or added dirs ("scriptPath must be a
|
|
502
|
+
script path this tool returned, or a file you can already read"), and the skill
|
|
503
|
+
dir is usually neither: `cp ${SKILL_ROOT}/workflow.js <scratchpad>/workflow.js`.
|
|
504
|
+
If it still rejects the path, pass the script inline via `script` instead.
|
|
505
|
+
|
|
497
506
|
Invoke the Workflow tool:
|
|
498
507
|
|
|
499
508
|
```
|
|
500
|
-
scriptPath:
|
|
509
|
+
scriptPath: <scratchpad>/workflow.js
|
|
501
510
|
args: {
|
|
502
511
|
repoSlug,
|
|
503
512
|
prNumber, // null in MODE=branch
|
|
@@ -718,6 +727,20 @@ codebase. Never on taste alone. These land in their own section, not in
|
|
|
718
727
|
Critical/Important. A finding without a cited precedent from the codebase is
|
|
719
728
|
dropped.
|
|
720
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
|
+
|
|
721
744
|
**Format** the review per `references/output-format.md` (read it now). Key
|
|
722
745
|
requirements:
|
|
723
746
|
|
|
@@ -792,6 +815,12 @@ that is about to be pushed, and a marker for a superseded sha would be a lie.
|
|
|
792
815
|
|
|
793
816
|
Then print the full review as the response.
|
|
794
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
|
+
|
|
795
824
|
---
|
|
796
825
|
|
|
797
826
|
## Step 3.5 - Living review artifact (skip when ARTIFACT=false)
|
|
@@ -844,9 +873,11 @@ Read `references/fix-mode.md` and follow it. Shape of the run:
|
|
|
844
873
|
stop - do not land onto a tip you did not review.
|
|
845
874
|
2. Select eligible findings (Critical/Important with a `fix`, real file,
|
|
846
875
|
non-generated). Never auto-fix Observation, Idiomatic, or a title/process rule.
|
|
847
|
-
3. Invoke the host's fix workflow. In Claude,
|
|
876
|
+
3. Invoke the host's fix workflow. In Claude, copy `fix-workflow.js` to the
|
|
877
|
+
scratchpad first (same reason as the review workflow; inline `script` if the
|
|
878
|
+
path is still rejected), then run:
|
|
848
879
|
```
|
|
849
|
-
scriptPath:
|
|
880
|
+
scriptPath: <scratchpad>/fix-workflow.js
|
|
850
881
|
args: { repoSlug, prNumber, targetLabel, worktreePath, diffFile, contextFile,
|
|
851
882
|
promptDir, findings: [<selected findings verbatim>], acList }
|
|
852
883
|
```
|
|
@@ -968,6 +999,7 @@ Follow `references/github-post.md`:
|
|
|
968
999
|
- Verify post-condition: fetch review list, confirm PENDING state exists.
|
|
969
1000
|
- Print count + link.
|
|
970
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.
|
|
971
1003
|
- Never submit the review programmatically.
|
|
972
1004
|
- When fix mode applied and committed a finding, EXCLUDE it from the inline
|
|
973
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)
|