sb-hub — onboarding check · orchestrator · navigator
The hub. You don't author here — you inspect state and route. Three read-only modes; pick by
intent. Read CONTEXT.md once for shared vocabulary, the storage map, and the resume protocol.
Load by mode (don't pull both refs up front):
references/runbook.md— the navigator engine (state detection → one next step). Load it for Mode 0 / Mode 2 (diagnose / "what's next").references/end-to-end-flow.md(~220 lines) — the full pipeline worked example for "messy app → design system". Load it only for Mode 1 (orchestrate a full audit). Do NOT load it for a Mode-0 onboarding check or a Mode-2 next-step lookup —runbook.mdis all those need.
Canonical context (one source). The bundle has exactly one CONTEXT.md (shared vocab · STORAGE
MAP · resume). It's never hand-duplicated: in the bundle/plugin install every skill reads this one file
(CONTEXT.md / ${CLAUDE_PLUGIN_ROOT}/CONTEXT.md); a standalone npx skills add <skill> ships a
byte-identical copy the build vendors, refreshed on update. sb-hub is the orchestrator — if skills
are installed separately, route through sb-hub so the pipeline + shared vocabulary stay authoritative.
Resolve the bundle
Scripts live in scripts/. In this repo: ${CLAUDE_PLUGIN_ROOT}/scripts/.
When installed as a plugin, use ${CLAUDE_PLUGIN_ROOT}/scripts/.
CORE=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}} # shared scripts: "$CORE/scripts/<name>"
Pick the mode
| User intent | Mode |
|---|---|
| "is this ready", "where do I start", fresh/unknown repo | 0 · Onboarding check |
| "audit this app", "do the whole thing", "build Storybook for this app" | 1 · Orchestrate |
| "what's next", or after any verb appends to the ledger | 2 · Navigate (default) |
| "this is wrong", "strange behavior", "report a bug", a skill misfired | 3 · Report |
Mode 0 — Onboarding check (phase-0 diagnose, read-only)
A fresh messy app: probe the stack + readiness before any verb, print a report, recommend the first step. No prompts, no writes — re-runnable.
echo "━━ Phase 0 — readiness ━━"
grep -q '"react"' package.json 2>/dev/null && echo " [ok] React" || echo " [warn] no React in package.json"
grep -qE '"vite"|"@vitejs' package.json 2>/dev/null && echo " [ok] Vite" || echo " [info] not Vite (skill targets React+Vite)"
test -d .storybook && grep -q '"storybook"' package.json 2>/dev/null && echo " [ok] Storybook present" || echo " [warn] NO_STORYBOOK → start with sb-setup"
test -d node_modules && echo " [ok] node_modules present" || echo " [warn] deps not installed — run your package-manager install"
lock=$(ls package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null | head -1); echo " [info] lockfile: ${lock:-none}"
grep -q '@storybook/addon-mcp' package.json 2>/dev/null && test -f .mcp.json && echo " [info] MCP wired → sb-stories uses with-mcp" || echo " [info] no addon-mcp → without-mcp path"
if find .storybook -maxdepth 1 -name '*.json' 2>/dev/null | grep -q .; then
find .storybook -maxdepth 1 -name '*.json' | sed 's/^/ [ok] discovery: /'
else echo " [info] no discovery JSON yet → sb-inventory not run"; fi
grep -q 'storiesLocation:' .storybook/audit/status.md 2>/dev/null && echo " [ok] stories location decided" || echo " [warn] stories location undecided → sb-setup must ASK (isolated .storybook/stories/ vs co-located) before sb-stories"
Worked example — a half-migrated app (Storybook installed, nothing audited yet):
━━ Phase 0 — readiness ━━
[ok] React
[ok] Vite
[ok] Storybook present
[ok] node_modules present
[info] lockfile: pnpm-lock.yaml
[info] no addon-mcp → without-mcp path
[info] no discovery JSON yet → sb-inventory not run
[warn] stories location undecided → sb-setup must ASK …
→ Storybook exists but no discovery JSON and location undecided, so don't jump to sb-stories.
Name sb-setup first (to record the stories location), then sb-inventory. That ordering —
setup before inventory before stories — is the whole point of the gate: the [info]/[warn] lines
are the routing signal, not noise.
Everything the bundle writes lives under .storybook/ (CONTEXT.md STORAGE MAP) — flag if you ever see
outputs elsewhere. The one decision is where stories go; if undecided, name sb-setup (it asks).
Discovery is precomputed. The discovery layers are JSON under
.storybook/:project-inventory(+tokens.map,storyCoverage) ·component-usage·flows·design-system-health·component-states·prop-shapes·page-patterns·runtime. The first four are rendered in Storybook (autodocs embeds + wrappers) and refreshed together byrefresh-usage.sh; the rest are authoring inputs (regenerated on demand). Never re-derive by shell scan what a script already wrote to.storybook/*.json— Read the field.
Then print → Start: <first verb> — sb-setup if NO_STORYBOOK, else sb-inventory. Don't run
the verb; name it and stop. For the full new-project arc, hand to Mode 1.
Mode 1 — Orchestrate (run the whole audit → stories)
"Audit this app end to end." Run the pipeline in order, gating between phases — never advance until the prior artifact exists (the discovery scripts write atomically, so an existing JSON is complete). You don't run sibling scripts directly; you invoke each focused skill and verify its output.
sb-setup— if Mode 0 reportedNO_STORYBOOK, or stories location is undecided. GATE:.storybook/+"storybook"in package.json, ANDstoriesLocationrecorded in.storybook/audit/status.md(sb-setup asks the user — isolated.storybook/stories/vs co-located), AND.storybook/runtime.jsonexists (discover-runtime.py— provider tree / root-CSS / portals / MSW the shared preview must supply), before step 2.sb-inventory— real-vs-slop + dominant design system. GATE:.storybook/project-inventory.jsonexists.sb-health— design-system health. GATE:.storybook/design-system-health.jsonexists; ifdesignSystem.mixed/ heavy raw-hex, surface the/ds-runbookhandoff before authoring.sb-flows— route map + nav edges. GATE:.storybook/flows.jsonexists.sb-stories <Component>— author a story for each component that needs one: work downcomponents.storyCoverage.needsStory[](own components without a story, top-imported first), each self-gating withvalidate-stories.sh. The honest progress meter isstoryCoverage.withRegisteredStory/ needsCountwhenstoryCoverage.source == "storybook-index"(reconciled against Storybook'sindex.json— the stories Storybook actually registers); otherwisewithColocatedStory/needsCount(heuristic;withStoryis a loose upper bound — it also counts components a story merely imports to mock). Re-runsb-inventory(orsb-audit) after authoring to refresh coverage from the index and shrinkneedsStory[]— iterate until empty.sb-wrappers— render each step's output as a view:ProjectInventory(after step 2),DesignSystemHealth/TokenMatrix(after step 3's health),AppFlowGraph/JourneyGraph(after step 4), andStateGrid/StateMatrixwhile authoring stories (step 5). The data wrappers need their step's JSON first — see sb-wrappers "When in the flow".sb-audit— periodic drift survey + decision board once stories land; runsrefresh-usage.sh(below).
Figma-originated design system (optional branch, after step 1). The pipeline above is code-first
(audit what exists). When the source of truth is an approved Figma file, insert sb-figma to
establish foundation-token parity before authoring: it captures the Figma MCP variables to
.storybook/figma/, writes .storybook/figma-token-parity.json (color/spacing/type, OKLCH→hex), wires the
Foundations/Colors|Tokens|Type stories' figmaVar/figmaHex, and reports drift. It feeds — doesn't
replace — sb-health (step 3): health checks the code-internal token set; sb-figma adds the design↔code
axis health can't see. (Delivering approved components from Figma is also sb-figma, authored via
sb-stories; iterating undecided designs stays sb-explore.)
Append a one-line finding to .storybook/audit/findings.md after each phase. Stop on the first GATE
that fails and tell the user which verb to fix. (Pattern: ordered pipeline with hard gates, like lfg.)
Keep rendered data fresh (the usage layer). The four rendered JSONs (inventory · component-usage ·
flows · design-system-health) feed the autodocs usage embed — UsageSection wired once into
preview.ts docs.page adds "Real usage in this app" to every component's Docs and each Foundation
(Colors/Semantic/Typography/Scales = token tables, Health = audit findings). refresh-usage.sh --docs
re-runs all four together so a rebuild reflects reality; sb-audit does this each pass, and it belongs in
CI before storybook build. (Setup wires the docs.page composition — see sb-setup install-wizard.)
Mode 2 — Navigate (default — name the single next step)
Inspect state + ledger, recommend exactly one next surgical skill (whose prerequisites are met).
test -d .storybook && grep -q '"storybook"' package.json 2>/dev/null && echo STORYBOOK_PRESENT || echo NO_STORYBOOK
ls .storybook/*.json 2>/dev/null # which discovery JSONs exist
cat .storybook/audit/status.md 2>/dev/null # ledger / resume point
# DRIFT — source/story files changed since the last discovery (newer than the inventory JSON). Plain
# `find -newer` → cross-agent (no git, no Claude hook), works on uncommitted/untracked files. Lists the
# exact files that moved this session; if any, the discovery JSONs are stale → refresh before routing.
if [ -f .storybook/project-inventory.json ]; then
find src app components .storybook/stories \( -name '*.tsx' -o -name '*.ts' \) \
-newer .storybook/project-inventory.json 2>/dev/null | grep -v '\.storybook/.*\.json' | head -20
fi
# COVERAGE — READ the field the script already wrote (CONTEXT.md doctrine: never re-derive). Extract
# ONLY storyCoverage so the FULL object surfaces complete — never a truncated dump of the whole
# inventory. Prefer the authoritative `withRegisteredStory` (source=storybook-index, reconciled against
# index.json); fall back to the heuristic `withColocatedStory`. needsStory[] is the iterate list.
if [ -f .storybook/project-inventory.json ]; then
python3 - <<'PY'
import json
try: c = json.load(open(".storybook/project-inventory.json"))["components"]["storyCoverage"]
except Exception: raise SystemExit
src = c.get("source", "heuristic")
done = c.get("withRegisteredStory") if src == "storybook-index" else c.get("withColocatedStory", 0)
print(f"coverage: {done}/{c.get('real',0)} components have a story [{src}] · {c.get('needsCount',0)} need one")
if c.get("needsStory"):
extra = " …" if c.get("needsCount", 0) > 8 else ""
print("iterate next (sb-stories): " + ", ".join(c["needsStory"][:8]) + extra)
PY
fi
If DRIFT lists files, the discovery JSONs no longer match the code — route to sb-inventory (or
sb-audit for a periodic pass) to refresh before naming the next step: its refresh-usage.sh re-runs
inventory/usage/flows AND reconciles story coverage against Storybook's own index.json (authoritative,
not a basename guess). Then route — name exactly one:
| State | Next step | Skill |
|---|---|---|
NO_STORYBOOK |
defer bootstrap to npx storybook ai setup, then align |
sb-setup |
No project-inventory.json |
discover real-vs-slop first | sb-inventory |
Inventory done, no design-system-health.json |
check health before authoring | sb-health |
Health done, no flows.json |
capture navigation + app-map | sb-flows |
| Inventory clean, want stories | author ONE component | sb-stories <Component> |
| New/redesigned component, undecided — iterating options | sandboxed iteration (+ Figma node) | sb-explore |
| Approved Figma design — map foundation tokens (color/spacing/type) or deliver an approved component | Figma → production Storybook | sb-figma |
| Check Figma↔code token parity / "is the design system in sync with Figma" | parity + drift report | sb-figma |
| Connect components back to Figma (Code Connect) / "show my code in Figma Dev Mode" | code → design (reverse) | sb-figma |
| Periodic check | drift survey + decision board | sb-audit |
| Explore meets graduation gate | propagate to production | sb-ship |
EXPLORE vs DELIVER (both are Figma-aware — route by stage, not by "uses Figma"): undecided / trying options →
sb-explore(Lab) →sb-ship(graduate). Already approved in Figma →sb-figma(deliver direct to prod).sb-figmawrites the Foundation token stories itself; for an approved component it extracts the design then authors viasb-stories' rules. It captures every Figma MCP output to.storybook/figma/(manifest + per-tool files) so the pipeline is reproducible/iterable, not MCP-bound.
Resume rule (CONTEXT.md): read status.md + check which JSONs exist; resume from the first
incomplete step. Never treat a file half-written in a prior session as complete — re-run that step.
Mode 3 — Report (a skill misfired / strange behavior)
When the user says a skill did something wrong, surprising, or broken, help them file a useful report — don't just apologize. Draft it with the agent-native reporter, then hand them the submit command.
CORE=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
"$CORE/scripts/report-issue.sh" --asked "<what they asked>" --observed "<what happened>" --expected "<what they wanted>"
It writes a sanitized draft (versions + .storybook/*.json shapes/counts only — never source, token
values, or component names) and prints a gh issue create … command + a blank-issue URL. It makes no
network call — the user submits. Fill --asked/--observed/--expected from the conversation, show them
the draft path + the gh line, and (only if they ask) offer to run gh for them. To target a different
repo: SB_ISSUE_REPO=owner/name. A good report becomes a reproduction → a new eval case → a fix → a
field-learnings.md entry — the loop that keeps the next run from re-hitting the same bug.
Never (orchestration failure modes, and why)
- NEVER advance past a failed/empty gate in Mode 1 — each phase's
.storybook/*.jsonis the precondition for the next (foundation tokens before components, flows before the app map). Skip one and every later phase builds on missing ground truth and silently under-delivers. - NEVER re-derive CONTEXT by shell-scanning what a script already wrote — the
.storybook/*.jsonfiles ARE the state. Hand-grepping drifts from what the scripts captured and what the wrappers render; cite the field, don't recompute it. - NEVER treat a half-written prior-session artifact as complete — writes are atomic per file, but an
interrupted run may have stopped mid-pipeline. Check
status.mdand re-run the first incomplete step. - NEVER author here — the hub inspects and routes. Writing a story/wrapper from the hub bypasses the focused skill's own checks and anti-patterns; invoke the skill instead.
- NEVER hand-duplicate
CONTEXT.md— every skill reads the one canonical file (CONTEXT.md); a copy is drift waiting to happen.
Cross-agent
- Claude: this skill is
/sb-hub(Mode 2 / "what's next" is the default;/sb-hub onboardor/sb-hub audit this appselects Mode 0 / 1). - Codex:
$sb-hub what's next·$sb-hub onboard·$sb-hub audit this app. - The loop: a verb runs → appends to
.storybook/audit/findings.md→ asksb-hubagain (Mode 2) → it names the next surgical skill → run it.