@olegkoval/agent-skills 1.40.1 → 1.41.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 (44) hide show
  1. package/.github/prompts/open-source-publisher.prompt.md +3 -4
  2. package/.kiro/steering/open-source-publisher.md +3 -4
  3. package/.windsurf/rules/open-source-publisher.md +3 -4
  4. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +15 -17
  5. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/implementation.md +2 -1
  6. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/prover.md +13 -10
  7. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/quality.md +1 -1
  8. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/test-quality.md +7 -0
  9. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/triage-quality.md +1 -1
  10. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/verifier.md +12 -9
  11. package/adapters/claude/olko-github-pr/skills/lekker-review/references/artifact-page.md +6 -1
  12. package/adapters/claude/olko-github-pr/skills/lekker-review/references/output-format.md +5 -8
  13. package/adapters/claude/olko-reflection/skills/self-critique/scripts/critique-nudge.mjs +3 -0
  14. package/adapters/claude/olko-release/skills/open-source-publisher/SKILL.md +4 -5
  15. package/adapters/cursor/olko-reflection/skills/self-critique/scripts/critique-nudge.mjs +3 -0
  16. package/adapters/cursor/olko-release/skills/open-source-publisher/SKILL.md +4 -5
  17. package/adapters/grok/olko-reflection/skills/self-critique/scripts/critique-nudge.mjs +3 -0
  18. package/adapters/grok/olko-release/skills/open-source-publisher/SKILL.md +4 -5
  19. package/package.json +1 -1
  20. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  21. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  22. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  23. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  24. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  25. package/plugins/olko-github-pr/skills/lekker-review/README.md +9 -9
  26. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +15 -17
  27. package/plugins/olko-github-pr/skills/lekker-review/references/agents/implementation.md +2 -1
  28. package/plugins/olko-github-pr/skills/lekker-review/references/agents/prover.md +13 -10
  29. package/plugins/olko-github-pr/skills/lekker-review/references/agents/quality.md +1 -1
  30. package/plugins/olko-github-pr/skills/lekker-review/references/agents/test-quality.md +7 -0
  31. package/plugins/olko-github-pr/skills/lekker-review/references/agents/triage-quality.md +1 -1
  32. package/plugins/olko-github-pr/skills/lekker-review/references/agents/verifier.md +12 -9
  33. package/plugins/olko-github-pr/skills/lekker-review/references/artifact-page.md +6 -1
  34. package/plugins/olko-github-pr/skills/lekker-review/references/output-format.md +5 -8
  35. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +176 -117
  36. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  37. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  38. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  39. package/plugins/olko-reflection/skills/self-critique/scripts/critique-nudge.mjs +3 -0
  40. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  41. package/plugins/olko-release/skills/open-source-publisher/SKILL.md +4 -5
  42. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  43. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  44. package/scripts/lib/catalog.mjs +3 -0
