Skip to content
Conduit

skill

skill-tree

Version control and evolution for AI agent skills — versioned storage, dynamic serving/loadouts, multi-agent git sync, and cross-repo federation.

alexngai/skill-treecommunitylistedunreviewed

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

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 26, 2026

skill-tree

npm version license CI

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:

🗂 VersionedSemantic versioning with full lineage. Fork, merge, roll back, and diff skills like code.
🌳 Tree & graph discoverySkills 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 servingDynamic loadouts select the right skills for a task and fit them to a token budget, with progressive expand/collapse.
🔄 Multi-agent syncGit-backed synchronization so a team of agents shares one evolving skill set.
🌐 FederationBrowse, import, and share skills across independent repositories.
🧩 OpenSkills-nativeReads and writes standard SKILL.md; materializes to .claude/skills/, .agent/skills/, and AGENTS.md so agents discover skills automatically.
📥 Import from anywherePull 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):

MethodDescription
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 / federationSync 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, updateFromUpstream performs a simple field-level merge and reports hadConflicts: false. Review upstream updates before relying on them for skills you've edited locally. (src/federation/federation-manager.ts)
  • The hierarchical-sync merge conflict strategy is a no-op. For cross-tier skill-id conflicts, use the promote or delete-lower strategies 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.

License

MIT

↑↓ navigate openesc close