happyskills 0.23.0 → 0.25.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/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/api/push.js +10 -8
- package/src/commands/convert.js +22 -1
- package/src/commands/init.js +28 -8
- package/src/commands/publish.js +11 -3
- package/src/commands/pull.js +4 -0
- package/src/validation/skill_md_rules.js +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.25.0] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `--agents` flag to `init` and `convert` commands for explicit agent targeting, matching `install` behavior
|
|
14
|
+
- Add `recommendations` array to `validate` description max_length error — provides a 4-step procedure (audit, lossless compression, lossy compression, verify) for safely shortening descriptions without breaking auto-invocation triggers
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fix `init` not creating symlinks to secondary agents (Cursor, Windsurf, Codex, etc.) — newly scaffolded skills now use the same multi-agent linking as `install`
|
|
18
|
+
- Fix `convert` not creating symlinks to secondary agents — converted skills now get symlinked to all detected agents
|
|
19
|
+
|
|
20
|
+
## [0.24.0] - 2026-03-30
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Add merge commit support to `pull` and `publish` — when `pull` auto-merges local and remote changes without conflicts, the lock file stores `merge_parents` (the old base and remote head). On `publish`, these are sent as `parent_shas` to create a two-parent merge commit, preserving DAG history. Conflicts fall back to rebase semantics (single-parent commit). `merge_parents` is cleared after publish, on fast-forward, or when conflicts exist.
|
|
24
|
+
|
|
10
25
|
## [0.23.0] - 2026-03-30
|
|
11
26
|
|
|
12
27
|
### Added
|
package/package.json
CHANGED
package/src/api/push.js
CHANGED
|
@@ -12,13 +12,15 @@ const estimate_payload_size = (files) => {
|
|
|
12
12
|
return size
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const smart_push = (owner, repo, { version, message, files, visibility, base_commit, force }, on_progress) =>
|
|
15
|
+
const smart_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas }, on_progress) =>
|
|
16
16
|
catch_errors('Smart push failed', async () => {
|
|
17
17
|
const payload_size = estimate_payload_size(files)
|
|
18
18
|
|
|
19
19
|
if (payload_size < DIRECT_PUSH_THRESHOLD) {
|
|
20
20
|
// Small payload — use direct push
|
|
21
|
-
const
|
|
21
|
+
const body = { version, message, files, visibility, base_commit, force }
|
|
22
|
+
if (parent_shas) body.parent_shas = parent_shas
|
|
23
|
+
const [err, data] = await repos_api.push(owner, repo, body)
|
|
22
24
|
if (err) throw e('Direct push failed', err)
|
|
23
25
|
return data
|
|
24
26
|
}
|
|
@@ -27,9 +29,9 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
|
|
|
27
29
|
const file_meta = files.map(f => ({ path: f.path, sha: f.sha, size: f.size }))
|
|
28
30
|
|
|
29
31
|
// Step 1: Initiate
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const init_body = { version, message, files: file_meta, visibility, base_commit, force }
|
|
33
|
+
if (parent_shas) init_body.parent_shas = parent_shas
|
|
34
|
+
const [init_err, init_data] = await initiate_upload(owner, repo, init_body)
|
|
33
35
|
if (init_err) throw e('Upload initiation failed', init_err)
|
|
34
36
|
|
|
35
37
|
const { upload_id, presigned_urls } = init_data
|
|
@@ -52,9 +54,9 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
|
|
|
52
54
|
if (upload_err) throw e('File uploads failed', upload_err)
|
|
53
55
|
|
|
54
56
|
// Step 3: Complete
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const complete_body = { upload_id, version, message, files: file_meta, base_commit, force }
|
|
58
|
+
if (parent_shas) complete_body.parent_shas = parent_shas
|
|
59
|
+
const [complete_err, complete_data] = await complete_upload(owner, repo, complete_body)
|
|
58
60
|
if (complete_err) throw e('Upload completion failed', complete_err)
|
|
59
61
|
|
|
60
62
|
return complete_data
|
package/src/commands/convert.js
CHANGED
|
@@ -13,6 +13,7 @@ const { write_lock, update_lock_skills } = require('../lock/writer')
|
|
|
13
13
|
const { hash_directory } = require('../lock/integrity')
|
|
14
14
|
const { skills_dir, find_project_root, lock_root } = require('../config/paths')
|
|
15
15
|
const { file_exists } = require('../utils/fs')
|
|
16
|
+
const { resolve_agents, link_to_agents } = require('../agents')
|
|
16
17
|
const { create_spinner } = require('../ui/spinner')
|
|
17
18
|
const { print_help, print_success, print_error, print_info, print_warn, print_label, print_json } = require('../ui/output')
|
|
18
19
|
const { exit_with_error, UsageError, CliError } = require('../utils/errors')
|
|
@@ -26,10 +27,12 @@ Arguments:
|
|
|
26
27
|
skill-name Name of the skill in .claude/skills/
|
|
27
28
|
|
|
28
29
|
Options:
|
|
29
|
-
-g, --global Look in global skills (~/.
|
|
30
|
+
-g, --global Look in global skills (~/.agents/skills/)
|
|
30
31
|
--workspace <slug> Target workspace (if you have multiple)
|
|
31
32
|
--version <ver> Initial version (default: 1.0.0)
|
|
32
33
|
--keywords <tags> Comma-separated keywords (e.g., "deploy,aws,iac")
|
|
34
|
+
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
35
|
+
Default: auto-detect. Env: HAPPYSKILLS_AGENTS
|
|
33
36
|
-y, --yes Skip confirmation prompt
|
|
34
37
|
|
|
35
38
|
Examples:
|
|
@@ -190,8 +193,25 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
190
193
|
const [lock_err] = await write_lock(lock_dir, new_skills)
|
|
191
194
|
if (lock_err) { pub_spinner.fail('Failed to write lock file'); throw e('Lock write failed', lock_err) }
|
|
192
195
|
|
|
196
|
+
// Link to detected agents (non-fatal — warnings only)
|
|
197
|
+
const linked_agents = []
|
|
198
|
+
const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
|
|
199
|
+
if (!agents_err && agents_result?.agents?.length > 0) {
|
|
200
|
+
pub_spinner.update(`Linking to ${agents_result.agents.length} agent(s)...`)
|
|
201
|
+
const [link_errs] = await link_to_agents(skill_dir, agents_result.agents, { global: is_global, project_root, skill_name })
|
|
202
|
+
if (link_errs) {
|
|
203
|
+
print_warn(`Warning: failed to link ${skill_name} to some agents`)
|
|
204
|
+
} else {
|
|
205
|
+
linked_agents.push(...agents_result.agents.map(a => a.id))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
193
209
|
pub_spinner.succeed(`Converted ${full_name}@${merged_version}`)
|
|
194
210
|
|
|
211
|
+
if (linked_agents.length > 0) {
|
|
212
|
+
print_info(`Linked to: ${agents_result.agents.map(a => a.display_name).join(', ')}`)
|
|
213
|
+
}
|
|
214
|
+
|
|
195
215
|
if (args.flags.json) {
|
|
196
216
|
const json_data = {
|
|
197
217
|
skill: full_name,
|
|
@@ -199,6 +219,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
199
219
|
workspace: workspace.slug,
|
|
200
220
|
description: merged_description || ''
|
|
201
221
|
}
|
|
222
|
+
if (linked_agents.length > 0) json_data.linked_agents = linked_agents
|
|
202
223
|
if (frontmatter_warnings.length > 0) json_data.warnings = frontmatter_warnings
|
|
203
224
|
print_json({ data: json_data })
|
|
204
225
|
return
|
package/src/commands/init.js
CHANGED
|
@@ -2,26 +2,29 @@ const path = require('path')
|
|
|
2
2
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
3
3
|
const { write_manifest } = require('../manifest/writer')
|
|
4
4
|
const { write_file, file_exists, ensure_dir } = require('../utils/fs')
|
|
5
|
-
const { print_success, print_error, print_help, print_hint, print_json, code } = require('../ui/output')
|
|
5
|
+
const { print_success, print_error, print_help, print_hint, print_json, print_info, print_warn, code } = require('../ui/output')
|
|
6
6
|
const { exit_with_error, CliError, UsageError } = require('../utils/errors')
|
|
7
7
|
const { SKILL_JSON, SKILL_MD, EXIT_CODES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
|
|
8
8
|
const { find_project_root, skills_dir } = require('../config/paths')
|
|
9
|
+
const { resolve_agents, link_to_agents } = require('../agents')
|
|
9
10
|
|
|
10
11
|
const HELP_TEXT = `Usage: happyskills init <name> [options]
|
|
11
12
|
|
|
12
|
-
Scaffold a new skill in .
|
|
13
|
+
Scaffold a new skill in .agents/skills/<name>/.
|
|
13
14
|
|
|
14
15
|
Creates:
|
|
15
|
-
.
|
|
16
|
-
.
|
|
16
|
+
.agents/skills/<name>/skill.json Skill manifest (name, version, deps)
|
|
17
|
+
.agents/skills/<name>/SKILL.md Skill instructions for AI agents
|
|
17
18
|
|
|
18
19
|
Arguments:
|
|
19
20
|
name Skill name (required, e.g., my-deploy-skill)
|
|
20
21
|
|
|
21
22
|
Options:
|
|
22
|
-
-g, --global
|
|
23
|
-
--kit
|
|
24
|
-
--
|
|
23
|
+
-g, --global Create in global skills (~/.agents/skills/)
|
|
24
|
+
--kit Initialize as a kit (collection of skills)
|
|
25
|
+
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
26
|
+
Default: auto-detect. Env: HAPPYSKILLS_AGENTS
|
|
27
|
+
--json Output as JSON
|
|
25
28
|
|
|
26
29
|
Examples:
|
|
27
30
|
happyskills init my-deploy-skill
|
|
@@ -104,14 +107,31 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
104
107
|
|
|
105
108
|
const label = is_kit ? 'kit' : 'skill'
|
|
106
109
|
|
|
110
|
+
// Link to detected agents (non-fatal — warnings only)
|
|
111
|
+
const linked_agents = []
|
|
112
|
+
const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
|
|
113
|
+
if (!agents_err && agents_result?.agents?.length > 0) {
|
|
114
|
+
const [link_errs] = await link_to_agents(dir, agents_result.agents, { global: is_global, project_root, skill_name: final_name })
|
|
115
|
+
if (link_errs) {
|
|
116
|
+
print_warn(`Warning: failed to link ${final_name} to some agents`)
|
|
117
|
+
} else {
|
|
118
|
+
linked_agents.push(...agents_result.agents.map(a => a.id))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
if (args.flags.json) {
|
|
108
|
-
|
|
123
|
+
const data = { name: final_name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir }
|
|
124
|
+
if (linked_agents.length > 0) data.linked_agents = linked_agents
|
|
125
|
+
print_json({ data })
|
|
109
126
|
return
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
print_success(`Initialized ${label} "${final_name}" at ${dir}`)
|
|
113
130
|
console.log(` ${SKILL_JSON} — manifest`)
|
|
114
131
|
console.log(` ${SKILL_MD} — ${is_kit ? 'kit description' : 'instructions'}`)
|
|
132
|
+
if (linked_agents.length > 0) {
|
|
133
|
+
print_info(`Linked to: ${agents_result.agents.map(a => a.display_name).join(', ')}`)
|
|
134
|
+
}
|
|
115
135
|
console.log()
|
|
116
136
|
print_hint(`Edit these files, then run ${code('happyskills publish')} to share.`)
|
|
117
137
|
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
package/src/commands/publish.js
CHANGED
|
@@ -158,17 +158,19 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// Read base_commit from lock file for divergence check
|
|
161
|
+
// Read base_commit and merge_parents from lock file for divergence check
|
|
162
162
|
const full_name_pre = `${workspace.slug}/${manifest.name}`
|
|
163
163
|
const project_root = find_project_root()
|
|
164
164
|
const [lock_err, lock_data] = await read_lock(project_root)
|
|
165
165
|
let base_commit = null
|
|
166
|
+
let merge_parents = null
|
|
166
167
|
if (!lock_err && lock_data) {
|
|
167
168
|
const all_skills = get_all_locked_skills(lock_data)
|
|
168
169
|
const suffix = `/${skill_name}`
|
|
169
170
|
const lock_key = Object.keys(all_skills).find(k => k.endsWith(suffix))
|
|
170
171
|
if (lock_key && all_skills[lock_key]) {
|
|
171
172
|
base_commit = all_skills[lock_key].base_commit || null
|
|
173
|
+
merge_parents = all_skills[lock_key].merge_parents || null
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -187,14 +189,18 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
187
189
|
spinner.update(`Uploading files (${completed}/${total})...`)
|
|
188
190
|
}
|
|
189
191
|
const visibility = args.flags.public ? 'public' : 'private'
|
|
190
|
-
const
|
|
192
|
+
const push_options = {
|
|
191
193
|
version: manifest.version,
|
|
192
194
|
message: `Release ${manifest.version}`,
|
|
193
195
|
files: skill_files,
|
|
194
196
|
visibility,
|
|
195
197
|
base_commit: force ? null : base_commit,
|
|
196
198
|
force
|
|
197
|
-
}
|
|
199
|
+
}
|
|
200
|
+
if (merge_parents && !force) {
|
|
201
|
+
push_options.parent_shas = merge_parents
|
|
202
|
+
}
|
|
203
|
+
const [push_err, push_data] = await smart_push(workspace.slug, manifest.name, push_options, on_progress)
|
|
198
204
|
if (push_err) {
|
|
199
205
|
const last = push_err[push_err.length - 1]
|
|
200
206
|
if (last?.message?.includes('diverged') || last?.message?.includes('DIVERGED')) {
|
|
@@ -226,6 +232,8 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
226
232
|
base_integrity: (!hash_err && integrity) ? integrity : null
|
|
227
233
|
}
|
|
228
234
|
if (!hash_err && integrity) updated_entry.integrity = integrity
|
|
235
|
+
delete updated_entry.merge_parents
|
|
236
|
+
delete updated_entry.conflict_files
|
|
229
237
|
const updated_skills = update_lock_skills(lock_data, { [lock_key]: updated_entry })
|
|
230
238
|
await write_lock(project_root, updated_skills)
|
|
231
239
|
} else {
|
package/src/commands/pull.js
CHANGED
|
@@ -205,6 +205,8 @@ const run = (args) => catch_errors('Pull failed', async () => {
|
|
|
205
205
|
version: cmp_data.head_version || lock_entry.version,
|
|
206
206
|
ref: clone_data.ref || lock_entry.ref
|
|
207
207
|
}
|
|
208
|
+
delete updated_entry.merge_parents
|
|
209
|
+
delete updated_entry.conflict_files
|
|
208
210
|
const merged_skills = update_lock_skills(lock_data, { [skill_name]: updated_entry })
|
|
209
211
|
const [wl_err] = await write_lock(lock_root(is_global, project_root), merged_skills)
|
|
210
212
|
if (wl_err) { spinner.fail('Failed to write lock file'); throw wl_err[0] }
|
|
@@ -440,8 +442,10 @@ const run = (args) => catch_errors('Pull failed', async () => {
|
|
|
440
442
|
}
|
|
441
443
|
if (conflict_files.length > 0) {
|
|
442
444
|
updated_entry.conflict_files = conflict_files
|
|
445
|
+
delete updated_entry.merge_parents // Conflicts → rebase fallback
|
|
443
446
|
} else {
|
|
444
447
|
delete updated_entry.conflict_files
|
|
448
|
+
updated_entry.merge_parents = [lock_entry.base_commit, cmp_data.head_commit]
|
|
445
449
|
}
|
|
446
450
|
const merged_skills = update_lock_skills(lock_data, { [skill_name]: updated_entry })
|
|
447
451
|
const [wl_err] = await write_lock(lock_root(is_global, project_root), merged_skills)
|
|
@@ -74,7 +74,19 @@ const validate_description = (fm) => {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (desc.length > 1024) {
|
|
77
|
-
|
|
77
|
+
const deficit = desc.length - 1024
|
|
78
|
+
results.push({
|
|
79
|
+
...result('description', 'max_length', 'error', `Description is ${desc.length} chars (max 1024). Must reduce by ${deficit} chars.`, desc),
|
|
80
|
+
recommendations: [
|
|
81
|
+
'STEP 1 - AUDIT: Before changing anything, read the skill\'s routing table or capability list. Map each phrase in the description to the capability it triggers. Mark each phrase as: IDENTITY (describes what the skill is, usually one phrase), UNIQUE (the only phrase matching a specific capability), or REINFORCING (overlaps with another phrase\'s coverage).',
|
|
82
|
+
'STEP 2 - LOSSLESS COMPRESSION: Apply these transformations that reduce characters without changing semantic meaning: (a) Remove articles (a, an, the). (b) Remove possessives (my, your) when the subject is implied. (c) Remove filler verbs (do, does, can, have, is, am). (d) Merge parallel structures that share the same verb (e.g., \'install kit. publish kit\' becomes \'install, publish kits\') or the same object (e.g., \'find kits, search kits\' becomes \'find, search kits\'). Stop here if now under the limit.',
|
|
83
|
+
'STEP 3 - LOSSY COMPRESSION (only if still over the limit after step 2): Remove REINFORCING phrases only. When two phrases overlap semantically, remove the more general one and keep the more specific one. In synonym clusters, keep the two most commonly used verbs and remove the rest.',
|
|
84
|
+
'NEVER remove an IDENTITY phrase — it anchors the understanding of the skill\'s purpose and provides context for all other trigger phrases.',
|
|
85
|
+
'NEVER remove a UNIQUE trigger phrase — it is the only semantic path to that capability. Removing it makes that capability unreachable via auto-invocation.',
|
|
86
|
+
'NEVER rephrase a trigger in a way that changes the core verb or noun (e.g., \'how many skills installed\' must not become \'skill count\'). The original phrasing matches how users naturally speak.',
|
|
87
|
+
'STEP 4 - VERIFY: Cross-check the shortened description against the skill\'s routing table or capability list. Every documented capability must still have at least one semantically matching phrase in the description. If any capability lost coverage, restore its trigger phrase and find different savings.'
|
|
88
|
+
]
|
|
89
|
+
})
|
|
78
90
|
} else {
|
|
79
91
|
results.push(result('description', 'max_length', 'pass', `Description: ${desc.length} chars (max 1024)`))
|
|
80
92
|
}
|