@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lythos/skill-deck
2
2
 
3
- ![Coverage](https://img.shields.io/badge/coverage-82%25-brightgreen)
3
+ ![Coverage](https://img.shields.io/badge/coverage-82%25-brightgreen) ![CI](https://img.shields.io/badge/CI-71%20unit%20%2B%2021%20CLI%20BDD-brightgreen) ![Agent BDD](https://img.shields.io/badge/Agent%20BDD-5%20local-blue) ![Intent/Plan](https://img.shields.io/badge/arch-intent%2Fplan%2Fexecute-8A2BE2)
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> [--via <backend>] [--as <alias>] [--type <type>] [--deck <path>]` | Download skill to cold pool and append to skill-deck.toml. |
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
- | `--via <backend>` | Download backend for `add`: `git` or `skills.sh` | `git` |
83
- | `--as <alias>` | Explicit alias for the skill (default: basename of path) | — |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lythos/skill-deck",
3
- "version": "0.9.3",
3
+ "version": "0.9.14",
4
4
  "description": "Declarative skill deck governance — cold pool, working set, deny-by-default",
5
5
  "keywords": [
6
6
  "ai-agent",
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, as: 'foo' })
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
- * Supports multiple backends (git clone, skills.sh) without locking users
7
- * into a single download method.
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: { via?: string; deck?: string; workdir?: string; as?: string; type?: string }) {
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
- let skillSourceDir: string
121
-
122
- if (backend === 'skills.sh' || backend === 'vercel') {
123
- const skillsShLocator = `${parsed.owner}/${parsed.repo}`
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.as || skillName
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 viaFlagIdx = args.indexOf('--via')
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 via = viaFlagIdx >= 0 ? args[viaFlagIdx + 1] : undefined
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
- { flag: '--via <backend>', description: 'Download backend: git (default) | skills.sh' },
46
- { flag: '--as <alias>', description: 'Explicit alias for the skill (default: basename of path)' },
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, { via, deck: deckPath, workdir, as, type })
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 --as to specify different aliases.`
247
+ `Alias collision: "${alias}" appears in [${types.join('], [')}]. Use --alias to specify different aliases.`
248
248
  );
249
249
  }
250
250
  }