@@ -8,43 +8,68 @@ export const meta = {
8
8
  // Schemas
9
9
  // ---------------------------------------------------------------------------
10
10
 
11
+ const FINDINGS_ARRAY_SCHEMA = {
12
+ type: 'array',
13
+ items: {
14
+ type: 'object',
15
+ required: ['file', 'line', 'severity', 'title', 'description', 'badCode', 'fix'],
16
+ properties: {
17
+ file: { type: 'string' },
18
+ line: { type: 'integer' },
19
+ severity: { enum: ['critical', 'important', 'observation', 'idiomatic'] },
20
+ title: { type: 'string' },
21
+ description: { type: 'string' },
22
+ badCode: { type: 'string' },
23
+ fix: { type: 'string' },
24
+ precedent: { type: 'string' },
25
+ rule: { type: 'string', minLength: 1, pattern: '\\S' },
26
+ },
27
+ },
28
+ }
29
+
30
+ const MOCK_SMELLS_SCHEMA = {
31
+ type: 'array',
32
+ items: {
33
+ type: 'object',
34
+ required: ['file', 'line', 'description', 'fix'],
35
+ properties: {
36
+ file: { type: 'string' },
37
+ line: { type: 'integer' },
38
+ description: { type: 'string' },
39
+ fix: { type: 'string' },
40
+ },
41
+ },
42
+ }
43
+
11
44
  const FINDINGS_SCHEMA = {
12
45
  type: 'object',
13
46
  required: ['findings'],
14
47
  properties: {
15
- findings: {
16
- type: 'array',
17
- items: {
18
- type: 'object',
19
- required: ['file', 'line', 'severity', 'title', 'description', 'badCode', 'fix'],
20
- properties: {
21
- file: { type: 'string' },
22
- line: { type: 'integer' },
23
- severity: { enum: ['critical', 'important', 'observation', 'idiomatic'] },
24
- title: { type: 'string' },
25
- description: { type: 'string' },
26
- badCode: { type: 'string' },
27
- fix: { type: 'string' },
28
- precedent: { type: 'string' },
29
- rule: { enum: ['TS-1', 'TS-2', 'GQL-1', 'PR-1'] },
30
- },
31
- },
32
- },
48
+ findings: FINDINGS_ARRAY_SCHEMA,
33
49
  acCoverage: { type: 'string' },
34
50
  mutationSlip: { type: 'string' },
35
51
  coverageVerdict: { type: 'string' },
36
- mockSmells: {
37
- type: 'array',
38
- items: {
39
- type: 'object',
40
- properties: {
41
- file: { type: 'string' },
42
- line: { type: 'integer' },
43
- description: { type: 'string' },
44
- fix: { type: 'string' },
45
- },
46
- },
47
- },
52
+ mockSmells: MOCK_SMELLS_SCHEMA,
53
+ },
54
+ }
55
+
56
+ const IMPLEMENTATION_SCHEMA = {
57
+ type: 'object',
58
+ required: ['findings', 'acCoverage'],
59
+ properties: {
60
+ findings: FINDINGS_ARRAY_SCHEMA,
61
+ acCoverage: { type: 'string' },
62
+ },
63
+ }
64
+
65
+ const TEST_QUALITY_SCHEMA = {
66
+ type: 'object',
67
+ required: ['findings', 'coverageVerdict', 'mutationSlip', 'mockSmells'],
68
+ properties: {
69
+ findings: FINDINGS_ARRAY_SCHEMA,
70
+ coverageVerdict: { type: 'string' },
71
+ mutationSlip: { type: 'string' },
72
+ mockSmells: MOCK_SMELLS_SCHEMA,
48
73
  },
49
74
  }
50
75
 
@@ -80,10 +105,11 @@ const CRITIC_SCHEMA = {
80
105
 
81
106
  const PROOF_SCHEMA = {
82
107
  type: 'object',
83
- required: ['attempted', 'proven', 'reason'],
108
+ required: ['attempted', 'proven', 'outcome', 'reason'],
84
109
  properties: {
85
110
  attempted: { type: 'boolean' },
86
111
  proven: { type: 'boolean' },
112
+ outcome: { enum: ['proven', 'passed', 'inconclusive', 'not_attempted'] },
87
113
  reason: { type: 'string' },
88
114
  testCode: { type: 'string' },
89
115
  testCommand: { type: 'string' },
@@ -95,54 +121,8 @@ const PROOF_SCHEMA = {
95
121
  // Helpers
96
122
  // ---------------------------------------------------------------------------
97
123
 
98
- const HARD_RULES = ['TS-1', 'TS-2', 'GQL-1', 'PR-1']
99
-
100
- // Does the finding's OWN evidence corroborate the hard rule it claims?
101
- //
102
- // A `rule` tag is worth a lot: it preserves Critical severity AND skips
103
- // adversarial verification. Nothing used to check that the tag matched the
104
- // finding, so any reviewer agent could bypass the verifier by writing
105
- // `rule: "TS-1"`. Seen in practice: a missing-test-coverage finding and a
106
- // comment-policy finding were both tagged TS-1, which is about `as X` casts
107
- // and `any`, and both shipped Critical and unverified.
108
- //
109
- // An uncorroborated tag does NOT lose its severity here; it simply stops being
110
- // exempt, so the verifier weighs it like any other finding. That is the
111
- // conservative direction: unproven claims get scrutiny, not a free pass.
112
- function hardRuleCorroborated(finding) {
113
- const evidence = [finding.badCode, finding.description, finding.title]
114
- .map(function(x) { return String(x || '') }).join('\n')
115
- const file = String(finding.file || '')
116
- // TS-1 is judged on the QUOTED CODE only. Prose is full of the words `as` and
117
- // `any` ("any cart with items"), and matching those re-opened the very bypass
118
- // this function exists to close.
119
- const code = String(finding.badCode || '')
120
-
121
- switch (finding.rule) {
122
- case 'TS-1':
123
- // A cast to a capitalised type OR to a lowercase built-in: `as string`
124
- // is every bit as much a TS-1 violation as `as Foo`, and missing it sent
125
- // a genuine hard-rule finding to a verifier whose five challenges cannot
126
- // answer a standards claim, where it could be dropped outright.
127
- return /\bas\s+(?:[A-Z_$][\w$]*|string|number|boolean|bigint|symbol|object|unknown|never|any|const)\b/.test(code)
128
- || /:\s*any\b|<\s*any[\s,>]|\bany\[\]/.test(code)
129
- case 'TS-2':
130
- return /\.js$/.test(file)
131
- case 'GQL-1':
132
- return /\bnodes\b|\bpageInfo\b|\bedges\b/.test(evidence)
133
- case 'PR-1':
134
- // PR-1 is about the PR title, so it has no source file to anchor to.
135
- return /pr\s*(title|description)/i.test(file + '\n' + evidence)
136
- default:
137
- return false
138
- }
139
- }
140
-
141
124
  function isHardRule(finding) {
142
- if (typeof finding.rule !== 'string' || HARD_RULES.indexOf(finding.rule) === -1) {
143
- return false
144
- }
145
- return hardRuleCorroborated(finding)
125
+ return typeof finding.rule === 'string' && finding.rule.trim().length > 0
146
126
  }
147
127
 
148
128
  const SEVERITY_RANK = { critical: 3, important: 2, observation: 1, idiomatic: 0 }
@@ -180,6 +160,13 @@ function mergeFindings(a, b) {
180
160
 
181
161
  if (SEVERITY_RANK[b.severity] > SEVERITY_RANK[merged.severity]) {
182
162
  merged.severity = b.severity
163
+ for (const field of ['verificationStatus', 'verifierReasoning', 'proof']) {
164
+ if (Object.prototype.hasOwnProperty.call(b, field)) {
165
+ merged[field] = b[field]
166
+ } else {
167
+ delete merged[field]
168
+ }
169
+ }
183
170
  }
184
171
 
185
172
  merged.description = longest(a.description, b.description)
@@ -273,21 +260,51 @@ function dedup(allFindings) {
273
260
  return result
274
261
  }
275
262
 
276
- function shouldVerify(finding, depth) {
277
- // house hard rules are policy violations, not runtime-failure claims. The
278
- // five adversarial challenges cannot be answered for them, so verifying
279
- // would systematically drop findings that are Critical by policy.
280
- if (isHardRule(finding)) {
281
- return false
263
+ function shouldVerify(finding) {
264
+ // Review depth controls breadth, not the trust bar. Every finding that can
265
+ // affect the verdict must be checked. Hard rules take the verifier's
266
+ // rule-specific anchor/applicability path instead of its runtime challenges.
267
+ return finding.severity === 'critical' || finding.severity === 'important'
268
+ }
269
+
270
+ function normalizeProof(result) {
271
+ const hasText = function(value) {
272
+ return typeof value === 'string' && value.trim().length > 0
282
273
  }
283
- if (depth === 'scan') {
284
- return false
274
+ const coherent = (
275
+ result.attempted === true
276
+ && result.proven === true
277
+ && result.outcome === 'proven'
278
+ && hasText(result.testCode)
279
+ && hasText(result.testCommand)
280
+ && hasText(result.redOutput)
281
+ ) || (
282
+ result.attempted === true
283
+ && result.proven === false
284
+ && result.outcome === 'passed'
285
+ && hasText(result.testCode)
286
+ && hasText(result.testCommand)
287
+ ) || (
288
+ result.attempted === true
289
+ && result.proven === false
290
+ && result.outcome === 'inconclusive'
291
+ ) || (
292
+ result.attempted === false
293
+ && result.proven === false
294
+ && result.outcome === 'not_attempted'
295
+ )
296
+
297
+ if (coherent) {
298
+ return result
285
299
  }
286
- if (depth === 'medium') {
287
- return finding.severity === 'critical'
300
+
301
+ const attempted = result.attempted === true
302
+ return {
303
+ attempted,
304
+ proven: false,
305
+ outcome: attempted ? 'inconclusive' : 'not_attempted',
306
+ reason: `Prover returned an inconsistent proof state; ignored. ${result.reason || ''}`.trim(),
288
307
  }
289
- // deep: critical + important
290
- return finding.severity === 'critical' || finding.severity === 'important'
291
308
  }
292
309
 
293
310
  // Run thunks in sequential batches, giving the per-finding stages a ceiling.
@@ -428,6 +445,7 @@ const budgetAtStart = budget.spent()
428
445
  `Read and follow ${promptDir}/verifier.md.`,
429
446
  `FINDING (JSON): ${JSON.stringify(finding)}.`,
430
447
  `DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${wtDisplay}.`,
448
+ `HOUSE_RULES_FILE: read key "houseRulesFile" from CONTEXT_FILE, then read that exact path before validating a hard rule.`,
431
449
  `Default to dropping when uncertain.`,
432
450
  ].join(' ')
433
451
  }
@@ -438,16 +456,22 @@ const budgetAtStart = budget.spent()
438
456
 
439
457
  async function reviewStage(dim) {
440
458
  agentCount++
459
+ let schema = FINDINGS_SCHEMA
460
+ if (dim.key === 'implementation') {
461
+ schema = IMPLEMENTATION_SCHEMA
462
+ } else if (dim.key === 'test-quality') {
463
+ schema = TEST_QUALITY_SCHEMA
464
+ }
441
465
  const result = await agent(reviewPrompt(dim), {
442
466
  label: `review:${dim.key}`,
443
467
  phase: 'Review',
444
- schema: FINDINGS_SCHEMA,
468
+ schema,
445
469
  model: dim.model,
446
470
  })
447
471
 
448
472
  if (!result) {
449
473
  log(`${dim.key}: agent returned null, skipping`)
450
- return []
474
+ return { key: dim.key, findings: [] }
451
475
  }
452
476
 
453
477
  const findings = (result.findings || []).map(function(f) {
@@ -457,44 +481,45 @@ const budgetAtStart = budget.spent()
457
481
  })
458
482
 
459
483
  log(`${dim.key}: ${findings.length} findings, ${
460
- findings.filter(function(f) { return shouldVerify(f, depth) }).length
484
+ findings.filter(function(f) { return shouldVerify(f) }).length
461
485
  } to verify`)
462
486
 
463
- return findings
487
+ return {
488
+ key: dim.key,
489
+ findings,
490
+ acCoverage: dim.key === 'implementation' ? result.acCoverage : undefined,
491
+ coverageVerdict: dim.key === 'test-quality' ? result.coverageVerdict : undefined,
492
+ mutationSlip: dim.key === 'test-quality' ? result.mutationSlip : undefined,
493
+ mockSmells: dim.key === 'test-quality' ? result.mockSmells : undefined,
494
+ }
464
495
  }
465
496
 
466
497
  // -------------------------------------------------------------------------
467
498
  // Verify stage: takes the deduped cross-dimension finding set, splits into
468
- // in-scope (adversarially verified) and out-of-scope (kept as-is, including
469
- // house hard rules which are exempt by policy), and runs verifiers in
470
- // parallel over the in-scope set.
499
+ // in-scope (verdict-affecting) and out-of-scope (non-blocking), and runs
500
+ // verifiers in parallel over the in-scope set. Hard rules use the verifier's
501
+ // rule-specific checks and skip only its runtime-oriented challenges.
471
502
  // -------------------------------------------------------------------------
472
503
 
473
504
  async function verifyAll(findings) {
474
505
  const inScope = findings.filter(function(f) {
475
- return shouldVerify(f, depth)
506
+ return shouldVerify(f)
476
507
  })
477
508
  const outOfScope = findings.filter(function(f) {
478
- return !shouldVerify(f, depth)
479
- })
480
-
481
- const exempted = outOfScope.map(function(f) {
482
- if (!isHardRule(f)) {
483
- return f
484
- }
485
- hardRuleCount++
486
- const exempt = Object.assign({}, f)
487
- exempt.verifierReasoning = `hard rule ${f.rule}: exempt from adversarial verification (standards violation, not a runtime-failure claim)`
488
- return exempt
509
+ return !shouldVerify(f)
489
510
  })
490
511
 
491
512
  if (inScope.length === 0) {
492
- return exempted
513
+ return outOfScope
493
514
  }
494
515
 
495
516
  const verified = await batched(inScope.map(function(finding) {
496
517
  return async function() {
497
518
  agentCount++
519
+ if (isHardRule(finding)) {
520
+ hardRuleCount++
521
+ }
522
+
498
523
  const verdict = await agent(verifierPrompt(finding), {
499
524
  label: `verify:${finding.file}:${finding.line}`,
500
525
  model: 'sonnet',
@@ -504,9 +529,12 @@ const budgetAtStart = budget.spent()
504
529
  })
505
530
 
506
531
  if (!verdict) {
507
- // Null agent result: treat as confirmed-unverified
532
+ // Missing verification cannot support a blocking finding.
533
+ downgradedCount++
508
534
  const kept = Object.assign({}, finding)
509
- kept.verifierReasoning = 'verifier agent returned null; kept unverified'
535
+ kept.severity = 'observation'
536
+ kept.verificationStatus = 'unavailable'
537
+ kept.verifierReasoning = 'verifier agent returned no usable result; downgraded to non-blocking'
510
538
  return kept
511
539
  }
512
540
 
@@ -519,18 +547,20 @@ const budgetAtStart = budget.spent()
519
547
  downgradedCount++
520
548
  const downgraded = Object.assign({}, finding)
521
549
  downgraded.severity = verdict.newSeverity || 'observation'
550
+ downgraded.verificationStatus = 'downgraded'
522
551
  downgraded.verifierReasoning = verdict.reasoning
523
552
  return downgraded
524
553
  }
525
554
 
526
555
  // confirmed
527
556
  const confirmed = Object.assign({}, finding)
557
+ confirmed.verificationStatus = isHardRule(finding) ? 'hard-rule-confirmed' : 'confirmed'
528
558
  confirmed.verifierReasoning = verdict.reasoning
529
559
  return confirmed
530
560
  }
531
561
  }), FANOUT_PLAN)
532
562
 
533
- return exempted.concat(verified.filter(Boolean))
563
+ return outOfScope.concat(verified.filter(Boolean))
534
564
  }
535
565
 
536
566
  // -------------------------------------------------------------------------
@@ -548,10 +578,19 @@ const budgetAtStart = budget.spent()
548
578
  return function() { return reviewStage(dim) }
549
579
  }), REVIEW_PLAN)
550
580
 
551
- const deduped = dedup(reviewed.filter(Boolean).flat())
581
+ const reviewResults = reviewed.filter(Boolean)
582
+ const implementationResult = reviewResults.find(function(result) {
583
+ return result.key === 'implementation'
584
+ }) || {}
585
+ const testQualityResult = reviewResults.find(function(result) {
586
+ return result.key === 'test-quality'
587
+ }) || {}
588
+ const deduped = dedup(reviewResults.flatMap(function(result) {
589
+ return result.findings
590
+ }))
552
591
 
553
592
  log(`${deduped.length} unique finding(s) after dedup; verifying ${
554
- deduped.filter(function(f) { return shouldVerify(f, depth) }).length
593
+ deduped.filter(function(f) { return shouldVerify(f) }).length
555
594
  }`)
556
595
 
557
596
  phase('Verify')
@@ -635,11 +674,17 @@ const budgetAtStart = budget.spent()
635
674
  return []
636
675
  }
637
676
 
677
+ if (isHardRule(newFinding)) {
678
+ hardRuleCount++
679
+ }
680
+
638
681
  if (verdict.verdict === 'downgraded') {
639
682
  downgradedCount++
640
683
  newFinding.severity = verdict.newSeverity || 'observation'
684
+ newFinding.verificationStatus = 'downgraded'
641
685
  newFinding.verifierReasoning = verdict.reasoning
642
686
  } else {
687
+ newFinding.verificationStatus = isHardRule(newFinding) ? 'hard-rule-confirmed' : 'confirmed'
643
688
  newFinding.verifierReasoning = verdict.reasoning
644
689
  }
645
690
 
@@ -705,9 +750,19 @@ const budgetAtStart = budget.spent()
705
750
  return
706
751
  }
707
752
 
708
- finding.proof = result
709
- if (result.proven === true) {
753
+ const proof = normalizeProof(result)
754
+ finding.proof = proof
755
+ if (proof.proven === true) {
710
756
  provenCount++
757
+ finding.verificationStatus = 'proven'
758
+ } else if (proof.outcome === 'passed') {
759
+ downgradedCount++
760
+ finding.severity = 'important'
761
+ finding.verificationStatus = 'counter-evidence'
762
+ finding.verifierReasoning = [
763
+ finding.verifierReasoning,
764
+ `Proof test passed: ${proof.reason}`,
765
+ ].filter(Boolean).join(' ')
711
766
  }
712
767
  }
713
768
  }), FANOUT_PLAN)
