How it works · architecture & data model

How the ainb Skill Manager works

A reconciler for units (skills, agents, commands, mcp-servers, hooks, and Claude plugins) across many tool homes and source repos. State lives in three YAML files — no database. Everything you press maps to an event and a real effect on disk.

← Back to the user guide

The data model — three YAML files, no DB

FileWhat it holds
manifest.yamlYour source of truth: the sources you've added (with a target_layout mapping) and the units you manage. User-authored, git-diffable.
lock.yamlTool-authored resolved state: each unit's pinned SHA, where it deployed on each tool, and usage telemetry (invocation count + last-used).
library.yamlYour own authored skills — a first-class personal library, kept separate from installed units.

All three live under ~/.agents-in-a-box/. Reads and writes are plain serde load/save — reviewable in a PR, never an opaque store.

Discovery — two walkers + a provenance matcher

When you open the screen with an empty manifest, ainb scans disk for things you already have and offers to adopt them.
# Class-A walker — Claude marketplace plugins
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.claude-plugin/plugin.json
    marketplace:<plugin>@<marketplace>

# Class-C walker — orphan units across 9 tool homes
~/.{claude,codex,copilot,gemini,cursor,amazonq,claude-desktop,cline,roo}/{skills,agents,commands,...}/
    one DiscoveredOrphanUnit per on-disk candidate

# Provenance matcher — attribute each finding to its REAL source (pure)
attribute(classA, classC, sources)  Vec<AttributedUnit>
   Marketplace | ExternalRepo (external-dependencies.yaml) | Toolkit | AlreadyAdopted | Local
Plugins are first-class. Class-A reads plugin.json and treats a plugin as a bundle of skills — the exact layout other skill managers deliberately filter out. The provenance matcher resolves an externally-cloned skill back to its gh: upstream (via external-dependencies.yaml) instead of mislabelling it local:.

Sources & target_layout — the fan-out

A source maps unit globs to per-tool destinations, so one upstream publishes to arbitrary subdirs across many tools:

sources:
  - name: my-skills
    uri: gh:owner/repo
    ref: main
    target_layout:
      - glob: "skills/*/SKILL.md"
        home: ".claude/skills"   # where it lands on your machine
        repo: "skills"           # where it lives in the repo

The pure resolve_pair(source, unit_path) translates each unit's path into its (home, repo) pair (first-glob-wins); an empty target_layout falls back to the bootstrap defaults.

Sync — pure planner, IO executors, bidirectional

SyncPlanner::plan_sync(source, mappings, units)  # pure: decide direction per unit
    Vec<SyncAction { direction: ToHome | ToRepo | NoOp }>
        ToHome  apply_to_home()   # fetch upstream bytes, atomic write
        ToRepo  apply_to_repo()   # copy, git add/commit/push (per-source lock)

Unlike one-way installers, ainb does push-back: edit a deployed file, press s, and apply_to_repo commits + pushes your change to the source. A per-source advisory lock at .git/ainb-sync.lock keeps two concurrent syncs from racing.

Drift & catalog — both behind a trait (mockable)

SubsystemProductionTests
DriftGitLsRemoteBackendgit ls-remote, compares deployed SHA vs upstream tip → ✓ / ⚠ / ▲ / ⟷. Background poll on screen open.MockBackend
CatalogSkillsShHttpBackend — reqwest to skills.sh; API key from config.toml [skills].api_key or AINB_SKILLS_API_KEY.MockCatalogBackend (zero network)

Every key → event → effect

KeyEventEffect
mGoToSkillManager / RefreshDiscoveryreload from disk · background drift poll · run walkers · (re-)show banner, clearing any skip-marker
Enter (banner)SkillManagerDiscoveryImportreconcile walker output → write manifest
iOpenAddSource → InputSubmitainb source add <uri> in-process → reload
bOpenBrowse → search → EnterCatalogBackend.search → results modal → install selected
lOpenLibraryrender library.yaml owned skills
u / rUpdate / Removeainb skill update/remove <uri> on the selected unit → notification + reload
cCheckre-trigger background drift scan → status glyphs refresh
sSync / ConflictFlipno peer → SyncPlanner intent + sync: toast; conflict pair → flip shadowed_by
/OpenSearchfilter Units case-insensitively (name / source / kind)

How it's tested

A feature-gated sandbox fixture (build_skill_manager_sandbox) seeds a fake ~/.claude/~/.codex + a bare git remote — used by both in-process tests and a scripts/skill-manager-sandbox.sh up/down launcher for manual TUI testing. Coverage is three tiers: pure unit tests (mapping / uri / sync / provenance / catalog), CLI integration tests (real dispatch), and live tmux tripwires that spawn the real binary, press keys, and assert the captured pane — so every keybind is proven end-to-end against the actual program, not a stub.

← Back to the user guide