Merge feature/21st-cli-migration into develop

This commit is contained in:
bastien
2026-09-22 06:52:22 +02:00
27 changed files with 823 additions and 255 deletions
+6
View File
@@ -242,3 +242,9 @@ rules:
- **Status**: resolved (A: ext hooks only terminals born after activation → install ext THEN start/re-attach session; B: Code app volume 0 in Windows mixer). - **Status**: resolved (A: ext hooks only terminals born after activation → install ext THEN start/re-attach session; B: Code app volume 0 in Windows mixer).
- **Lesson**: two independent client faults presented as one symptom ("nothing works"). Splitting probe = run signal in FRESH terminal + play VS Code's own sound preview. Preview bypasses terminal/BEL/hook/dtach/ext → isolates renderer audio in one step. Do that FIRST next time, before any server-side archaeology. - **Lesson**: two independent client faults presented as one symptom ("nothing works"). Splitting probe = run signal in FRESH terminal + play VS Code's own sound preview. Preview bypasses terminal/BEL/hook/dtach/ext → isolates renderer audio in one step. Do that FIRST next time, before any server-side archaeology.
- **Reference**: [[BLK-019]] bell-only variant (resolved differently — setting alone insufficient here), [[LRN-145]] terminalSequence-not-/dev/tty pattern. Silent-degradation class [[LRN-047]]. - **Reference**: [[BLK-019]] bell-only variant (resolved differently — setting alone insufficient here), [[LRN-145]] terminalSequence-not-/dev/tty pattern. Silent-degradation class [[LRN-047]].
## BLK-021 — Bash tool dead mid-session ("every command exits 1"): /tmp usrquota blown by a dead session's probe HOMEs — 2026-09-22
- **Friction**: previous session on `feature/21st-cli-migration` lost its shell before tests + commit: every Bash call, `echo` included, returned 1. Its harness file `imptest2/step8d-test.sh` landed as 0 bytes.
- **Real cause** (strong evidence, not reproduced on purpose): `/tmp` = tmpfs 7.4 GB mounted `usrquota`; `/tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-…/scratchpad` holds 5.9 GB of sandbox HOMEs (`pinprobe/` 2.1 GB, `pinrc/` 1.6 GB, `imp1 impg imptest sbx1 sbx2 v3.2.0 v3.6.1 v4.0.5 …`) from the impeccable pin probes. `dd` 40 MB to `/tmp/claude-1000` → "Disk quota exceeded" (EDQUOT) while `df` still shows 1.6 GB avail. Same write to `~/.cache` OK. Bash tool + `mktemp` + heredocs live in /tmp → all die together. This session: first impeccable probe failed with `Quota exceeded (os error 122)` on the installer's `/tmp/impeccable-update-*` staging, same cause.
- **Solution**: this round ran everything with `TMPDIR=~/.cache/imp-probe/tmp` (probe, harness, `make test`). Durable fix = delete the dead session's scratchpad: `rm -rf /tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-e143-4d51-b589-a566641079b5` (agent's `rm -rf` on /tmp denied by the classifier → user action). Rule for probes: sandbox HOMEs that pull npm/node payloads go under `~/.cache/<probe>/`, never the /tmp scratchpad, and get removed at the end of the session.
- **Status**: open until the user frees /tmp. Links [[BDR-094]], [[LRN-159]].
+18
View File
@@ -102,6 +102,7 @@ rules:
| BDR-090 | 2026-09-15 | Destructive shell work → autoMode soft_deny/hard_deny; `ask` tier abandoned (inert under auto) | accepted | | BDR-090 | 2026-09-15 | Destructive shell work → autoMode soft_deny/hard_deny; `ask` tier abandoned (inert under auto) | accepted |
| BDR-091 | 2026-09-16 | Ask, don't guess: open-choice sweep (3 classes) at plan step + mid-run CLASS channel supersede "one question upfront" | accepted | | BDR-091 | 2026-09-16 | Ask, don't guess: open-choice sweep (3 classes) at plan step + mid-run CLASS channel supersede "one question upfront" | accepted |
| BDR-092 | 2026-09-16 | docker + node framed by the classifier via autoMode.allow + soft_deny; ask entries retired | accepted | | BDR-092 | 2026-09-16 | docker + node framed by the classifier via autoMode.allow + soft_deny; ask entries retired | accepted |
| BDR-093 | 2026-09-22 | 21st.dev: magic MCP → CLI + skill pack; staged install past the ~/.claude/skills symlink; CLI = gate's required-manual | accepted |
--- ---
@@ -1178,3 +1179,20 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Guardrail**: S6 (loosening = user's own edit) overridden explicitly by the user for this change; diff reviewed on the branch before merge. - **Guardrail**: S6 (loosening = user's own edit) overridden explicitly by the user for this change; diff reviewed on the branch before merge.
- **Status**: accepted. Verified: `jq` valid; `claude auto-mode config` shows the 4 entries with `$defaults` expanded; `doctor.sh` autoMode PASS; live `docker exec -i supabase_db_game psql … -f - < verify/0043 … | tail` → `ROLLBACK`, exit 0, no prompt. `claude auto-mode critique` printed nothing (2.1.273). - **Status**: accepted. Verified: `jq` valid; `claude auto-mode config` shows the 4 entries with `$defaults` expanded; `doctor.sh` autoMode PASS; live `docker exec -i supabase_db_game psql … -f - < verify/0043 … | tail` → `ROLLBACK`, exit 0, no prompt. `claude auto-mode critique` printed nothing (2.1.273).
- **Reference**: `settings.json`, `templates/settings/SETTINGS.md` (`autoMode.allow` row + interpreter note), commit `5eccc3f`, merge `ddadca6`. Links [[BDR-090]], [[LRN-153]], [[LRN-155]], [[LRN-156]]. - **Reference**: `settings.json`, `templates/settings/SETTINGS.md` (`autoMode.allow` row + interpreter note), commit `5eccc3f`, merge `ddadca6`. Links [[BDR-090]], [[LRN-153]], [[LRN-155]], [[LRN-156]].
## BDR-093 — 21st.dev: magic MCP retired for the `21st` CLI + skill pack
- **Date**: 2026-09-22
- **Decision**: `@21st-dev/magic` MCP out, `@21st-dev/cli` (bin `21st`) in — upstream supersedes it (README 1.17.1: "one unified CLI", old magic config now a thin proxy to the same endpoint). Auth = `21st login`, browser token in `~/.config/21st`; no API key, no MCP process. `install-plugins.sh` Step 8.7: `npm i -g` (pin `21st` in plugins.lock.json) + staged `21st skills install --global --agent claude` + TTY-only login offer + pack disabled by default. `toggle-external.sh` manages `21st` as a pack (names globbed from `skills-external/21st-*`, parked under plain names = interoperable with profile.sh's external path). 5 design skills (`21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`) in design/web/web-full/full + `MANAGED_EXTERNALS`; `-registry`/`-design-sync` installed, parked. `MANAGED_MCPS` now empty (mcp type kept, advisory). Gate: `GATE-BLOCK: 21st 21st-ui-build` — CLI = required-manual class, `magic`'s old slot. Outward-facing verbs (`publish*`, `submit`, `edit`, `delete`, `remove-from-catalog`, `profile set|upload`) → one `autoMode.soft_deny` entry, NOT `ask` ([[LRN-153]]).
- **Why**: user ask ("plus besoin de mcp / api, juste en cli"), confirmed at the source, not the marketing page — the 21st.dev web docs still show the MCP `init --client` flow and an API key; the npm package README is what states the supersession. Net wins: one less MCP loaded per session, no API key to protect by reference ([[BDR-026]]/[[BDR-057]] vector gone), no unauthenticated local callback server ([[LRN-110]] gone with the tool).
- **Blocker + shape it forced**: `21st skills install --global` writes `<HOME>/.claude/skills/<n>/SKILL.md` and calls `assertNoSymlinkComponents` on every path segment — `~/.claude/skills` IS a symlink to `repo/skills`, so the documented `21st install-skill` fails hard ("Refusing to access symbolic link …/.claude/skills", reproduced live). → install under `mktemp -d` as HOME, move each skill into `skills-external/21st-*` (gitignored), symlink on demand. Same impeccable/ctx7 machine-owned pattern.
- **Alternatives rejected**: project-scope install (`<cwd>/.claude/skills`) — Claude Code would ALSO scan it as project skills in this repo = every 21st skill listed twice; add the pack to `link.sh`'s `EXTERNAL_SKILLS` — that loop force-creates symlinks, resurrecting a default-disabled pack on every `make link`; all 7 skills in the design profiles — publishing flows cost 2 descriptions/session for a workflow the user does not run; `ask` entries for the publish verbs — inert under auto ([[LRN-153]], [[BDR-090]]/[[BDR-092]] already retired that tier).
- **Status**: accepted. Verified: toggle enable/disable/restore/idempotent round-trip (7 skills), `profile.sh show design`, gate INCOMPLETE→names `21st` with the two commands, gate PATH repair proven under `env -i PATH=/usr/bin:/bin` with an nvm-stub, `profile-set-managed.test.sh` 17/17, `make test` (2 pre-existing FAILs, gitleaks binary absent on this host), shellcheck clean. OPEN for the user: `npm i -g @21st-dev/cli && 21st login` — the deny rule `Bash(npm install -g *)` means the agent cannot run it.
- **Reference**: `install-plugins.sh` Step 8.7, `update-all.sh` 7.4, `lib/toggle-external.sh`, `lib/profile.sh`, `lib/profiles/*.profile`, `lib/design-tool-gate.sh`, `lib/design-gate.md`, `CLAUDE.global.md`, `README.md`, `settings.json`, `.gitignore`, `.gitleaks.toml`, `.env.example`, `link.sh`. Supersedes the operative parts of [[BDR-059]] (the 4 `mcp__magic__*` ask entries) and the magic instance of [[BDR-026]]/[[BDR-057]]; [[BDR-025]]'s required-manual class stands, its magic example does not. Links [[LRN-158]], [[LRN-110]].
## BDR-094 — impeccable: global-scope install through the repo symlinks, pin + @latest fallback, output-read failure check
- **Date**: 2026-09-22
- **Decision**: `install-plugins.sh` Step 8d + `update-all.sh` run `npx -y impeccable@<pin> skills install -y --providers=claude --scope=global --no-hooks` straight through the `~/.claude/{skills,agents}` symlinks → lands in `skills/impeccable` + `agents/impeccable-*.md` (both gitignored, machine-owned). No staging, no `mv`. Precondition guard: both symlinks must already point into the repo, else "run make link first". Pin failure → `@latest` + loud "bump plugins.lock.json" warn. Profile-parked copy stays parked (install to live slot, `mv` back to `skills-disabled/`). Success = rc 0 AND installer output free of `Download failed|Could not check for skill updates` (`imp_install`, mirrored in both scripts, sets `IMP_FAIL`). Pin 3.2.0 → 4.1.0 (CLI only; skill dist 4.3.1 + engine 0.1.5 own tracks). `link.sh` `EXTERNAL_SKILLS` drops impeccable; `skills-external/impeccable/` gone. `lib/design-gate.md` §5: suggest `/impeccable init` once when frontend project lacks `PRODUCT.md`.
- **Why**: (1) 3.2.0 skill dist gone upstream → rc 1 → `make plugin` printed "run manually" forever. (2) `--scope=project` + staged `mv` moved skill dir only, dropped the 4 subagents the same run wrote. (3) Global scope IS the repo install under the symlink model; staging bought nothing. (4) rc lies once a copy exists. Probe 2026-09-22, sandbox HOME, real installer: 4.1.0 then 3.2.0 → rc 0, "Could not check for skill updates: invalid zip data … Existing skills were left unchanged"; same-pin rerun → rc 0, "Skills are up to date (v4.3.1)"; both leave SKILL.md byte-identical (same mtime, same sha).
- **Alternatives rejected**: before/after skill-version compare (first idea) → cannot separate rotted-pin no-op from up-to-date no-op, identical files + rc 0 both times → false warn on every rerun. `--force` → re-downloads ~15 MB engine + dist on every `make plugin`, and the CLI's own update check already refreshes without it. Shared `lib/impeccable.sh` for `imp_install` → deferred: two mirrored 12-line helpers vs new lib + test; revisit at a third caller. Project-scope install inside this repo → Claude Code scans `.claude/skills` too = skill listed twice, shadows the global copy (seen live, TODO T6).
- **Status**: accepted. Verified: harness on extracted Step 8d, sandbox HOME, real installer, 4/4: fresh install (skill 4.3.1, 4 agents); rotted pin over a copy → fallback fires; same pin rerun → no false warn; parked + rotted → fallback, returned to `skills-disabled/`. `make test` green minus 2 pre-existing T16a (gitleaks absent), shellcheck clean. `update-all.sh` block: `bash -n` + shellcheck only, same helper, not run end to end.
- **Reference**: `install-plugins.sh` Step 8d, `update-all.sh`, `plugins.lock.json`, `.gitignore`, `link.sh`, `lib/design-gate.md` §5. Links [[LRN-159]], [[LRN-158]] (21st: opposite case, installer refuses symlinks → stage), [[LRN-077]] (pin doctrine), [[BLK-021]].
+9
View File
@@ -476,3 +476,12 @@ rules:
- Ask, don't guess ([[BDR-091]]): spec + plan, 9 lock-first tasks (contract-interview CLARIFY two passes, MID-RUN CLARIFICATION with `CLASS:` tag, HOW TO ASK; global rule; feat / bugfix / hotfix / ship-feature / init-project wired; interviewer; 3 executors), suite green. Behavioral fixture check still open ([[LRN-157]]). - Ask, don't guess ([[BDR-091]]): spec + plan, 9 lock-first tasks (contract-interview CLARIFY two passes, MID-RUN CLARIFICATION with `CLASS:` tag, HOW TO ASK; global rule; feat / bugfix / hotfix / ship-feature / init-project wired; interviewer; 3 executors), suite green. Behavioral fixture check still open ([[LRN-157]]).
- docker + node under auto mode ([[BDR-092]]): `ask` entries retired (inert on 2.1.273, probe — [[LRN-155]]), `autoMode.allow` + 2 soft_deny, live `docker exec … psql` OK. Static interpreter allow is suspended under auto → prose only ([[LRN-156]]). - docker + node under auto mode ([[BDR-092]]): `ask` entries retired (inert on 2.1.273, probe — [[LRN-155]]), `autoMode.allow` + 2 soft_deny, live `docker exec … psql` OK. Static interpreter allow is suspended under auto → prose only ([[LRN-156]]).
- Both merged into develop 2026-09-17 via gitflow (`ddadca6`, `56bd035`), two stack conflicts (TODO, CHANGELOG) resolved keeping both blocks. Symlinked `settings.json` follows the checkout: live config = whatever branch is out. - Both merged into develop 2026-09-17 via gitflow (`ddadca6`, `56bd035`), two stack conflicts (TODO, CHANGELOG) resolved keeping both blocks. Symlinked `settings.json` follows the checkout: live config = whatever branch is out.
## 2026-09-22
- 21st.dev magic MCP → `@21st-dev/cli` + 7-skill pack, user ask. Install/update/toggle/profiles/gate/docs/permissions migrated on `feature/21st-cli-migration`.
- Blocker: documented `21st install-skill` refuses the `~/.claude/skills` symlink → staged install under a throwaway HOME (LRN-158).
- Gate: `magic`+MAGIC_API_KEY required-manual slot → the `21st` CLI; publish verbs moved to `autoMode.soft_deny` (ask inert under auto).
- BDR-093, LRN-158. `make test` green except 2 pre-existing gitflow FAILs (gitleaks binary absent on this host). Branch UNMERGED — human gate.
- impeccable install repaired ([[BDR-094]]): global scope through the symlinks, 4 agents kept, pin 3.2.0 → 4.1.0 with @latest fallback, design-gate §5 `/impeccable init` hint. Residue probed: rotted pin over an existing copy exits 0 → `imp_install` reads the installer output ([[LRN-159]]); before/after version compare rejected (identical no-op). Harness 4/4, sandbox HOME, real installer.
- Previous shell death traced: /tmp tmpfs usrquota blown by 5.9 GB of dead-session probe HOMEs ([[BLK-021]], open, user frees). Tests + harness ran with TMPDIR under ~/.cache. `make test` green minus 2 pre-existing T16a, shellcheck clean. Committed on feature/21st-cli-migration, UNMERGED. `skills/synced/` (claude.ai synced skills, 4.4 MB) untracked + unignored, left for the user.
+16
View File
@@ -147,6 +147,7 @@ rules:
| LRN-155 | 2026-09-16 | ask under auto: doc says prompt, probe on 2.1.273 says no; re-probe after upgrades | any permission-tier reasoning | | LRN-155 | 2026-09-16 | ask under auto: doc says prompt, probe on 2.1.273 says no; re-probe after upgrades | any permission-tier reasoning |
| LRN-156 | 2026-09-16 | autoMode.allow = exception tier; static interpreter allow suspended under auto → conditions live in prose | conditional permissions | | LRN-156 | 2026-09-16 | autoMode.allow = exception tier; static interpreter allow suspended under auto → conditions live in prose | conditional permissions |
| LRN-157 | 2026-09-16 | gap-only trigger blind to taste → add a trigger class, not budget; ask at plan, mid-run for leftovers | any "ask more" request | | LRN-157 | 2026-09-16 | gap-only trigger blind to taste → add a trigger class, not budget; ask at plan, mid-run for leftovers | any "ask more" request |
| LRN-158 | 2026-09-22 | Installer refusing symlinked paths vs a symlinked config dir → stage under a throwaway HOME, move the result | any vendor installer writing into ~/.claude or ~/.config |
--- ---
@@ -1490,3 +1491,18 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
- **Pattern**: a trigger that fires only on missing outcome / scope / constraints lets every taste choice through — "add a share icon" is complete by those criteria and the icon's side is decided downstream. More budget changes nothing; the fix is a new trigger class (VISIBLE / PUBLIC NAME / SCOPE). Cost geometry: a fresh re-dispatch keeps the working tree and loses the executor's reasoning → the same question costs about one executor run more mid-run than at PLAN. So: sweep once at the plan step, keep the mid-run channel for leftovers. Executor tags the class; orchestrator re-reads it (tag = hint, a mis-tag would offload class 4 onto the human). Relayed questions obey [[LRN-102]]: context inside `AskUserQuestion`, nothing the user needs printed before it. - **Pattern**: a trigger that fires only on missing outcome / scope / constraints lets every taste choice through — "add a share icon" is complete by those criteria and the icon's side is decided downstream. More budget changes nothing; the fix is a new trigger class (VISIBLE / PUBLIC NAME / SCOPE). Cost geometry: a fresh re-dispatch keeps the working tree and loses the executor's reasoning → the same question costs about one executor run more mid-run than at PLAN. So: sweep once at the plan step, keep the mid-run channel for leftovers. Executor tags the class; orchestrator re-reads it (tag = hint, a mis-tag would offload class 4 onto the human). Relayed questions obey [[LRN-102]]: context inside `AskUserQuestion`, nothing the user needs printed before it.
- **Future application**: any "ask more" request → check WHICH trigger is blind before touching a quota. Any orchestrator with a "decide it yourself" fallback on an executor halt → route by class first. - **Future application**: any "ask more" request → check WHICH trigger is blind before touching a quota. Any orchestrator with a "decide it yourself" fallback on an executor halt → route by class first.
- **Reference**: [[BDR-091]], `lib/contract-interview.md` STEP 2 + MID-RUN CLARIFICATION. - **Reference**: [[BDR-091]], `lib/contract-interview.md` STEP 2 + MID-RUN CLARIFICATION.
## LRN-158 — A hardened installer + a symlinked config dir = documented command fails; stage under a throwaway HOME
- **Date**: 2026-09-22
- **Context**: `21st install-skill` (= `21st skills install --global`) is upstream's documented one-liner. Here it dies: `Refusing to access symbolic link /home/…/.claude/skills`. The installer walks every segment of `<HOME>/.claude/skills/<n>/SKILL.md` with an `assertNoSymlinkComponents` guard (anti symlink-escape); this repo's whole model is `~/.claude/skills -> repo/skills`. Two correct designs, mutually exclusive on the same path.
- **Pattern**: don't fight the guard and don't unlink the config dir. Run the installer with `HOME=$(mktemp -d)` so it writes into a pristine real tree, then move the output to the vendored dir the repo controls and symlink from there. Same shape as the impeccable/ctx7 staging (`mktemp -d`, install, `mv` into `skills-external/`), with HOME as the extra lever. Two conditions make it safe: the command must need nothing else from HOME (checked: manifest + content fetch are unauthenticated, hash-verified), and the moved payload must be self-contained.
- **Also**: read the npm tarball, not the vendor's web page. 21st.dev's `/mcp` and `/llms.txt` still document the MCP `init --client` flow with an API key; the package README states the CLI supersedes it. `curl registry.npmjs.org/<pkg>` + untar + read `README.md`/`dist` answered every question (commands, exit codes, where files land) that the site got wrong.
- **Future application**: any vendor installer that writes into `~/.claude`, `~/.config` or `~/.agents` on this machine. Probe first with a fake HOME containing the symlink, before wiring it into `install-plugins.sh` — the failure is instant and unambiguous.
- **Reference**: [[BDR-093]], `install-plugins.sh` Step 8.7, `update-all.sh` 7.4. Links [[LRN-034]] (run the real thing), [[BLK-014]]-class symlink/self-heal issues.
## LRN-159 — A pin whose payload is fetched at install time rots: pin + fallback, and read the installer's output, not its exit code
- **Date**: 2026-09-22
- **Context**: `impeccable@3.2.0` still on npm, but `skills install` downloads the skill dist at run time and that release's zip is gone → "Download failed: invalid zip data". Strict pin = `make plugin` fails forever, prints "run it yourself". Second layer: once a copy exists, same CLI exits 0 on the same failure ("Could not check for skill updates … Existing skills were left unchanged"), indistinguishable by rc, by SKILL.md version or by mtime/sha from "Skills are up to date".
- **Pattern**: two classes of npm pin. (a) self-contained package → pin freezes behaviour, rc is truth. (b) package that fetches its payload at install time (impeccable, ctx7, `skills add` style) → pin freezes only the fetcher; payload can vanish or drift. For (b): pin + `@latest` fallback + loud "bump the lock" warn, never pin-or-die. And when the tool has an "already installed" branch, capture stdout+stderr and match the failure text; rc and before/after compare both read "unchanged" for a no-op AND for a swallowed failure.
- **Future application**: any `install-plugins.sh` step whose pinned tool downloads something at install time. Probe both HOME states (clean, copy present) before trusting rc. Cheap recipe: sandbox HOME with the repo-shaped symlinks, pinned install twice, then the rotted pin; diff rc + output + `stat`/`sha256sum` of the landed file.
- **Reference**: [[BDR-094]], `install-plugins.sh` Step 8d `imp_install`, `update-all.sh`. Links [[LRN-077]] (why pin), [[LRN-034]] (run the real thing), [[LRN-158]].
+108
View File
@@ -1,5 +1,113 @@
# TODO # TODO
## 2026-09-22 — impeccable install repaired: global scope + agents + rotted pin (feature/21st-cli-migration)
User: `make plugin` never installs impeccable, it just prints "run it
yourself"; running it by hand needs `--scope=global` to land right, and then
`/impeccable init` is still required. Three separate defects, all confirmed:
1. **Pin rotted.** `npx -y impeccable@3.2.0 skills install` → `Download
failed: invalid zip data`, rc 1. The CLI fetches its skill dist at install
time and that release's artifact is gone. 3.6.1 / 4.0.5 / 4.1.0 all work.
That rc 1 is the "run manually" warn the user sees.
2. **Wrong scope + half the payload dropped.** The step staged
`--scope=project` in a tmpdir and `mv`'d only the skill dir, silently
discarding the 4 `impeccable-*` subagents the installer also writes.
`--scope=global` writes `~/.claude/skills/impeccable` +
`~/.claude/agents/impeccable-*.md`, and both are symlinks INTO this repo,
so a global install is the repo install. Verified in a sandbox HOME.
3. **`/impeccable init` never surfaced.** It writes per-project PRODUCT.md
(design context the skill reads); it runs in the agent chat, so install
can only announce it and the design gate has to check it.
- [x] T1 install-plugins.sh Step 8d rewritten: global scope, no staging,
pin→latest fallback with a loud bump-the-lock warn, park-aware
(profile may hold impeccable in skills-disabled), symlink precondition
guard, agent count + skill version reported, init hint printed.
Harness-tested against a fake HOME with repo-shaped symlinks: happy
path OK, park/restore OK. Caught + fixed there: `find` stops at the
`~/.claude/agents` symlink without `-L`, so the agent count read 0
while 4 agents were installed.
- [x] T2 update-all.sh impeccable block: same shape. `bash -n` only, NOT
run end to end.
- [x] T3 plugins.lock.json: 3.2.0 → 4.1.0 + honest note (pin covers the CLI
only; skill dist 4.3.1 and engine 0.1.5 have their own tracks).
- [x] T4 .gitignore: `agents/impeccable-*.md` (machine-owned, tracked dir);
drop `skills-external/impeccable/`. link.sh: impeccable out of
EXTERNAL_SKILLS (nothing to symlink any more). `git check-ignore`
confirms both paths.
- [x] T5 lib/design-gate.md §5: suggest-only PRODUCT.md / `/impeccable init`
check, same shape as the §4 animation-library check.
- [x] T6 duplicate project-scope install: already gone at resume (user ran
`rm -rf .claude/skills .claude/agents` before restarting).
- [x] T7 CHANGELOG (Added/Changed/Fixed) + BDR-094 + LRN-159 + BLK-021 +
journal. `make test` green except the 2 pre-existing gitflow T16a FAILs
(gitleaks binary absent on this host), shellcheck clean. Committed on
the branch, UNMERGED — human gate.
**Residual, probed and fixed (round 3)**: with a copy already installed a
rotted pin DOES exit 0 ("Could not check for skill updates: invalid zip data
… Existing skills were left unchanged"), and so does a genuine rerun of a
good pin ("Skills are up to date (v4.3.1)"). Both leave SKILL.md
byte-identical, so a before/after version compare cannot separate them.
`imp_install` (Step 8d and update-all.sh) now captures the installer output
and fails on `Download failed|Could not check for skill updates`, whatever
the exit code. Harness on the extracted step, sandbox HOME, real installer:
fresh install; rotted pin over a copy → fallback fires; same pin rerun → no
false warn; parked copy + rotted pin → fallback, then returned to
skills-disabled/. update-all.sh: `bash -n` + shellcheck only.
OPEN for the user:
- /tmp is a tmpfs with a per-user quota and the dead session's scratchpad
holds 5.9 GB of probe HOMEs. Writes to /tmp fail with EDQUOT: the likely
cause of the "every command exits 1" shell death (BLK-021). Free it:
`rm -rf /tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-e143-4d51-b589-a566641079b5`
(the agent's `rm -rf` under /tmp is denied). This round ran tests and the
harness with TMPDIR under ~/.cache.
- `skills/synced/` (4.4 MB, untracked, not ignored): claude.ai's synced
skills, written through the ~/.claude/skills symlink. Decide whether to
gitignore it; not touched here.
## 2026-09-22 — 21st: magic MCP → CLI + skills (feature/21st-cli-migration)
User: "remplacer pour 21st, il n'y a plus besoin de mcp / api, mais juste en
cli". Upstream confirmed (`@21st-dev/cli` 1.17.1 README): the CLI supersedes
`@21st-dev/magic`; auth is `21st login` (browser token in `~/.config/21st`),
no API key; `21st install-skill` = alias of `21st skills install --global`.
Gates answered by user: 5 design skills in profiles (registry + design-sync
parked), `make plugin` auto-installs the CLI + offers login on TTY only,
missing `21st` CLI trips the design gate (magic's old required-manual slot).
Blocker found + solved: `21st skills install --global` REFUSES to write
through a symlinked path (`assertNoSymlinkComponents`), and `~/.claude/skills`
IS a symlink → repo/skills. Verified live: "Refusing to access symbolic link
…/.claude/skills". → install into a staged HOME (mktemp), move each skill to
`skills-external/21st-*/` (impeccable pattern), symlink from there.
- [x] T1 install-plugins.sh STEP 8.7: magic block → 21st CLI (`npm i -g`,
pinned via plugins.lock.json) + staged `skills install` →
skills-external/21st-*, TTY-gated `21st login`, pack disabled by default.
- [x] T2 lib/toggle-external.sh: managed tool `magic` (mcp) → `21st` (skill
pack, glob-derived from skills-external/21st-*), drop load_env.
- [x] T3 profiles + profile.sh: `magic mcp` → 5 externals + `21st cli` in
design/web/web-full/full; GATE-BLOCK `21st 21st-ui-build`;
MANAGED_EXTERNALS += the 5; MANAGED_MCPS emptied (kept as a live
allowlist, mcp type machinery stays generic).
- [x] T4 lib/design-tool-gate.sh + lib/design-gate.md: manual-step hint
magic/MAGIC_API_KEY → 21st/`npm i -g` + `21st login`; PATH repair
extended to the npm-global bin dir (21st lives in nvm's bin, the
existing repair only fires when `claude` itself is unresolvable).
- [x] T5 doctrine + docs: CLAUDE.global.md design toolchain, README (drop the
magic callback-injection section + the MCP env-var worked example),
.env.example, link.sh MAGIC_API_KEY warning, .gitleaks.toml allowlist,
update-all.sh, .gitignore, settings.json (drop 4 mcp__magic__*; the
outward-facing verbs landed in autoMode.soft_deny, NOT ask — LRN-153
says ask is inert under auto mode).
- [x] T6 lib/tests/profile-set-managed.test.sh retargeted (mcp fixture → 21st
external pack), `make test` + shellcheck green.
- [x] T7 CHANGELOG + BDR-093 + LRN-158 + journal. Also cleaned along the way:
dead `magic` branches in profile.sh enable/disable_skill,
skills/profile/SKILL.md. OPEN for the user: `npm i -g @21st-dev/cli`
then `21st login` (`Bash(npm install -g *)` is denied to the agent).
Branch UNMERGED — human gate.
## 2026-09-17 — /deploy hand-back: one-line commands + post-deploy test list (feature/deploy-oneline-tests) ## 2026-09-17 — /deploy hand-back: one-line commands + post-deploy test list (feature/deploy-oneline-tests)
User: commands in the /deploy checklist arrive broken across lines (cannot User: commands in the /deploy checklist arrive broken across lines (cannot
copy-paste), and the hand-back stops at the deploy steps — wants, after the copy-paste), and the hand-back stops at the deploy steps — wants, after the
+3 -3
View File
@@ -1,9 +1,9 @@
# Local secrets for Claude Code plugin install scripts. # Local secrets for Claude Code plugin install scripts.
# Copy to ~/.claude/.env and fill in real values. link.sh symlinks repo/.env to it; the secret never enters git. # Copy to ~/.claude/.env and fill in real values. link.sh symlinks repo/.env to it; the secret never enters git.
# #
# Used by: lib/toggle-external.sh enable|disable magic # 21st.dev needs nothing here since 2026-09-22: the Magic MCP server was
# Get a key at: https://21st.dev/magic (dashboard → API keys) # replaced by the `21st` CLI, whose auth is `21st login` (browser token in
MAGIC_API_KEY=your_21st_dev_magic_api_key_here # ~/.config/21st). Any leftover MAGIC_API_KEY line is dead — delete it.
# ── Google SEO data layer (lib/seo-data) — used by /seo FULL ── # ── Google SEO data layer (lib/seo-data) — used by /seo FULL ──
# OAuth Desktop client: GCP console → APIs & Services → Credentials → OAuth client (Desktop). # OAuth Desktop client: GCP console → APIs & Services → Credentials → OAuth client (Desktop).
+20 -5
View File
@@ -64,11 +64,25 @@ skills/ios-sync
skills/design-motion-principles skills/design-motion-principles
skills/emil-design-eng skills/emil-design-eng
skills/frontend-design skills/frontend-design
# Impeccable — NOT a symlink: `impeccable skills install --scope=global`
# writes the skill dir (and its ~15 MB engine binary) straight in through the
# ~/.claude/skills symlink. Machine-owned, regenerated by make plugin/update.
skills/impeccable skills/impeccable
# …and the 4 subagents the same installer drops through ~/.claude/agents.
# agents/ is a tracked directory, so these need naming explicitly.
agents/impeccable-*.md
# External skills installed via `npx skills add` — auto-created by link.sh # External skills installed via `npx skills add` — auto-created by link.sh
skills/darwin-skill skills/darwin-skill
# 21st.dev skill pack symlinks — created on demand by toggle-external.sh /
# profile.sh (the pack is DISABLED by default, so these usually don't exist).
# A glob, not one line per skill: the `21st skills install` manifest owns the
# membership, so the pack can gain a skill with no edit here.
skills/21st-*
# Context7 docs-lookup skill — installed by `ctx7 setup --claude --cli` # Context7 docs-lookup skill — installed by `ctx7 setup --claude --cli`
# (install-plugins.sh Step 6, when absent) into ~/.claude/skills (a symlink to # (install-plugins.sh Step 6, when absent) into ~/.claude/skills (a symlink to
# this repo's skills/). ctx7-managed and re-created on demand — not vendored here. # this repo's skills/). ctx7-managed and re-created on demand — not vendored here.
@@ -156,11 +170,12 @@ skills-external/frontend-design/
# an edit. The source is always re-fetched, so no offline copy is needed. # an edit. The source is always re-fetched, so no offline copy is needed.
skills-external/emil-design-eng/ skills-external/emil-design-eng/
# Impeccable — machine-owned dist produced by `npx impeccable skills install` # 21st.dev skill pack — machine-owned: `21st skills install` output, staged by
# (install-plugins.sh Step 8d, update-all.sh), pinned in plugins.lock.json. # install-plugins.sh Step 8.7 (the installer refuses to write through the
# Not vendored: the installer owns the layout and rewrites it on update # ~/.claude/skills symlink, so it runs under a throwaway HOME and the skills
# (ctx7 pattern). Symlinked into skills/ by link.sh. # are moved here). Refreshed by update-all.sh. Not vendored: the CLI owns the
skills-external/impeccable/ # layout and the content is sha256-verified against 21st.dev's manifest.
skills-external/21st-*/
# npx `skills add` project-scope artifacts — darwin-skill copies itself into # npx `skills add` project-scope artifacts — darwin-skill copies itself into
# the repo's .agents/ and writes skills-lock.json at root. Our own agents live # the repo's .agents/ and writes skills-lock.json at root. Our own agents live
+3 -2
View File
@@ -67,8 +67,9 @@ regexTarget = "line"
regexes = [ regexes = [
'''X-Amz-Credential=AKIA[0-9A-Z]{16}''', '''X-Amz-Credential=AKIA[0-9A-Z]{16}''',
'''private-user-images\.githubusercontent\.com/[^"]*\?jwt=''', '''private-user-images\.githubusercontent\.com/[^"]*\?jwt=''',
'''MAGIC_API_KEY=abc123''', # Docs/test example — base64 of the "the ..." ASCII sample text, never a key.
# magic MCP docs example — base64 of "the ..." ASCII sample text. # (The MAGIC_API_KEY=abc123 placeholder that sat here went with the magic
# MCP, removed 2026-09-22 when 21st.dev moved to a CLI with no API key.)
'''clientKey = 'dGhlIH[A-Za-z0-9+/=]*'''', '''clientKey = 'dGhlIH[A-Za-z0-9+/=]*'''',
] ]
+73
View File
@@ -26,8 +26,41 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
classifier lists, `$defaults` splice semantics, `classifyAllShell`, the classifier lists, `$defaults` splice semantics, `classifyAllShell`, the
user-scope vs project-scope rule, and why `ask` is the wrong tier for a user-scope vs project-scope rule, and why `ask` is the wrong tier for a
destructive command under auto mode. destructive command under auto mode.
- **21st.dev moved from an MCP server to a CLI.** `install-plugins.sh` Step 8.7
installs `@21st-dev/cli` globally (pinned in `plugins.lock.json`), offers
`21st login` in an interactive terminal only, and stages the 7-skill pack
into `skills-external/21st-*`. `update-all.sh` refreshes both. The pack
ships disabled, same policy the MCP had.
- `lib/toggle-external.sh` manages `21st` as a skill pack (glob-derived from
`skills-external/21st-*`, parked under plain names so `profile.sh`'s
external park/restore stays interoperable). `magic` is gone from the
managed tools.
- The five design skills (`21st-ui-build`, `-ui-explore`, `-ui-review`,
`-cli-use`, `-ai`) are in the `design`, `web`, `web-full` and `full`
profiles and in `profile.sh`'s `MANAGED_EXTERNALS`; `21st-registry` and
`21st-design-sync` are installed but left parked.
- `autoMode.soft_deny` gains one entry for the outward-facing 21st verbs
(`publish*`, `submit`, `edit`, `delete`, `remove-from-catalog`,
`profile set|upload`) — publishing puts a component on a public listing.
That tier rather than `ask`, per LRN-153.
- `lib/design-gate.md` §5: a suggest-only check, same shape as the §4
animation-library one. When impeccable is active and the frontend project
has no `PRODUCT.md` at its root, the gate proposes `/impeccable init` once
and never runs it itself (it interviews the user). Skipped for single
component reviews and non-UI work.
### Changed ### Changed
- **Design gate: `magic` → the `21st` CLI in the required-manual slot.**
`design.profile`'s `GATE-BLOCK` now lists `21st` (CLI channel) and
`21st-ui-build` (the pack's canary on the skill channel); a missing CLI
trips the gate with `npm i -g @21st-dev/cli` + `21st login` instead of the
old `MAGIC_API_KEY` hint. `design-tool-gate.sh` also repairs `PATH` for the
npm global bin, whose absence in a hook's sanitized `PATH` would otherwise
read as "21st missing" (the existing repair only fired when `claude` itself
was unresolvable).
- `profile.sh`'s `MANAGED_MCPS` is empty: no MCP server is auto-toggled any
more. The `mcp` type stays supported for an advisory profile entry.
- **`/deploy` hand-back: one physical line per command, then a post-deploy - **`/deploy` hand-back: one physical line per command, then a post-deploy
tests block.** Every command in the checklist is emitted on exactly one tests block.** Every command in the checklist is emitted on exactly one
line, however long; a legacy `\` continuation in the runbook is joined at line, however long; a legacy `\` continuation in the runbook is joined at
@@ -92,6 +125,23 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
`bypassPermissions`). Adding a restriction stays allowed; removing one `bypassPermissions`). Adding a restriction stays allowed; removing one
does not. No instruction clears these. does not. No instruction clears these.
- **impeccable installs at `--scope=global`, subagents included, and the
pin fails safe.** `install-plugins.sh` Step 8d no longer stages a
`--scope=project` install in a tmpdir and moves the skill directory alone.
The installer writes through the `~/.claude/skills` and `~/.claude/agents`
symlinks straight into the repo: `skills/impeccable` plus the four
`agents/impeccable-*.md`, both gitignored and machine-owned, which is what
the manual `--scope=global` command already did. The step refuses to run
before `make link` has created those symlinks (an install before them
materializes real directories that `link.sh` then refuses to replace),
keeps a profile-parked copy parked, reports the skill version and agent
count, and prints the per-project `/impeccable init` hint. A pinned
install that fails falls back to `impeccable@latest` with a warning to
bump `plugins.lock.json`. `update-all.sh` follows the same shape.
`plugins.lock.json` pin 3.2.0 → 4.1.0 (the CLI only: the skill dist and
the engine binary have their own release tracks). `link.sh` drops
impeccable from `EXTERNAL_SKILLS`; `skills-external/impeccable/` is gone.
### Security ### Security
- **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`, - **Ten secret-reader deny rules added**: `sed`, `awk`, `cut`, `tr`,
`sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of `sort`, `uniq`, `diff`, `od`, `xxd`, `strings` against `.env*`. Six of
@@ -100,6 +150,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
`Bash(grep * .env*)` family: a `cat .env | sed` pipe still slips past, `Bash(grep * .env*)` family: a `cat .env | sed` pipe still slips past,
which is what the `hard_deny` exfiltration rule is there to catch. which is what the `hard_deny` exfiltration rule is there to catch.
### Removed
- **`magic` MCP (`@21st-dev/magic`) and `MAGIC_API_KEY`**, with the two risks
attached to them: the unauthenticated `127.0.0.1` callback server
`21st_magic_component_builder` opened (LRN-110) and the plaintext key copy
that `claude mcp add --env` wrote into `~/.claude.json` (BDR-026/057). Gone
with it: the 4 `mcp__magic__*` `permissions.ask` entries (BDR-059), the
`MAGIC_API_KEY` block in `.env.example`, `link.sh`'s missing-key warning,
and the dead `MAGIC_API_KEY=abc123` gitleaks allowlist regex.
### Fixed ### Fixed
- **`make update` no longer drops the Playwright OS-support bump** — a - **`make update` no longer drops the Playwright OS-support bump** — a
gstack submodule update used to leave the bump unapplied until the next gstack submodule update used to leave the bump unapplied until the next
@@ -124,6 +183,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
holds under `defaultMode: default`, not under this config's `auto`. The holds under `defaultMode: default`, not under this config's `auto`. The
paragraph now separates what is verified from what is not, and names paragraph now separates what is verified from what is not, and names
`deny` as the only tier the classifier cannot lift. `deny` as the only tier the classifier cannot lift.
- **`make plugin` never installed impeccable.** Three defects. The 3.2.0
pin had rotted upstream: the CLI fetches its skill dist at install time and
that release's artifact is gone (`Download failed: invalid zip data`),
which the step reported as "run it yourself" on every run. The
project-scope staging dropped the four subagents the same install writes.
And `/impeccable init` was never announced. A fourth, found while probing
the fix: once a copy is already installed, a rotted pin exits 0
(`Could not check for skill updates … Existing skills were left
unchanged`), byte-identical on disk to a genuine "Skills are up to date"
rerun, so `imp_install` now reads the installer output instead of trusting
the exit code or a version compare. Verified with the real installer in a
sandbox HOME: fresh install, rotted pin over a copy (fallback fires), same
pin rerun (no false warning), parked copy plus rotted pin (fallback, then
returned to `skills-disabled/`).
## [1.5.0] — 2026-09-13 ## [1.5.0] — 2026-09-13
+6 -4
View File
@@ -290,16 +290,18 @@ OR a design/UI request — not the keyword "design" alone in a prompt. Single
source for design routing; the design-toolchain hook reinforces it. source for design routing; the design-toolchain hook reinforces it.
- Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain. - Trivial (≤2 files, one cosmetic value) → /hotfix, no toolchain.
- Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design - Build UI (component, page, redesign) → ui-ux-pro-max + frontend-design
(anti-slop) + Magic MCP /ui + emil-design-eng (polish) + (anti-slop) + 21st-ui-build (catalog + generation) + emil-design-eng
design-motion-principles (if motion) + design-html (if static). (polish) + design-motion-principles (if motion) + design-html (if static).
Post-build floor: `npx impeccable detect <files>` (45 deterministic Post-build floor: `npx impeccable detect <files>` (45 deterministic
anti-slop rules, exit 2 = findings) when impeccable installed. anti-slop rules, exit 2 = findings) when impeccable installed.
- Design system / brand → design-consultation first, then the build tools. - Design system / brand → design-consultation first, then the build tools.
- Review / audit → design-review + emil-design-eng + design-motion-principles - Review / audit → design-review + emil-design-eng + design-motion-principles
+ /impeccable audit|critique (skill) + `impeccable detect` floor. + 21st-ui-review + /impeccable audit|critique + `impeccable detect` floor.
Scope doubt → don't silently skip: ask, or default to Build tier. Scope doubt → don't silently skip: ask, or default to Build tier.
Gate: lightweight skills run `~/.claude/lib/design-gate.md`; orchestrators via Gate: lightweight skills run `~/.claude/lib/design-gate.md`; orchestrators via
plugin-check. Magic MCP costs API calls — generation, not micro-tweaks. plugin-check. 21st = CLI (`npm i -g @21st-dev/cli`, `21st login`), no MCP,
no API key. Search is free; `21st get` and `21st generate` are metered —
generation, not micro-tweaks.
## graphify ## graphify
+44 -22
View File
@@ -253,10 +253,9 @@ in `env`, `command`, `args`, `url`, and `headers` — for both project (`.mcp.js
and user (`~/.claude.json`) scope. Use that instead of a literal value: and user (`~/.claude.json`) scope. Use that instead of a literal value:
```bash ```bash
MAGIC_API_KEY=<Enter your magic api key here from https://21st.dev/settings/api-keys >
# single-quoted so bash doesn't expand it; Claude Code expands it at # single-quoted so bash doesn't expand it; Claude Code expands it at
# launch, reading the var from its own process environment: # launch, reading the var from its own process environment:
claude mcp add magic --scope user --env 'API_KEY=${MAGIC_API_KEY}' -- npx -y @21st-dev/magic@latest claude mcp add <name> --scope user --env 'API_KEY=${SOME_API_KEY}' -- <command>
``` ```
The var still has to exist in the **environment of the process that starts The var still has to exist in the **environment of the process that starts
@@ -265,8 +264,12 @@ would defeat the point (every subprocess, every stray `env`/`printenv`, would
then see it). This repo's `~/.bashrc` instead wraps the `claude` command then see it). This repo's `~/.bashrc` instead wraps the `claude` command
itself: a `claude()` shell function sources `~/.claude/.env` into a subshell itself: a `claude()` shell function sources `~/.claude/.env` into a subshell
and `exec`s the real binary, so the var reaches `claude` and its children only and `exec`s the real binary, so the var reaches `claude` and its children only
— never the ambient shell. See `lib/toggle-external.sh`'s `magic` case for — never the ambient shell.
the pattern to copy for a new MCP server.
This config currently registers no MCP server at all. The one it used to
carry, `@21st-dev/magic`, is gone: 21st.dev replaced it with a plain CLI (see
below), so there is no key left to protect by reference. The pattern stays
documented for the next MCP server that needs a secret.
There is no `claude mcp add` flag that writes the reference form for you — There is no `claude mcp add` flag that writes the reference form for you —
the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as
@@ -292,25 +295,44 @@ Then run the one-time consent flow: `make seo-connect` (per-label token
store, multi-site safe). Missing credentials never break an audit — `/seo` store, multi-site safe). Missing credentials never break an audit — `/seo`
degrades gracefully to anonymous PageSpeed lab data. degrades gracefully to anonymous PageSpeed lab data.
### magic MCP (`@21st-dev/magic`) — known callback-injection risk ### 21st.dev CLI (replaces the magic MCP)
`21st_magic_component_builder` opens an **unauthenticated** local callback `@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server that
server (`127.0.0.1:9221+`, `Access-Control-Allow-Origin: *`, no token/origin this config used to register. Same endpoint, one browser login, no API key,
check) for up to 10 minutes per call; any local process or open browser tab and nothing loaded into a session that isn't using it:
can `POST` to it and that body is injected **verbatim** into the tool result
the model consumes (job8 audit, `dist/utils/callback-server.js:36`). This is ```bash
in the third-party package's code, not this repo's config — **we don't patch npm i -g @21st-dev/cli
it**. The mitigation lives on our side: `settings.json` 21st login # browser flow, token saved in ~/.config/21st
`permissions.ask` explicitly lists all 4 `mcp__magic__*` tools. ```
Read that as a declared intent, not a proven hard gate: under
`defaultMode: auto` (this config's default) Bash `ask` rules were observed `make plugin` does both (Step 8.7 installs the CLI, then offers the login in
auto-approving with no prompt raised (LRN-146). Whether MCP `ask` rules an interactive terminal) and installs the skill pack that drives it:
behave the same has not been verified here, so re-check before relying on `21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`, plus the two
it. `deny` is the only tier the auto-mode classifier cannot lift; for a publishing skills `-registry` and `-design-sync`. The pack is disabled by
gate that holds under auto mode without banning the tool outright, the default, the same policy the MCP had. `/profile design` turns on the five
right home is `autoMode.soft_deny`. Don't allowlist design skills; `bash lib/toggle-external.sh enable 21st` turns on all seven.
`21st_magic_component_builder` or `21st_magic_component_refiner` (arbitrary
absolute-path read → vendor exfil, same audit) under any circumstance. The pack is machine-owned and gitignored. It cannot be installed the way
upstream documents it (`21st install-skill`, i.e. `21st skills install
--global`): that writes into `~/.claude/skills/`, and the installer refuses to
follow a symlink anywhere on that path, while `~/.claude/skills` is itself a
symlink to this repo's `skills/`. So the install runs under a throwaway `HOME`
and the result is moved into `skills-external/21st-*`, where
`toggle-external.sh` and `profile.sh` symlink it in on demand.
Two risks from the MCP era go away with it. The unauthenticated local callback
server `21st_magic_component_builder` opened (`127.0.0.1:9221+`, CORS `*`, a
10-minute local prompt-injection window, job8 audit / LRN-110). And the API
key that `claude mcp add --env` materialized into `~/.claude.json`.
The permission gate is now one `autoMode.soft_deny` entry covering the
outward-facing verbs (`21st publish*`, `submit`, `edit`, `delete`,
`remove-from-catalog`, `profile set|upload`), because publishing a component
puts it on a public listing under your account. That tier rather than `ask`:
under `defaultMode: auto` (this config's default) `ask` rules were observed
auto-approving with no prompt raised (LRN-153), so an `ask` entry would have
declared an intent without gating anything.
--- ---
+2 -2
View File
@@ -292,8 +292,8 @@ activate a curated subset of skills + plugins + MCPs and disable the rest of
gstack + managed plugins — sessions stay focused and passive token cost drops. gstack + managed plugins — sessions stay focused and passive token cost drops.
`profile set <name>` actually toggles plugins (`claude plugin enable|disable`) `profile set <name>` actually toggles plugins (`claude plugin enable|disable`)
and MCPs (delegates to `lib/toggle-external.sh` for `magic`) — not just and external skill packs (delegates to `lib/toggle-external.sh`) — not just
advisory. Always-on plugins (`security-guidance`, `superpowers`) advisory. No MCP server is auto-toggled today. Always-on plugins (`security-guidance`, `superpowers`)
are protected. Managed plugins that `set` may toggle: are protected. Managed plugins that `set` may toggle:
`ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`, `ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`,
`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled. `pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled.
+185 -63
View File
@@ -788,54 +788,114 @@ else
fi fi
echo "" echo ""
# ── Step 8d: Impeccable (design anti-pattern detector + skill) ── # ── Step 8d: Impeccable (design detector + skill + subagents) ──
# 45 deterministic detector rules (CLI `impeccable detect`, exit 0/2) + # 45 deterministic detector rules (`impeccable detect`, exit 0/2), the
# /impeccable skill (23 verbs). Machine-owned dist: the installer produces # /impeccable skill (23 verbs) and 4 `impeccable-*` subagents.
# it, we stage it in a tmpdir then move it under skills-external/ #
# (gitignored, ctx7 pattern) — never let the installer write through the # GLOBAL scope, no staging: the installer writes ~/.claude/skills/impeccable/
# ~/.claude/skills symlink into the tracked repo dir. # (skill + its self-contained engine binary) and ~/.claude/agents/
echo "── Step 8d: Impeccable — design anti-pattern detector ────" # impeccable-*.md, and both of those are symlinks into this repo — so the
# global install IS the repo install. Machine-owned and gitignored on both
# sides. `--scope=project` was wrong twice over: it writes <cwd>/.claude/,
# which serves only the directory it ran in, and the staged `mv` that
# followed it moved the skill alone, silently dropping the subagents.
#
# The pin rots. The CLI downloads its skill dist at install time and an older
# release's artifact eventually disappears (`impeccable@3.2.0` → "Download
# failed: invalid zip data", 2026-09-22) — which is what left `make plugin`
# telling the user to run the command by hand. So a pin failure falls back to
# @latest and says, loudly, that the lock needs bumping.
echo "── Step 8d: Impeccable — design detector, skill + agents ──"
echo "" echo ""
IMP_DIR="$REPO/skills-external/impeccable" IMP_SKILL_DIR="$HOME/.claude/skills/impeccable"
IMP_PARKED="$REPO/skills-disabled/impeccable"
IMP_VER=$(pinned_version "impeccable") IMP_VER=$(pinned_version "impeccable")
NODE_MAJOR=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1) NODE_MAJOR=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
if [ -z "${NODE_MAJOR:-}" ] || [ "$NODE_MAJOR" -lt 24 ]; then
if [ -f "$IMP_DIR/SKILL.md" ]; then # One install attempt. $1 = "latest" or an exact version. On failure, IMP_FAIL
# holds the reason. The exit code alone is not enough: with a copy already in
# place, a rotted pin exits 0 ("Could not check for skill updates: invalid
# zip data … Existing skills were left unchanged"), exactly like a genuine
# up-to-date no-op ("Skills are up to date") — only the output tells them
# apart. Probed 2026-09-22 on 4.1.0 vs 3.2.0 in a sandbox HOME.
imp_install() {
local pkg="impeccable" out rc=0
[ "$1" != "latest" ] && pkg="impeccable@$1"
out=$(npx -y "$pkg" skills install -y --providers=claude --scope=global \
--no-hooks 2>&1) || rc=$?
IMP_FAIL=$(printf '%s\n' "$out" \
| grep -E 'Download failed|Could not check for skill updates' \
| head -1 || true)
if [ "$rc" -ne 0 ] && [ -z "$IMP_FAIL" ]; then
IMP_FAIL="installer exited $rc"
fi
[ -z "$IMP_FAIL" ]
}
# Precondition: ~/.claude/{skills,agents} must already be link.sh's symlinks.
# Installing before they exist materializes real directories there, and
# link.sh then refuses to replace them ("is a real directory") — a worse
# failure than skipping, because it needs manual repair.
IMP_READY=true
for _imp_d in skills agents; do
if [ "$(readlink "$HOME/.claude/$_imp_d" 2>/dev/null || true)" != "$REPO/$_imp_d" ]; then
IMP_READY=false
fi
done
if [ "$IMP_READY" != true ]; then
warn "impeccable: ~/.claude/skills and ~/.claude/agents are not this repo's symlinks yet"
warn " → run 'make link' first, then re-run 'make plugin'"
elif [ -z "${NODE_MAJOR:-}" ] || [ "$NODE_MAJOR" -lt 24 ]; then
if [ -f "$IMP_SKILL_DIR/SKILL.md" ] || [ -f "$IMP_PARKED/SKILL.md" ]; then
ok "impeccable already present (update skipped — needs Node >= 24, found ${NODE_MAJOR:-none})" ok "impeccable already present (update skipped — needs Node >= 24, found ${NODE_MAJOR:-none})"
else else
warn "impeccable: needs Node >= 24 (found ${NODE_MAJOR:-none}) — skipped. Bump Node, then: make plugin" warn "impeccable: needs Node >= 24 (found ${NODE_MAJOR:-none}) — skipped. Bump Node, then: make plugin"
fi fi
else else
IMP_PKG="impeccable" # A profile may hold impeccable parked in skills-disabled/. Install writes
# to the live slot, so remember the state and put the fresh copy back where
# it was — otherwise `make plugin` silently re-enables a disabled skill.
IMP_WAS_PARKED=false
[ -d "$IMP_PARKED" ] && IMP_WAS_PARKED=true
IMP_USED=""
if [ "$IMP_VER" != "latest" ]; then if [ "$IMP_VER" != "latest" ]; then
IMP_PKG="impeccable@${IMP_VER}" info "Installing impeccable ${IMP_VER} (pinned in plugins.lock.json, global scope)..."
info "Installing impeccable ${IMP_VER} (pinned in plugins.lock.json, staged)..." if imp_install "$IMP_VER"; then
IMP_USED="$IMP_VER"
else
warn "impeccable@${IMP_VER} did not install (${IMP_FAIL}) — that release's skill dist is gone upstream"
info "Falling back to impeccable@latest..."
if imp_install latest; then
IMP_USED="latest"
warn "installed @latest instead of the pin. Bump \"impeccable\".version in plugins.lock.json to the version this produced, so the next run is reproducible again."
fi
fi
else else
info "Installing impeccable latest (consider pinning in plugins.lock.json)..." info "Installing impeccable latest (consider pinning in plugins.lock.json)..."
imp_install latest && IMP_USED="latest"
fi fi
IMP_STAGE=$(mktemp -d)
if (cd "$IMP_STAGE" && npx -y "$IMP_PKG" skills install -y --providers=claude --scope=project --no-hooks >/dev/null 2>&1); then if [ -n "$IMP_USED" ] && [ -f "$IMP_SKILL_DIR/SKILL.md" ]; then
IMP_SRC=$(find "$IMP_STAGE" -type d -name impeccable -path "*skills*" 2>/dev/null | head -1) IMP_SKILL_VER=$(sed -n 's/^version:[[:space:]]*//p' "$IMP_SKILL_DIR/SKILL.md" | head -1)
if [ -n "$IMP_SRC" ] && [ -f "$IMP_SRC/SKILL.md" ]; then # -L: ~/.claude/agents is a symlink, and find would otherwise stop on it.
rm -rf "$IMP_DIR" IMP_AGENTS=$(find -L "$HOME/.claude/agents" -maxdepth 1 -name 'impeccable-*.md' 2>/dev/null | wc -l)
mv "$IMP_SRC" "$IMP_DIR" ok "impeccable installed (CLI ${IMP_USED}, skill ${IMP_SKILL_VER:-?}, ${IMP_AGENTS} agents)"
ok "impeccable synced to skills-external/ (CLI ${IMP_VER})" if [ "$IMP_AGENTS" -eq 0 ]; then
else warn "no impeccable-* agent landed in agents/ — the skill's finish/document verbs dispatch to them"
warn "impeccable: installer ran but produced no skills/impeccable/SKILL.md — layout changed? Inspect: npx impeccable skills install"
fi fi
if [ "$IMP_WAS_PARKED" = true ]; then
rm -rf "${IMP_PARKED:?}"
mv "$IMP_SKILL_DIR" "$IMP_PARKED"
info "impeccable was parked by a profile — refreshed copy returned to skills-disabled/"
fi
info "Per-project step, in the agent chat of each frontend project: /impeccable init"
info " (writes PRODUCT.md — the design context every impeccable verb reads)"
elif [ -f "$IMP_SKILL_DIR/SKILL.md" ] || [ -f "$IMP_PARKED/SKILL.md" ]; then
ok "impeccable already present (install failed: ${IMP_FAIL:-no SKILL.md written} — existing copy kept)"
else else
if [ -f "$IMP_DIR/SKILL.md" ]; then warn "impeccable install failed (${IMP_FAIL:-no SKILL.md written}) — run manually: npx impeccable skills install -y --providers=claude --scope=global --no-hooks"
ok "impeccable already present (installer failed — existing dist kept)"
else
warn "impeccable install failed — run manually: npx impeccable skills install -y --providers=claude --scope=project --no-hooks"
fi
fi fi
rm -rf "$IMP_STAGE"
fi
if [ -L "$HOME/.claude/skills/impeccable" ]; then
ok "impeccable symlink OK"
else
info "Symlinking — will be created by link.sh"
fi fi
echo "" echo ""
@@ -892,42 +952,104 @@ done
echo "" echo ""
# ============================================================ # ============================================================
# STEP 8.7 — MAGIC MCP (21st-dev) — installed but DISABLED by default # STEP 8.7 — 21ST.DEV CLI + SKILL PACK — installed but DISABLED by default
# ============================================================ # ============================================================
# Magic MCP is a stdio MCP server providing UI component generation # `@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server:
# from 21st.dev. Toggled via lib/toggle-external.sh (same interface as # same endpoint, one browser login (`21st login`, token in ~/.config/21st),
# gstack, emil-design-eng, etc.). Registered in Claude Code user scope. # no API key, no MCP process loaded into every session. It ships a pack of
# verified skills (21st-ui-build / -explore / -review / -cli-use / -ai /
# -registry / -design-sync) that drive the CLI from Claude Code.
# #
# Default policy: DISABLED at install time. Rationale: MCP tools load # Machine-owned dist (impeccable pattern): `21st skills install` writes to
# into every Claude Code session and consume context tokens. Enable # <HOME>/.claude/skills/<name>/ and REFUSES to follow a symlink anywhere on
# only when you're actively using Magic. # that path — and ~/.claude/skills IS a symlink to this repo's skills/. So
# install under a staged HOME, then move each skill into skills-external/
# (gitignored), where toggle-external.sh / profile.sh symlink it in.
# #
# API key: read from $REPO/.env (MAGIC_API_KEY=...) — NEVER committed. # Default policy: pack DISABLED at install time — every skill description
# Template: $REPO/.env.example. Get a key at https://21st.dev/magic # loads into every session. Enable on demand:
echo "── Step 8.7: Magic MCP (21st-dev) ──────────────────────────" # bash lib/toggle-external.sh enable 21st (whole pack)
# /profile design (the 5 design skills)
echo "── Step 8.7: 21st.dev CLI + skill pack ─────────────────────"
echo "" echo ""
if [ -x "$REPO/lib/toggle-external.sh" ]; then if command -v 21st &>/dev/null; then
MAGIC_STATUS="$(bash "$REPO/lib/toggle-external.sh" status magic 2>/dev/null || echo missing)" ok "21st CLI already installed"
if [ "$MAGIC_STATUS" = "enabled" ]; then else
info "Disabling magic MCP by default (enable on demand)..." TFD_VER=$(pinned_version "21st")
bash "$REPO/lib/toggle-external.sh" disable magic >/dev/null if [ "$TFD_VER" != "latest" ]; then
ok "magic MCP disabled — enable with: bash lib/toggle-external.sh enable magic" info "Installing @21st-dev/cli@${TFD_VER} (pinned in plugins.lock.json)..."
npm install -g "@21st-dev/cli@${TFD_VER}"
else else
ok "magic MCP disabled (default)" info "Installing @21st-dev/cli@latest (consider pinning in plugins.lock.json)..."
npm install -g @21st-dev/cli
fi fi
# The key lives in ~/.claude/.env (canonical, BDR-026), reached via the if command -v 21st &>/dev/null; then
# repo/.env symlink that toggle-external.sh sources. Self-heal the common ok "21st CLI installed"
# fresh-machine case: ~/.claude/.env was created AFTER link.sh ran, so the else
# symlink is missing and the key looks absent though it's set. err "21st CLI install failed — run manually: npm install -g @21st-dev/cli"
HOME_ENV="$HOME/.claude/.env"
if [ ! -e "$REPO/.env" ] && [ -f "$HOME_ENV" ]; then
ln -sf "$HOME_ENV" "$REPO/.env" 2>/dev/null \
&& info "Linked repo/.env → ~/.claude/.env (was missing)"
fi fi
# Tolerate optional `export ` and leading whitespace; require a value. fi
MAGIC_KEY_RE='^[[:space:]]*(export[[:space:]]+)?MAGIC_API_KEY=.'
if [ ! -f "$REPO/.env" ] || ! grep -qE "$MAGIC_KEY_RE" "$REPO/.env" 2>/dev/null; then # Skill pack — staged install, then moved under skills-external/.
warn "MAGIC_API_KEY not set in ~/.claude/.env — add it (and run 'make link') before enabling magic" if command -v 21st &>/dev/null; then
TFD_STAGE=$(mktemp -d)
if HOME="$TFD_STAGE" 21st skills install --global --agent claude >/dev/null 2>&1; then
TFD_N=0
for _tfd in "$TFD_STAGE"/.claude/skills/*/; do
[ -f "${_tfd}SKILL.md" ] || continue
_tfd_name=$(basename "$_tfd")
rm -rf "${REPO:?}/skills-external/${_tfd_name:?}"
mv "$_tfd" "$REPO/skills-external/$_tfd_name"
TFD_N=$((TFD_N + 1))
done
if [ "$TFD_N" -gt 0 ]; then
ok "21st skill pack synced to skills-external/ ($TFD_N skills)"
else
warn "21st skills install ran but produced no SKILL.md — layout changed? Inspect: 21st skills install --global --agent claude"
fi
elif [ -f "$REPO/skills-external/21st-ui-build/SKILL.md" ]; then
ok "21st skill pack already present (refresh failed — existing copy kept)"
else
warn "21st skill pack install failed — run manually: 21st skills install --global --agent claude"
fi
rm -rf "$TFD_STAGE"
fi
# Auth — detect, then offer login ONLY in an interactive TTY. A non-interactive
# run (CI / headless / re-run) must never open a browser or block on OAuth.
# Search and logo lookup are free; retrieving component code and 21st AI need
# the session. Mirrors the ctx7 auth block (Step 6).
if command -v 21st &>/dev/null; then
# `whoami` is a local token read (no network): "Logged in as <user> (saved …)."
TFD_WHO="$(21st whoami 2>/dev/null | head -1)"
if [[ "$TFD_WHO" == "Logged in as "* ]]; then
ok "21st: ${TFD_WHO%.}"
elif [ -t 0 ] && [ -t 1 ]; then
printf '%b' "${BLUE}→${NC} Sign in to 21st now? (opens a browser) [y/N] "
read -r tfd_ans || tfd_ans=""
if [[ "$tfd_ans" =~ ^[Yy]([Ee][Ss])?$ ]]; then
if 21st login; then
ok "21st authenticated"
else
warn "21st login did not finish — re-run '21st login' anytime"
fi
else
info "Skipped — sign in later with: 21st login"
fi
else
info "Not signed in. Component retrieval and 21st AI need: 21st login"
fi
fi
# Default-disabled, same policy as before the MCP→CLI move.
if [ -x "$REPO/lib/toggle-external.sh" ]; then
TFD_STATUS="$(bash "$REPO/lib/toggle-external.sh" status 21st 2>/dev/null || echo missing)"
if [ "$TFD_STATUS" = "enabled" ]; then
info "Disabling the 21st skill pack by default (enable on demand)..."
bash "$REPO/lib/toggle-external.sh" disable 21st >/dev/null
ok "21st skill pack disabled — enable with: bash lib/toggle-external.sh enable 21st"
else
ok "21st skill pack disabled (default)"
fi fi
else else
warn "lib/toggle-external.sh not found or not executable — skipping" warn "lib/toggle-external.sh not found or not executable — skipping"
@@ -1040,7 +1162,7 @@ echo " 🔄 frontend-design — distinctive frontend interfaces, anti-AI-
echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)" echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)" echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)" echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)"
echo " 🔄 magic MCP — 21st-dev UI generation MCP (toggle: lib/toggle-external.sh enable magic)" echo " 🔄 21st skill pack — 21st.dev CLI skills, 7 (toggle: lib/toggle-external.sh enable 21st)"
echo "" echo ""
echo " All plugins installed at: user scope (~/.claude/plugins/)" echo " All plugins installed at: user scope (~/.claude/plugins/)"
echo " GStack skills symlinked individually into ~/.claude/skills/ (→ submodule)" echo " GStack skills symlinked individually into ~/.claude/skills/ (→ submodule)"
+40 -12
View File
@@ -41,7 +41,8 @@ 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 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, tools** (the `# GATE-BLOCK:` allowlist in `design.profile`: ui-ux-pro-max,
frontend-design, emil-design-eng, design-motion-principles, impeccable, design-html, frontend-design, emil-design-eng, design-motion-principles, impeccable, design-html,
design-review, design-consultation, magic). The profile also bundles 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 browser/plan/shotgun tooling and graphify for convenience; those never trip the
gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are gate. Motion (`design-motion-principles`) and static-HTML (`design-html`) are
already in the core set — checked regardless; their CLAUDE.md "+motion / already in the core set — checked regardless; their CLAUDE.md "+motion /
@@ -54,7 +55,7 @@ already in the core set — checked regardless; their CLAUDE.md "+motion /
It reads the design-core tools (`# GATE-BLOCK:` in `design.profile`) plus their 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 — 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 skill symlink, `claude plugin list`, `claude mcp list`, `command -v`. It never
reads `disabledMcpServers` (unreliable for bi-modal servers like magic/context7). reads `disabledMcpServers` (unreliable for bi-modal servers like context7).
The core set lives in `design.profile`, not in the script or here — single source. 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) · `2` = error.
@@ -67,21 +68,21 @@ Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it)
🎨 DESIGN DETECTED — the design toolchain isn't fully active. 🎨 DESIGN DETECTED — the design toolchain isn't fully active.
activate with /profile design: <skills / ui-ux-pro-max> activate with /profile design: <skills / ui-ux-pro-max>
required + manual step: <e.g. magic — needs MAGIC_API_KEY> required + manual step: <e.g. 21st — needs the CLI>
→ run /profile design to activate it, then continue. → run /profile design to activate it, then continue.
- **activate with /profile design** → skills + the plugin; `/profile design` - **activate with /profile design** → skills + the plugin; `/profile design`
turns them on directly. turns them on directly.
- **required + manual step** → required tools the profile can't flip silently. - **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 **the `21st` CLI lands here: it TRIPS the gate** (it's required for Build),
a silent "optional". `/profile design` runs `toggle-external.sh` for magic, it is NOT a silent "optional". `/profile design` symlinks the 21st skills,
which needs a valid `MAGIC_API_KEY` in `~/.claude/.env` — tell the user to verify 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. - Do NOT hand-activate individual tools. The profile is the unit of activation.
- **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design - **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design
plugin/MCP (magic, ui-ux-pro-max) could NOT be checked. Do NOT report a plain plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready":
"ready": proceed only after telling the user that N tool(s) went unverified and proceed only after telling the user that N tool(s) went unverified and having
having them confirm with `claude mcp list` / `claude plugin list`. Fail-visible, them confirm with `claude plugin list`. Fail-visible, not fail-silent.
not fail-silent — the most important tool (magic) is exactly an unverifiable one.
### 4. Animation library — suggest-only (fires only on a real motion signal) ### 4. Animation library — suggest-only (fires only on a real motion signal)
@@ -138,6 +139,33 @@ count:
toolchain check handles the skill; this step handles the lib. Don't conflate toolchain check handles the skill; this step handles the lib. Don't conflate
them when talking to the user. 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:
1. impeccable is active (`skills/impeccable` present, i.e. it did not trip §3).
2. The project has no `PRODUCT.md` at 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 `init` unprompted: 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 ### Other toolchains
The script defaults to the `design` profile. A task needing another profile's The script defaults to the `design` profile. A task needing another profile's
@@ -149,8 +177,8 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
- Remedy is ALWAYS a profile (`/profile design`), never an atomic tool toggle — - 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 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 - the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot
if `MAGIC_API_KEY` is in `~/.claude/.env` — the gate says so; surface that to the user. install it — the gate names the two commands; surface them to the user.
- The design-core set (what trips the gate) is declared in `design.profile` on - 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, the `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool,
not in the script. not in the script.
+34 -9
View File
@@ -29,12 +29,13 @@
# required-manual required but the profile can't flip it silently (API # required-manual required but the profile can't flip it silently (API
# key / external install) — the gate STILL trips, names # key / external install) — the gate STILL trips, names
# it, and the remedy is `/profile design` + a manual step. # it, and the remedy is `/profile design` + a manual step.
# This is where magic lands: required, never silent. # This is where the `21st` CLI lands: required, never
# silent (npm i -g @21st-dev/cli, then 21st login).
# Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are # Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are
# ignored entirely (browser/plan/shotgun tooling, graphify). # ignored entirely (browser/plan/shotgun tooling, graphify).
# #
# disabledMcpServers is NEVER read — unreliable for bi-modal servers # disabledMcpServers is NEVER read — unreliable for bi-modal servers
# (magic/context7 can appear there yet be active via another channel). # (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) · 2 = error.
# Usage: design-tool-gate.sh [profile] (default profile: design) # Usage: design-tool-gate.sh [profile] (default profile: design)
@@ -79,6 +80,30 @@ ensure_claude_on_path() {
} }
ensure_claude_on_path ensure_claude_on_path
# Same sanitized-PATH problem for `21st` (an npm global bin), with a twist:
# the repair above only fires when claude ITSELF is unresolvable, and claude
# often lives in ~/.local/bin while the npm global bin dir is missing from a
# hook's PATH. Probe for the binary directly and prepend the dir that has it,
# otherwise a perfectly installed CLI reads as "missing" and trips the gate.
ensure_21st_on_path() {
command -v 21st >/dev/null 2>&1 && return
local cand
for cand in \
"$HOME/.local/bin/21st" \
/usr/local/bin/21st; do
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
done
local m newest matches=()
for m in "$HOME"/.nvm/versions/node/*/bin/21st; do
[ -x "$m" ] && matches+=("$m")
done
if [ "${#matches[@]}" -gt 0 ]; then
newest="$(printf '%s\n' "${matches[@]}" | sort -V | tail -1)"
PATH="$(dirname "$newest"):$PATH"
fi
}
ensure_21st_on_path
# Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated). # Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated).
# Empty => fall back to "every gate-relevant entry is in scope" (coarse). # Empty => fall back to "every gate-relevant entry is in scope" (coarse).
core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \ core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \
@@ -143,8 +168,8 @@ done <<< "$plain"
# Verdict — three outcomes: # Verdict — three outcomes:
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips. # blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE. # only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE.
# claude was unreachable, so the plugin/MCP (magic, ui-ux-pro-max) could # claude was unreachable, so the plugin channel (ui-ux-pro-max) could not
# not be checked. Never pass this as a silent READY — proceed, but say so. # be checked. Never pass this as a silent READY — proceed, but say so.
# nothing pending -> READY (exit 0). # nothing pending -> READY (exit 0).
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
echo "design toolchain: INCOMPLETE" echo "design toolchain: INCOMPLETE"
@@ -152,9 +177,9 @@ if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
echo " activate with /profile $PROFILE: ${blocking[*]}" echo " activate with /profile $PROFILE: ${blocking[*]}"
fi fi
if [ "${#manual[@]}" -gt 0 ]; then if [ "${#manual[@]}" -gt 0 ]; then
echo " required + manual step (API key / external install): ${manual[*]}" echo " required + manual step (external install / sign-in): ${manual[*]}"
case " ${manual[*]} " in case " ${manual[*]} " in
*" magic "*) echo " magic needs MAGIC_API_KEY in ~/.claude/.env (/profile $PROFILE runs toggle-external.sh)" ;; *" 21st "*) echo " 21st needs the CLI: npm i -g @21st-dev/cli then 21st login" ;;
esac esac
fi fi
if [ "${#unverified[@]}" -gt 0 ]; then if [ "${#unverified[@]}" -gt 0 ]; then
@@ -167,9 +192,9 @@ fi
if [ "${#unverified[@]}" -gt 0 ]; then if [ "${#unverified[@]}" -gt 0 ]; then
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked" echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked"
echo " unverified (claude CLI unreachable): ${unverified[*]}" echo " unverified (claude CLI unreachable): ${unverified[*]}"
echo " the gate could NOT confirm the design plugin/MCP (e.g. magic," echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
echo " ui-ux-pro-max) are active. Proceed only after checking manually:" echo " active. Proceed only after checking manually:"
echo " claude mcp list claude plugin list" echo " claude plugin list"
exit 11 exit 11
fi fi
+16 -21
View File
@@ -11,7 +11,7 @@
# Mechanism: # Mechanism:
# - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/ # - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/
# - Plugins: `claude plugin enable|disable <name>@<marketplace>` # - Plugins: `claude plugin enable|disable <name>@<marketplace>`
# - MCPs: delegated to lib/toggle-external.sh for known servers (magic), # - MCPs: advisory (none managed since BDR-093 — MANAGED_MCPS is empty),
# advisory otherwise # advisory otherwise
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally) # - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
# - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs # - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs
@@ -51,7 +51,6 @@ SKILLS_DIR="$REPO/skills"
DISABLED_DIR="$REPO/skills-disabled" DISABLED_DIR="$REPO/skills-disabled"
GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills
PROFILES_DIR="$REPO/lib/profiles" PROFILES_DIR="$REPO/lib/profiles"
TOGGLE_EXTERNAL="$REPO/lib/toggle-external.sh"
ACTIVE_CACHE="$REPO/.active-profile" # statusline reads this — keep fast (single-line file, profile name only) ACTIVE_CACHE="$REPO/.active-profile" # statusline reads this — keep fast (single-line file, profile name only)
# Plugins that are toggle-managed by `set`. Anything NOT in this list is # Plugins that are toggle-managed by `set`. Anything NOT in this list is
@@ -73,13 +72,20 @@ MANAGED_EXTERNALS=(
frontend-design frontend-design
design-motion-principles design-motion-principles
impeccable impeccable
21st-ui-build
21st-ui-explore
21st-ui-review
21st-cli-use
21st-ai
) )
# MCP servers that are toggle-managed by `set`, both ways (enable AND # MCP servers that are toggle-managed by `set`, both ways (enable AND
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine. # disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
MANAGED_MCPS=( # Empty since 2026-09-22: `magic` was the only entry and 21st.dev replaced
magic # its MCP server with a CLI + skill pack (the 5 design skills are managed as
) # externals above). The `mcp` type itself stays supported — a profile can
# still list an MCP, it is then advisory rather than auto-toggled.
MANAGED_MCPS=()
# Plugins that MUST stay enabled — `set` will refuse to disable these even if # Plugins that MUST stay enabled — `set` will refuse to disable these even if
# they're not in the profile. (Defensive: belt-and-suspenders alongside # they're not in the profile. (Defensive: belt-and-suspenders alongside
@@ -324,15 +330,12 @@ enable_skill() {
fi fi
;; ;;
mcp) mcp)
# Advisory only. The delegation branch that lived here served `magic`,
# the single managed MCP; 21st.dev replaced it with a CLI (BDR-093), so
# MANAGED_MCPS is empty and nothing is auto-registered. Re-add a branch
# here the day a profile owns an MCP server again.
if [ "$(skill_status "$skill" mcp)" = "enabled" ]; then if [ "$(skill_status "$skill" mcp)" = "enabled" ]; then
: # already on : # already on
elif [ "$skill" = "magic" ] && [ -x "$TOGGLE_EXTERNAL" ]; then
# Known MCP — delegate to lib/toggle-external.sh which handles env vars.
if bash "$TOGGLE_EXTERNAL" enable magic 2>&1 | grep -qE "enabled|already"; then
ok "enabled MCP: magic"
else
info "MCP 'magic' could not be enabled (check .env for MAGIC_API_KEY)"
fi
else else
info "MCP '$skill' not registered — run: claude mcp add $skill -- <command>" info "MCP '$skill' not registered — run: claude mcp add $skill -- <command>"
fi fi
@@ -394,15 +397,7 @@ disable_skill() {
info "plugin '$skill' — manual: claude plugin disable $skill@<marketplace>" info "plugin '$skill' — manual: claude plugin disable $skill@<marketplace>"
;; ;;
mcp) mcp)
if [ "$skill" = "magic" ] && [ -x "$TOGGLE_EXTERNAL" ]; then info "MCP '$skill' — manual: claude mcp remove $skill"
if bash "$TOGGLE_EXTERNAL" disable magic 2>&1 | grep -qE "disabled|already"; then
ok "disabled MCP: magic"
else
info "MCP 'magic' — manual disable failed"
fi
else
info "MCP '$skill' — manual: claude mcp remove $skill"
fi
;; ;;
cli) cli)
: # never auto-uninstall CLIs : # never auto-uninstall CLIs
+13 -4
View File
@@ -7,7 +7,8 @@
# tooling, graphify) is bundled for convenience but never blocks. Keep these # tooling, graphify) is bundled for convenience but never blocks. Keep these
# lines in sync when adding/removing a core design tool. # 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: frontend-design ui-ux-pro-max emil-design-eng design-html
# GATE-BLOCK: design-motion-principles design-review design-consultation magic # GATE-BLOCK: design-motion-principles design-review design-consultation
# GATE-BLOCK: 21st 21st-ui-build
# Core design skills (gstack) # Core design skills (gstack)
design-shotgun design-shotgun
@@ -30,11 +31,19 @@ frontend-design external
design-motion-principles external design-motion-principles external
impeccable external impeccable external
# External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry
# and 21st-design-sync are publishing flows; installed but left parked.
21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use external
21st-ai external
# Plugin (auto-toggle) # Plugin (auto-toggle)
ui-ux-pro-max plugin@ui-ux-pro-max-skill ui-ux-pro-max plugin@ui-ux-pro-max-skill
# MCP — auto-toggle via lib/toggle-external.sh (needs MAGIC_API_KEY in .env)
magic mcp
# CLIs (advisory only — installed/not-installed) # CLIs (advisory only — installed/not-installed)
# 21st is NOT advisory: it is on the GATE-BLOCK list, so a missing CLI trips
# the design gate. Install: npm i -g @21st-dev/cli then 21st login
21st cli
graphify cli graphify cli
+6 -1
View File
@@ -86,9 +86,14 @@ ui-ux-pro-max plugin@ui-ux-pro-max-skill
# claude plugin enable pr-review-toolkit@claude-code-plugins # claude plugin enable pr-review-toolkit@claude-code-plugins
# or profile-based: bash lib/profile.sh apply audit (audit.profile keeps it; # or profile-based: bash lib/profile.sh apply audit (audit.profile keeps it;
# a later `set full` re-disables it — MANAGED_PLUGINS lifecycle). # a later `set full` re-disables it — MANAGED_PLUGINS lifecycle).
magic mcp 21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use external
21st-ai external
# === CLIs (advisory) ================================================= # === CLIs (advisory) =================================================
21st cli
ctx7 cli ctx7 cli
graphify cli graphify cli
gsd cli gsd cli
+7 -2
View File
@@ -49,9 +49,14 @@ emil-design-eng external
frontend-design external frontend-design external
design-motion-principles external design-motion-principles external
impeccable external impeccable external
21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use external
21st-ai external
ui-ux-pro-max plugin@ui-ux-pro-max-skill ui-ux-pro-max plugin@ui-ux-pro-max-skill
magic mcp
# === CLIs (advisory) ================================================= # === CLIs ============================================================
21st cli
ctx7 cli ctx7 cli
graphify cli graphify cli
+10 -2
View File
@@ -38,11 +38,19 @@ frontend-design external
design-motion-principles external design-motion-principles external
impeccable external impeccable external
# External: 21st.dev pack (publishing flows 21st-registry / -design-sync
# stay parked)
21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use external
21st-ai external
# Plugin: UI/UX intelligence (auto-toggle) # Plugin: UI/UX intelligence (auto-toggle)
ui-ux-pro-max plugin@ui-ux-pro-max-skill ui-ux-pro-max plugin@ui-ux-pro-max-skill
# MCP: 21st-dev Magic component generator # CLI: 21st.dev component catalog + UI generation (needs `21st login`)
magic mcp 21st cli
# CLI: ctx7 (doc lookup for fast-evolving libs like Next.js) # CLI: ctx7 (doc lookup for fast-evolving libs like Next.js)
ctx7 cli ctx7 cli
+16 -16
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# lib/tests/profile-set-managed.test.sh — `set` symmetry on managed # lib/tests/profile-set-managed.test.sh — `set` symmetry on managed
# externals + MCPs, gstack on-demand, external from-source (BDR-079). # externals, gstack on-demand, external from-source (BDR-079). The MCP
# assertions went with `magic` (2026-09-22): MANAGED_MCPS is empty now, the
# 21st skills that replaced it are managed as externals, so the pack's
# park/restore round-trip is what this covers on that side.
# Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH. # Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH.
set -u set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)" ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
@@ -10,13 +13,13 @@ check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT
mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \ mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \
"$FX/skills-external/emil-design-eng" "$FX/skills-external/other-ext" "$FX/skills-external/emil-design-eng" "$FX/skills-external/other-ext" \
"$FX/skills-external/21st-ui-build"
for g in gs-a gs-b gs-c; do for g in gs-a gs-b gs-c; do
mkdir -p "$FX/skills-external/gstack/$g" mkdir -p "$FX/skills-external/gstack/$g"
touch "$FX/skills-external/gstack/$g/SKILL.md" touch "$FX/skills-external/gstack/$g/SKILL.md"
done done
cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" "$FX/lib/" cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" "$FX/lib/"
printf 'MAGIC_API_KEY=test-secret-000\n' > "$FX/.env"
# Non-managed external, enabled from the start — must never be touched. # Non-managed external, enabled from the start — must never be touched.
ln -s "$FX/skills-external/other-ext" "$FX/skills/other-ext" ln -s "$FX/skills-external/other-ext" "$FX/skills/other-ext"
@@ -25,22 +28,18 @@ cat > "$FX/lib/profiles/designish.profile" <<'EOF'
gs-a gs-a
gs-b gs-b
emil-design-eng external emil-design-eng external
magic mcp 21st-ui-build external
EOF EOF
cat > "$FX/lib/profiles/backendish.profile" <<'EOF' cat > "$FX/lib/profiles/backendish.profile" <<'EOF'
gs-c gs-c
EOF EOF
# Fake claude: logs every call; keeps MCP registry state in a flat file. # Fake claude: logs every call. No MCP state to keep — MANAGED_MCPS is empty,
# so `set` must never reach for `claude mcp` at all (asserted below).
cat > "$FX/bin/claude" <<EOF cat > "$FX/bin/claude" <<EOF
#!/usr/bin/env bash #!/usr/bin/env bash
FX="$FX" FX="$FX"
echo "\$*" >> "\$FX/claude-calls.log" echo "\$*" >> "\$FX/claude-calls.log"
case "\$1 \${2:-}" in
"mcp list") cat "\$FX/mcp-state" 2>/dev/null ;;
"mcp add") echo "magic: stub" > "\$FX/mcp-state" ;;
"mcp remove") : > "\$FX/mcp-state" ;;
esac
exit 0 exit 0
EOF EOF
chmod +x "$FX/bin/claude" chmod +x "$FX/bin/claude"
@@ -48,14 +47,14 @@ chmod +x "$FX/bin/claude"
run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \ run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" "$@"; } TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" "$@"; }
# --- set designish: gstack on-demand + external from-source + magic on --- # --- set designish: gstack on-demand + externals from-source (21st + emil) ---
run set designish >/dev/null 2>&1 run set designish >/dev/null 2>&1
check T1-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on check T1-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
check T2-gsb-on "$([ -e "$FX/skills/gs-b" ] && echo on || echo off)" on check T2-gsb-on "$([ -e "$FX/skills/gs-b" ] && echo on || echo off)" on
check T3-gsc-off "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" off check T3-gsc-off "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" off
check T4-emil-src "$([ -L "$FX/skills/emil-design-eng" ] && echo on || echo off)" on check T4-emil-src "$([ -L "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
check T5-magic-on "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1 check T5-21st-src "$([ -L "$FX/skills/21st-ui-build" ] && echo on || echo off)" on
check T6-add-call "$(grep -c '^mcp add magic' "$FX/claude-calls.log")" 1 check T6-no-mcp "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
# --- set backendish: managed leftovers parked/unregistered --- # --- set backendish: managed leftovers parked/unregistered ---
run set backendish >/dev/null 2>&1 run set backendish >/dev/null 2>&1
@@ -63,14 +62,15 @@ check T7-gsc-on "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" on
check T8-gsa-park "$([ -e "$FX/skills-disabled/gstack__gs-a" ] && echo p || echo n)" p check T8-gsa-park "$([ -e "$FX/skills-disabled/gstack__gs-a" ] && echo p || echo n)" p
check T9-emil-off "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" off check T9-emil-off "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" off
check T10-emil-park "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" p check T10-emil-park "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" p
check T11-magic-off "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null || true)" 0 check T11-21st-off "$([ -e "$FX/skills/21st-ui-build" ] && echo on || echo off)" off
check T12-rm-call "$(grep -c '^mcp remove magic' "$FX/claude-calls.log")" 1 check T12-21st-park "$([ -e "$FX/skills-disabled/21st-ui-build" ] && echo p || echo n)" p
check T13-other-untouched "$([ -e "$FX/skills/other-ext" ] && echo on || echo off)" on check T13-other-untouched "$([ -e "$FX/skills/other-ext" ] && echo on || echo off)" on
# --- back to designish: parked external restored (not re-sourced) --- # --- back to designish: parked external restored (not re-sourced) ---
run set designish >/dev/null 2>&1 run set designish >/dev/null 2>&1
check T14-emil-back "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on check T14-emil-back "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
check T15-park-gone "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" n check T15-park-gone "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" n
check T16-magic-back "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1 check T16-21st-back "$([ -e "$FX/skills/21st-ui-build" ] && echo on || echo off)" on
check T17-no-mcp-ever "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ] printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+61 -41
View File
@@ -8,7 +8,7 @@
# as symlinks inside skills/. This script moves those symlinks # as symlinks inside skills/. This script moves those symlinks
# to/from skills-disabled/ so Claude Code stops/starts scanning them. # to/from skills-disabled/ so Claude Code stops/starts scanning them.
# #
# MCP servers are toggled via `claude mcp add|remove` (not symlinks). # A multi-skill pack (gstack, 21st) toggles all of its skills at once.
# #
# Usage: # Usage:
# toggle-external.sh list # toggle-external.sh list
@@ -20,7 +20,7 @@
# gstack — per-skill symlinks populated by gstack's own setup # gstack — per-skill symlinks populated by gstack's own setup
# emil-design-eng — single symlink → skills-external/emil-design-eng # emil-design-eng — single symlink → skills-external/emil-design-eng
# darwin-skill — single symlink → ~/.agents/skills/darwin-skill # darwin-skill — single symlink → ~/.agents/skills/darwin-skill
# magic — 21st-dev Magic MCP server (API key in .env) # 21st — 21st.dev skill pack (needs the `21st` CLI + login)
# #
# For fine-grained activation (only design skills, only qa skills, only # For fine-grained activation (only design skills, only qa skills, only
# audit skills, etc.) instead of all-or-nothing gstack toggling, use: # audit skills, etc.) instead of all-or-nothing gstack toggling, use:
@@ -40,17 +40,17 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; }
err() { echo -e "${RED}✗${NC} $1"; } err() { echo -e "${RED}✗${NC} $1"; }
# All non-plugin tools this script can toggle. # All non-plugin tools this script can toggle.
MANAGED_TOOLS=(gstack emil-design-eng darwin-skill magic) MANAGED_TOOLS=(gstack emil-design-eng darwin-skill 21st)
# Load MAGIC_API_KEY (and any other secrets) from $REPO/.env if present. # Prints the skill names that belong to the "21st" pack. Source of truth:
# Called only by the magic branch — other tools don't need env vars. # skills-external/21st-* — the `21st skills install` run in install-plugins.sh
load_env() { # owns that list, so adding a skill upstream needs no edit here.
if [ -z "${MAGIC_API_KEY:-}" ] && [ -f "$REPO/.env" ]; then twentyfirst_skills() {
set -a local d
# shellcheck source=/dev/null for d in "$REPO"/skills-external/21st-*/; do
source "$REPO/.env" [ -f "${d}SKILL.md" ] || continue
set +a basename "$d"
fi done
} }
# Prints the names (directory basenames) that belong to "gstack". # Prints the names (directory basenames) that belong to "gstack".
@@ -84,13 +84,13 @@ status_tool() {
[ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; } [ -d "$HOME/.agents/skills/$tool" ] || { echo "missing"; return; }
[ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled" [ -e "$SKILLS_DIR/$tool" ] && echo "enabled" || echo "disabled"
;; ;;
magic) 21st)
command -v claude >/dev/null || { echo "missing"; return; } local installed=0
if claude mcp list 2>/dev/null | grep -q '^magic:'; then while read -r name; do
echo "enabled" installed=1
else [ -e "$SKILLS_DIR/$name" ] && { echo "enabled"; return; }
echo "disabled" done < <(twentyfirst_skills)
fi [ "$installed" -eq 1 ] && echo "disabled" || echo "missing"
;; ;;
*) *)
echo "unknown"; return 1 ;; echo "unknown"; return 1 ;;
@@ -124,12 +124,20 @@ disable_tool() {
warn "$tool already disabled" warn "$tool already disabled"
fi fi
;; ;;
magic) 21st)
if [ "$(status_tool magic)" = "enabled" ]; then # Parked under the plain skill name — same convention as the other
claude mcp remove magic -s user >/dev/null # externals, so profile.sh's park/restore path stays interoperable.
ok "magic disabled" local parked=0
while read -r name; do
[ -e "$SKILLS_DIR/$name" ] || continue
rm -rf "${DISABLED_DIR:?}/${name:?}"
mv "$SKILLS_DIR/$name" "$DISABLED_DIR/$name"
parked=$((parked + 1))
done < <(twentyfirst_skills)
if [ "$parked" -gt 0 ]; then
ok "21st disabled ($parked skills parked)"
else else
warn "magic already disabled" warn "21st already disabled"
fi fi
;; ;;
*) err "Unknown tool: $tool"; return 1 ;; *) err "Unknown tool: $tool"; return 1 ;;
@@ -177,25 +185,37 @@ enable_tool() {
return 1 return 1
fi fi
;; ;;
magic) 21st)
load_env local restored=0 linked=0
if [ -z "${MAGIC_API_KEY:-}" ]; then while read -r name; do
err "MAGIC_API_KEY not set — add it to ~/.claude/.env (template: .env.example)" if [ -e "$DISABLED_DIR/$name" ]; then
return 1 rm -rf "${SKILLS_DIR:?}/${name:?}"
fi mv "$DISABLED_DIR/$name" "$SKILLS_DIR/$name"
if [ "$(status_tool magic)" = "enabled" ]; then restored=$((restored + 1))
warn "magic already enabled" elif [ -e "$SKILLS_DIR/$name" ]; then
: # already enabled
else
ln -sf "$REPO/skills-external/$name" "$SKILLS_DIR/$name"
linked=$((linked + 1))
fi
done < <(twentyfirst_skills)
if [ "$((restored + linked))" -eq 0 ]; then
if [ "$(status_tool 21st)" = "missing" ]; then
err "21st pack not installed in $REPO/skills-external — run: make plugin"
return 1
fi
warn "21st already enabled"
return 0 return 0
fi fi
# Reference, not value: Claude Code expands ${VAR} in mcpServers.env at ok "21st enabled ($((restored + linked)) skills: $restored restored, $linked linked)"
# launch (job7/BDR-026) — MAGIC_API_KEY itself never lands in # The skills shell out to the CLI; without it (or without a session)
# ~/.claude.json. The check above still confirms the var IS set in # they can only report failure. Warn, never block — the pack is still
# ~/.claude/.env before wiring the reference, so a missing key fails # correctly wired and `make plugin` installs the CLI.
# here instead of silently at Claude Code startup. if ! command -v 21st >/dev/null 2>&1; then
claude mcp add magic --scope user \ warn "the \`21st\` CLI is not on PATH — install it: npm i -g @21st-dev/cli"
--env 'API_KEY=${MAGIC_API_KEY}' \ elif ! 21st whoami 2>/dev/null | grep -q '^Logged in as '; then
-- npx -y @21st-dev/magic@latest warn "not signed in to 21st — component retrieval and 21st AI need: 21st login"
ok "magic enabled (user scope)" fi
;; ;;
*) err "Unknown tool: $tool"; return 1 ;; *) err "Unknown tool: $tool"; return 1 ;;
esac esac
+4 -3
View File
@@ -71,7 +71,10 @@ if [ -d "$GSTACK_SRC/browse/dist" ]; then
fi fi
fi fi
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles impeccable) # impeccable is NOT here: its installer writes the skill straight into
# skills/ (and its agents into agents/) at --scope=global, so there is no
# skills-external/ copy to symlink. See install-plugins.sh Step 8d.
EXTERNAL_SKILLS=(emil-design-eng frontend-design design-motion-principles)
for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do for _ext_skill in "${EXTERNAL_SKILLS[@]}"; do
if [ -d "$REPO/skills-external/$_ext_skill" ]; then if [ -d "$REPO/skills-external/$_ext_skill" ]; then
if [ -L "$CLAUDE/skills/$_ext_skill" ] && [ "$(readlink "$CLAUDE/skills/$_ext_skill")" = "$REPO/skills-external/$_ext_skill" ]; then if [ -L "$CLAUDE/skills/$_ext_skill" ] && [ "$(readlink "$CLAUDE/skills/$_ext_skill")" = "$REPO/skills-external/$_ext_skill" ]; then
@@ -117,8 +120,6 @@ link_env() {
echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\"" echo " cp \"$REPO/.env.example\" \"$home_env\" && \"\${EDITOR:-nano}\" \"$home_env\""
return return
fi fi
grep -qE '^[[:space:]]*(export[[:space:]]+)?MAGIC_API_KEY=.' "$home_env" 2>/dev/null \
|| echo "⚠️ $home_env has no MAGIC_API_KEY line — magic won't enable until added."
if [ -L "$repo_env" ]; then if [ -L "$repo_env" ]; then
[ "$(readlink "$repo_env")" = "$home_env" ] && return [ "$(readlink "$repo_env")" = "$home_env" ] && return
ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1)) ln -sf "$home_env" "$repo_env"; CHANGED=$((CHANGED + 1))
+7 -2
View File
@@ -20,6 +20,11 @@
"version": "latest", "version": "latest",
"note": "Context7 CLI — doc lookup for fast-evolving libs. Standalone CLI, not an MCP server. Install: npm install -g ctx7. Standalone: ctx7 docs /vercel/next.js \"middleware\"." "note": "Context7 CLI — doc lookup for fast-evolving libs. Standalone CLI, not an MCP server. Install: npm install -g ctx7. Standalone: ctx7 docs /vercel/next.js \"middleware\"."
}, },
"21st": {
"source": "npm:@21st-dev/cli",
"version": "latest",
"note": "21st.dev CLI (bin `21st`) — supersedes the @21st-dev/magic MCP server (2026-09-22). Standalone CLI + a pack of 7 skills, no MCP, no API key: auth is `21st login` (browser token in ~/.config/21st). Install: npm install -g @21st-dev/cli. The skill pack is staged-installed into skills-external/21st-* by install-plugins.sh Step 8.7 — `21st skills install` refuses to write through the ~/.claude/skills symlink."
},
"graphifyy": { "graphifyy": {
"source": "pypi:graphifyy", "source": "pypi:graphifyy",
"version": "latest", "version": "latest",
@@ -40,7 +45,7 @@
}, },
"impeccable": { "impeccable": {
"source": "npm:impeccable", "source": "npm:impeccable",
"version": "3.2.0", "version": "4.1.0",
"note": "Design anti-pattern detector (45 deterministic rules, CLI 'impeccable detect', exit 0/2) + /impeccable skill (23 verbs) by pbakaus. Pin = CLI version; the skill dist has its own release track fetched by 'skills install'. Pinned for audit reproducibility (LRN-077 class: a rules update silently changes audit output). Requires Node >= 24 — install step skips gracefully below that. Machine-owned: synced to skills-external/impeccable/ (gitignored), symlinked by link.sh." "note": "Design anti-pattern detector (45 deterministic rules, CLI 'impeccable detect', exit 0/2) + /impeccable skill (23 verbs) + 4 impeccable-* subagents, by pbakaus. Pin = CLI version ONLY: the skill dist and the engine binary have their own release tracks, fetched by 'skills install' at install time, so this pin does not freeze audit output the way a semgrep pin does. It still gates the CLI deliberately (LRN-077 class). BEWARE: the pin rots — the CLI downloads its skill dist at install time and an older release's artifact disappears upstream (3.2.0 -> 'Download failed: invalid zip data', 2026-09-22, which left 'make plugin' printing a run-it-yourself warning). install-plugins.sh Step 8d and update-all.sh therefore fall back to @latest on a pin failure and warn to bump this version. Requires Node >= 24. Installed at --scope=global: lands in ~/.claude/skills/impeccable + ~/.claude/agents/impeccable-*.md, both symlinks into this repo, both gitignored."
} }
} }
+3 -6
View File
@@ -235,11 +235,7 @@
"WebFetch", "WebFetch",
"Bash(git stash pop*)", "Bash(git stash pop*)",
"Bash(git stash drop*)", "Bash(git stash drop*)",
"Bash(git stash clear)", "Bash(git stash clear)"
"mcp__magic__21st_magic_component_builder",
"mcp__magic__21st_magic_component_refiner",
"mcp__magic__21st_magic_component_inspiration",
"mcp__magic__logo_search"
], ],
"defaultMode": "auto", "defaultMode": "auto",
"disableBypassPermissionsMode": "disable", "disableBypassPermissionsMode": "disable",
@@ -370,7 +366,8 @@
"Moving or renaming a directory inside the repo (`mv src/api src/api_old`, or any `mv` of a tree). It breaks imports and paths silently, and the breakage surfaces far from the command. Clear only when the user asked for that move.", "Moving or renaming a directory inside the repo (`mv src/api src/api_old`, or any `mv` of a tree). It breaks imports and paths silently, and the breakage surfaces far from the command. Clear only when the user asked for that move.",
"An inline interpreter or `xargs` that deletes, or that writes outside the current working directory: `python3 -c`, `python -c` or `node -e` calling `rmtree`, `remove`, `unlink` or `truncate`; `xargs` feeding `rm`, `mv` or `dd`. `find ... | xargs rm` is the case that matters, since it routes around the `find * -exec rm` deny rule. Reading, computing, and editing a file inside the working directory pass untouched.", "An inline interpreter or `xargs` that deletes, or that writes outside the current working directory: `python3 -c`, `python -c` or `node -e` calling `rmtree`, `remove`, `unlink` or `truncate`; `xargs` feeding `rm`, `mv` or `dd`. `find ... | xargs rm` is the case that matters, since it routes around the `find * -exec rm` deny rule. Reading, computing, and editing a file inside the working directory pass untouched.",
"Docker data destruction on this workstation: `docker rm -f`, `docker volume rm` or `prune`, `docker system prune`, `docker compose down -v` (drops named volumes, which hold local database data with no undo), and `docker run` with `--privileged` or a bind mount outside the current working directory. Clear only when the user named the container or volume in this turn.", "Docker data destruction on this workstation: `docker rm -f`, `docker volume rm` or `prune`, `docker system prune`, `docker compose down -v` (drops named volumes, which hold local database data with no undo), and `docker run` with `--privileged` or a bind mount outside the current working directory. Clear only when the user named the container or volume in this turn.",
"Undeclared node packages: `npx <pkg>`, `pnpm dlx` or `yarn dlx` of a package absent from the manifest and lockfile runs code fetched at call time; `npm install <name>` or `pnpm add <name>` adds a dependency the house rule requires naming first. Clear only when the user named the package in this turn." "Undeclared node packages: `npx <pkg>`, `pnpm dlx` or `yarn dlx` of a package absent from the manifest and lockfile runs code fetched at call time; `npm install <name>` or `pnpm add <name>` adds a dependency the house rule requires naming first. Clear only when the user named the package in this turn.",
"Publishing to the 21st.dev public catalog: `21st publish`, `publish-theme`, `publish-template`, `publish-gradient`, `publish-ascii`, `submit` and `resubmit` push a component, theme or template from this machine onto a public listing under the user's account; `21st edit`, `delete`, `withdraw`, `remove-from-catalog` and `21st profile set|upload` change or remove what is already published there. Retrieval and generation (`search`, `logo`, `get`, `add`, `generate`, `iterate`) are ordinary design work and pass. Clear only when the user asked to publish or change that specific item in this turn."
], ],
"hard_deny": [ "hard_deny": [
"$defaults", "$defaults",
+15 -14
View File
@@ -52,8 +52,7 @@ lists items + types:
| `personal` | symlink move skills/ ↔ skills-disabled/\<name\> (no prefix) | | `personal` | symlink move skills/ ↔ skills-disabled/\<name\> (no prefix) |
| `external` | symlink move skills/ ↔ skills-disabled/\<name\> | | `external` | symlink move skills/ ↔ skills-disabled/\<name\> |
| `plugin@<marketplace>` | `claude plugin enable\|disable <name>@<marketplace>` (auto) | | `plugin@<marketplace>` | `claude plugin enable\|disable <name>@<marketplace>` (auto) |
| `mcp` (known: magic) | delegate to `lib/toggle-external.sh` (uses `.env`) | | `mcp` | advisory — prints manual `claude mcp add …` command (no server is managed today: `MANAGED_MCPS` is empty since 21st.dev moved to a CLI) |
| `mcp` (other) | advisory — prints manual `claude mcp add …` command |
| `cli` | advisory only — reports installed/not-installed | | `cli` | advisory only — reports installed/not-installed |
**Always-on plugins** (`security-guidance`, `superpowers`) are **Always-on plugins** (`security-guidance`, `superpowers`) are
@@ -62,12 +61,14 @@ protected — `set` will refuse to disable them even if the profile omits them.
`ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`, `ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`,
`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled. `pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled.
**Managed externals** (`emil-design-eng`, `frontend-design`, **Managed externals** (`emil-design-eng`, `frontend-design`,
`design-motion-principles`, `impeccable`) and **managed MCPs** (`magic`) `design-motion-principles`, `impeccable`, and the five 21st design skills
follow the same symmetry (BDR-079): `set` enables them when the profile `21st-ui-build`, `21st-ui-explore`, `21st-ui-review`, `21st-cli-use`,
lists them (from parked state, or from `skills-external/` if the symlink `21st-ai`) follow the same symmetry (BDR-079): `set` enables them when the
never existed) and parks/unregisters them when it does not — e.g. `set profile lists them (from parked state, or from `skills-external/` if the
backend` after design work turns emil and magic off. `darwin-skill` and any symlink never existed) and parks them when it does not — e.g. `set backend`
other unlisted external are never auto-touched. gstack works the same after design work turns emil and the 21st pack off. `darwin-skill`,
`21st-registry`, `21st-design-sync` and any other unlisted external are never
auto-touched. gstack works the same
all the way down: a profile listing gstack skills while the whole pack is all the way down: a profile listing gstack skills while the whole pack is
off (via `toggle-external.sh`) re-enables JUST those skills on demand. off (via `toggle-external.sh`) re-enables JUST those skills on demand.
@@ -137,11 +138,11 @@ bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS
update-check, learnings — script doesn't touch that infra. Disabled skills update-check, learnings — script doesn't touch that infra. Disabled skills
are just hidden from Claude Code's scanner; the gstack repo stays installed. are just hidden from Claude Code's scanner; the gstack repo stays installed.
- Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max, - Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max,
plugin-dev, pr-review-toolkit), the managed external packs (emil-design-eng, plugin-dev, pr-review-toolkit) and the managed external packs
frontend-design, design-motion-principles, impeccable) and the `magic` MCP — (emil-design-eng, frontend-design, design-motion-principles, impeccable,
in BOTH directions: `set` enables what the profile lists and disables the the 21st design skills) — in BOTH directions: `set` enables what the profile
managed leftovers it doesn't (BDR-008, BDR-079). Anything outside those lists and disables the managed leftovers it doesn't (BDR-008, BDR-079).
allowlists stays manual: `claude plugin enable|disable`, `claude mcp Anything outside those allowlists stays manual: `claude plugin
add|remove`. enable|disable`, `bash lib/toggle-external.sh enable|disable <tool>`.
- `set` is destructive in the sense that it disables non-listed gstack skills. - `set` is destructive in the sense that it disables non-listed gstack skills.
Use `apply` if the user wants additive behavior. Use `apply` if the user wants additive behavior.
+98 -21
View File
@@ -321,8 +321,6 @@ else
info "bun not installed — skipping" info "bun not installed — skipping"
fi fi
# NOT updated here, deliberately (audit 2026-07-02): # NOT updated here, deliberately (audit 2026-07-02):
# - magic MCP: registered as `npx -y @21st-dev/magic@latest` — npx resolves
# the latest release at every invocation, nothing to upgrade.
# - graphify Claude integration (`graphify claude install`): rewrites curated # - graphify Claude integration (`graphify claude install`): rewrites curated
# CLAUDE.md / .claude/settings.json (BDR-028 guard territory) — re-run # CLAUDE.md / .claude/settings.json (BDR-028 guard territory) — re-run
# MANUALLY only if a graphify upgrade changes its hook format. # MANUALLY only if a graphify upgrade changes its hook format.
@@ -381,11 +379,34 @@ else
info "design-motion-principles not installed — skipping" info "design-motion-principles not installed — skipping"
fi fi
# ── Impeccable (design anti-pattern detector + skill) ── # ── Impeccable (design detector + skill + subagents) ──
# Global scope: the installer writes through the ~/.claude/{skills,agents}
# symlinks straight into this repo (install-plugins.sh Step 8d explains why
# staging + project scope was wrong). The pin can rot upstream, so a pinned
# failure falls back to @latest rather than leaving the tool stale forever.
#
# One install attempt. $1 = "latest" or an exact version. On failure, IMP_FAIL
# holds the reason. Same helper as Step 8d: with a copy already in place a
# rotted pin exits 0 and says "Could not check for skill updates … left
# unchanged", so the exit code cannot tell it from an up-to-date no-op.
imp_install() {
local pkg="impeccable" out rc=0
[ "$1" != "latest" ] && pkg="impeccable@$1"
out=$(npx -y "$pkg" skills install -y --providers=claude --scope=global \
--no-hooks 2>&1) || rc=$?
IMP_FAIL=$(printf '%s\n' "$out" \
| grep -E 'Download failed|Could not check for skill updates' \
| head -1 || true)
if [ "$rc" -ne 0 ] && [ -z "$IMP_FAIL" ]; then
IMP_FAIL="installer exited $rc"
fi
[ -z "$IMP_FAIL" ]
}
echo "" echo ""
echo "── Updating impeccable..." echo "── Updating impeccable..."
IMP_DIR="$REPO/skills-external/impeccable" IMP_SKILL_DIR="$HOME/.claude/skills/impeccable"
if [ ! -f "$IMP_DIR/SKILL.md" ]; then IMP_PARKED="$REPO/skills-disabled/impeccable"
if [ ! -f "$IMP_SKILL_DIR/SKILL.md" ] && [ ! -f "$IMP_PARKED/SKILL.md" ]; then
info "impeccable not installed — skipping (run: make plugin)" info "impeccable not installed — skipping (run: make plugin)"
else else
IMP_VER="" IMP_VER=""
@@ -399,29 +420,85 @@ print(d.get('impeccable',{}).get('version','latest'))
fi fi
IMP_NODE=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1) IMP_NODE=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
if [ -z "${IMP_NODE:-}" ] || [ "$IMP_NODE" -lt 24 ]; then if [ -z "${IMP_NODE:-}" ] || [ "$IMP_NODE" -lt 24 ]; then
info "impeccable update skipped — needs Node >= 24 (found ${IMP_NODE:-none}); existing dist kept" info "impeccable update skipped — needs Node >= 24 (found ${IMP_NODE:-none}); existing copy kept"
else else
IMP_PKG="impeccable" IMP_WAS_PARKED=false
# Pin honored (LRN-077 class: a silent rules update changes audit [ -d "$IMP_PARKED" ] && IMP_WAS_PARKED=true
# output on unchanged code) — bump the pin deliberately, then update. # Pin honored (LRN-077 class: a silent rules update changes audit output
[ -n "$IMP_VER" ] && [ "$IMP_VER" != "latest" ] && IMP_PKG="impeccable@${IMP_VER}" # on unchanged code) — bump the pin deliberately, then update.
IMP_STAGE=$(mktemp -d) IMP_PIN="${IMP_VER:-latest}"
if (cd "$IMP_STAGE" && npx -y "$IMP_PKG" skills install -y --providers=claude --scope=project --no-hooks >/dev/null 2>&1); then IMP_OK=false
IMP_SRC=$(find "$IMP_STAGE" -type d -name impeccable -path "*skills*" 2>/dev/null | head -1) if imp_install "$IMP_PIN"; then
if [ -n "$IMP_SRC" ] && [ -f "$IMP_SRC/SKILL.md" ]; then IMP_OK=true
rm -rf "$IMP_DIR" elif [ "$IMP_PIN" != "latest" ]; then
mv "$IMP_SRC" "$IMP_DIR" warn "impeccable@${IMP_PIN} did not install (${IMP_FAIL}) — that release's skill dist is gone upstream; trying @latest"
ok "impeccable refreshed (CLI ${IMP_VER:-latest})" if imp_install latest; then
else IMP_OK=true
warn "impeccable: installer produced no dist — existing kept" warn "refreshed from @latest, not the pin — bump \"impeccable\".version in plugins.lock.json"
fi
fi
if [ "$IMP_OK" = true ] && [ -f "$IMP_SKILL_DIR/SKILL.md" ]; then
IMP_SKILL_VER=$(sed -n 's/^version:[[:space:]]*//p' "$IMP_SKILL_DIR/SKILL.md" | head -1)
ok "impeccable refreshed (CLI ${IMP_VER:-latest}, skill ${IMP_SKILL_VER:-?})"
if [ "$IMP_WAS_PARKED" = true ]; then
rm -rf "${IMP_PARKED:?}"
mv "$IMP_SKILL_DIR" "$IMP_PARKED"
info "impeccable was parked by a profile — refreshed copy returned to skills-disabled/"
fi fi
else else
warn "impeccable refresh failed — existing dist kept" warn "impeccable refresh failed (${IMP_FAIL:-no SKILL.md written}) — existing copy kept"
fi fi
rm -rf "$IMP_STAGE"
fi fi
fi fi
# ── 7.4. Update the 21st.dev CLI + skill pack ──
# The CLI is a global npm bin; the skills are its hash-verified output, staged
# under a throwaway HOME because `21st skills install` refuses to write
# through the ~/.claude/skills symlink (see install-plugins.sh Step 8.7).
echo ""
echo "── Updating 21st.dev CLI + skill pack..."
if ! command -v 21st &>/dev/null; then
info "21st CLI not installed — skipping (run: make plugin)"
else
TFD_VER=""
if [ -f "$REPO/plugins.lock.json" ] && command -v python3 &>/dev/null; then
TFD_VER=$(python3 -c "
import json
with open('$REPO/plugins.lock.json') as f:
d = json.load(f)
print(d.get('21st',{}).get('version','latest'))
" 2>/dev/null || true)
fi
TFD_PKG="@21st-dev/cli@latest"
[ -n "$TFD_VER" ] && [ "$TFD_VER" != "latest" ] && TFD_PKG="@21st-dev/cli@${TFD_VER}"
if npm install -g "$TFD_PKG" 2>/dev/null; then
ok "21st CLI updated (${TFD_VER:-latest})"
else
warn "21st CLI update failed — existing binary kept"
fi
TFD_STAGE=$(mktemp -d)
if HOME="$TFD_STAGE" 21st skills install --global --agent claude >/dev/null 2>&1; then
TFD_N=0
for _tfd in "$TFD_STAGE"/.claude/skills/*/; do
[ -f "${_tfd}SKILL.md" ] || continue
_tfd_name=$(basename "$_tfd")
# Refresh the SOURCE only. A parked copy in skills-disabled/ is left
# alone: re-enabling restores it, and the next update refreshes it.
rm -rf "${REPO:?}/skills-external/${_tfd_name:?}"
mv "$_tfd" "$REPO/skills-external/$_tfd_name"
TFD_N=$((TFD_N + 1))
done
if [ "$TFD_N" -gt 0 ]; then
ok "21st skill pack refreshed ($TFD_N skills)"
else
warn "21st skills install produced no SKILL.md — existing pack kept"
fi
else
warn "21st skill pack refresh failed — existing pack kept"
fi
rm -rf "$TFD_STAGE"
fi
# ── 7.5. Update external skills (npx skills) ── # ── 7.5. Update external skills (npx skills) ──
echo "" echo ""
echo "── Updating external skills (npx skills)..." echo "── Updating external skills (npx skills)..."