@@ -731,6 +786,10 @@ const budgetAtStart = budget.spent()
731
786
  agentCount,
732
787
  proveAttemptCount,
733
788
  provenCount,
789
+ acCoverage: implementationResult.acCoverage || null,
790
+ coverageVerdict: testQualityResult.coverageVerdict || null,
791
+ mutationSlip: testQualityResult.mutationSlip || null,
792
+ mockSmells: Array.isArray(testQualityResult.mockSmells) ? testQualityResult.mockSmells : [],
734
793
  outputTokens: budget.spent() - budgetAtStart,
735
794
  turnTokensTotal: budget.spent(),
736
795
  }
@@ -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.40.1",
4
+ "version": "1.41.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, launch plans.",
4
- "version": "1.40.1",
4
+ "version": "1.41.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.40.1",
4
+ "version": "1.41.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -11,6 +11,9 @@ import { readFileSync } from 'node:fs';
11
11
  const THRESHOLD = 1500;
12
12
  const OFFER_MARKER = /critical-thinking review/i; // the offer line must contain this; doubles as a loop guard
13
13
 
14
+ /**
15
+ * Exits the hook cleanly when conditions for offering a critique are not met.
16
+ */
14
17
  function bail() { process.exit(0); } // any uncertainty => do nothing, never block the user
