How the SkillManager works in ainb-tui
m on the home screen and ainb rehydrates two
YAML files (manifest.yaml + lock.yaml) into three panes
(Sources · Units · Detail), kicks off a background git ls-remote
sweep for the drift glyphs (✓ ⚠ ▲ ⟷), and — if the manifest is empty — runs two
discovery walkers that scan your Claude marketplace cache plus the 9 v1 adapter
tool homes (Claude · Codex · Copilot · Gemini · Cursor · AmazonQ · Claude-Desktop
· Cline · Roo) and pops a banner offering to import everything found.
Only the [s] hotkey is fully wired in the TUI today; the help bar
also advertises [i]/[u]/[c]/[r]/[/]
but those still go through the ainb skill ... CLI in v1.2.
What's wired in the TUI vs what the help bar promises
The bottom help bar reads:
[i]nstall [u]pdate [c]heck [r]emove [s]ync [/]search [?]help [q] quit
Reality is narrower. In v1.2 the SkillManager screen only handles the keys in the left column below as TUI events. The right column is advertised but ships as CLI-only.
| Wired in TUI (v1.2) | Advertised, CLI-only today |
|---|---|
m — open SkillManager · events.rs:2032s — Sync or ConflictFlip · events.rs:1136↑/k ↓/j g/Home G/End — selectionEsc/q — back to HomeEnter/d/s — discovery banner (when visible)
|
i · u · c · r · / — no match arm fires in events.rs:1126-1153Use the CLI instead: ainb skill install <uri>ainb skill update [--check] [uri]ainb skill check [--source <name>] [--json]ainb skill remove <uri>
|
i /
u / c / r / / to events
in the TUI is the natural v1.3 follow-up.The data model — three files, two derived caches
Everything the SkillManager screen renders comes from two YAML files on disk and two in-memory caches the runtime builds from them.
First run — what an empty $AINB_HOME looks like
If ~/.agents-in-a-box/manifest.yaml doesn't exist or has no
units:, the SkillManager opens with a discovery banner
overlay instead of an empty Units panel. The banner is built from two pure
walkers that scan disk for anything that looks like a unit ainb hasn't
adopted yet.
The banner is idempotent: re-entering an already-Visible
banner does nothing (you see the same counts you saw the first time, not a
freshly-walked snapshot). And it never re-fires once [s] has
written the skip-marker.
Every key, every match arm, expanded
Each item below cites the file + line where the keypress hits, the event it synthesises, and the state mutation that follows. Open in order to read the whole path; or jump to whatever's broken.
m Open SkillManager events.rs:2032 → 3543
On the HomeScreen, lowercase m synthesises
AppEvent::GoToSkillManager. The handler does five things in this
exact order, every press:
state.current_screen = SKILL_MANAGERstate.skill_manager_state.reload_from_disk(&ainb_home)— rebuildsSources/Units/Detailfrom the current YAML files. Pure (no writes), so safe to call on every open.state.start_background_drift_load(&ainb_home, GitLsRemoteBackend)— spawns a tokio task; coalesces if a prior scan is in flight (no piling up).run_discovery_walkers(&claude_home)— runs class_a + class_c unconditionally; the cost is inmaybe_show_discovery_banner's gating (manifest non-empty → banner stays Hidden and the walker output is dropped).maybe_show_discovery_banner(state, ainb_home, walker)— flipsbannertoVisible(counts)only when all of: manifest empty, no skip-marker on disk, walker output non-empty.
s Sync the selected unit or ConflictFlip events.rs:1136
The single most overloaded key in the v1.2 SkillManager. The match arm dispatches
on a per-keystroke check of selected_unit_has_conflict_peer(state, ainb_home):
- If a peer exists (the selected unit has
shadowed_by = Some(X), or some other unit shadowsselected.uri) →SkillManagerConflictFlipswaps which side of the pair carries theshadowed_byedge, persists the mutated manifest, and refreshes the view-model. - If no peer →
SkillManagerSyncreloadsskill_manager_statefrom disk (so freshly-deployed paths and usage counters show up) and fires async: <unit-name>info notification so you can see which branch you got. The actual content sync runs out-of-band via the CLI surface (ainb skill sync) — the TUI fires-and-forgets the intent.
Wired separately, the v1.2.1 testing rollup added a fix here: notifications
weren't painting on the SkillManager screen at all because layout.rs
was returning before the render_notifications call on every
registry-routed screen. Latent for many screens; surfaced because T3 needed it.
Enter Import everything the discovery banner found events.rs:3596
Only handled while the banner is Visible or Details.
The cached WalkerOutput is fed into
discovery::reconcile::reconcile, which produces a manifest patch
(new sources + new units). The patch is merged into the on-disk manifest
(dedup by source name + unit URI), the screen view-model is rebuilt, and the
banner flips to Hidden. No skip-marker is written — the import
itself is the "yes" answer.
d Toggle banner details events.rs:3608
While the banner is up: flip between the compact Visible count
("found N things") and the expanded Details view that
enumerates which tool each candidate came from. Underlying counts are the
same; only the rendering changes.
s Skip the discovery banner (only when banner is up) events.rs:3613
While the banner is up, s is intercepted before the
Units-panel arm and writes ~/.agents-in-a-box/.skip-banner.
Subsequent SkillManager opens will read that marker and skip the walkers
entirely (no more banner). To re-enable: just delete the file.
↑↓ / jk / gG Selection navigation events.rs:1148-1151
All five keystrokes hit move_selection with a
SelectionMove enum: Prev / Next /
First / Last. Wraps at list ends. The Detail pane is
recomputed on every move so the right-hand pane mirrors the cursor without an
extra keystroke.
Esc / q Back to Home events.rs:1127
The escape hatch — always exits the SkillManager, even when the banner is visible. Banner state is left as-is; if you re-enter with the manifest still empty and the skip-marker still absent, the banner pops back up with the same counts (per the empty-state idempotency rule).
i u c r / Advertised but CLI-only in v1.2 — no match arm yet
The help bar lists these hotkeys but the SkillManager's key handler in
events.rs:1126-1153 has no match arm for any of them — they
fall through to None and the keystroke is dropped.
Use the CLI instead:
# Install one unit URI to one or more tool adapters. # Source URI must already be `enabled` in the manifest. ainb skill install gh:owner/repo@main/skills/commit \ --targets claude,codex \ --dry-run # preview the plan ainb skill install gh:owner/repo@main/skills/commit \ --targets claude,codex \ --yes # skip the diff/confirm prompt
# Read-only drift check — does NOT touch disk. ainb skill update --check # every locked unit ainb skill update --check gh:owner/repo@main/skills/commit # Apply: re-fetch source, re-resolve SHA, diff vs lockfile, confirm, write. ainb skill update gh:owner/repo@main/skills/commit ainb skill update --all --yes
# v1.2 — drift report against the source's upstream tip. # Same data the Units panel's status column shows (✓ ⚠ ▲ ⟷). ainb skill check # tabular default ainb skill check --source stevie-skills # scope to one source ainb skill check --json # pipeable
# Uninstall one unit from the tools where it's currently deployed. ainb skill remove gh:owner/repo@main/skills/commit ainb skill remove gh:owner/repo@main/skills/commit --targets codex ainb skill remove gh:owner/repo@main/skills/commit --dry-run
# v1.2 — bidirectional reconcile between $HOME edits and the source repo. ainb skill sync # every source ainb skill sync stevie-skills # one source ainb skill sync --to-repo # publish HOME → repo only ainb skill sync --to-home # pull repo → HOME only ainb skill sync --dry-run # print the plan, don't apply
# v1.2 — scan tool session logs, populate LockedUnit.usage. ainb skill usage # every unit ainb skill usage commit # one unit ainb skill usage --verbose # per-tool breakdown
Per-key visual cheatsheet — what each letter actually does
One mini-flow per key. Each cell reads left-to-right: trigger state → event synthesised → resulting state mutation. Same information as the collapsibles above, compressed into a single scan.
The async drift pipeline — what fires after every m
Pressing m kicks off a background git ls-remote sweep so
the Units panel's status column can fill in. Until results arrive every row
shows the muted … placeholder; as the channel drains during each
tick, glyphs paint in.
Three v1.2.1 hardening fixes live in this loop:
GIT_TERMINAL_PROMPT=0+GIT_ASKPASS=/bin/true— without these, an unreachable / private remote freezes the entire TUI on a credential prompt for ~95s.- Argv-smuggle reject — a source URI starting with
-short-circuits beforegit ls-remoteis invoked, plus--terminator separates the option list from the positional URL. - Background poll coalesces — if a previous scan is still in flight when
mis pressed again, no new task spawns.
Sync journey — where the bytes actually move
Pressing s on the Units panel is intent-only. The bytes move through
ainb skill sync, which runs pure planning first, then per-action
execution split between an atomic home-side writer and a git-mediated repo-side writer.
The diagram below shows both paths next to each other; the dashed arrow shows how
the TUI hand-off works.
Two CLI flags shape this loop:
--to-home· suppress theToRepobranch entirely. Useful when you want to pull a teammate's edits into your tool home without publishing your own.--to-repo· the mirror image — publish home edits to the repo without re-pulling.--dry-run· print theSyncActionplan and exit before anyapply_to_*call. Pure-planner output, zero side effects.
Multi-tool deployment — one source, many homes
The v1.2 work added SourceEntry.target_layout: Vec<TargetMapping>,
which decides per-unit-glob where each file lands on each tool. A single source
can publish skills to Claude AND Codex from the same upstream repo.
Onboarding journey — configure an external repo, end to end
"I have a github repo with skills in it. How do those skills end up deployed in my Claude home and my Codex home?". Six steps; ainb does the heavy lifting in step 2 (fetch) and step 3 (resolve + fan-out + atomic write).
What happens when upstream advances (step 6 onward, after time passes):
# Open ainb, press m. The background drift poll re-runs git ls-remote. # Units status column flips from ✓ to ⚠ when the upstream tip has advanced. # CLI equivalent (read-only): $ ainb skill check # Or just for one source: $ ainb skill check --source stevie-skills
# Re-fetch, diff, and apply (writes new SHA + new file hashes to lock.yaml). $ ainb skill update gh:stevie/skills@main/skills/commit # Or in bulk: $ ainb skill update --all --yes # --check is read-only — same data shape as `ainb skill check`. $ ainb skill update --check
# You edited ~/.claude/skills/commit/SKILL.md locally and want to publish # that change back upstream: $ ainb skill sync stevie-skills --to-repo --yes # --to-repo gates the apply_to_repo() branch only (no home pull). # Under the hood: acquire .git/ainb-sync.lock, write_atomic into the # promote-cache, git add/commit/push (gated by AINB_SYNC_SKIP_PUSH).
# Uninstall everywhere it was deployed: $ ainb skill remove gh:stevie/skills@main/skills/commit # Or only from one tool home: $ ainb skill remove gh:stevie/skills@main/skills/commit --targets codex
~/.claude/skills/") shortcuts steps 2 + 3 into a single
[Enter] press — but only synthesises local: URIs,
not gh: ones. To later "promote" a local skill to a git repo,
see ainb skill promote.How a unit gets classified — plugin vs skill vs which tool
Classification by source-type URI
The Uri::parse grammar in ainb-skill-core/src/uri.rs
decides the source-type prefix up front. Every unit URI in the manifest /
lockfile is one of:
| Prefix | Means | Where it ends up |
|---|---|---|
gh:owner/repo | GitHub HTTPS | Cloned + fetched via git ls-remote https://github.com/owner/repo.git |
git:<url> | Any git URL | Passes verbatim to git; file://, ssh, etc. |
gist:<id> | GitHub Gist | Hosts a one-file plugin |
https: / http: | Raw URL | Single-file fetch |
local:<path> | Filesystem-local | Used by Class-C adoption — orphan SKILLs get a local: URI synthesised |
npm: | npm package | Reserved for future plugin packaging |
marketplace:<plugin>@<mp>[@<ver>] | Claude Code marketplace | Maps to /plugin install <plugin>@<mp> |
Classification by directory (the orphan walker)
The Class-C walker doesn't trust URIs because the units it's about to discover
have no URI yet — they're just files sitting in a tool home.
It infers UnitKind from the subdir name:
| Subdir on disk | UnitKind | Layout shape |
|---|---|---|
skills/<name>/SKILL.md | Skill | Directory unit; SKILL.md required |
agents/<name>.md | Agent | Flat-md unit; file-stem is name |
commands/<name>.md | Command | Flat-md |
mcp-servers/<name>/… | McpServer | Directory unit; tool-specific shape |
hooks/<name>.md | Hook | Flat-md (some tools) |
Frontmatter kind: wins when present and parseable; otherwise the
subdir is authoritative. frontmatter_valid: false is recorded on
the discovered unit so the reconciler can flag "best-effort" entries in the
banner.
Per-tool fingerprints — "is this a Claude thing or a Codex thing?"
Each of the 9 v1 adapters has its own canonical layout. The same commit
skill exists in different shapes depending on which tool owns it:
# ~/.claude/skills/commit/SKILL.md (directory unit) --- name: commit kind: skill --- Skill body in markdown.
# ~/.codex/skills/commit/SKILL.md (same shape as Claude) # Codex re-uses the Claude convention for skills; agents live # at ~/.codex/agents/<name>.md (flat-md).
# ~/.cursor/skills/commit/SKILL.md (directory unit; same convention) # Cursor stores agents in ~/.cursor/agents/<name>.md. # The convention is consistent across the 9 v1 adapters; only the home # directory differs.
# ~/.claude/plugins/cache/<marketplace>/<plugin>/<ver>/ (Class-A walker) .claude-plugin/plugin.json # required — identifies a plugin skills/commit/SKILL.md # bundled skill(s) agents/reviewer.md # bundled agent(s) # Plugin scope = entire dir; ainb adopts via `marketplace:plugin@mp` URI.
.claude-plugin/plugin.json. Class-A only walks
~/.claude/plugins/cache/; if you've sideloaded a plugin
elsewhere it won't be discovered automatically — see the gap section below.External vs local — what's scanned, what isn't
You asked specifically: "should there be a Claude scan to find plugins and skills installed from external vs local?". Here's the answer in one table.
| What | Scanned today? | By which walker |
|---|---|---|
Claude marketplace plugins installed via /plugin install | ✓ Yes | Class-A — ~/.claude/plugins/cache/ |
| Orphan skills/agents on disk in any of the 9 tool homes | ✓ Yes | Class-C — ~/.claude/skills/, ~/.codex/skills/, … |
| Codex plugins installed via Codex's own marketplace | ✗ No | — (no Class-A equivalent for Codex) |
| Cursor plugins / extensions | ✗ No | — (Cursor's plugin layout differs; not walked) |
| npm-installed agent packages | ✗ No | — (npm: URI prefix reserved but unused) |
Sideloaded Claude plugins outside plugins/cache/ | ✗ No | — (Class-A's path is hardcoded) |
| Gemini extension folders | ✗ No | — (no ~/.gemini/extensions/ walker) |
| Skills under a manifest entry but with a missing source on disk | Partial | Drift backend returns InSync (can't claim drift on missing repo); silent failure mode |
ainb skill — including
Codex / Cursor / Gemini's own marketplaces.What a "scan for external dependencies" would look like
A v1.3 design that closes this gap would extend the walker tier to:
- Tool-marketplace walkers. One Class-A
sibling per tool whose plugin model differs from Claude's. Each emits the
same
DiscoveredMarketplaceUnitshape and feeds the same reconciler. The hard problem is each tool's plugin metadata format, not the walker plumbing. - Manifest-driven assist. When a manifest
references a unit URI whose source is enabled but whose source-fetch
failed (no
fetched_pathin the lockfile),ainb skill checkcurrently treats it asInSync. A newSourceUnreachablestatus would route to a "would you like to add this dependency?" prompt. - External
plugin.jsonprobe. A new walker that scans$XDG_DATA_HOME+$HOMEfor.claude-plugin/plugin.jsonin non-canonical locations — finds sideloaded plugins regardless of marketplace cache layout.
CLI parity — what's TUI-only, what's CLI-only, what's both
| Capability | TUI | CLI |
|---|---|---|
| Open SkillManager screen | m | — |
| Discovery banner + import all | m → banner → Enter | ainb migrate --discover |
| Selection-pane navigation | ↑↓jkgG | — |
| Bidirectional sync (selected unit) | s (no peer) | ainb skill sync |
Conflict-flip shadowed_by | s (with peer) | — (edit manifest manually) |
| Drift check (visual) | Units column glyphs (async) | ainb skill check [--json] |
| Install one unit | — | ainb skill install <uri> |
| Update / re-fetch | — | ainb skill update [--check] [uri] |
| Remove / uninstall | — | ainb skill remove <uri> |
| Search / filter | — (help bar lies) | ainb skill check --source <name> |
| Usage scan (populate lockfile) | auto on every reload_from_disk (read-only render of usage already in lock) | ainb skill usage [--verbose] |
What's actually tested — the three-tier pyramid + v1.2.1 additions
The SkillManager surface ships with three tiers of coverage that each prove a
different guarantee. The wider the tier, the cheaper the test, the further from
a real binary. The v1.2.1 rollup added 17 tests + 4 proptest properties × 1024
cases across every tier. Final scoped verify on
chore/v12-1-testing tip: 2074 passed / 28 pre-existing failed
across -p ainb-skill-core -p ainb-cli -p ainb.
What each tier actually proves
| Tier | Example test file | What it proves | What it can't catch |
|---|---|---|---|
| T1 · pure | mapping_tests.rsuri_property_tests.rsnow_iso_* |
Algorithm correctness · no I/O · fast (millisecs) | Wiring bugs · disk semantics · concurrent races |
| T2 · CLI | skill_check_tests.rssync_to_repo_tests.rsdrift_tests_integration.rs |
End-to-end CLI flow · real git ls-remote · real bare repo · real fs writes | TUI render path · keystroke routing · screen overlay z-order |
| T3 · TUI (in-process) | tripwire_core_skill_manager_*.rs (TestBackend) |
Buffer contents at render time · component composition | Live event loop · keystroke timing · terminal-side regressions |
| T3 · TUI (live tmux) | tripwire_core_skill_manager_*_live.rs |
Spawn real ainb binary · capture pane · prove the rendered pixels Stevie sees |
Tests skip when tmux unavailable; otherwise nothing — caught the f34d851 freeze the in-process tests missed |
The v1.2.1 bead breakdown
| Bead | Tier | Asserts | Tests |
|---|---|---|---|
| T1 · agents-in-a-box-2ss | T3 live | Pane contains "12 invocations" after seeding usage + pressing m | 1 (passed twice) |
| T2 · agents-in-a-box-h6k | T3 live | ⚠ glyph appears in Units column after detect_all round-trips real git ls-remote file://<bare> | 1 (passed twice) |
| T3 · agents-in-a-box-03u | T3 live | sync: <unit> notification paints after [s] on a unit with no peer; deployed path unchanged | 1 (passed twice) |
| T4 · agents-in-a-box-i2m | T2 | GitLsRemoteBackend InSync / Outdated round-trips · argv-smuggle reject · GIT_TERMINAL_PROMPT no-hang bound 10s | 4 (passed twice) |
| T5 · agents-in-a-box-1jy | T1 | Uri::parse fixed point · arbitrary input no-panic · serde yaml round-trip | 5 (4 props × 1024 cases + 22 edge cases) |
| T6 · agents-in-a-box-kgd | T1 | now_iso() shape + agreement with chrono on representative epochs after the days_from_civil swap | 2 (passed twice) |
| T7 · agents-in-a-box-5we | T1 + T2 | Pre-held lock surfaces SyncInProgress · RAII release · two-thread contention | 3 (passed twice) |
Pre-existing failures (out of scope)
The "28 failed" in the final acceptance run breaks down into three documented buckets that predate the rollup:
agents-in-a-box-3fq— 9 docker tests that require a running docker daemon (×2 because lib + bin both run them). Should be gated by feature flag.agents-in-a-box-1id— ~10 runtime flakes (git config dependence, gpg signing, timing-sensitive renders, plugin-runtime registration races). Documented; will need a separate stabilisation pass.agents-in-a-box-887— 4 integration test files with compile drift from theNewSessionStaterefactor in commitfd8e813. Quarantined with#![cfg(any())]/#[ignore]as part of this rollup so scoped verify could even run; tracked for a migration follow-up.
ainb binary under tmux, send keystrokes via
tmux send-keys, and assert on tmux capture-pane output.
They caught the f34d851 credential-prompt freeze that
every in-process test missed — the regression we'd otherwise have shipped
again next time someone refactored the drift backend.Your current setup — and the matcher that's missing
You asked the right question. Today every skill the discovery walker finds
gets a local: URI even when it was installed from
external-dependencies.yaml or shipped inside a Claude marketplace
plugin. The bug isn't in the walker code per se — the walker simply has no
knowledge of the manifests that would let it attribute provenance.
Audit of your actual ~/.claude/ state
| Where on disk | Count | What ainb thinks today | What it actually is |
|---|---|---|---|
~/.claude/plugins/cache/<mp>/<plugin>/<ver>/ |
13 plugins · 63 SKILL.md | Class-A walks; emits marketplace: URIs but does not read installed_plugins.json |
Missing scope (user/project), version, gitCommitSha — all sitting in installed_plugins.json |
~/.claude/skills/ |
192 top-level dirs | Class-C tags every one as orphan/local | Mixed: hand-authored + 7 known external clones from external-dependencies.yaml (fireworks-tech-graph, ui-ux-pro-max, notebooklm, scrapling-official, reflect, reflect-status, self-reflection) + likely more |
~/.claude/plugins/installed_plugins.json |
13 entries | Never read by any walker | Authoritative scope / version / SHA / install path for every Claude-managed plugin |
<project>/toolkit/external-dependencies.yaml |
3 sections × N entries | Never read by any walker | Declares agent-skills (repo URLs), bundled-skills (toolkit paths), external-packages (uv/npm CLIs) |
The journey that's missing — discover-and-attribute, in four phases
To fix this, ainb needs one new pass: read provenance manifests before walking disk, then correlate each on-disk finding against them. Same walker code, plus one matcher.
The Rust shape it would land in
The smallest correct change: add a Provenance enum to the
discovery output and a matcher pass between walkers and reconciler. Pure
function. No new I/O after Phase 1's manifest reads.
// ainb-cli/src/discovery/provenance.rs (new file) pub enum Provenance { /// Skill is inside a Claude marketplace plugin's installed dir. Marketplace { plugin: String, marketplace: String, version: String, git_commit_sha: Option<String>, scope: PluginScope, // User | Project { path } }, /// Cloned via bootstrap from external-dependencies.yaml.agent-skills. ExternalRepo { repo: String, // e.g. "github.com/owner/repo" version: Option<String>, applies_to: Vec<String>, // claude/codex/copilot/gemini }, /// In external-dependencies.yaml.bundled-skills (lives inside toolkit). Toolkit { path: PathBuf }, /// Already adopted by ainb's manifest — preferred attribution. AlreadyAdopted { uri: Uri }, /// Genuinely local — hand-authored, no upstream. Local, }
// ainb-cli/src/discovery/matcher.rs (new file) pub struct ProvenanceInput { pub installed_plugins: BTreeMap<String, Vec<InstalledPlugin>>, pub external_deps: Option<ExternalDependenciesYaml>, pub manifest: Manifest, } /// Match every Class-A/-C output against the enumerated sources. /// Pure function. No I/O. Same input → same output. pub fn attribute( classA: &[DiscoveredMarketplaceUnit], classC: &[DiscoveredOrphanUnit], sources: &ProvenanceInput, ) -> Vec<AttributedUnit>; pub struct AttributedUnit { pub name: String, pub tool: String, // claude / codex / cursor / … pub kind: UnitKind, pub path: PathBuf, pub provenance: Provenance, // the missing field today }
// ainb-cli/src/discovery/reconcile.rs (modify existing) // Today: every Class-C orphan gets a synthesised local: URI. // New: URI depends on provenance. fn synth_uri(u: &AttributedUnit) -> Uri { match &u.provenance { Provenance::Marketplace { plugin, marketplace, version, .. } => Uri::marketplace(plugin, marketplace, Some(version)), Provenance::ExternalRepo { repo, version, .. } => Uri::parse(&format!("gh:{repo}@{}", version.as_deref().unwrap_or("main"))).unwrap(), Provenance::Toolkit { path } => Uri::parse(&format!("local:{}", path.display())).unwrap(), Provenance::AlreadyAdopted { uri } => uri.clone(), Provenance::Local => Uri::parse(&format!("local:~/.{tool}/skills/{name}", tool = u.tool, name = u.name)).unwrap(), } }
# Surfaces this would unlock — proposed CLI: ainb skill scan # Phase 1+2+3 above, then prints a tree by source: # # ⬡ claude-plugins-official (5 plugins · 12 skills) # └─ discord@0.0.4 (access, configure) # └─ skill-creator@unknown (skill-creator) # ... # ↗ external-deps (7 skills cloned) # └─ fireworks-tech-graph ← github.com/yizhiyanhua-ai/fireworks-tech-graph # └─ ui-ux-pro-max ← github.com/nextlevelbuilder/ui-ux-pro-max-skill # ... # • local (~185 skills — hand-authored) ainb skill scan --json # pipeable ainb skill scan --provenance external # filter by class ainb skill scan --tool claude # filter by tool home
What this fixes — concretely, against your setup
| Today's behaviour | After the matcher lands |
|---|---|
fireworks-tech-graph → discovery banner shows as local: orphan |
→ banner shows under external-deps with URI gh:yizhiyanhua-ai/fireworks-tech-graph; pressing Enter writes the proper source entry |
discord plugin's two skills (access, configure) are walked by Class-A but lose their scope=user + gitCommitSha attribution |
→ Class-A output joined to installed_plugins.json; SHA pinned in the lockfile, drift can be computed against the original commit |
| Sources panel shows: nothing (manifest empty) or hand-rolled sources only | → Sources panel auto-populates with one row per marketplace + one per external-deps clone group + a local bucket |
| Drift detection fails silently for anything without a manifest source | → Every attributed unit has a known upstream; ainb skill check can poll the right ref for the right glyph |
| No way to answer "which of my 192 skills are external?" | → ainb skill scan --provenance external answers in <1s |
Add Source wizard — configuring a sync target without YAML
You're right — telling a user "open manifest.yaml, write four nested
target_layout entries, count the slashes in your glob, get the
static-prefix arithmetic right, then run --dry-run" is a footgun.
The TUI should walk them through it. Proposal: a 5-step modal wizard
triggered by [a] on the Units panel (currently unwired, perfect
candidate). State machine, ASCII mockups, and the implementation slice are
below.
What the user actually wants to express
Four facts, in plain English:
- Which repo are skills going to live in?
(
gh:owner/repo) - Which branch? (default
main) - Which folder inside that repo? (root / toolkit-style / custom)
- Which tools should pull from this source? (claude, codex, …)
The wizard collects these and generates the right
SourceEntry { uri, ref, target_layout, ... } — no YAML
arithmetic required.
The five steps · ASCII mockups
Step 1 — Repo URI
╭─ Add source ──────────────────────────────────────────────────────╮ │ │ │ Step 1 of 5 · Repo URI │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ gh:stevengonsalvez/agents-in-a-box _ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ ✓ valid · resolved → https://github.com/stevengonsalvez/… │ │ │ │ Examples: │ │ gh:owner/repo # GitHub HTTPS │ │ git:ssh://git@host/repo.git # any git URL │ │ local:/path/to/repo # filesystem-local clone │ │ │ │ [Tab] autocomplete from installed plugins [Enter] next │ │ [Esc] cancel │ ╰────────────────────────────────────────────────────────────────────╯
Step 2 — Branch / ref
╭─ Add source ──────────────────────────────────────────────────────╮ │ │ │ Step 2 of 5 · Branch / ref │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ main _ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ Branch, tag, or 40-char SHA. ainb pulls / pushes against │ │ this ref. To pin a release: enter the tag name (e.g. v1.2.1). │ │ │ │ [↑] back [Esc] cancel [Enter] next │ ╰────────────────────────────────────────────────────────────────────╯
Step 3 — Folder layout in the repo
╭─ Add source ──────────────────────────────────────────────────────╮ │ │ │ Step 3 of 5 · Folder layout │ │ │ │ Where do skills / agents / commands live IN the repo? │ │ │ │ ▶ [1] Root skills/ agents/ commands/ │ │ [2] Toolkit toolkit/packages/{skills,agents,commands}/ │ │ [3] Custom enter folder paths manually │ │ │ │ Preview · option [2] toolkit: │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ ~/.claude/skills/commit/SKILL.md │ │ │ │ ⇄ toolkit/packages/skills/commit/SKILL.md (in repo) │ │ │ │ │ │ │ │ ~/.claude/agents/distinguished-engineer.md │ │ │ │ ⇄ toolkit/packages/agents/distinguished-engineer.md │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ [↑↓] choose [Enter] next [Esc] cancel │ ╰────────────────────────────────────────────────────────────────────╯
Option [3] Custom opens three text inputs for skills /
agents / commands paths each. Live preview pane updates as the user
types so they see exactly where files will land before committing.
Step 4 — Target tools
╭─ Add source ──────────────────────────────────────────────────────╮ │ │ │ Step 4 of 5 · Target tools │ │ │ │ Which tools should this source publish to? │ │ │ │ [x] claude (auto: ~/.claude/skills exists) │ │ [x] codex (auto: ~/.codex/skills exists) │ │ [ ] cursor (no ~/.cursor/skills dir) │ │ [ ] gemini │ │ [ ] copilot │ │ [ ] amazonq │ │ [ ] claude-desktop │ │ [ ] cline │ │ [ ] roo │ │ │ │ [Space] toggle [↑↓] move [a] toggle-all [Enter] next │ ╰────────────────────────────────────────────────────────────────────╯
Defaults are auto-detected: any tool whose
~/.<tool>/skills/ directory exists gets pre-checked. User
can override with Space. No tool home → unchecked; the user
shows intent by ticking it.
Step 5 — Preview + save
╭─ Add source ──────────────────────────────────────────────────────╮ │ │ │ Step 5 of 5 · Preview │ │ │ │ Will be appended to ~/.agents-in-a-box/manifest.yaml: │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ sources: │ │ │ │ - name: agents-in-a-box-toolkit │ │ │ │ type: gh │ │ │ │ uri: gh:stevengonsalvez/agents-in-a-box │ │ │ │ ref: main │ │ │ │ enabled: true │ │ │ │ target_layout: │ │ │ │ - glob: "toolkit/packages/skills/*/SKILL.md" │ │ │ │ home: ".claude/skills" │ │ │ │ repo: "toolkit/packages/skills" │ │ │ │ - glob: "toolkit/packages/agents/*.md" │ │ │ │ home: ".claude/agents" │ │ │ │ repo: "toolkit/packages/agents" │ │ │ │ - glob: "toolkit/packages/commands/*.md" │ │ │ │ home: ".claude/commands" │ │ │ │ repo: "toolkit/packages/commands" │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ Units on disk that would match (first 3): │ │ ~/.claude/skills/commit → packages/skills/commit │ │ ~/.claude/skills/deploy → packages/skills/deploy │ │ ~/.claude/agents/dist-eng.md → packages/agents/dist-eng.md │ │ +47 more · press [m] to see full list │ │ │ │ [s]ave [↑] back [c]ancel │ ╰────────────────────────────────────────────────────────────────────╯
State machine + flow
Implementation slice — what would actually ship
| File | Change |
|---|---|
ainb-core/src/app/state.rs |
New AddSourceWizardState { step: WizardStep, uri_input, ref_input, layout: LayoutPreset, custom_paths: [String; 3], selected_tools: BTreeSet<String>, validation_error: Option<String> } on AppState |
ainb-core/src/app/events.rs |
New SkillManagerAddSource event; KeyCode::Char('a') arm on the SkillManager view (events.rs:1136 sibling). New step-navigation events: WizardNext / WizardBack / WizardCancel / WizardSave / WizardToggleTool |
ainb-core/src/components/skill_manager_add_source.rs |
New file. Pure render function for each of the 5 steps. ~250 lines. Reuses focus-ring + text-input widgets from new_session/configure.rs |
ainb-core/src/app/screens/builtin.rs |
SkillManagerScreen::render checks state.add_source_wizard.active; if so, overlays the modal via centered_rect (same pattern as ClaudeChat popup) |
ainb-cli/src/lib.rs |
No change required — wizard ends by writing to the same SourceEntry shape ainb source add already produces. CLI ↔ TUI parity preserved. |
ainb-core/tests/tripwire_core_skill_manager_add_source_wizard.rs |
New TestBackend tripwire: open wizard, type URI, advance 5 steps, save, assert manifest.yaml gained the right block. Plus a live tmux tripwire that drives keystrokes through a real binary. |
Three preset layouts cover ~90% of cases
The wizard's step 3 ships with three pre-canned glob/home/repo triples so the user never types YAML for the common patterns:
| Preset | Repo layout assumed | Generated target_layout |
|---|---|---|
| Root | skills/<name>/SKILL.mdagents/<name>.mdcommands/<name>.md |
BOOTSTRAP_DEFAULT_MAPPINGS · omits target_layout field (uses fallback) |
| Toolkit | toolkit/packages/skills/<name>/SKILL.mdtoolkit/packages/agents/<name>.mdtoolkit/packages/commands/<name>.md |
3 mappings under toolkit/packages/{skills,agents,commands}/ |
| Custom | Anything else | Wizard prompts for skills_path, agents_path, commands_path; emits 3 mappings using whatever the user typed |
Editing an existing source — same wizard, different entrypoint
From the SkillManager's Sources panel, pressing [e] on a
selected source row opens the same wizard pre-populated from the existing
SourceEntry. User can change URI, ref, layout, or tools and save
back over the same source. The matcher (proposed above) means edits don't
re-import — they just update the SourceEntry in place.
FAQ — your explicit questions
- Why does pressing
ssometimes do nothing visible? - Two reasons. (a) Before the v1.2.1 notification-render fix, the
sync: <unit>banner was being eaten bylayout.rsskipping notifications on registry-routed screens. Fixed in7a999cc. (b) The Sync handler only fires the intent + reloads from disk — actual bidirectional content reconciliation runs out-of-band viaainb skill sync. If nothing changed on disk between reloads, the Detail pane looks identical. - Can the TUI tell me which tool a skill belongs to?
- Yes for orphans (Class-C tags each discovered unit with its
toolfield — claude / codex / cursor / …) and yes for installed units (the Units panel'ssourcecolumn shows the manifest source the unit was installed from). What's not rendered today is a per-target deployment list — that information lives inLockedUnit.deployed: BTreeMap<String, DeployedRef>but the Detail pane only surfaces a comma-joined string of target names. - What happens if my
~/.claude/plugins/cache/is empty but I have skills in~/.claude/skills/? - The Class-A walker returns an empty list (no marketplace plugins). Class-C
finds the orphan skills under
~/.claude/skills/and shows them in the banner withtool: claude. PressEnterand the reconciler synthesiseslocal:URIs for each, writes them into the manifest, and the next render shows them in the Units panel under a synthetic source. - How does ainb know a plugin is "a Claude plugin" vs something else?
- The fingerprint is
.claude-plugin/plugin.jsonpresent in the directory. Class-A reads that file to determine plugin name + bundled units (skills, agents). Anything without that file isn't a marketplace plugin in ainb's eyes — it's either an orphan skill/agent (Class-C) or invisible. - If I sideload a Claude plugin outside
plugins/cache/, does ainb find it? - No. Class-A's path is hardcoded. This is a real gap (see the External vs
Local section). The workaround today is to write a manifest source entry by
hand pointing at the sideloaded location (or
gh:URI). - Why does the drift glyph sometimes never appear?
- Three known causes: (1) the unit has no
shain the lockfile — drift backend treats this asInSyncand ✓ paints; (2) the source URI uses a schemesource_to_remote_urlcan't translate (onlygh:,git:,https:,http:are supported); (3) the backend errored — failed units are dropped from the cache silently and the row shows…indefinitely. The v1.2.1 T4 integration test pins each variant against a real bare repo. - How do I avoid the banner ever showing up again?
- Press
swhile the banner is visible. That writes~/.agents-in-a-box/.skip-bannerand every subsequent SkillManager open will skip the walkers entirely. To reset: delete the file. - Can I get a "scan for external dependencies" today?
- Partially.
ainb skill check --jsonreports every unit whose source isn't currentlyInSyncupstream — that surfaces source URIs that need a follow-up. But there's no walker for non-Claude marketplaces, nonpm:walker, no sideloaded-plugin probe. That's the v1.3 design surface.