@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.
Files changed (26) hide show
  1. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +24 -0
  2. package/adapters/claude/olko-github-pr/skills/lekker-review/references/artifact-page.md +3 -0
  3. package/adapters/claude/olko-github-pr/skills/lekker-review/references/fun.json +222 -0
  4. package/adapters/claude/olko-github-pr/skills/lekker-review/references/github-post.md +5 -0
  5. package/adapters/claude/olko-github-pr/skills/lekker-review/references/output-format.md +4 -0
  6. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/pick-closer.mjs +99 -0
  7. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +60 -0
  8. package/package.json +1 -1
  9. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  10. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  11. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  12. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  13. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  14. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +24 -0
  15. package/plugins/olko-github-pr/skills/lekker-review/references/artifact-page.md +3 -0
  16. package/plugins/olko-github-pr/skills/lekker-review/references/fun.json +222 -0
  17. package/plugins/olko-github-pr/skills/lekker-review/references/github-post.md +5 -0
  18. package/plugins/olko-github-pr/skills/lekker-review/references/output-format.md +4 -0
  19. package/plugins/olko-github-pr/skills/lekker-review/scripts/pick-closer.mjs +99 -0
  20. package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +60 -0
  21. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  22. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  23. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  24. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  25. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  26. 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: `![${label}](${item.img})\n[${label}](${item.url})`,