15
18
 
16
19
  let raw = '';
@@ -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.40.1",
4
+ "version": "1.41.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: open-source-publisher
3
- description: Prepare an open-source repository for polished public publishing. Use when a user asks to publish, open-source, launch, polish, package, brand, or make a GitHub project presentable with a minimal project icon, social preview image, GitHub Pages landing page, standardized README, essential shields, CI/CD quality gates, release automation checks, and optional donation setup. Prefer the external `logo-generator` skill for icon generation when available, and `nolangz/pixel2motion` for optional motion previews of generated static images.
3
+ description: Prepare an open-source repository for polished public publishing. Use when a user asks to publish, open-source, launch, polish, package, brand, or make a GitHub project presentable with a minimal project icon, social preview image, GitHub Pages landing page, standardized README, essential shields, CI/CD quality gates, release automation checks, and optional donation setup. Prefer the external `logo-generator` skill for icon generation when available, and `nolangz/pixel2motion` for optional motion previews of supported logos or simple artwork.
4
4
  license: MIT
5
5
  allowed-tools: Bash, Read, Write, Edit, WebSearch, WebFetch
6
6
  compatibility: Codex, Claude Code, Cursor, GitHub Copilot, Windsurf, Kiro, and other Agent Skills compatible tools. Requires a writable git repository; browser or image rendering tools are useful for visual validation.
