feat(design-gate): profile-based toolchain gate + design-tool-gate.sh

design-tool-gate.sh: deterministic design-toolchain state check. Reads the
design-core tools from design.profile's `# GATE-BLOCK:` allowlist + their
types via `profile.sh show design --plain` (claude-free parse contract),
checks each on its own channel (skill symlink / claude plugin list / claude
mcp list / command -v). Never reads disabledMcpServers. Exit 0 ready · 10
incomplete · 2 error.

Remedy is always a profile (/profile design), never an atomic tool toggle —
the profile system stays the single source of truth for activation. magic is
required-but-manual: it TRIPS the gate (not advisory) and the output names
the MAGIC_API_KEY step. Non-design tools bundled in the profile (browse,
plan-*, design-shotgun, graphify) are excluded from the trip via GATE-BLOCK,
so the gate fires only on real design tools.

design-gate.md: §DECISION rewritten profile-based (tier → run script → branch
on 3 groups), replacing the old atomic "ask user to activate ui-ux-pro-max".
§DETECTION unchanged. design.profile: add the `# GATE-BLOCK:` allowlist
(8 design-core tools); it is a comment, so read_profile/--plain are unaffected.

Verified: shellcheck clean; magic-off (real design profile) → exit 10 +
API-key line; all active → exit 0; browse-off (non-GATE-BLOCK) → exit 0,
no trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bastien Chanot 2026-06-21 11:21:48 +02:00
parent 9d8b4cc38c
commit 3eefb8ad7c
3 changed files with 216 additions and 30 deletions

View File

