feat(design-gate): ask for 21st login and wait instead of skipping

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.
This commit is contained in:
bastien
2026-09-28 12:51:55 +02:00
parent 729d71546f
commit bd3e525bb3
6 changed files with 307 additions and 32 deletions
+14
View File
@@ -7,6 +7,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- **Design gate asks the user to sign in to 21st instead of skipping it**:
`lib/design-tool-gate.sh` adds a three-state 21st auth predicate
(`twentyfirst_auth_state`, honors `TWENTYFIRST_TOKEN`/`API_KEY_21ST` or a
local `21st whoami` read). Signed out now trips a new `SIGN-IN REQUIRED`
state (exit 12) instead of silently proceeding or reporting a plain
INCOMPLETE. The agent asks the user to run `! 21st login` in-session and
waits, re-running the gate on reply; an explicit "proceed without 21st"
opt-out is honored and never re-asked. A `whoami` answer that can't be
classified (unexpected line, nonzero rc, timeout) surfaces as unverified
with the raw diagnostic (`21st (whoami: rc=… …)`), never guessed as
signed-in or signed-out. `lib/design-gate.md` and the
`skills/feat`/`skills/bugfix` STEP 0.5 design-gate bullets document the
new branch. Hermetic suite `lib/tests/design-tool-gate.test.sh`, 7 named
cases.
- **`make doctor` checks the vendored externals** — new
`lib/doctor-vendored.sh` (`check_vendored_skills`), wired into doctor.sh
after the gstack section: every curl-pinned entry of plugins.lock.json
+30 -6
View File
@@ -61,7 +61,7 @@ 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) · `2` = error.
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
@@ -82,18 +82,39 @@ Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it)
but the CLI they shell out to is a global npm install: tell the user to run
`npm i -g @21st-dev/cli` then `21st login` (no API key, no MCP).
- Do NOT hand-activate individual tools. The profile is the unit of activation.
- **12 / `SIGN-IN REQUIRED`** → STOP. The 21st CLI is installed but `21st
whoami` reports 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 run `21st login` in 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-run `design-tool-gate.sh` before any 21st step: `READY` →
continue; still `12` → ask again once, then offer the opt-out. Explicit
refusal — the user answers "proceed without 21st" (or words to that
effect) → say visibly `21st skipped for this run at your request` and
continue with the rest of the toolchain, 21st steps left out; after that,
a later `12` in 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 run `21st login` yourself — it opens a browser
and needs the human. `TWENTYFIRST_TOKEN` is a shell-profile setting
followed by a session restart, never an in-session `export` (tool calls
don't share a shell, and a secret doesn't belong in the transcript).
- **11 / `READY BUT UNVERIFIED`** → `claude` was 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 with `claude plugin list`. Fail-visible, not fail-silent.
them confirm with `claude plugin list`. Fail-visible, not fail-silent. A
`21st (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, or after the user
ran `/profile design`). Never on the INCOMPLETE stop path — that path has one
action only (`/profile design`); don't stack an optional note on it.
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:
@@ -182,11 +203,14 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
the profile system is the single source of truth for what's active.
- the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot
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.profile` on
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 MIRROR
`profile.sh:skill_status()` — change one, sync the other.
`profile.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).
+96 -17
View File
@@ -17,7 +17,8 @@
# -> fall back to every skill/plugin/mcp entry (coarse).
#
# State (active or not) is checked per channel, by type. These per-type
# checks MIRROR profile.sh:skill_status() — change one, sync the other.
# checks MIRROR profile.sh:skill_status() — change one, sync the other,
# except the 21st auth state: gate-only, no skill_status counterpart.
#
# type channel class
# gstack|external|personal skill symlink in skills/ blocking
@@ -31,18 +32,21 @@
# it, and the remedy is `/profile design` + a manual step.
# This is where the `21st` CLI lands: required, never
# silent (npm i -g @21st-dev/cli, then 21st login).
# 21st's sign-in state is three-valued: in (active),
# out (exit 12, ask to sign in), unknown (exit 11).
# Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are
# ignored entirely (browser/plan/shotgun tooling, graphify).
#
# disabledMcpServers is NEVER read — unreliable for bi-modal servers
# (context7 can appear there yet be active via another channel).
#
# Exit: 0 = ready · 11 = ready-but-unverified (proceed, say so) · 10 = incomplete (trips) · 2 = error.
# Exit: 0 = ready · 11 = ready-but-unverified (proceed, say so) ·
# 10 = incomplete (trips) · 12 = sign-in required (21st) · 2 = error.
# Usage: design-tool-gate.sh [profile] (default profile: design)
# ============================================================
set -euo pipefail
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO="${DESIGN_GATE_REPO_OVERRIDE:-$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
PROFILE_SH="${DESIGN_GATE_PROFILE_SH:-$REPO/lib/profile.sh}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
PROFILES_DIR="$REPO/lib/profiles"
@@ -104,6 +108,35 @@ ensure_21st_on_path() {
}
ensure_21st_on_path
# 21st's sign-in state, three-valued. `whoami` is a local token read (no
# network), rc 0 either way — so rc alone can't tell signed-in from signed-
# out; the FIRST LINE of stdout does. A token env, when already exported by
# the user's shell profile, wins without a CLI call (never requested here:
# tool calls don't share a shell, and a secret doesn't belong in a comment
# or the transcript). `timeout 15` bounds a hung CLI; stdin is closed so a
# CLI that reads stdin can't eat the gate's own `read` loop; stderr never
# enters the match (stdout only). Echoes: in | out | unknown:<diagnostic>.
twentyfirst_auth_state() {
if [ -n "${TWENTYFIRST_TOKEN:-}" ] || [ -n "${API_KEY_21ST:-}" ]; then
echo in
return
fi
local line rc
if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)"; then
rc=0
else
rc=$?
fi
if [ "$rc" -eq 0 ]; then
case "$line" in
"Logged in as "*) echo in; return ;;
"Not logged in"*) echo out; return ;;
esac
fi
[ -n "$line" ] || line="no output"
echo "unknown:whoami: rc=$rc ${line:0:60}"
}
# Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated).
# Empty => fall back to "every gate-relevant entry is in scope" (coarse).
core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \
@@ -115,8 +148,10 @@ in_scope() {
case " $core_set " in *" $1 "*) return 0 ;; *) return 1 ;; esac
}
# State of one tool, by type. Mirrors profile.sh:skill_status() — keep in sync.
# Echoes: active | inactive | unknown (unknown = can't verify, claude absent)
# State of one tool, by type. Mirrors profile.sh:skill_status() — keep in
# sync, except the 21st auth state (gate-only, no skill_status counterpart).
# Echoes: active | inactive | unknown (can't verify, claude absent) |
# signedout | unknown:<diagnostic> (last two: 21st CLI only)
tool_active() {
local name="$1" type="$2"
case "$type" in
@@ -135,7 +170,15 @@ tool_active() {
if "$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${name}"; then echo active; else echo inactive; fi
;;
cli)
if command -v "$name" >/dev/null 2>&1; then echo active; else echo inactive; fi
command -v "$name" >/dev/null 2>&1 || { echo inactive; return; }
[ "$name" = "21st" ] || { echo active; return; }
local auth
auth="$(twentyfirst_auth_state)"
case "$auth" in
in) echo active ;;
out) echo signedout ;;
unknown:*) echo "$auth" ;;
esac
;;
*) echo inactive ;;
esac
@@ -150,12 +193,17 @@ plain="$("$PROFILE_SH" show "$PROFILE" --plain 2>/dev/null)" \
blocking=() # inactive, /profile design activates it (skill/plugin)
manual=() # inactive, required but needs a manual step (mcp key / cli install)
unverified=() # can't check (claude CLI absent)
signedout=() # 21st CLI installed, not signed in
unverified_cli=() # 21st CLI: whoami answered something unexpected
while IFS=$'\t' read -r type name; do
[ -n "$type" ] || continue
in_scope "$name" || continue # ignore non-core tooling (browser, plan-*, graphify)
case "$(tool_active "$name" "$type")" in
state="$(tool_active "$name" "$type")"
case "$state" in
active) ;;
signedout) signedout+=("$name") ;;
unknown) unverified+=("$name") ;;
unknown:*) unverified_cli+=("$name (${state#unknown:})") ;;
*)
case "$type" in
gstack|external|personal|plugin) blocking+=("$name") ;;
@@ -165,11 +213,31 @@ while IFS=$'\t' read -r type name; do
esac
done <<< "$plain"
# Verdict — three outcomes:
# print_unverified — the "also unverified" lines shared by the 10, 11 and 12
# blocks: a claude-unreachable tool keeps its existing remedy; a 21st CLI
# that answered whoami with something unexpected gets its own — the two are
# never merged, so no block blames 21st for a claude problem or vice versa.
print_unverified() {
if [ "${#unverified[@]}" -gt 0 ]; then
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
fi
local entry name diag
for entry in "${unverified_cli[@]}"; do
name="${entry%% (*}"
diag="${entry#*\(}"; diag="${diag%\)}"
echo " $name could not answer: $diag —" \
"a CLI runtime/PATH problem (node under nvm?)," \
"not a sign-in problem; fix it, then re-run"
done
}
# Verdict — four outcomes, checked in order:
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE.
# claude was unreachable, so the plugin channel (ui-ux-pro-max) could not
# be checked. Never pass this as a silent READY — proceed, but say so.
# else signedout non-empty -> SIGN-IN REQUIRED (exit 12): ask the user to
# run `21st login`, end the turn, wait, re-run — never a silent skip.
# else unverified/unverified_cli non-empty -> READY BUT UNVERIFIED (exit
# 11): fail-VISIBLE. claude unreachable and/or 21st couldn't answer
# whoami — never pass either as a silent READY.
# nothing pending -> READY (exit 0).
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
echo "design toolchain: INCOMPLETE"
@@ -182,19 +250,30 @@ if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
*" 21st "*) echo " 21st needs the CLI: npm i -g @21st-dev/cli then 21st login" ;;
esac
fi
if [ "${#unverified[@]}" -gt 0 ]; then
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
fi
print_unverified
echo " → run: /profile $PROFILE"
exit 10
fi
if [ "${#unverified[@]}" -gt 0 ]; then
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked"
echo " unverified (claude CLI unreachable): ${unverified[*]}"
if [ "${#signedout[@]}" -gt 0 ]; then
echo "design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in"
echo " ask the user to run in this session:" \
" ! 21st login" \
" (browser flow, saves a local token)"
echo " then re-run this gate before any 21st step — never skip 21st silently"
print_unverified
exit 12
fi
if [ "${#unverified[@]}" -gt 0 ] || [ "${#unverified_cli[@]}" -gt 0 ]; then
echo "design toolchain: READY BUT UNVERIFIED —" \
"$(( ${#unverified[@]} + ${#unverified_cli[@]} )) tool(s) not checked"
print_unverified
if [ "${#unverified[@]}" -gt 0 ]; then
echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
echo " active. Proceed only after checking manually:"
echo " claude plugin list"
fi
exit 11
fi
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# lib/tests/design-tool-gate.test.sh — hermetic suite for the 21st sign-in
# state added to lib/design-tool-gate.sh (contract 2026-09-28-21st-signin-
# gate-1215): a fake `21st` CLI on a fixture PATH drives every whoami answer
# (signed in / signed out / garbage / nonzero rc) through the real gate
# script, with HOME and PATH redirected into the fixture so the machine's
# real CLI is never reachable. A loud precondition proves that redirection
# actually holds before any case runs.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
GATE="$ROOT/lib/design-tool-gate.sh"
PASS=0; FAIL=0
ok() { echo "PASS $1"; PASS=$((PASS + 1)); }
bad() { echo "FAIL $1 — $2"; FAIL=$((FAIL + 1)); }
# Precondition, loud: the sanitized PATH below and ensure_21st_on_path()'s
# own probes must never resolve a REAL 21st — else CLI_ABSENT_10 (and every
# other case) would silently exercise this machine's CLI instead of the stub.
if PATH=/usr/bin:/bin command -v 21st >/dev/null 2>&1 \
|| [ -e /usr/local/bin/21st ]; then
echo "FAIL precondition: system-wide 21st present," \
"CLI_ABSENT case not hermetic"
FAIL=$((FAIL + 1))
echo "PASS=$PASS FAIL=$FAIL"
exit 1
fi
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/repo/lib/profiles" "$WORK/repo/skills" "$WORK/bin" "$WORK/home"
# GATE-BLOCK allowlist: 21st (cli) always, ghost-skill (external) only used
# by INCOMPLETE_WINS to prove a blocking miss still trips the gate.
cat > "$WORK/repo/lib/profiles/design.profile" <<'EOF'
# GATE-BLOCK: 21st ghost-skill
21st cli
ghost-skill external
EOF
# Fake profile.sh: `show design --plain` cats whatever the case wrote to
# plain.txt. $WORK is read from the environment at run time (exported
# below), never baked in here — the heredoc is quoted on purpose.
cat > "$WORK/repo/lib/profile.sh" <<'EOF'
#!/usr/bin/env bash
[ "$1" = show ] && [ "$3" = --plain ] && { cat "$WORK/plain.txt"; exit 0; }
exit 1
EOF
chmod +x "$WORK/repo/lib/profile.sh"
export WORK
# Fake 21st CLI: `whoami` answers per $FAKE_21ST_MODE, the real CLI's exact
# sentences for in/out (proven by STUB_CONTROL below), a garbage line at
# rc 0, or the signed-out sentence at a nonzero rc (proves rc wins).
cat > "$WORK/bin/21st" <<'EOF'
#!/usr/bin/env bash
[ "${1:-}" = whoami ] || exit 1
signedout='Not logged in. Run `21st login`, or set TWENTYFIRST_TOKEN.'
case "${FAKE_21ST_MODE:-in}" in
in) echo "Logged in as tester (saved locally)."; exit 0 ;;
out) echo "$signedout"; exit 0 ;;
garbage) echo "Something unexpected"; exit 0 ;;
fail) echo "$signedout"; exit 3 ;;
esac
EOF
chmod +x "$WORK/bin/21st"
# gate_run <FAKE_21ST_MODE> <plain, \t and \n escapes> [PATH override]
# -> sets $GATE_OUT / $GATE_RC. TWENTYFIRST_TOKEN and API_KEY_21ST are
# always unset here; TOKEN_READY below sets them explicitly instead.
gate_run() {
local mode="$1" plain="$2" gate_path="${3:-$WORK/bin:/usr/bin:/bin}"
printf '%b\n' "$plain" > "$WORK/plain.txt"
GATE_OUT="$(env -u TWENTYFIRST_TOKEN -u API_KEY_21ST \
HOME="$WORK/home" PATH="$gate_path" \
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
FAKE_21ST_MODE="$mode" bash "$GATE" 2>&1)"
GATE_RC=$?
}
# ── stub positive control ────────────────────────────────────────────────
if FAKE_21ST_MODE=in "$WORK/bin/21st" whoami | grep -q '^Logged in as ' \
&& FAKE_21ST_MODE=out "$WORK/bin/21st" whoami | grep -q '^Not logged in'
then ok STUB_CONTROL; else bad STUB_CONTROL "stub sentences wrong"; fi
# ── SIGNED_IN_READY: whoami says logged in -> exit 0, READY ────────────────
gate_run in 'cli\t21st'
if [ "$GATE_RC" -eq 0 ] && echo "$GATE_OUT" | grep -q 'toolchain: READY'; then
ok SIGNED_IN_READY
else
bad SIGNED_IN_READY "rc=$GATE_RC out=$GATE_OUT"
fi
# ── SIGNED_OUT_12: whoami says not logged in -> exit 12, ask to sign in ────
gate_run out 'cli\t21st'
if [ "$GATE_RC" -eq 12 ] && echo "$GATE_OUT" | grep -q 'SIGN-IN REQUIRED' \
&& echo "$GATE_OUT" | grep -q '21st login' \
&& ! echo "$GATE_OUT" | grep -q 'INCOMPLETE'; then
ok SIGNED_OUT_12
else
bad SIGNED_OUT_12 "rc=$GATE_RC out=$GATE_OUT"
fi
# ── TOKEN_READY: a token env wins even while whoami reports signed out ─────
printf 'cli\t21st\n' > "$WORK/plain.txt"
out_t="$(env HOME="$WORK/home" PATH="$WORK/bin:/usr/bin:/bin" \
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
FAKE_21ST_MODE=out TWENTYFIRST_TOKEN=x bash "$GATE" 2>&1)"; rc_t=$?
out_k="$(env HOME="$WORK/home" PATH="$WORK/bin:/usr/bin:/bin" \
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
FAKE_21ST_MODE=out API_KEY_21ST=x bash "$GATE" 2>&1)"; rc_k=$?
if [ "$rc_t" -eq 0 ] && [ "$rc_k" -eq 0 ]; then
ok TOKEN_READY
else
bad TOKEN_READY "TOKEN rc=$rc_t ($out_t) | API_KEY rc=$rc_k ($out_k)"
fi
# ── CLI_ABSENT_10: 21st off PATH -> exit 10, INCOMPLETE (not sign-in) ──────
gate_run in 'cli\t21st' /usr/bin:/bin
if [ "$GATE_RC" -eq 10 ] && echo "$GATE_OUT" | grep -q 'INCOMPLETE'; then
ok CLI_ABSENT_10
else
bad CLI_ABSENT_10 "rc=$GATE_RC out=$GATE_OUT"
fi
# ── INCOMPLETE_WINS: a blocking miss outranks a signed-out 21st ────────────
gate_run out 'cli\t21st\nexternal\tghost-skill'
if [ "$GATE_RC" -eq 10 ] && echo "$GATE_OUT" | grep -q 'INCOMPLETE' \
&& ! echo "$GATE_OUT" | grep -q 'SIGN-IN REQUIRED'; then
ok INCOMPLETE_WINS
else
bad INCOMPLETE_WINS "rc=$GATE_RC out=$GATE_OUT"
fi
# ── UNKNOWN_11: whoami answers something else -> surfaced, never guessed ───
gate_run garbage 'cli\t21st'
if [ "$GATE_RC" -eq 11 ] && echo "$GATE_OUT" | grep -q 'whoami: rc=0' \
&& echo "$GATE_OUT" | grep -q 'Something unexpected' \
&& ! echo "$GATE_OUT" | grep -q '21st login' \
&& ! echo "$GATE_OUT" | grep -q 'claude CLI unreachable'; then
ok UNKNOWN_11
else
bad UNKNOWN_11 "garbage: rc=$GATE_RC out=$GATE_OUT"
fi
gate_run fail 'cli\t21st'
if [ "$GATE_RC" -eq 11 ] && echo "$GATE_OUT" | grep -q 'whoami: rc=3'; then
ok UNKNOWN_11
else
bad UNKNOWN_11 "fail: rc=$GATE_RC out=$GATE_OUT"
fi
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
+3 -2
View File
@@ -55,8 +55,9 @@ git log --oneline -20 --all -- <suspected files>
Follow `$HOME/.claude/lib/design-gate.md`:
- Scan $ARGUMENTS and target files for design/UI/style signals (CSS, component, layout, animation).
- If signals found → run `design-tool-gate.sh`; if it reports INCOMPLETE,
tell the user to run `/profile design` before proceeding.
- If signals found → run `design-tool-gate.sh`; INCOMPLETE → tell the user
to run `/profile design`; SIGN-IN REQUIRED → design-gate.md §3 (ask
`! 21st login`, wait) before proceeding.
- If no signals → skip (zero overhead).
## STEP 2 — INVESTIGATE
+3 -2
View File
@@ -71,8 +71,9 @@ FEAT: <feature name> — rule <N>, ~<N> files, <brief approach>
Follow `$HOME/.claude/lib/design-gate.md`:
- Scan $ARGUMENTS and target files for design/UI/style signals.
- If signals found → run `design-tool-gate.sh`; if it reports INCOMPLETE,
tell the user to run `/profile design` before proceeding.
- If signals found → run `design-tool-gate.sh`; INCOMPLETE → tell the user
to run `/profile design`; SIGN-IN REQUIRED → design-gate.md §3 (ask
`! 21st login`, wait) before proceeding.
- If no signals → skip (zero overhead).
## STEP 0.6 — MEMORY READ-BEFORE (decisions-first)