@lythos/skill-deck 0.9.3 → 0.9.14
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/README.md +90 -4
- package/package.json +1 -1
- package/src/add.test.ts +1 -1
- package/src/add.ts +8 -48
- package/src/cli.ts +5 -7
- package/src/link.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lythos/skill-deck
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+
   
|
|
4
4
|
|
|
5
5
|
> Declarative skill deck governance. Reconcile declared skills against your cold pool via symlinks — deny-by-default, max-cards budgeting, transient expiry.
|
|
6
6
|
|
|
@@ -68,7 +68,7 @@ expires = "2026-05-01" # ISO date; warns at ≤14 days
|
|
|
68
68
|
|---------|------|-------------|
|
|
69
69
|
| `link` | `[--deck <path>] [--workdir <dir>]` | Sync working set. Removes undeclared skills (deny-by-default). |
|
|
70
70
|
| `validate` | `[deck.toml] [--workdir <dir>]` | Validate deck config without modifying files. |
|
|
71
|
-
| `add` | `<locator> [--
|
|
71
|
+
| `add` | `<locator> [--alias <alias>] [--type <type>] [--deck <path>]` | Git clone skill to cold pool and append to skill-deck.toml. |
|
|
72
72
|
| `refresh` | `[<fq|alias>] [--deck <path>]` | Pull latest versions of declared skills from upstream git repos. Pass a name to refresh one skill. |
|
|
73
73
|
| `remove` | `<fq|alias> [--deck <path>]` | Remove skill from deck.toml and working set. Cold pool untouched. |
|
|
74
74
|
| `prune` | `[--yes] [--deck <path>]` | GC cold pool repos no longer referenced. Interactive confirm (skip with `--yes`). |
|
|
@@ -79,8 +79,8 @@ expires = "2026-05-01" # ISO date; warns at ≤14 days
|
|
|
79
79
|
|------|-------------|---------|
|
|
80
80
|
| `--deck <path>` | Path to skill-deck.toml | Find upward from cwd |
|
|
81
81
|
| `--workdir <dir>` | Working directory | cwd |
|
|
82
|
-
|
|
83
|
-
| `--
|
|
82
|
+
|
|
83
|
+
| `--alias <alias>` | Explicit alias for the skill (default: basename of path) | — |
|
|
84
84
|
| `--type <type>` | Target section for `add`: `innate`, `tool`, or `combo` | `tool` |
|
|
85
85
|
|
|
86
86
|
### Safety guards
|
|
@@ -157,6 +157,92 @@ Different agents look for skills in different directories. `skill-deck.toml` con
|
|
|
157
157
|
| `deck add` fails with 404 | Locator format wrong or repo doesn't exist | Format: `github.com/owner/repo/skill-name` (path to skill directory inside repo) |
|
|
158
158
|
| `skill-deck.toml not found` | Running `link` outside project tree | Run from project root, or use `--deck ./path/to/skill-deck.toml` |
|
|
159
159
|
|
|
160
|
+
## K8s-Style Reconciliation: Agent as Controller
|
|
161
|
+
|
|
162
|
+
Deck follows Kubernetes' reconciliation model. The agent (Claude, Cursor, etc.) is the **controller manager** — it reads state, builds a plan, shows it to the user, then executes:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
scan (observe state) → plan (compute diff) → confirm → execute → verify
|
|
166
|
+
↑ │
|
|
167
|
+
└──────────────────── reconciliation loop ─────────────────────┘
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| K8s Concept | Deck Equivalent |
|
|
171
|
+
|-------------|-----------------|
|
|
172
|
+
| Desired state (YAML manifest) | `skill-deck.toml` |
|
|
173
|
+
| Actual state (running pods) | Working set (`~/.claude/skills/`) |
|
|
174
|
+
| Controller manager (reconcile loop) | Agent reads state → builds plan → user confirms |
|
|
175
|
+
| `kubectl apply` | `deck link` |
|
|
176
|
+
| Namespace (isolation) | Per-project deck file |
|
|
177
|
+
| PersistentVolume | Cold pool (`~/.agents/skill-repos/`) |
|
|
178
|
+
|
|
179
|
+
The loop doesn't run automatically (no daemon). The agent is the loop — it observes, plans, confirms, and executes on demand. This is K8s-style **declarative governance**: declare what you want, reconcile to match.
|
|
180
|
+
|
|
181
|
+
## Multi-Agent POSSE Syndication
|
|
182
|
+
|
|
183
|
+
Not "switching between agents" — **syndicating everywhere simultaneously**. Like IndieWeb's POSSE (Publish on your Own Site, Syndicate Elsewhere):
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Cold Pool (~/.agents/skill-repos/) ← canonical "own site"
|
|
187
|
+
↓ deck link --workdir
|
|
188
|
+
├── .claude/skills/ ← syndicate to Claude Code
|
|
189
|
+
├── .cursor/skills/ ← syndicate to Cursor
|
|
190
|
+
├── .codex/skills/ ← syndicate to Codex
|
|
191
|
+
└── .windsurf/skills/ ← syndicate to Windsurf
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
One cold pool, one deck declaration, synced to every agent you use. Adding a new platform is updating a key-value registry — no code changes needed. See [multi-agent-posse-syndication](https://github.com/lythos-labs/lythoskill/blob/main/cortex/wiki/01-patterns/2026-05-05-multi-agent-posse-syndication.md).
|
|
195
|
+
|
|
196
|
+
## Migration: For Existing Skill Users
|
|
197
|
+
|
|
198
|
+
If you already have skills installed (in working set, globally, or mixed), deck respects your existing state:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
1. SCAN Agent surveys: what's in ~/.claude/skills/? What's global? What's mixed?
|
|
202
|
+
curator scan helps — indexes cold pool or existing working set.
|
|
203
|
+
|
|
204
|
+
2. PLAN Agent shows: "We found 12 skills. After migration:
|
|
205
|
+
- 2 → innate (deck infrastructure)
|
|
206
|
+
- 4 → tool section
|
|
207
|
+
- 3 → cold pool (already there, just link)
|
|
208
|
+
- 3 → backup only (unused, stale)
|
|
209
|
+
All 12 backed up to ~/.agents/lythos/backups/<date>.tar.gz"
|
|
210
|
+
|
|
211
|
+
3. BACKUP Always. `link` creates tar backups for non-symlink entries before removal.
|
|
212
|
+
Use `--no-backup` only if you're certain.
|
|
213
|
+
|
|
214
|
+
4. EXECUTE deck link — creates symlinks, removes undeclared, leaves real files untouched.
|
|
215
|
+
|
|
216
|
+
5. VERIFY Agent checks: all declared skills resolve? Working set clean?
|
|
217
|
+
If unhappy: tar xf backup → rollback to pre-migration state.
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Key principle**: existing skill users aren't beginners. They have working setups. Migration is a conversation — scan, show the plan, confirm before acting. Backup is non-negotiable.
|
|
221
|
+
|
|
222
|
+
## Architecture: Intent / Plan / Execute
|
|
223
|
+
|
|
224
|
+
Deck commands separate pure logic from IO:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
deck.toml → RefreshPlan / PrunePlan (pure) → execute with injectable IO
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Plan**: `buildRefreshPlan()`, `buildPrunePlan()` — pure functions, unit-testable
|
|
231
|
+
- **Execute**: `executeRefreshPlan(plan, io)`, `executePrunePlan(plan, io)` — IO injected (`gitPull`, `delete`, `log`)
|
|
232
|
+
- **Config**: `workdir`, `coldPool`, `deckPath` all accept explicit overrides, defaults are fallback
|
|
233
|
+
|
|
234
|
+
This enables testing without real git operations — inject mock `gitPull`, capture `log` output, assert expected behavior.
|
|
235
|
+
|
|
236
|
+
## Test Coverage
|
|
237
|
+
|
|
238
|
+
| Layer | Count | CI | Notes |
|
|
239
|
+
|-------|-------|----|-------|
|
|
240
|
+
| Unit tests | 71 | ✅ | Plan generation, link, add, remove, schema |
|
|
241
|
+
| CLI BDD | 21 | ✅ | End-to-end via real CLI invocations in tmpdir |
|
|
242
|
+
| Agent BDD | 5 | ❌ | Requires `claude -p` CLI; `.agent.test.ts` convention |
|
|
243
|
+
|
|
244
|
+
Coverage is honest — no gate, no inflation. Agent BDD scenarios run locally only.
|
|
245
|
+
|
|
160
246
|
## More Documentation
|
|
161
247
|
|
|
162
248
|
- **Skill layer** (agent-facing instructions):
|
package/package.json
CHANGED
package/src/add.test.ts
CHANGED
|
@@ -137,7 +137,7 @@ describe('addSkill', () => {
|
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
139
|
const { addSkill } = await import('./add.ts')
|
|
140
|
-
await addSkill('github.com/owner/repo-b', { workdir: projectDir, deck: deckPath,
|
|
140
|
+
await addSkill('github.com/owner/repo-b', { workdir: projectDir, deck: deckPath, alias: 'foo' })
|
|
141
141
|
expect(false).toBe(true) // should not reach here
|
|
142
142
|
} catch (err: any) {
|
|
143
143
|
expect(exitCode).toBe(1)
|
package/src/add.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* deck-add.ts — Skill acquisition command
|
|
4
4
|
*
|
|
5
5
|
* Downloads a skill to the cold pool, updates skill-deck.toml, and links.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Single backend: git clone. For feed-based discovery with decision tracking,
|
|
7
|
+
* use curator add instead.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
@@ -16,7 +16,6 @@ import { parse as parseToml, stringify as stringifyToml } from '@iarna/toml'
|
|
|
16
16
|
import { findDeckToml, expandHome } from './link.js'
|
|
17
17
|
import { parseDeck } from './parse-deck.js'
|
|
18
18
|
|
|
19
|
-
const CLAUDE_SKILLS_DIR = join(homedir(), '.claude', 'skills')
|
|
20
19
|
|
|
21
20
|
interface ParsedLocator {
|
|
22
21
|
host: string
|
|
@@ -76,7 +75,7 @@ function resolvePath(p: string): string {
|
|
|
76
75
|
return resolve(p)
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
export async function addSkill(locator: string, options: {
|
|
78
|
+
export async function addSkill(locator: string, options: { deck?: string; workdir?: string; alias?: string; type?: string }) {
|
|
80
79
|
const workdir = options.workdir ? resolvePath(options.workdir) : process.cwd()
|
|
81
80
|
const deckPath = options.deck
|
|
82
81
|
? resolvePath(options.deck)
|
|
@@ -89,8 +88,6 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
89
88
|
process.exit(1)
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
const backend = options.via || 'git'
|
|
93
|
-
|
|
94
91
|
let coldPool = join(homedir(), '.agents', 'skill-repos')
|
|
95
92
|
if (existsSync(deckPath)) {
|
|
96
93
|
try {
|
|
@@ -117,47 +114,10 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
117
114
|
const tmpRepo = join(tmpDir, 'repo')
|
|
118
115
|
|
|
119
116
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(`📦 Downloading via skills.sh: ${skillsShLocator}`)
|
|
125
|
-
|
|
126
|
-
// Snapshot existing directories in ~/.claude/skills/
|
|
127
|
-
const beforeDirs = existsSync(CLAUDE_SKILLS_DIR)
|
|
128
|
-
? new Set(readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true })
|
|
129
|
-
.filter(e => e.isDirectory())
|
|
130
|
-
.map(e => e.name))
|
|
131
|
-
: new Set<string>()
|
|
132
|
-
|
|
133
|
-
execFileSync('npx', ['skills', 'add', skillsShLocator, '-g'], { cwd: tmpDir, stdio: 'inherit' })
|
|
134
|
-
|
|
135
|
-
// Detect the newly installed directory
|
|
136
|
-
const afterDirs = existsSync(CLAUDE_SKILLS_DIR)
|
|
137
|
-
? readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true })
|
|
138
|
-
.filter(e => e.isDirectory())
|
|
139
|
-
.map(e => e.name)
|
|
140
|
-
: []
|
|
141
|
-
const newDirs = afterDirs.filter(d => !beforeDirs.has(d))
|
|
142
|
-
|
|
143
|
-
if (newDirs.length === 0) {
|
|
144
|
-
console.error(`❌ skills.sh installed nothing new to ~/.claude/skills/`)
|
|
145
|
-
console.error(` The skill may already be installed, or the install failed.`)
|
|
146
|
-
process.exit(1)
|
|
147
|
-
}
|
|
148
|
-
if (newDirs.length > 1) {
|
|
149
|
-
console.warn(`⚠️ Multiple new directories detected in ~/.claude/skills/`)
|
|
150
|
-
console.warn(` Using the first one: ${newDirs[0]}`)
|
|
151
|
-
}
|
|
152
|
-
const installedName = newDirs[0]
|
|
153
|
-
skillSourceDir = join(CLAUDE_SKILLS_DIR, installedName)
|
|
154
|
-
console.log(` Detected install: ${installedName}`)
|
|
155
|
-
} else {
|
|
156
|
-
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
157
|
-
console.log(`📦 Cloning: ${gitUrl}`)
|
|
158
|
-
execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpRepo], { stdio: 'inherit' })
|
|
159
|
-
skillSourceDir = tmpRepo
|
|
160
|
-
}
|
|
117
|
+
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
118
|
+
console.log(`📦 Cloning: ${gitUrl}`)
|
|
119
|
+
execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpRepo], { stdio: 'inherit' })
|
|
120
|
+
let skillSourceDir = tmpRepo
|
|
161
121
|
|
|
162
122
|
if (!existsSync(skillSourceDir)) {
|
|
163
123
|
console.error(`❌ Download failed: expected output not found at ${skillSourceDir}`)
|
|
@@ -175,7 +135,7 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
175
135
|
}
|
|
176
136
|
|
|
177
137
|
const skillName = parsed.skill ? basename(parsed.skill) : parsed.repo
|
|
178
|
-
const alias = options.
|
|
138
|
+
const alias = options.alias || skillName
|
|
179
139
|
const skillType = (options.type || 'tool').toLowerCase()
|
|
180
140
|
|
|
181
141
|
if (!['innate', 'tool', 'combo'].includes(skillType)) {
|
package/src/cli.ts
CHANGED
|
@@ -14,14 +14,12 @@ const command = args[0]
|
|
|
14
14
|
|
|
15
15
|
const deckFlagIdx = args.indexOf('--deck')
|
|
16
16
|
const workdirFlagIdx = args.indexOf('--workdir')
|
|
17
|
-
const
|
|
18
|
-
const asFlagIdx = args.indexOf('--as')
|
|
17
|
+
const aliasFlagIdx = args.indexOf('--alias')
|
|
19
18
|
const typeFlagIdx = args.indexOf('--type')
|
|
20
19
|
|
|
21
20
|
const deckPath = deckFlagIdx >= 0 ? args[deckFlagIdx + 1] : undefined
|
|
22
21
|
const workdir = workdirFlagIdx >= 0 ? args[workdirFlagIdx + 1] : undefined
|
|
23
|
-
const
|
|
24
|
-
const as = asFlagIdx >= 0 ? args[asFlagIdx + 1] : undefined
|
|
22
|
+
const alias = aliasFlagIdx >= 0 ? args[aliasFlagIdx + 1] : undefined
|
|
25
23
|
const type = typeFlagIdx >= 0 ? args[typeFlagIdx + 1] : undefined
|
|
26
24
|
const noBackup = args.includes('--no-backup')
|
|
27
25
|
const yes = args.includes('--yes')
|
|
@@ -42,8 +40,8 @@ const HELP_CONFIG = {
|
|
|
42
40
|
{ flag: '--deck <path>', description: 'Specify skill-deck.toml path (default: find upward from cwd)' },
|
|
43
41
|
{ flag: '--workdir <dir>', description: 'Specify working directory (default: cwd)' },
|
|
44
42
|
{ flag: '--no-backup', description: 'Skip tar backup when removing non-symlink entries' },
|
|
45
|
-
|
|
46
|
-
{ flag: '--
|
|
43
|
+
|
|
44
|
+
{ flag: '--alias <name>', description: 'Explicit alias for the skill (default: basename of path)' },
|
|
47
45
|
{ flag: '--type <type>', description: 'Target section: innate | tool | combo (default: tool)' },
|
|
48
46
|
{ flag: '--yes', description: 'Skip interactive confirmation (for prune)' },
|
|
49
47
|
],
|
|
@@ -63,7 +61,7 @@ switch (command) {
|
|
|
63
61
|
console.error('❌ Missing locator. Usage: deck add <github.com/owner/repo[/skill]>')
|
|
64
62
|
process.exit(1)
|
|
65
63
|
}
|
|
66
|
-
await addSkill(locator, {
|
|
64
|
+
await addSkill(locator, { deck: deckPath, workdir, alias, type })
|
|
67
65
|
break
|
|
68
66
|
}
|
|
69
67
|
case 'refresh': {
|
package/src/link.ts
CHANGED
|
@@ -244,7 +244,7 @@ for (const d of declared) {
|
|
|
244
244
|
for (const [alias, types] of aliasToTypes) {
|
|
245
245
|
if (types.length > 1) {
|
|
246
246
|
errors.push(
|
|
247
|
-
`Alias collision: "${alias}" appears in [${types.join('], [')}]. Use --
|
|
247
|
+
`Alias collision: "${alias}" appears in [${types.join('], [')}]. Use --alias to specify different aliases.`
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
250
|
}
|