The design gate checked the 21st CLI with command -v only, so an installed-but-signed-out CLI read as READY and every 21st step failed downstream. tool_active now probes 21st whoami through a three-state function: signed in (TWENTYFIRST_TOKEN or API_KEY_21ST set, or 'Logged in as'), signed out (exact 'Not logged in'), unknown (rc != 0, timeout, unexpected output). Signed out is a new verdict, SIGN-IN REQUIRED, exit 12: design-gate.md tells the orchestrator to ask the user to run ! 21st login, end the turn, re-run the gate on their reply, and to skip 21st only on an explicit 'proceed without 21st', never silently. Unknown surfaces as exit 11 with the whoami diagnostic and a CLI-runtime remedy, so a node/PATH failure can never loop on a sign-in prompt. INCOMPLETE still wins. Hermetic suite lib/tests/design-tool-gate.test.sh (stub CLI, fixture repo through DESIGN_GATE_REPO_OVERRIDE) covers every state.
12 KiB
DESIGN GATE — Auto-detect design tasks, ensure the design toolchain is active
Inline snippet. Include in any agent STEP 0 that may touch UI/design.
WHEN TO RUN
Run this gate when the task description OR target files match design signals.
DETECTION
Check BOTH the task description AND the filesystem:
Task description signals (case-insensitive match on $ARGUMENTS):
- UI keywords:
component,button,card,modal,dialog,tooltip,dropdown,sidebar,navbar,header,footer,layout,grid,form,input,table - Style keywords:
css,style,theme,color,font,spacing,margin,padding,border,shadow,animation,transition,hover,motion,animate,responsive,dark mode,light mode - Design keywords:
design,ui,ux,visual,polish,pixel,figma,mockup,wireframe,prototype - Framework UI:
tailwind,styled-component,emotion,chakra,radix,shadcn,headless
Filesystem signals (quick check, no deep scan):
- Target files have
.tsx,.jsx,.vue,.svelte,.astro,.css,.scss,.less, or.module.cssextension tailwind.configorpostcss.configpresent in project roottokens/,theme/, ordesign-system/directory exists- Storybook config (
.storybook/) present - Animation lib in
package.jsondeps: any packageis_anim_lib_installedrecognizes (lib/animation-lib-check.sh, the single source)
DECISION
Source of truth for activation is the profile system — never an atomic
per-tool toggle. The gate's whole job: confirm the design toolchain is active,
and if not, point at ONE command — /profile design.
1. Tier — does the gate even apply?
- Trivial (≤2 files, single cosmetic value, one CSS tweak — same scope as
/hotfix) → no design tools required. Skip the gate, proceed. - Build UI / design system / review-audit → toolchain required, continue.
- In doubt (trivial tweak vs real UI change) → do NOT silently skip: ask the user, or default to the Build tier.
Tier does NOT change WHAT gets checked. Every non-trivial design tier draws from
the one design profile — so the gate checks that profile's design-core
tools (the # GATE-BLOCK: allowlist in design.profile: ui-ux-pro-max,
frontend-design, emil-design-eng, design-motion-principles, design-html,
design-review, design-consultation, the 21st CLI and 21st-ui-build — the
canary for the whole 21st skill pack). The profile also bundles
browser/plan/shotgun tooling and graphify for convenience; those never trip the
gate. Motion (design-motion-principles) and static-HTML (design-html) are
already in the core set — checked regardless; their CLAUDE.md "+motion /
+static" notes say which tool you'll lean on, not a separate activation step.
site-motion (personal skill, site-level scroll/page choreography) rides the
same Build chain but isn't on the GATE-BLOCK list: it ships with the repo,
nothing to install or verify.
2. State — run the deterministic check
bash "$HOME/.claude/lib/design-tool-gate.sh"
It reads the design-core tools (# GATE-BLOCK: in design.profile) plus their
types (profile.sh show design --plain) and checks each on its own channel —
skill symlink, claude plugin list, claude mcp list, command -v. It never
reads disabledMcpServers (unreliable for bi-modal servers like context7).
The core set lives in design.profile, not in the script or here — single source.
Exit codes: 0 = ready · 11 = ready-but-unverified (proceed, but surface it) · 10 = incomplete (gate trips) · 12 = sign-in required (21st installed, signed out) · 2 = error.
3. Branch on the result
-
0 /
READY→ proceed silently. Toolchain is active. -
10 /
INCOMPLETE→ STOP. The script reports up to three groups; relay them and the remedy to the user:🎨 DESIGN DETECTED — the design toolchain isn't fully active. activate with /profile design: <skills / ui-ux-pro-max> required + manual step: <e.g. 21st — needs the CLI> → run /profile design to activate it, then continue.- activate with /profile design → skills + the plugin;
/profile designturns them on directly. - required + manual step → required tools the profile can't flip silently.
the
21stCLI lands here: it TRIPS the gate (it's required for Build), it is NOT a silent "optional"./profile designsymlinks the 21st skills, but the CLI they shell out to is a global npm install: tell the user to runnpm i -g @21st-dev/clithen21st login(no API key, no MCP). - Do NOT hand-activate individual tools. The profile is the unit of activation.
- activate with /profile design → skills + the plugin;
-
12 /
SIGN-IN REQUIRED→ STOP. The 21st CLI is installed but21st whoamireports signed out. Relay the script's block, then ask the user to run! 21st login(the!prefix runs it in this session, browser flow, saves a local token) — or run21st loginin any terminal on this machine, then reply (the token is a local file; any terminal works,!in-session is just the convenient form). END THE TURN and wait. On the user's reply, re-rundesign-tool-gate.shbefore any 21st step:READY→ continue; still12→ ask again once, then offer the opt-out. Explicit refusal — the user answers "proceed without 21st" (or words to that effect) → say visibly21st skipped for this run at your requestand continue with the rest of the toolchain, 21st steps left out; after that, a later12in the same run is reported in one line, never re-asked. Never skip silently ("not logged in, so we don't use it" is the failure this branch closes). Never run21st loginyourself — it opens a browser and needs the human.TWENTYFIRST_TOKENis a shell-profile setting followed by a session restart, never an in-sessionexport(tool calls don't share a shell, and a secret doesn't belong in the transcript). -
11 /
READY BUT UNVERIFIED→claudewas unreachable, so the design plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready": proceed only after telling the user that N tool(s) went unverified and having them confirm withclaude plugin list. Fail-visible, not fail-silent. A21st (whoami: rc=… …)entry in this block means the CLI itself could not answer (a runtime/PATH problem, e.g. node under nvm) — the remedy is the diagnostic the script prints, never a sign-in prompt; relay its own line.
4. Animation library — suggest-only (fires only on a real motion signal)
Orthogonal to the toolchain check above: §2-3 are about Claude's design TOOLS;
this is about the PROJECT's runtime dep. Evaluate it only once the toolchain is
resolved and you're actually proceeding with the build (READY, after the user
ran /profile design, or after the sign-in re-run returns READY). Never on
the INCOMPLETE stop path — that path has one action only (/profile design); don't stack an optional note on it.
Fires only when ALL THREE hold — drop any one → no suggestion, stay silent:
- Motion signal — the task matched a motion keyword from §DETECTION:
animation,transition,hover,motion, oranimate. A static button / card / layout with no motion signal needs no anim lib → skip. - Stack eligible —
detect_anim_eligibilityreturnseligible|…. - No anim lib yet —
is_anim_lib_installedfinds none.
Only if condition 1 holds, run the helper for 2 and 3 — do NOT re-list packages
here; the helper's is_anim_lib_installed is the single source of which libs
count:
source "$HOME/.claude/lib/animation-lib-check.sh"
result=$(detect_anim_eligibility) # '<status>|<package>|<reason>'
status=$(echo "$result" | cut -d'|' -f1)
pkg=$(echo "$result" | cut -d'|' -f2)
reason=$(echo "$result" | cut -d'|' -f3)
if [ "$status" = "eligible" ] && ! is_anim_lib_installed >/dev/null; then
cmd=$(recommend_anim_install_cmd "$pkg") # pnpm/yarn/bun/npm per lockfile
# → surface the one-line suggestion below. Do NOT run $cmd.
fi
Surface — always this single line (non-blocking, suggest-only):
🎬 Stack motion-eligible (<reason>), no anim lib — `<cmd>`? (optional; say the word, I'll add it)
Rules:
- Suggest-only, never auto-install. Run
<cmd>ONLY on explicit user consent. BDR-005: mid-session + existingpackage.json= consent required — same contract as/onboardSTEP 2.5, opposite of/init-projectSTEP 5e (auto-install on a just-validated fresh scaffold). - Non-blocking. Never halts the build; NOT a second gate. The toolchain stop (§3, exit 10) is the only STOP. Surface the line, keep going.
- Stateless dedup, by construction. The suggestion is ALWAYS the single line
above — no first-time-block / later-short split. That split would need session
state the gate doesn't have, and a file marker would persist forever (per
project, not per session). Determinism here comes from having nothing to
remember, not from a behavioral "the agent recalls it" guard. Re-fire is one
ignorable line, on a narrow population: condition 3 (
is_anim_lib_installed, 10 libs incl gsap / react-spring / lottie) kills it the instant any anim lib lands, so only "eligible + pure-CSS + actively declined" ever sees it twice. - Two "motion"s (agent-facing). The lib
motion(npm dep, this step) ≠ the skilldesign-motion-principles(# GATE-BLOCK:core set, §2-3). The toolchain check handles the skill; this step handles the lib. Don't conflate them when talking to the user.
5. Impeccable design context — suggest-only (one check, one line)
Same class as §4: a PROJECT-side prerequisite, not a tool. impeccable
installs globally, but every one of its verbs reads a per-project PRODUCT.md
that only /impeccable init writes. Without it the skill runs on invented
context, which is worse than not running it — and nothing else in the process
says so, because init has to happen in the agent chat, not in an installer.
Fires when BOTH hold — else stay silent:
- impeccable symlink present under
skills/(non-blocking external — not on the# GATE-BLOCK:list, so §3 never checks it). - The project has no
PRODUCT.mdat its root.
Evaluate it on the same path as §4: after the toolchain resolves, never on the INCOMPLETE stop path. One line, non-blocking:
🧭 impeccable has no project context here (no PRODUCT.md) — run `/impeccable init` first? (optional)
Rules:
- Non-blocking, and never run
initunprompted: it interviews the user about the product, so it needs their attention, not their absence. - One line per session at most. A refusal is an answer; do not re-ask inside the same task.
- Skip entirely for a review/audit of a single component and for any non-UI work. This is for Build and design-system tiers.
Other toolchains
The script defaults to the design profile. A task needing another profile's
toolchain passes it: design-tool-gate.sh <profile>. Scope comes from that
profile's # GATE-BLOCK: line (absent → every skill/plugin/mcp entry). The
remedy is always /profile <that> — a profile, never a lone tool.
IMPORTANT
- Remedy is ALWAYS a profile (
/profile design), never an atomic tool toggle — the profile system is the single source of truth for what's active. - the
21stCLI is REQUIRED (it trips the gate) and/profile designcannot install it — the gate names the two commands; surface them to the user. Signed out → exit 12: ask! 21st login, wait, re-run; explicit opt-out only, never a silent skip. - The design-core set (what trips the gate) is declared in
design.profileon the# GATE-BLOCK:line(s) — edit there to add/remove a blocking design tool, not in the script. - The state check shells out to
claude(plugin/mcp list): a few seconds. Trivial / non-design tasks skip it entirely (no signal, or trivial tier). design-tool-gate.sh's per-type state checks MIRRORprofile.sh:skill_status()— change one, sync the other, except the 21st auth state: gate-only, no skill_status counterpart.- Do NOT run this gate on pure backend/API/CLI tasks (no signals = no gate).