happyskills 0.27.0 → 0.27.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.27.1] - 2026-04-02
11
+
12
+ ### Fixed
13
+ - Fix `no_executable_code` validator producing thousands of false-positive warnings for documentation-heavy skills — rule now scans only SKILL.md, not `references/*.md` files where code blocks are expected content
14
+ - Fix `publish` flooding stderr with one warning per line — warnings are now summarized into a single line grouped by rule (e.g., `⚠ 3 warnings: no_executable_code (2), name_match (1)`), and suppressed from stderr entirely in `--json` mode
15
+
10
16
  ## [0.27.0] - 2026-04-02
11
17
 
12
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -18,9 +18,10 @@ const { validate_skill_json } = require('../validation/skill_json_rules')
18
18
  const { validate_cross } = require('../validation/cross_rules')
19
19
  const { validate_no_conflict_markers } = require('../validation/conflict_marker_rules')
20
20
  const { create_spinner } = require('../ui/spinner')
21
- const { print_help, print_success, print_error, print_warn, print_hint, print_json, code } = require('../ui/output')
21
+ const { print_help, print_success, print_error, print_warn, print_hint, print_json, code, summarize_warnings } = require('../ui/output')
22
22
  const { exit_with_error, UsageError, CliError } = require('../utils/errors')
23
23
  const { EXIT_CODES } = require('../constants')
24
+ const { is_json_mode } = require('../state')
24
25
 
25
26
  const HELP_TEXT = `Usage: happyskills publish <skill-name> [options]
26
27
 
@@ -108,7 +109,6 @@ const run = (args) => catch_errors('Publish failed', async () => {
108
109
  const validation_warnings = all_results.filter(r => r.severity === 'warning')
109
110
 
110
111
  if (validation_errors.length > 0) {
111
- const { is_json_mode } = require('../state')
112
112
  if (is_json_mode()) {
113
113
  const structured_errors = validation_errors.map(({ severity, ...rest }) => rest)
114
114
  print_json({
@@ -125,8 +125,8 @@ const run = (args) => catch_errors('Publish failed', async () => {
125
125
  throw new CliError(`Skill failed validation with ${validation_errors.length} error(s):\n ${error_msgs.join('\n ')}\n\nRun \`happyskills validate ${skill_name}\` for full details.`, EXIT_CODES.ERROR)
126
126
  }
127
127
 
128
- for (const w of validation_warnings) {
129
- print_warn(`${w.file || 'General'}${w.field ? ` (${w.field})` : ''}: ${w.message}`)
128
+ if (validation_warnings.length > 0 && !is_json_mode()) {
129
+ print_warn(summarize_warnings(validation_warnings, skill_name))
130
130
  }
131
131
 
132
132
  const spinner = create_spinner('Preparing to publish...')
package/src/ui/output.js CHANGED
@@ -97,6 +97,17 @@ const print_table = (headers, rows) => {
97
97
  })
98
98
  }
99
99
 
100
+ const summarize_warnings = (warnings, skill_name) => {
101
+ if (!warnings || warnings.length === 0) return null
102
+ const by_rule = new Map()
103
+ for (const w of warnings) {
104
+ const key = w.rule || 'unknown'
105
+ by_rule.set(key, (by_rule.get(key) || 0) + 1)
106
+ }
107
+ const parts = [...by_rule.entries()].map(([rule, count]) => `${rule} (${count})`)
108
+ return `${warnings.length} warning${warnings.length === 1 ? '' : 's'}: ${parts.join(', ')} — run 'happyskills validate ${skill_name}' for details`
109
+ }
110
+
100
111
  const print_json = (data) => {
101
112
  console.log(JSON.stringify(data, null, 2))
102
113
  }
@@ -105,4 +116,4 @@ const print_label = (label, value) => {
105
116
  console.log(`${gray(label + ':')} ${value}`)
106
117
  }
107
118
 
108
- module.exports = { print_success, print_error, print_warn, print_info, print_hint, print_help, print_table, print_json, print_label, code, format_help, visible_len, ansi_pad, truncate }
119
+ module.exports = { print_success, print_error, print_warn, print_info, print_hint, print_help, print_table, print_json, print_label, code, format_help, visible_len, ansi_pad, truncate, summarize_warnings }
@@ -2,7 +2,7 @@
2
2
  const { describe, it, before, after, beforeEach, afterEach } = require('node:test')
3
3
  const assert = require('node:assert/strict')
4
4
 
5
- const { visible_len, ansi_pad, truncate, format_help, print_table } = require('./output')
5
+ const { visible_len, ansi_pad, truncate, format_help, print_table, summarize_warnings } = require('./output')
6
6
 
7
7
  // ─── visible_len ─────────────────────────────────────────────────────────────
8
8
 
@@ -182,6 +182,53 @@ describe('print_table', () => {
182
182
  })
183
183
  })
184
184
 