@ -1,4 +1,4 @@
# DESIGN GATE — Auto-detect design tasks, activate ui-ux-pro-max
# 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.
@ -25,41 +25,79 @@ Check BOTH the task description AND the filesystem:
## DECISION
If **at least one signal** is detected:
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. Check if `ui-ux-pro-max`, `frontend-design`, and `design-motion-principles` are active:
```bash
source "$HOME/.claude/lib/detect-plugins.sh"
detect_uiux_pro_max && echo "ui-ux-pro-max: ACTIVE" || echo "ui-ux-pro-max: INACTIVE"
[ -L "$HOME/.claude/skills/frontend-design" ] && echo "frontend-design: ACTIVE" || echo "frontend-design: INACTIVE"
[ -L "$HOME/.claude/skills/design-motion-principles" ] && echo "design-motion-principles: ACTIVE" || echo "design-motion-principles: INACTIVE"
```
### 1. Tier — does the gate even apply?
2. If **all three ACTIVE** → proceed silently. Design context is fully available.
- **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.
3. If **ui-ux-pro-max INACTIVE** → ask the user:
```
🎨 DESIGN DETECTED — task touches UI/styling.
ui-ux-pro-max is not active. Activate it for design-aware guidance?
(yes / no)
```
- On **yes** → print `⚡ Activating ui-ux-pro-max...` and proceed with design context.
- On **no** → print `Proceeding without design plugin.` and continue normally.
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, magic). 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.
4. If **frontend-design INACTIVE** → warn (non-blocking):
```
frontend-design skill not installed — anti-AI-slop design guidelines unavailable.
Install: run install-plugins.sh or symlink skills-external/frontend-design to ~/.claude/skills/frontend-design
```
### 2. State — run the deterministic check
5. If **design-motion-principles INACTIVE** and task mentions animation/motion/transition → warn (non-blocking):
```
design-motion-principles skill not installed — motion design guidelines unavailable.
Install: run install-plugins.sh or symlink skills-external/design-motion-principles to ~/.claude/skills/design-motion-principles
```
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 magic/context7).
The core set lives in `design.profile`, not in the script or here — single source.
Exit codes: `0` = ready (proceed) · `10` = incomplete (gate trips) · `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. magic needs MAGIC_API_KEY>
→ run /profile design to activate it, then continue.
- **activate with /profile design** → skills + the plugin; `/profile design`
turns them on directly.
- **required + manual step** → required tools the profile can't flip silently.
**magic lands here: it TRIPS the gate** (it's required for Build), it is NOT
a silent "optional". `/profile design` runs `toggle-external.sh` for magic,
which needs a valid `MAGIC_API_KEY` in `.env` — tell the user to verify it.
- Do NOT hand-activate individual tools. The profile is the unit of activation.
- **`unverified` line** (claude CLI absent) → the state of a plugin/mcp couldn't
be checked; it does not block. Mention it, proceed.
### 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
- This gate adds ~5 seconds overhead. Worth it for design quality.
- 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.
- magic is REQUIRED (it trips the gate), but `/profile design` only enables it
if `MAGIC_API_KEY` is in `.env` — the gate says so; surface that to the user.
- 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.
- Do NOT run this gate on pure backend/API/CLI tasks (no signals = no gate).
- If no signal detected → skip entirely, zero overhead.

141
lib/design-tool-gate.sh Executable file
View File

@ -0,0 +1,141 @@
#!/usr/bin/env bash
# ============================================================
# lib/design-tool-gate.sh — Deterministic design-toolchain state check.
#
# Answers ONE question for the design gate (design-gate.md §DECISION):
# "Is the design toolchain active enough to proceed?"
#
# Source of truth = the profile system. The gate never activates a single
# tool atomically; it checks whether a profile's DESIGN-CORE tools (default
# profile: `design`) are active and, if not, points at `/profile <name>`.
#
# Two inputs from the profile, both claude-free:
# - structure: profile.sh show <profile> --plain -> "<type>\t<name>"
# - gate scope: the "# GATE-BLOCK:" line(s) in <profile>.profile — the
# allowlist of tools the gate trips on. A comment, so
# read_profile strips it and --plain never shows it. Absent
# -> 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.
#
# type channel class
# gstack|external|personal skill symlink in skills/ blocking
# plugin `claude plugin list` -> enabled blocking
# mcp | cli `claude mcp list` / command -v required-manual
#
# Class:
# blocking required + `/profile design` activates it directly.
# required-manual required but the profile can't flip it silently (API
# key / external install) — the gate STILL trips, names
# it, and the remedy is `/profile design` + a manual step.
# This is where magic lands: required, never silent.
# 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
# (magic/context7 can appear there yet be active via another channel).
#
# Exit: 0 = ready (proceed) · 10 = incomplete (gate trips) · 2 = error.
# Usage: design-tool-gate.sh [profile] (default profile: design)
# ============================================================
set -euo pipefail
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROFILE_SH="$REPO/lib/profile.sh"
PROFILES_DIR="$REPO/lib/profiles"
SKILLS_DIR="$REPO/skills"
PROFILE="${1:-design}"
PROFILE_FILE="$PROFILES_DIR/$PROFILE.profile"
[ -x "$PROFILE_SH" ] || { echo "design-gate: profile.sh not executable at $PROFILE_SH" >&2; exit 2; }
[ -f "$PROFILE_FILE" ] || { echo "design-gate: profile '$PROFILE' not found" >&2; exit 2; }
# 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 \
| sed 's/^# GATE-BLOCK:[[:space:]]*//' | tr '\n' ' ' || true)"
# Membership in the allowlist. Empty allowlist = everything in scope.
in_scope() {
[ -z "$core_set" ] && return 0
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)
tool_active() {
local name="$1" type="$2"
case "$type" in
gstack|external|personal)
if [ -e "$SKILLS_DIR/$name" ]; then echo active; else echo inactive; fi
;;
plugin)
if ! command -v claude >/dev/null 2>&1; then echo unknown; return; fi
if claude plugin list 2>/dev/null \
| awk -v p="^[[:space:]]* ${name}@" '$0 ~ p {f=1; next} f && /Status:/ {print; exit}' \
| grep -q "✔ enabled"
then echo active; else echo inactive; fi
;;
mcp)
if ! command -v claude >/dev/null 2>&1; then echo unknown; return; fi
if claude 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
;;
*) echo inactive ;;
esac
}
# Structure via the parse contract (claude-free). Fail loud on a bad profile —
# an empty read must NOT silently report "ready".
plain="$("$PROFILE_SH" show "$PROFILE" --plain 2>/dev/null)" \
|| { echo "design-gate: 'profile.sh show $PROFILE --plain' failed" >&2; exit 2; }
[ -n "$plain" ] || { echo "design-gate: profile '$PROFILE' is empty or unreadable" >&2; exit 2; }
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)
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
active) ;;
unknown) unverified+=("$name") ;;
*)
case "$type" in
gstack|external|personal|plugin) blocking+=("$name") ;;
*) manual+=("$name") ;;
esac
;;
esac
done <<< "$plain"
# Verdict. Either class trips the gate.
trip=0
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then trip=1; fi
if [ "$trip" -eq 0 ]; then
echo "design toolchain: READY — profile '$PROFILE' design tools active"
if [ "${#unverified[@]}" -gt 0 ]; then
echo " note: could not verify (claude CLI absent): ${unverified[*]}"
fi
exit 0
fi
echo "design toolchain: INCOMPLETE"
if [ "${#blocking[@]}" -gt 0 ]; then
echo " activate with /profile $PROFILE: ${blocking[*]}"
fi
if [ "${#manual[@]}" -gt 0 ]; then
echo " required + manual step (API key / external install): ${manual[*]}"
case " ${manual[*]} " in
*" magic "*) echo " magic needs MAGIC_API_KEY in .env (/profile $PROFILE runs toggle-external.sh)" ;;
esac
fi
if [ "${#unverified[@]}" -gt 0 ]; then
echo " unverified (claude CLI absent): ${unverified[*]}"
fi
echo " → run: /profile $PROFILE"
exit 10

View File

@ -1,6 +1,13 @@
# DESC: Design work — visual QA, design systems, mockups, polish
# Activate when: building/reviewing UI, picking aesthetics, design tokens.
# Companion CLIs (advisory): graphify (visual structure).
#
# Gate scope (design-tool-gate.sh): only the tools on the GATE-BLOCK lines
# below trip the design gate. The rest of this profile (browser/plan/shotgun
# tooling, graphify) is bundled for convenience but never blocks. Keep these
# lines in sync when adding/removing a core design tool.
# GATE-BLOCK: frontend-design ui-ux-pro-max emil-design-eng design-html
# GATE-BLOCK: design-motion-principles design-review design-consultation magic
# Core design skills (gstack)
design-shotgun