@@ -43,12 +43,11 @@ Use this skill to audit whether an OSS repository is ready to publish, then help
43
43
  5. For existing usable pieces, do not propose replacement by default. Ask a change-oriented question only when useful, for example:
44
44
  - "You already have a terminal-style GitHub Pages site. Do you want to keep it or restyle it?"
45
45
  - "You already have an icon and social card. Do you want a refresh, or should I leave them as-is?"
46
- - "You already have a static social card. Do you want me to animate it with pixel2motion, or leave it static?"
47
46
  - "You already have release automation. Do you want me to audit only, or also tighten it?"
48
47
  6. Ask only for choices needed to fix missing or weak pieces:
49
48
  - If GitHub Pages is missing or weak, ask for style: `oldschool linux`, `terminal`, `modern`, `brutalist`, `glassmorphism`, `y2k`, `hacker`, or custom.
50
49
  - If donation wiring is missing, ask whether to enable it: `none`, `GitHub Sponsors`, `Ko-fi`, `Buy Me a Coffee`, `Open Collective`, `Thanks.dev`, or custom URL.
51
- - If the repo has a generated static social image or hero card, ask whether to animate it using `nolangz/pixel2motion`.
50
+ - If the repo has a generated logo or simple hero artwork supported by `nolangz/pixel2motion`, ask whether to animate it or leave it static.
52
51
  - If the repo has no clear product essence, ask for a one-sentence positioning statement.