43
+ }
44
+ }
45
+ return {
46
+ ...item,
47
+ md_terminal: `🎬 ${item.alt} - ${item.url}`,
48
+ md_github: `![${item.alt}](${item.url})`,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Validates fun.json: required fields, known verdicts, coverage of every
54
+ * verdict, and no em dashes. Prints a summary and exits 1 on problems.
55
+ * @returns {void}
56
+ */
57
+ function check() {
58
+ const items = loadPool()
59
+ const problems = []
60
+ for (const it of items) {
61
+ if (!it.url || !Array.isArray(it.verdicts) || it.verdicts.length === 0) problems.push(`incomplete: ${JSON.stringify(it)}`)
62
+ if (it.kind === 'xkcd' && (!it.img || !it.title || !it.num)) problems.push(`xkcd missing img/title/num: ${it.url}`)
63
+ if (it.kind === 'gif' && !it.alt) problems.push(`gif missing alt: ${it.url}`)
64
+ // title and alt become markdown link labels; these characters break them
65
+ if (/[[\]\n]/.test(it.title || it.alt || '')) problems.push(`"[", "]" or newline in title/alt: ${it.url}`)
66
+ for (const v of it.verdicts || []) if (!VERDICTS.includes(v)) problems.push(`unknown verdict "${v}": ${it.url}`)
67
+ }
68
+ for (const v of VERDICTS) if (!items.some(i => i.verdicts?.includes(v))) problems.push(`no entry for verdict ${v}`)
69
+ if (JSON.stringify(items).includes('\u2014')) problems.push('em dash in pool')
70
+ if (problems.length) { console.error(problems.join('\n')); process.exit(1) }
71
+ console.log(`fun.json ok: ${items.length} entries, ${VERDICTS.map(v => `${v}=${items.filter(i => i.verdicts.includes(v)).length}`).join(' ')}`)
72
+ }
73
+
74
+ /**
75
+ * Picks a random pool item matching the verdict and prints it as JSON.
76
+ * Fails soft: any error or empty match is silently swallowed.
77
+ * @param {string} verdict - One of VERDICTS.
78
+ * @returns {void}
79
+ */
80
+ function pick(verdict) {
81
+ try {
82
+ const matches = loadPool().filter(i => i.verdicts?.includes(verdict))
83
+ if (matches.length === 0) return
84
+ console.log(JSON.stringify(render(matches[Math.floor(Math.random() * matches.length)])))
85
+ } catch { /* fail soft */ }
86
+ }
87
+
88
+ function isMain() {
89
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
90
+ }
91
+
92
+ if (isMain()) {
93
+ const args = process.argv.slice(2)
94
+ if (args.includes('--check')) check()
95
+ else {
96
+ const i = args.indexOf('--verdict')
97
+ if (i !== -1) pick(args[i + 1])
98
+ }
99
+ }
@@ -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, '![xkcd #1 "T"](https://imgs.xkcd.com/1.png)\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, '![Nod](https://media.giphy.com/media/x/giphy.gif)')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olegkoval/agent-skills",
3
- "version": "1.51.3",
3
+ "version": "1.52.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-apple-kit",
3
3
  "description": "Build and ship Apple platform apps: macOS menubar apps, App Store submissions.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-creative",
3
3
  "description": "Creative and personal projects: photo galleries, music players, listings, wiki editing.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-garmin-kit",
3
3
  "description": "Build, test and publish Garmin Connect IQ watch faces.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-git-tools",
3
3
  "description": "Everyday git and GitHub CLI operations: conventional commits, branch hygiene.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-github-pr",
3
3
  "description": "Drive GitHub pull requests to merge-ready: review-bot loops, CI fixes, descriptions, dependency triage.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -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: `![${label}](${item.img})\n[${label}](${item.url})`,
43
+ }
44
+ }
45
+ return {
46
+ ...item,
47
+ md_terminal: `🎬 ${item.alt} - ${item.url}`,
48
+ md_github: `![${item.alt}](${item.url})`,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Validates fun.json: required fields, known verdicts, coverage of every
54
+ * verdict, and no em dashes. Prints a summary and exits 1 on problems.
55
+ * @returns {void}
56
+ */
57
+ function check() {
58
+ const items = loadPool()
59
+ const problems = []
60
+ for (const it of items) {
61
+ if (!it.url || !Array.isArray(it.verdicts) || it.verdicts.length === 0) problems.push(`incomplete: ${JSON.stringify(it)}`)
62
+ if (it.kind === 'xkcd' && (!it.img || !it.title || !it.num)) problems.push(`xkcd missing img/title/num: ${it.url}`)
63
+ if (it.kind === 'gif' && !it.alt) problems.push(`gif missing alt: ${it.url}`)
64
+ // title and alt become markdown link labels; these characters break them
65
+ if (/[[\]\n]/.test(it.title || it.alt || '')) problems.push(`"[", "]" or newline in title/alt: ${it.url}`)
66
+ for (const v of it.verdicts || []) if (!VERDICTS.includes(v)) problems.push(`unknown verdict "${v}": ${it.url}`)
67
+ }
68
+ for (const v of VERDICTS) if (!items.some(i => i.verdicts?.includes(v))) problems.push(`no entry for verdict ${v}`)
69
+ if (JSON.stringify(items).includes('\u2014')) problems.push('em dash in pool')
70
+ if (problems.length) { console.error(problems.join('\n')); process.exit(1) }
71
+ console.log(`fun.json ok: ${items.length} entries, ${VERDICTS.map(v => `${v}=${items.filter(i => i.verdicts.includes(v)).length}`).join(' ')}`)
72
+ }
73
+
74
+ /**
75
+ * Picks a random pool item matching the verdict and prints it as JSON.
76
+ * Fails soft: any error or empty match is silently swallowed.
77
+ * @param {string} verdict - One of VERDICTS.
78
+ * @returns {void}
79
+ */
80
+ function pick(verdict) {
81
+ try {
82
+ const matches = loadPool().filter(i => i.verdicts?.includes(verdict))
83
+ if (matches.length === 0) return
84
+ console.log(JSON.stringify(render(matches[Math.floor(Math.random() * matches.length)])))
85
+ } catch { /* fail soft */ }
86
+ }
87
+
88
+ function isMain() {
89
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
90
+ }
91
+
92
+ if (isMain()) {
93
+ const args = process.argv.slice(2)
94
+ if (args.includes('--check')) check()
95
+ else {
96
+ const i = args.indexOf('--verdict')
97
+ if (i !== -1) pick(args[i + 1])
98
+ }
99
+ }
@@ -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, '![xkcd #1 "T"](https://imgs.xkcd.com/1.png)\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, '![Nod](https://media.giphy.com/media/x/giphy.gif)')
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)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-obsidian",
3
3
  "description": "Keep an Obsidian vault in sync with work: PR sync, task rollover, morning routine.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-product",
3
3
  "description": "Take a product idea to a shippable build: MVP passes, full-stack scaffolds, UX/UI quality loops, launch plans.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-reflection",
3
3
  "description": "Look back and improve: self-critique, retrospectives, performance review, rapid learning.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-release",
3
3
  "description": "Ship a release: semantic-release setup, changelogs, store listing copy, release-day routine.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-skill-meta",
3
3
  "description": "Author and maintain agent skills and the AI toolchain itself.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-web-ops",
3
3
  "description": "Operate a website: WAF rules, search console audits, analytics bootstrap, docs indexes.",
4
- "version": "1.51.3",
4
+ "version": "1.52.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },