Research & Learning · feature deep-dive

How the SkillManager works in ainb-tui

TL;DR — Press 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:2032
s — Sync or ConflictFlip · events.rs:1136
↑/k ↓/j g/Home G/End — selection
Esc/q — back to Home
Enter/d/s — discovery banner (when visible)
i · u · c · r · / — no match arm fires in events.rs:1126-1153
Use the CLI instead:
ainb skill install <uri>
ainb skill update [--check] [uri]
ainb skill check [--source <name>] [--json]
ainb skill remove <uri>
The help bar reflects the v1 design intent. The v1.2 work focused on usage, sync, and drift; wiring 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.

ON DISK · ~/.agents-in-a-box/ manifest.yaml sources: [SourceEntry] units: [UnitEntry] User-authored. Source of truth. lock.yaml sources: [LockedSource] (paths) units: [LockedUnit] (SHA, usage) Tool-authored. Resolved state. 9 ADAPTER TOOL HOMES ~/.claude/{skills,agents,commands,…} ~/.codex/{skills,agents,mcp-servers} ~/.copilot/, ~/.gemini/, ~/.cursor/ ~/.amazonq/, ~/.cline/, ~/.roo/ ~/.claude-desktop/ Where deployed unit files actually live. AINB_TOOL_HOME_<TOOL> sandbox in tests. IN MEMORY · SkillsScreenData sources: Vec<SourceRow> units: Vec<UnitRow> selected: usize detail: Option<UnitDetail> rebuilt on every reload_from_disk() banner: DiscoveryBannerState Hidden | Visible(c) | Details(c) walker_cache: Option<WalkerOutput> populated only on first-run empty state drift_cache: BTreeMap<String, DriftStatus> keyed by declared_uri → ✓ ⚠ ▲ ⟷ glyphs in Units status col reload_from_disk tick drains mpsc ON SCREEN (ratatui frame) Sources panel name · type · #units · status Units panel name · kind · source · ref · status ↑ drift glyph column lives here Detail pane URI: gh:stevie/skills@main/… Deployed: ~/.claude/skills/… Usage: 12 invocations · last 3h ago populated from LockedUnit.usage
Diagram 1 — data model · how three on-disk surfaces become three on-screen panes

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.

user presses [m] on HomeScreen GoToSkillManager event events.rs:3543 reload_from_disk() pure manifest.units empty? maybe_show_discovery_banner walkers run class_a + class_c DISCOVERY WALKERS — pure, read-only, never panic class_a — marketplace plugins ~/.claude/plugins/cache/ <mp>/<plugin>/<ver>/ .claude-plugin/plugin.json marketplace="unknown" if no registry class_c — orphan units (9 tools) claude · codex · copilot · gemini cursor · amazonq · cline · roo claude-desktop frontmatter parsed best-effort BANNER STATE MACHINE — DiscoveryBannerState Hidden Visible(counts) Details walkers≠∅ [d] [d] [Enter] = import all [s] = write SKIP_MARKER_FILE [Enter] import path reconcile::reconcile(walker) → patch { new_sources, new_units } manifest.save_to(manifest_path) banner = Hidden; refresh view-model [s] skip path touch ainb_home/.skip-banner banner = Hidden walker_cache = None subsequent opens skip the walkers non-empty manifest path banner stays Hidden walkers do NOT run (gated) Sources/Units paint live data manifest already authoritative
Diagram 2 — empty-state flow · two walkers feed a three-state banner

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_MANAGER
  • state.skill_manager_state.reload_from_disk(&ainb_home) — rebuilds Sources/Units/Detail from 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 in maybe_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) — flips banner to Visible(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 shadows selected.uri) → SkillManagerConflictFlip swaps which side of the pair carries the shadowed_by edge, persists the mutated manifest, and refreshes the view-model.
  • If no peerSkillManagerSync reloads skill_manager_state from disk (so freshly-deployed paths and usage counters show up) and fires a sync: <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 stateevent synthesisedresulting state mutation. Same information as the collapsibles above, compressed into a single scan.

m Open SkillManager events.rs:2032 → 3543 HomeScreen GoToSkillManager SkillManager → reload_from_disk(ainb_home) → start_background_drift_load → run_discovery_walkers → maybe_show_discovery_banner No-op when manifest non-empty AND skip-marker present s Sync — no peer Units panel · events.rs:1136 selected unit SkillManagerSync notification if !has_conflict_peer(selected): reload_from_disk(ainb_home) add_info_notification( "sync: <unit-name>") Intent only — actual bytes move via `ainb skill sync` CLI s ConflictFlip — has peer Units panel · events.rs:1136 A active B shadow shadowed_by [s] A shadow B active shadowed_by manifest.units[sel].shadowed_by swapped with peer.shadowed_by manifest.save_to(path) Silent no-op if no peer exists Import all (banner) events.rs:3596 WalkerOutput reconcile() manifest patch for src in patch.new_sources: manifest.add_source(src) for u in patch.new_units: manifest.units.push(u) manifest.save_to(path) banner → Hidden · no skip-marker d Toggle details (banner) events.rs:3608 Visible(c) [d] Details toggle_discovery_details(data) Same counts, different render — Details enumerates which tool each discovered unit came from s Skip banner events.rs:3613 (banner active) Visible(c) apply_skip Hidden write(ainb_home/.skip-banner) data.banner = Hidden data.walker_cache = None Subsequent opens skip walkers. To reset: delete the marker file. ↑↓ Move cursor — j / k / ↑ / ↓ events.rs:1148-1149 commit deploy reflect handover test selected=0 ↓ / j commit deploy reflect handover test selected=1 move_selection(Prev / Next) Wraps at ends · Detail pane recomputed gG Jump to ends — g / G events.rs:1150-1151 g / Home → SelectFirst selected = 0 G / End → SelectLast selected = units.len()-1 Mirrors vim navigation. Detail pane recomputed on every jump — no extra keystroke needed to refresh detail. q Back — Esc / q events.rs:1127 + 1121 SkillManager SkillManagerBack HomeScreen state.current_screen = HOME Banner state preserved. Re-entry re-shows the same counts if walker cache still primed.
Diagram 3 — per-key visual cheatsheet · trigger → event → mutation, one row per keybind

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.

GoToSkillManager events.rs:3565 tokio::spawn_blocking over Arc<dyn DriftBackend> detect_all(manifest, lockfile) per-unit backend.compare() mpsc::send unbounded GitLsRemoteBackend.compare git ls-remote --exit-code -- <url> <ref> · GIT_TERMINAL_PROMPT=0 every AppState::tick drain mpsc → drift_cache channel ✓ InSync ⚠ Outdated ▲ Ahead ⟷ Diverged … unknown Outdated{behind: 0} when remote tip != deployed SHA but behind count can't be cheaply computed
Diagram 4 — async drift pipeline · spawn_blocking → git ls-remote → mpsc → tick drain

Three v1.2.1 hardening fixes live in this loop:

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.

TUI PATH · intent-only user presses [s] on Units no shadowed_by peer SkillManagerSync event events.rs:3625 reload_from_disk(ainb_home) re-read manifest + lock rebuild SkillsScreenData notification: "sync: <unit>" add_info_notification() 3s top-right banner handoff actual bytes move via CLI → user runs `ainb skill sync` v1.3 candidate: wire to CLI CLI PATH · `ainb skill sync` — the real work read manifest.yaml + lock.yaml SyncPlanner inputs assembled plan_sync(source, mappings, units) — pure → Vec<SyncAction { unit_name, direction, reason }> direction: ToHome | ToRepo | NoOp for each action, dispatch on direction: ToHome — apply_to_home() 1 · resolve_pair(source, unit_path) → (home_rel, repo_rel) 2 · fetcher.fetch_content(ref, repo_rel) → upstream bytes 3 · write_atomic(tool_home/home_rel) → *.tmp + rename, fsync No git. No network beyond the fetcher. Idempotent for unchanged upstream bytes. ~/.claude/skills/<name>/SKILL.md ToRepo — apply_to_repo() 1 · acquire_sync_lock(cache_dir) ↳ .git/ainb-sync.lock (T7) 2 · resolve_pair → repo path 3 · read tool_home/home_rel 4 · write_atomic(cache/repo_rel) 5 · git add -- <path> 6 · git commit -m "sync: <name>" 7 · git push -- origin <ref> ↳ skipped if AINB_SYNC_SKIP_PUSH=1 8 · RAII Drop releases lock contention → SyncEngineError::SyncInProgress NoOp · neither path · skip silently (already-in-sync rows)
Diagram 5 — sync journey · TUI fires intent; CLI runs pure planner then per-action atomic write or git mediation

Two CLI flags shape this loop:

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.

SourceEntry name: stevie-skills uri: gh:stevie/skills ref: main target_layout: [TargetMapping] empty → BOOTSTRAP_DEFAULT_MAPPINGS BOOTSTRAP_DEFAULT_MAPPINGS skills/* /SKILL.md agents/* .md commands/* .md mcp-servers/* … hooks/* … + top-level *.md First-glob-wins translator MappingEngine::resolve_pair (source, unit_path) -> Option<(home_rel, repo_rel)> pure · zero side-effects no match → unit is NoOp claude — ~/.claude/skills/commit/SKILL.md home_rel = .claude/skills codex — ~/.codex/skills/commit/SKILL.md home_rel = .codex/skills cursor — ~/.cursor/skills/commit/SKILL.md home_rel = .cursor/skills … one row per entry in unit.targets unit.targets: [claude, codex, cursor] → resolve_pair runs N times → N atomic writes via write_atomic()
Diagram 6 — multi-tool fan-out · target_layout → resolve_pair → per-tool deploy paths

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).

1 You have a github repo with skills/agents/commands github.com/stevie/skills └─ skills/commit/SKILL.md └─ skills/deploy/SKILL.md └─ agents/reviewer.md $AINB_HOME state: manifest.yaml: (empty) lock.yaml: (empty) 2 Register the source — `ainb source add gh:owner/repo` $ ainb source add gh:stevie/skills $ ainb source add gh:stevie/skills --name skills-src \ --type manifest Optionally pin a ref: gh:owner/repo@v1.0 $AINB_HOME state: manifest.yaml: +SourceEntry lock.yaml: +LockedSource (fetched_path → cache) 3 Install a unit to one-or-more tools — `ainb skill install` $ ainb skill install \ gh:stevie/skills@main/skills/commit \ --targets claude,codex,cursor \ --yes --dry-run prints diff without writing $AINB_HOME state: manifest.yaml: +UnitEntry lock.yaml: +LockedUnit sha: abc123… deployed: {claude, codex, cursor} 4 Files land on disk — atomic writes per tool MappingEngine.resolve_pair → write_atomic — fan-out (parallel-safe, per-target) ~/.claude/skills/commit/SKILL.md ← 0o644, fsync'd, atomic via *.tmp + rename ~/.codex/skills/commit/SKILL.md ← same ~/.cursor/skills/commit/SKILL.md ← same 5 Open ainb (m), see the unit · drift backend rounds-trips $ ainb # then press m Sources panel: stevie-skills · gh · 1 unit Units panel: commit · skill · stevie-skills ✓ in-sync Drift glyph paints async after git ls-remote returns downstream effects: /commit (skill) callable in Claude /commit (skill) callable in Codex /commit (skill) callable in Cursor All three from the same upstream
Diagram 7 — onboarding journey · gh repo → ainb source add → ainb skill install → atomic fan-out → live in three tool homes

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
The TUI's banner-driven import path (step 1 was "no repo, just orphan files in ~/.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:

PrefixMeansWhere it ends up
gh:owner/repoGitHub HTTPSCloned + fetched via git ls-remote https://github.com/owner/repo.git
git:<url>Any git URLPasses verbatim to git; file://, ssh, etc.
gist:<id>GitHub GistHosts a one-file plugin
https: / http:Raw URLSingle-file fetch
local:<path>Filesystem-localUsed by Class-C adoption — orphan SKILLs get a local: URI synthesised
npm:npm packageReserved for future plugin packaging
marketplace:<plugin>@<mp>[@<ver>]Claude Code marketplaceMaps 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 diskUnitKindLayout shape
skills/<name>/SKILL.mdSkillDirectory unit; SKILL.md required
agents/<name>.mdAgentFlat-md unit; file-stem is name
commands/<name>.mdCommandFlat-md
mcp-servers/<name>/…McpServerDirectory unit; tool-specific shape
hooks/<name>.mdHookFlat-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.
The key fingerprint for "is this a Claude marketplace plugin?" is the presence of .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.

WhatScanned today?By which walker
Claude marketplace plugins installed via /plugin install✓ YesClass-A — ~/.claude/plugins/cache/
Orphan skills/agents on disk in any of the 9 tool homes✓ YesClass-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 diskPartialDrift backend returns InSync (can't claim drift on missing repo); silent failure mode
The gap in one sentence. v1.2 walks local files on disk in 9 specific layouts plus Claude marketplace cache. Anything installed via a different package manager, marketplace, or sideloaded into a non-canonical path is invisible to 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:

CLI parity — what's TUI-only, what's CLI-only, what's both

CapabilityTUICLI
Open SkillManager screenm
Discovery banner + import allm → banner → Enterainb migrate --discover
Selection-pane navigation↑↓jkgG
Bidirectional sync (selected unit)s (no peer)ainb skill sync
Conflict-flip shadowed_bys (with peer)— (edit manifest manually)
Drift check (visual)Units column glyphs (async)ainb skill check [--json]
Install one unitainb skill install <uri>
Update / re-fetchainb skill update [--check] [uri]
Remove / uninstallainb 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.

T3 · TUI tripwires in-process TestBackend + LIVE tmux capture-pane proves rendered pixels T2 · CLI integration ainb-cli + ainb-skill-core vs real bare repos / real disk proves CLI flow + I/O T1 · pure unit tests ainb-skill-core · ainb-usage parsers · planners · pure fns proves correctness in isolation PRE-V1.2.1 COVERAGE T1 — 100+ tests mapping (16) · manifest (23) sync (12) · drift (14) auto-infer (6) · misc T2 — ~15 tests skill_check / sync (4 + 5) sync_roundtrip (2) cli_skill_install_layout cli_migrate_upgrade_schema T3 — 7 tripwires usage_renders (TestBackend) drift_column (5 glyphs) drift_background_poll (2) screen_opens / conflict_flip / esc_returns_home / discovery_banner (live tmux) V1.2.1 TESTING ROLLUP — added 17 tests + 4 properties × 1024 cases T3 (live): T1 usage_renders_live · T2 drift_glyph_live · T3 s_routes_to_sync_live T2: T4 drift_tests_integration.rs (4) · GitLsRemoteBackend vs real bare repo + argv-smuggle + GIT_TERMINAL_PROMPT T1: T5 uri_property_tests (5 tests / 4 props × 1024 cases) · T6 chrono pin-down (2) · T7 concurrent_sync_test (3) Reviewer cycle: code-reviewer agent run between green and bd close on every bead. PR #178 merged to feat/skill-manager.
Diagram 8 — three-tier test pyramid · counts before v1.2.1 + the seven new beads' additions per tier

What each tier actually proves

TierExample test fileWhat it provesWhat it can't catch
T1 · pure mapping_tests.rs
uri_property_tests.rs
now_iso_*
Algorithm correctness · no I/O · fast (millisecs) Wiring bugs · disk semantics · concurrent races
T2 · CLI skill_check_tests.rs
sync_to_repo_tests.rs
drift_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

BeadTierAssertsTests
T1 · agents-in-a-box-2ssT3 livePane contains "12 invocations" after seeding usage + pressing m1 (passed twice)
T2 · agents-in-a-box-h6kT3 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-03uT3 livesync: <unit> notification paints after [s] on a unit with no peer; deployed path unchanged1 (passed twice)
T4 · agents-in-a-box-i2mT2GitLsRemoteBackend InSync / Outdated round-trips · argv-smuggle reject · GIT_TERMINAL_PROMPT no-hang bound 10s4 (passed twice)
T5 · agents-in-a-box-1jyT1Uri::parse fixed point · arbitrary input no-panic · serde yaml round-trip5 (4 props × 1024 cases + 22 edge cases)
T6 · agents-in-a-box-kgdT1now_iso() shape + agreement with chrono on representative epochs after the days_from_civil swap2 (passed twice)
T7 · agents-in-a-box-5weT1 + T2Pre-held lock surfaces SyncInProgress · RAII release · two-thread contention3 (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:

Live tmux tripwires are real: they spawn the production 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 diskCountWhat ainb thinks todayWhat 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.

1 SOURCE ENUMERATION — read all provenance manifests installed_plugins.json ~/.claude/plugins/ [plugin@mp]={scope, ver, sha, installPath, projectPath?} known_marketplaces.json ~/.claude/plugins/ marketplace metadata — name, URL, last-fetched external-dependencies.yaml <project>/toolkit/ agent-skills · bundled-skills external-packages manifest.yaml ~/.agents-in-a-box/ sources + units already adopted by ainb 2 DISK WALK — Class-A + Class-C as today (no change) Class-A · ~/.claude/plugins/cache/* 63 SKILL.md across 13 plugins on your machine → yields { plugin, marketplace, version, name, path } Class-C · ~/.<tool>/skills/* across 9 tools 192 top-level dirs under ~/.claude/skills/ alone → yields { tool, kind, name, path, frontmatter_valid } 3 PROVENANCE MATCHER — the new pass that's missing for each (unit_name, abs_path) from Phase 2, in order: if abs_path.starts_with(plugin.installPath) → Marketplace { plugin, mp, ver, sha } elif name == agent-skills[*].name → ExternalRepo { repo, version, applies-to } elif name == bundled-skills[*].name → Toolkit { repo_relative_path } elif manifest.units.find(name) → AlreadyAdopted { manifest_uri } elif name matches plugin's bundled list → Sidelinked { plugin@mp } (rare) else → Local { authored_locally: true } 4 RENDER — Sources panel grows; Units gain a provenance badge claude-plugins-official 5 plugins · 12 skills marketplace badge ⬡ agents-in-a-box 2 plugins · 5 skills marketplace badge ⬡ external-deps 7 cloned skills external-repo badge ↗ local (truly authored) ~185 skills local badge •
Diagram 9 — proposed provenance matcher · enumerate manifests, walk disk, attribute every finding, render with source badges

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 behaviourAfter 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
The actual implementation note. Phase 1 file reads are idempotent + cheap (<100 KB total on your machine). Phase 3 is a pure function over Phase 1's parsed structs + Phase 2's walker output — no new file I/O, no network, no clones. The reconciler change in tab 3 is the only behavioural change to the existing import path; the matcher could ship behind a feature flag and gated by manifest schema version to keep v1.2 lock files round-tripping byte-stable.

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:

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

SkillManager screen user presses [a] AddSource event step 1 Repo URI Uri::parse + validate step 2 Branch default: main step 3 Folder layout root · toolkit · custom step 4 Target tools auto-detect ~/.<t>/skills step 5 · PREVIEW + SAVE Generated YAML diff + on-disk match preview [s] save [↑] back [c] cancel [↑] back save handler 1 · manifest.add_source(SourceEntry { ... }) 2 · manifest.save_to(manifest_path) 3 · skill_manager_state.reload_from_disk() return to SkillManager screen Sources panel highlights the new source row info notification: "source added: <name>" [c]/[Esc] cancel VALIDATION GATES step 1: Uri::parse() must succeed · step 2: ref non-empty · step 3: at least one of the 3 paths set step 4: at least one tool ticked · step 5: manifest.source_mut(name) is None (no duplicate name)
Diagram 10 — Add Source wizard · 5-step modal with back-nav, validation gates, and manifest persistence

Implementation slice — what would actually ship

FileChange
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:

PresetRepo layout assumedGenerated target_layout
Root skills/<name>/SKILL.md
agents/<name>.md
commands/<name>.md
BOOTSTRAP_DEFAULT_MAPPINGS · omits target_layout field (uses fallback)
Toolkit toolkit/packages/skills/<name>/SKILL.md
toolkit/packages/agents/<name>.md
toolkit/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.

Together with the provenance matcher, this closes the configuration-experience gap end-to-end: discovery auto-creates sources for things already on disk, add creates sources for upstreams the user wants to publish to, edit tweaks both — none of it touches YAML by hand.

FAQ — your explicit questions

Why does pressing s sometimes do nothing visible?
Two reasons. (a) Before the v1.2.1 notification-render fix, the sync: <unit> banner was being eaten by layout.rs skipping notifications on registry-routed screens. Fixed in 7a999cc. (b) The Sync handler only fires the intent + reloads from disk — actual bidirectional content reconciliation runs out-of-band via ainb 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 tool field — claude / codex / cursor / …) and yes for installed units (the Units panel's source column shows the manifest source the unit was installed from). What's not rendered today is a per-target deployment list — that information lives in LockedUnit.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 with tool: claude. Press Enter and the reconciler synthesises local: 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.json present 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 sha in the lockfile — drift backend treats this as InSync and ✓ paints; (2) the source URI uses a scheme source_to_remote_url can't translate (only gh:, 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 s while the banner is visible. That writes ~/.agents-in-a-box/.skip-banner and 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 --json reports every unit whose source isn't currently InSync upstream — that surfaces source URIs that need a follow-up. But there's no walker for non-Claude marketplaces, no npm: walker, no sideloaded-plugin probe. That's the v1.3 design surface.