skill
skill-tree
Version control and evolution for AI agent skills — versioned storage, dynamic serving/loadouts, multi-agent git sync, and cross-repo federation.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/alexngai/skill-tree itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 26, 2026skill-tree
Version control and structured discovery for your AI agents' skills. skill-tree
turns a folder of agent skills into a versioned, navigable library: skills are
organized in a taxonomy tree and linked into a relationship graph, then served through
progressive disclosure — so an agent finds the right few skills (and the ones they
depend on) without flooding the context window. Fully compatible with the open
Agent Skills / OpenSkills SKILL.md format your tools
already read.
npm install skill-tree
Why skill-tree?
Reusable skills — SKILL.md playbooks an agent can load on demand — are fast
becoming how agents carry expertise between sessions. But a plain folder of Markdown
files doesn't scale:
- Skills drift. Edit one and the old, working version is gone — no history, no rollback, no diff.
- Skills don't fit. Twenty skills won't fit in one context window. You need to serve the right few for the task, within a token budget.
- Skills are siloed. They live in one repo, for one agent. Sharing means copy-paste, and the copies rot.
skill-tree treats a skill library like a codebase — versioned, served within a budget, and synced across agents and repos — without leaving the standard format:
| 🗂 Versioned | Semantic versioning with full lineage. Fork, merge, roll back, and diff skills like code. |
| 🌳 Tree & graph discovery | Skills sit in a hierarchical taxonomy and are linked by typed relationships (depends_on, extends, alternative). Expanding one surfaces what it builds on — nested, not flat. |
| 🎯 Context-budgeted serving | Dynamic loadouts select the right skills for a task and fit them to a token budget, with progressive expand/collapse. |
| 🔄 Multi-agent sync | Git-backed synchronization so a team of agents shares one evolving skill set. |
| 🌐 Federation | Browse, import, and share skills across independent repositories. |
| 🧩 OpenSkills-native | Reads and writes standard SKILL.md; materializes to .claude/skills/, .agent/skills/, and AGENTS.md so agents discover skills automatically. |
| 📥 Import from anywhere | Pull skills from SkillNet, local folders, or the indexer — all versioned on arrival. |
Inspired by research on skill libraries (arXiv:2512.17102), Claudeception, and OpenSkills. Requires Node.js 20+.
Works with your coding agent
Point a coding agent — Claude Code, or anything that
can run a shell — at the skill-tree CLI, and it discovers and loads the right skill on
its own: skill-tree search to find one, skill-tree read to load its full
instructions. (Or materialize skills into .claude/skills/ + AGENTS.md so they're
picked up natively — see the Materialization section below.)
Quick Start
import { createSkillBank, type Skill } from 'skill-tree';
// A skill bank backed by the filesystem (JSON source of truth + SQLite cache)
const bank = createSkillBank({ storage: { basePath: './skills' } });
await bank.initialize();
// Save a skill
await bank.saveSkill({
id: 'typescript-esm-import-fix',
name: 'TypeScript ESM Import Fix',
version: '1.0.0',
description: 'Fix TypeScript ES module import errors',
instructions:
'Add .js extension to all relative imports, even for .ts files.\n\n' +
'Run `tsc --noEmit` to verify no import errors.',
author: 'me',
tags: ['typescript', 'esm'],
createdAt: new Date(),
updatedAt: new Date(),
status: 'active',
} as Skill);
// Search, then evolve it into a new version
const hits = await bank.searchSkills('typescript');
await bank.createVersion('typescript-esm-import-fix', {
solution: 'Add .js extension, or use "moduleResolution": "bundler".',
}, { bumpType: 'minor' });
await bank.shutdown();
That's the whole loop: store → search → version. Everything below builds on it.
Core Concepts
Skill
A skill is a reusable piece of knowledge, stored as SKILL.md (YAML frontmatter +
Markdown body) with versioning and lineage:
interface Skill {
id: string; // Unique identifier (kebab-case)
name: string; // Human-readable name
version: string; // Semantic version
description: string; // Short summary for discovery and matching
instructions: string; // Free-form markdown body (the SKILL.md content)
tags: string[]; // Categorization
status: SkillStatus; // 'draft' | 'active' | 'deprecated' | 'experimental'
// ... namespace, taxonomy, lineage, serving metadata
}
SkillBank
The main orchestrator, created via createSkillBank(config). One interface for CRUD,
versioning, serving, sync, and federation.
Storage
Two backends — swap them with one config field:
// Filesystem: JSON source of truth + SQLite cache (FTS5 full-text search)
createSkillBank({ storage: { basePath: './skills' } });
// Memory: fast, ephemeral — ideal for tests
createSkillBank({ storage: { type: 'memory' } });
On disk, skills follow the OpenSkills layout:
skills/
├── my-skill/
│ ├── SKILL.md # Skill content (YAML + Markdown)
│ └── .skilltree.json # Metadata and lineage
└── .versions/
└── my-skill/
├── 1.0.0.json # Version snapshots
└── 1.1.0.json
Features
Versioning & lineage
Treat skills like code — semantic versions, forks, merges, rollbacks, and diffs:
// Create a new version
const v2 = await bank.createVersion('my-skill', updates, {
bumpType: 'minor',
changelog: 'Added alternative solution',
});
const history = await bank.getVersionHistory('my-skill');
const restored = await bank.rollbackSkill('my-skill', '1.0.0');
const diff = await bank.compareVersions('my-skill', '1.0.0', '2.0.0');
// Fork for a specialized variant
const forked = await bank.forkSkill('my-skill', {
newId: 'my-skill-react',
newName: 'My Skill (React variant)',
reason: 'Specialized for React projects',
});
Nested discovery — a skill graph, not a flat list
skill-tree is a tree, not a bucket. Each skill can carry a place in a hierarchical taxonomy and typed relationships to other skills, so a large library stays navigable instead of collapsing into one giant prompt:
await bank.saveSkill({
id: 'react-component-testing',
// ...name, version, description, instructions...
taxonomy: { primaryPath: ['Development', 'Frontend', 'Testing'] },
relationships: [
{ targetSkillId: 'jest-setup', type: 'depends_on', confidence: 0.9 },
{ targetSkillId: 'vitest-migration', type: 'alternative', confidence: 0.7 },
],
} as Skill);
Agents then discover skills by traversal, not by dumping. They see a compact catalog (names + one-line descriptions), expand the one they need, and its dependencies come along automatically — one hop down the graph, within the budget:
const { server } = await bank.createServingLayer({
maxExpanded: 5,
autoExpandRelated: true, // expanding a skill pulls in its depends_on / related neighbors
});
await server.setLoadoutForTask('Add tests to a React component');
server.agentExpandSkill('react-component-testing');
// → jest-setup (a `depends_on` neighbor) expands too — no extra call, still budgeted
Skills can also declare context triggers (serving.autoExpand) that surface them when
the task matches — so the right branch of the tree opens on its own.
Serving layer — fit the right skills in the context window
The serving layer builds dynamic loadouts: it selects skills for the current task and renders them into a system prompt within a token budget, with progressive expand/collapse so full instructions load only when needed.
const { server } = await bank.createServingLayer({ maxExpanded: 5 });
// Orchestrator picks a loadout for the task...
await server.setLoadoutForTask('Fix authentication bug');
// ...or use a built-in profile
await server.setLoadoutFromProfile('debugging');
// Render into a system prompt (budget-aware)
const prompt = server.renderSystemPrompt();
// Agent-side API: browse compact, expand on demand
const view = server.agentListLoadout();
server.agentExpandSkill('some-skill-id');
await server.agentRequestSkills(['another-skill']);
Want ranked loadouts driven by live usage metrics? skill-tree stays deliberately
metric-free (see Design notes) — rank externally, then pin the winners
with include: [...], a presence guarantee that keeps every listed skill in the
result, in order. Pair with maxSkills: include.length for "exactly these N."
Multi-agent sync
Git-backed synchronization so a fleet of agents shares one evolving skill set:
import { createDefaultSyncConfig } from 'skill-tree';
const bank = createSkillBank({
storage: { basePath: './skills' },
sync: {
config: createDefaultSyncConfig({
repoUrl: 'git@github.com:org/skills.git',
agentId: 'agent-1',
}),
pullOnInit: true,
},
});
await bank.initialize(); // auto-pulls remote changes
await bank.sync.push();
await bank.sync.pull();
await bank.shutdown(); // flushes pending sync
Federation
Connect independent skill repositories and move skills between them:
await bank.federation.addRemote('team', {
url: 'git@github.com:org/team-skills.git',
access: 'read-write',
});
const remoteSkills = await bank.federation.browse('team');
await bank.federation.import('team', 'useful-pattern');
await bank.federation.share('my-skill', 'team');
const updates = await bank.federation.checkUpstream();
Materialization — agents discover skills automatically
Skills stored in .skilltree/ are symlinked to standard discovery paths, and
AGENTS.md is regenerated on every change — so agents following the
Agent Skills standard find them with no extra wiring.
const bank = createSkillBank({
storage: { basePath: './my-project' },
materialization: {
enabled: true,
symlinkPaths: ['.claude/skills', '.agent/skills'],
agentsMdPath: './AGENTS.md',
agentsMdFormat: 'xml', // 'xml' | 'markdown' | 'json'
debounceMs: 500,
},
});
await bank.initialize(); // symlinks created, AGENTS.md generated
await bank.saveSkill(mySkill); // symlinks + AGENTS.md updated automatically
Agents activate a skill on demand — OpenSkills-compatible progressive disclosure:
skill-tree read my-skill # print a skill's SKILL.md to stdout
skill-tree read skill-one,skill-two # or several at once
Import from anywhere
Every import path funnels through one converter and lands as a versioned skill.
SkillNet — the public index of GitHub-hosted skills (search is free, no key):
skill-tree skillnet search "pdf" --mode vector --threshold 0.85
skill-tree skillnet import "pdf" --limit 5
skill-tree skillnet import --url https://github.com/owner/repo/tree/main/skills/foo
Local folders — a single SKILL.md or a whole tree of them:
skill-tree import ./downloaded-skills # directory (auto-detected)
skill-tree import ./skills/deploy-helper/SKILL.md
Both are available programmatically too (createSkillNetClient,
importLocalSkillDir, importSkillMdFile, skillFromSkillMd).
Events & hooks
React to changes with simple listeners or advanced async hooks:
const unsubscribe = bank.on((event) => {
if (event.type === 'skill:created') console.log('New skill:', event.skill.name);
});
bank.getHookRegistry().register({
event: 'storage:after-save',
handler: async (ctx) => { /* ... */ },
priority: 'normal',
});
Namespaces for teams
Multi-tier skill trees with scope and visibility:
const bank = createSkillBank({
storage: { basePath: './skills' },
namespace: { agentId: 'agent-1', team: 'frontend', defaultScope: 'personal' },
});
const personal = await bank.listSkills({ scope: 'personal', owner: 'agent-1' });
const teamSkills = await bank.listSkills({ scope: 'team', team: 'frontend' });
API Reference
SkillBank — created via createSkillBank(config):
| Method | Description |
|---|---|
initialize() / shutdown() | Start up (storage, federation, sync) / clean shutdown |
getSkill(id, version?) | Get a skill by ID |
listSkills(filter?) / searchSkills(query) | List with filters / full-text search |
saveSkill(skill) / deleteSkill(id, v?) / deprecateSkill(id) | Write operations |
createVersion(id, updates, opts?) / forkSkill(id, opts) | Evolve a skill |
getVersionHistory(id) / getLineage(id) | Inspect history |
rollbackSkill(id, v) / compareVersions(id, a, b) | Roll back / diff |
on(handler) / off(handler) | Subscribe to events |
getStats() / exportAll() / importSkills(skills) | Utilities |
createServingLayer(config?) | Create a SkillGraphServer |
getStorage() / getHookRegistry() | Access underlying components |
sync / federation | Sync and federation managers (getters) |
Versioning helpers are exported standalone too:
import { bumpVersion, satisfiesRange } from 'skill-tree';
bumpVersion('1.2.3', 'minor'); // '1.3.0'
satisfiesRange('1.5.0', '^1.2.0'); // true
Skill Format
Skills use YAML frontmatter + Markdown body, compatible with Agent Skills:
---
name: typescript-esm-import-fix
description: Fix TypeScript ES module import errors by adding .js extension
version: 1.0.0
status: active
tags: [typescript, esm, imports]
---
TypeScript with ES modules requires explicit `.js` extensions in imports.
1. Add `.js` extension to relative imports
2. Even for `.ts` files, use `.js` in the import path
Run `tsc` and verify no module-resolution errors.
Known Limitations
A couple of sync/federation paths currently take a deliberately simple approach:
- Federation upstream merges don't detect conflicts. When a local skill and its
upstream have both diverged,
updateFromUpstreamperforms a simple field-level merge and reportshadConflicts: false. Review upstream updates before relying on them for skills you've edited locally. (src/federation/federation-manager.ts) - The hierarchical-sync
mergeconflict strategy is a no-op. For cross-tier skill-id conflicts, use thepromoteordelete-lowerstrategies for now. (src/sync/hierarchical-sync-adapter.ts)
Design notes
skill-tree does not track live per-skill usage metrics (usageCount,
successRate, etc. were removed in 0.2.0). Those signals belong to systems that
observe agents actually running (e.g. cognitive-core); persisting stale snapshots
here only caused drift. If you want usage-ranked loadouts, rank in that system and pass
the IDs via include: [...]. See CHANGELOG.md for the migration guide.
Contributing
Contributions welcome! See CONTRIBUTING.md to get set up, and SECURITY.md to report vulnerabilities.