185
+ // ─── summarize_warnings ──────────────────────────────────────────────────────
186
+
187
+ describe('summarize_warnings', () => {
188
+ it('returns null for empty array', () => {
189
+ assert.strictEqual(summarize_warnings([], 'my-skill'), null)
190
+ })
191
+
192
+ it('returns null for null/undefined input', () => {
193
+ assert.strictEqual(summarize_warnings(null, 'my-skill'), null)
194
+ assert.strictEqual(summarize_warnings(undefined, 'my-skill'), null)
195
+ })
196
+
197
+ it('uses singular "warning" for a single warning', () => {
198
+ const warnings = [{ rule: 'name_match', message: 'names differ' }]
199
+ const result = summarize_warnings(warnings, 'my-skill')
200
+ assert.ok(result.startsWith('1 warning:'))
201
+ assert.ok(result.includes('name_match (1)'))
202
+ assert.ok(result.includes("'happyskills validate my-skill'"))
203
+ })
204
+
205
+ it('uses plural "warnings" for multiple warnings', () => {
206
+ const warnings = [
207
+ { rule: 'no_executable_code', message: 'found code block' },
208
+ { rule: 'no_executable_code', message: 'found another block' },
209
+ { rule: 'name_match', message: 'names differ' }
210
+ ]
211
+ const result = summarize_warnings(warnings, 'deploy-aws')
212
+ assert.ok(result.startsWith('3 warnings:'))
213
+ assert.ok(result.includes('no_executable_code (2)'))
214
+ assert.ok(result.includes('name_match (1)'))
215
+ assert.ok(result.includes("'happyskills validate deploy-aws'"))
216
+ })
217
+
218
+ it('groups by rule and counts correctly', () => {
219
+ const warnings = Array.from({ length: 100 }, () => ({ rule: 'no_executable_code', message: 'x' }))
220
+ const result = summarize_warnings(warnings, 'big-skill')
221
+ assert.ok(result.startsWith('100 warnings:'))
222
+ assert.ok(result.includes('no_executable_code (100)'))
223
+ })
224
+
225
+ it('falls back to "unknown" for warnings without a rule field', () => {
226
+ const warnings = [{ message: 'something' }]
227
+ const result = summarize_warnings(warnings, 'my-skill')
228
+ assert.ok(result.includes('unknown (1)'))
229
+ })
230
+ })
231
+
185
232
  // ─── json mode silencing ──────────────────────────────────────────────────────
186
233
 
187
234
  describe('print_success / print_info / print_hint — json mode silencing', () => {
@@ -1,5 +1,3 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
1
  const { error: { catch_errors } } = require('puffy-core')
4
2
  const { SKILL_TYPES } = require('../constants')
5
3
 
@@ -61,20 +59,6 @@ const validate_cross = (skill_dir, frontmatter, manifest, skill_md_content, skil
61
59
  results.push(...scan_code_blocks(skill_md_content, 'SKILL.md'))
62
60
  }
63
61
 
64
- // Executable code in references/*.md
65
- const refs_dir = path.join(skill_dir, 'references')
66
- try {
67
- const entries = await fs.promises.readdir(refs_dir, { withFileTypes: true })
68
- for (const entry of entries) {
69
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue
70
- const ref_path = path.join(refs_dir, entry.name)
71
- const ref_content = await fs.promises.readFile(ref_path, 'utf-8')
72
- results.push(...scan_code_blocks(ref_content, `references/${entry.name}`))
73
- }
74
- } catch {
75
- // No references dir — that's fine
76
- }
77
-
78
62
  return results
79
63
  })
80
64
 
@@ -89,7 +89,7 @@ describe('validate_cross — executable code detection', () => {
89
89
  assert.ok(!results.some(r => r.rule === 'no_executable_code'))
90
90
  })
91
91
 
92
- it('warns for executable code in references/*.md', async () => {
92
+ it('does not scan references/*.md for executable code', async () => {
93
93
  const refs_dir = path.join(tmp, 'references')
94
94
  fs.mkdirSync(refs_dir)
95
95
  const code = '```bash\n#!/bin/bash\nimport os\nset -e\necho "deploy"\n' + 'echo "step"\n'.repeat(8) + '```'
@@ -97,14 +97,6 @@ describe('validate_cross — executable code detection', () => {
97
97
 
98
98
  const [err, results] = await validate_cross(tmp, { name: 'x' }, { name: 'x' }, '')
99
99
  assert.ifError(err)
100
- const check = results.find(r => r.rule === 'no_executable_code' && r.file === 'references/deploy.md')
101
- assert.strictEqual(check.severity, 'warning')
102
- })
103
-
104
- it('does not error when references dir is missing', async () => {
105
- const [err, results] = await validate_cross(tmp, { name: 'x' }, { name: 'x' }, '')
106
- assert.ifError(err)
107
- // Should not throw or produce error results — just no code block warnings
108
- assert.ok(!results.some(r => r.severity === 'error'))
100
+ assert.ok(!results.some(r => r.rule === 'no_executable_code'))
109
101
  })
110
102
  })