53
52
  7. Implement only approved, missing, or weak work in this order:
54
53
  - OSS governance and support files
@@ -212,8 +211,8 @@ Use `magick` or another renderer when `rsvg-convert` is unavailable.
212
211
 
213
212
  Some repos benefit from a short animated preview. Keep this opt-in.
214
213
 
215
- - If you already generated a static image, ask: `Do you want me to animate the static image using pixel2motion?`
216
- - If the user says yes, use `nolangz/pixel2motion` as the optional dependency for logo animation, animated HTML demos, and GIF/video previews.
214
+ - After the user opts in during workflow step 6, animate only the approved generated logo or simple artwork.
215
+ - Use `nolangz/pixel2motion` as the optional dependency for that animation, animated HTML demos, and GIF/video previews.
217
216
  - Do not auto-animate by default.
218
217
 
219
218
  ## README Standard
@@ -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.40.1",
4
+ "version": "1.41.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.40.1",
4
+ "version": "1.41.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -54,6 +54,9 @@ export const PLUGIN_ASSIGNMENT = {
54
54
  },
55
55
  }
56
56
 
57
+ /**
58
+ * Loads and parses the skills catalog from the specified JSON file.
59
+ */
57
60
  export function loadCatalog(catalogPath = 'catalog/skills.json') {
58
61
  const raw = JSON.parse(readFileSync(catalogPath, 'utf8'))
59
62
  const plugins = raw.plugins || []