From 3eefb8ad7c2e7d29de938c5a46164ecab06eafa0 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Sun, 21 Jun 2026 11:21:48 +0200 Subject: [PATCH] feat(design-gate): profile-based toolchain gate + design-tool-gate.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- lib/design-gate.md | 98 +++++++++++++++++-------- lib/design-tool-gate.sh | 141 ++++++++++++++++++++++++++++++++++++ lib/profiles/design.profile | 7 ++ 3 files changed, 216 insertions(+), 30 deletions(-) create mode 100755 lib/design-tool-gate.sh diff --git a/lib/design-gate.md b/lib/design-gate.md index 1ffd3dc..915bcc0 100644 --- a/lib/design-gate.md +++ b/lib/design-gate.md @@ -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: + required + manual step: + → 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 `. Scope comes from that +profile's `# GATE-BLOCK:` line (absent → every skill/plugin/mcp entry). The +remedy is always `/profile ` — 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. diff --git a/lib/design-tool-gate.sh b/lib/design-tool-gate.sh new file mode 100755 index 0000000..e9732d7 --- /dev/null +++ b/lib/design-tool-gate.sh @@ -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 `. +# +# Two inputs from the profile, both claude-free: +# - structure: profile.sh show --plain -> "\t" +# - gate scope: the "# GATE-BLOCK:" line(s) in .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 diff --git a/lib/profiles/design.profile b/lib/profiles/design.profile index 2a9a3f6..d03de02 100644 --- a/lib/profiles/design.profile +++ b/lib/profiles/design.profile @@ -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