Merge feature/skill-catalog-prune into develop

This commit is contained in:
bastien
2026-09-28 13:55:04 +02:00
50 changed files with 2195 additions and 180 deletions
+7
View File
@@ -42,6 +42,7 @@ rules:
| BLK-020 | 2026-09-02 | notify-attention: both channels dead on one VS Code client — 2026-09-02 | resolved | | BLK-020 | 2026-09-02 | notify-attention: both channels dead on one VS Code client — 2026-09-02 | resolved |
| BLK-021 | 2026-09-22 | Bash tool dead mid-session ("every command exits 1"): /tmp usrquota blown by a dead session's probe HOMEs — 2… | open | | BLK-021 | 2026-09-22 | Bash tool dead mid-session ("every command exits 1"): /tmp usrquota blown by a dead session's probe HOMEs — 2… | open |
| BLK-022 | 2026-09-22 | `hooks/guard-bash.sh` withheld by the safety classifier; executable spec shipped instead — 2026-09-22 | open | | BLK-022 | 2026-09-22 | `hooks/guard-bash.sh` withheld by the safety classifier; executable spec shipped instead — 2026-09-22 | open |
| BLK-023 | 2026-09-28 | floor-guard SKIP pattern `xit(` (Jasmine) matches any `exit(` in python/JS test helpers → false ECARTS; workaround: no `exit(` in inline python, bash derives rc from output — 2026-09-28 | open |
--- ---
@@ -261,3 +262,9 @@ rules:
- **Real cause**: the hook body is a dense list of destructive-command patterns (rm -r forms, disk tools, docker escapes, history rewrites); the classifier reads it as harmful capability regardless of the defensive frame. - **Real cause**: the hook body is a dense list of destructive-command patterns (rm -r forms, disk tools, docker escapes, history rewrites); the classifier reads it as harmful capability regardless of the defensive frame.
- **Solution**: `lib/tests/guard-bash.test.sh` (214 cases, deny/allow) stays as the spec and SKIPs while the hook is absent, so `make test` stays green. Options: user writes the hook against the spec (start from `/mnt/cloudpex/RECOVERY/01-prochain-systeme/claude-config/hooks/guard-bash.sh`, already on disk, then iterate to green); or a different design (allowlist of first words + path containment) requested explicitly. Until then: static deny (BDR-095) covers the direct forms; nested forms rely on the classifier prose. - **Solution**: `lib/tests/guard-bash.test.sh` (214 cases, deny/allow) stays as the spec and SKIPs while the hook is absent, so `make test` stays green. Options: user writes the hook against the spec (start from `/mnt/cloudpex/RECOVERY/01-prochain-systeme/claude-config/hooks/guard-bash.sh`, already on disk, then iterate to green); or a different design (allowlist of first words + path containment) requested explicitly. Until then: static deny (BDR-095) covers the direct forms; nested forms rely on the classifier prose.
- **Status**: open. Links [[BDR-095]], [[LRN-160]]. - **Status**: open. Links [[BDR-095]], [[LRN-160]].
## BLK-023 — floor-guard `xit(` substring flags every `exit(` — 2026-09-28
- **Friction**: fresh verifier returned ECARTS(1) on a fully conform diff: `FLOOR SKIP lib/tests/profile-census.test.sh:116 sys.exit(1 if violations else 0)`. One re-dispatch spent on a tool artefact.
- **Real cause**: `lib/floor-guard.sh` SKIP_SUBSTRINGS holds the bare fragment `'xit('` to catch Jasmine's `xit(…)`; `skip_kind()` is a plain substring match, so `sys.exit(`, `SystemExit(`, `process.exit(` all hit.
- **Solution**: workaround applied — the inline python prints violations only, the bash wrapper derives the return code from the captured output (no `exit(` anywhere). Root fix pending: word-bound the pattern (`(^|[^a-zA-Z_.])xit\(`) or match `xit(` only in JS/TS test files; hotfix-sized.
- **Status**: open. Links [[BDR-105]], [[BDR-102]] (floor-guard origin), [[EVAL-034]].
+10
View File
@@ -126,6 +126,7 @@ rules:
| BDR-102 | 2026-09-27 | agent-skills: no plugin, vendor 3 skills + build floor-guard + routing census + rest-api rule | accepted | | BDR-102 | 2026-09-27 | agent-skills: no plugin, vendor 3 skills + build floor-guard + routing census + rest-api rule | accepted |
| BDR-103 | 2026-09-27 | 6-repo review: 5 verdicts, 3 criteria (grep-verified coverage, per-session cost, doctrine conflict); stars decided nothing | accepted | | BDR-103 | 2026-09-27 | 6-repo review: 5 verdicts, 3 criteria (grep-verified coverage, per-session cost, doctrine conflict); stars decided nothing | accepted |
| BDR-104 | 2026-09-28 | MengTo motion pack: vendor 5 scroll skills pinned via shared lib/vendor-skills.sh + build personal skill site-motion; 17 skipped | accepted | | BDR-104 | 2026-09-28 | MengTo motion pack: vendor 5 scroll skills pinned via shared lib/vendor-skills.sh + build personal skill site-motion; 17 skipped | accepted |
| BDR-105 | 2026-09-28 | skill-catalog prune: 9 gstack out via GSTACK_REMOVED, full ⊇ every profile, max = everything, brightdata + frontend-design plugin off, security-guidance Stop review off, design gate asks `21st login` and waits | accepted |
--- ---
@@ -1309,3 +1310,12 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Reference**: commits 2a1ad17 (helper + vendoring), ba14b5e (site-motion); contracts `.claude/tasks/contracts/2026-09-27-{mengto-vendor,site-motion-skill}-0002.md`; gates MET, verifiers CONFORME after 3 re-dispatches (frontmatter shape, refresh convention, security env override + traversal), security PASS ×2, `make test` 36 suites green minus 2 pre-existing T16a. Links [[BDR-103]] [[BDR-102]] [[LRN-141]] [[LRN-174]] [[EVAL-033]]. - **Reference**: commits 2a1ad17 (helper + vendoring), ba14b5e (site-motion); contracts `.claude/tasks/contracts/2026-09-27-{mengto-vendor,site-motion-skill}-0002.md`; gates MET, verifiers CONFORME after 3 re-dispatches (frontmatter shape, refresh convention, security env override + traversal), security PASS ×2, `make test` 36 suites green minus 2 pre-existing T16a. Links [[BDR-103]] [[BDR-102]] [[LRN-141]] [[LRN-174]] [[EVAL-033]].
- **Amendment 2026-09-28**: the two LOW caveats closed on user ask (415b44e): `re.fullmatch` guard, `commit`/`source`/`path` validated before URL construction, 4 more hermetic cases (12). Security PASS, verifier CONFORME 9/9. - **Amendment 2026-09-28**: the two LOW caveats closed on user ask (415b44e): `re.fullmatch` guard, `commit`/`source`/`path` validated before URL construction, 4 more hermetic cases (12). Security PASS, verifier CONFORME 9/9.
- **Amendment 2026-09-28 (doctor)**: `make doctor` now checks every vendored external (6394fa7, feature/doctor-vendored-skills): lock files present, symlink per active profile, parked ≠ failed, hints. Lib sourceable for the hermetic suite (11 cases); doctor mirrors `active_profile()` instead of sourcing profile.sh (its main runs on source). Security: lock shape-validated, allowlists on profile/item names. Closes the gap that emil/frontend-design/motion had since their install. - **Amendment 2026-09-28 (doctor)**: `make doctor` now checks every vendored external (6394fa7, feature/doctor-vendored-skills): lock files present, symlink per active profile, parked ≠ failed, hints. Lib sourceable for the hermetic suite (11 cases); doctor mirrors `active_profile()` instead of sourcing profile.sh (its main runs on source). Security: lock shape-validated, allowlists on profile/item names. Closes the gap that emil/frontend-design/motion had since their install.
## BDR-105 — skill-catalog prune: 9 gstack removed, `full` ⊇ every profile, `max` = everything, two plugins off, Stop review off, 21st sign-in gate
- **Date**: 2026-09-28
- **Status**: accepted, feature/skill-catalog-prune, merged to develop 2026-09-28 (user go "merge le tout")
- **Decision**: (1) `lib/gstack-removed.sh` = single denylist (ship, land-and-deploy, setup-deploy, autoplan, context-save, learn, careful, guard, design-shotgun): in no profile, `max` included; `profile.sh gstack on` and `toggle-external enable gstack` skip them. (2) User rule: `full` (default) carries everything every other profile carries, one allowlisted exception pr-review-toolkit; `max` (`# SUPERSET-OF: full`) = full + parked (make-pdf, diagram, 21st-ai/ui-explore/ui-review) + pr-review-toolkit. 21st trio out of full/web/web-full/design. `lib/tests/profile-census.test.sh` locks the three invariants live + baseline/mutant fixtures. (3) Live plugin state: brightdata-plugin@synced `false`, frontend-design@claude-plugins-official uninstalled (byte-dup of the managed skills-external copy). (4) settings.json `env.ENABLE_STOP_REVIEW=0`: security-guidance keeps regex + commit/push agentic review, loses the Opus call per code-changing turn. (5) `lib/gstack-links.sh` builds the whole helper tree under `~/.claude/skills/gstack/` (83 hardcoded paths), one lib for link.sh, install-plugins.sh, update-all.sh; `lib/doctor-skills.sh` counts through symlinks with the census parser. (6) Routing: Ship/PR → ship-feature, never gstack ship. (7) Design gate exit 12 `SIGN-IN REQUIRED`: installed-but-signed-out 21st → ask `! 21st login`, end turn, re-run; explicit "proceed without 21st" = only skip; unknown whoami → 11 with diagnostic.
- **Why**: 5-analyzer audit over 150 skills / 53.5k chars of descriptions; harness shows ~19k, least-invoked lose theirs → 78 name-only this session ([[LRN-175]]). ship base = origin/HEAD = main on Gitea; land-and-deploy `gh pr merge --squash` + deploy; autoplan/make-pdf/diagram/careful/guard/freeze hardcode paths only bin + browse/dist linked ([[LRN-177]]); context-save without restore; brightdata keyless + `bright-data-mcp` orders WebFetch replaced; security-guidance 0 findings / 6 days, 1 FP ([[EVAL-024]]); doctor undercount ×6. 21st CLI `Not logged in` → user: ask and wait, not skip.
- **Alternatives rejected**: rm symlinks by hand (set/reset re-materialize, `gstack on` restores everything → denylist instead); per-skill toggles inside ui-ux-pro-max (all-or-nothing, unverified); drop superpowers in the same run (7 skills wired in ship-feature/init-project → tier 2, own branch); keep the 21st trio in design profiles (redundant with impeccable + ui-ux-pro-max, CLI signed out); raise `SLASH_COMMAND_TOOL_CHAR_BUDGET` (costs context, the opposite goal); keep the official frontend-design plugin and drop the copy (copy is profile-managed and gate-checked); shared 21st auth helper across 3 scripts (breaks 4 fixture suites, changes installer semantics — [[LRN-178]]); in-session `export TWENTYFIRST_TOKEN` remedy (env does not persist across tool calls).
- **Caveats**: kept gstack skills still route to removed names in their upstream prose (Skill call fails, doctrine applies); helper tree links every top-level submodule entry (no SKILL.md exposed, asserted); security-guidance commit review quota unmeasured; doctor constants rebased on 2026-09-28 measures; `apply` is additive → other machines run `set full`, not `apply`.
- **Reference**: f83f8f7 02b62f7 4c86d6d 729d715 (prune), bd3e525 132bcdf (21st gate); contracts `2026-09-28-skill-catalog-prune-0554` (18 criteria, oracles in `.oracles/`) and `2026-09-28-21st-signin-gate-1215` (7); plans r4 / r3 after 3 challengers + 1 confirmation each; GATE 0 MET, verifiers CONFORME (iter 2 / iter 1), security PASS ×2; 42 suites green minus 2 pre-existing T16a. Links [[BDR-030]] [[BDR-101]] [[BDR-093]] [[BDR-095]] [[BDR-080]] [[BDR-025]] [[BDR-070]] [[LRN-175]] [[LRN-176]] [[LRN-177]] [[LRN-178]] [[BLK-023]] [[EVAL-034]].
+8
View File
@@ -54,6 +54,7 @@ rules:
| EVAL-031 | 2026-09-25 | /feat run for BDR-101: challenge round earned its cost, two blockers sat in my own premises | keep challenge round on state-detection plans; check live state before planning; pin grep in oracles | | EVAL-031 | 2026-09-25 | /feat run for BDR-101: challenge round earned its cost, two blockers sat in my own premises | keep challenge round on state-detection plans; check live state before planning; pin grep in oracles |
| EVAL-032 | 2026-09-27 | 4 parallel feater executors, one tree, gate loop: verifier caught a vacuous test, security caught a partial-write; my oracles wrong twice | keep same-tree parallel dispatch with disjoint FILE SCOPE + orchestrator-owned shared files; blind verifier stays; measure oracles on precedents | | EVAL-032 | 2026-09-27 | 4 parallel feater executors, one tree, gate loop: verifier caught a vacuous test, security caught a partial-write; my oracles wrong twice | keep same-tree parallel dispatch with disjoint FILE SCOPE + orchestrator-owned shared files; blind verifier stays; measure oracles on precedents |
| EVAL-033 | 2026-09-28 | case 7: 2 analyzers + 2 executors + 3 re-dispatches; verifiers caught shape, convention and my wrong count; security caught an env override | brief names the scratchpad path explicitly (3 /tmp leftovers); keep blind verifiers; count claims get an artifact | | EVAL-033 | 2026-09-28 | case 7: 2 analyzers + 2 executors + 3 re-dispatches; verifiers caught shape, convention and my wrong count; security caught an env override | brief names the scratchpad path explicitly (3 /tmp leftovers); keep blind verifiers; count claims get an artifact |
| EVAL-034 | 2026-09-28 | catalog prune + 21st gate: two challenge rounds each found what r3 missed (nested SKILL.md, fixture cp lists, in-session export); my ledgers failed twice (heredoc CHECKs); 5 executors DONE first pass; verifier gap = tool false positive | keep the confirmation pass on any plan that changed materially; one-line CHECKs; grep fixture cp lists before a `source` |
--- ---
@@ -322,3 +323,10 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
- **Result**: both CONFORME after 3 re-dispatches: frontmatter shape (executor mirrored external peers instead of the named personal ones), update-all refresh convention (executor's "additive" install broke "not installed — skipping"), security MEDIUM env override + LOW traversal. Verifier also doubted my CHANGELOG "sixteen skipped" → it was seventeen. Byte-for-byte fidelity of 14 files confirmed twice. - **Result**: both CONFORME after 3 re-dispatches: frontmatter shape (executor mirrored external peers instead of the named personal ones), update-all refresh convention (executor's "additive" install broke "not installed — skipping"), security MEDIUM env override + LOW traversal. Verifier also doubted my CHANGELOG "sixteen skipped" → it was seventeen. Byte-for-byte fidelity of 14 files confirmed twice.
- **Anomalies**: (1) three sub-agents wrote to `/tmp` outside the scratchpad then could not `rm -rf` (refused, correctly) — the brief must name the scratchpad path; (2) executors' self-justified deviations were plausible each time and wrong twice → blind verifier stays mandatory; (3) analyzer reports at ~120-180 words per skill were the right grain, two of them fit my context; (4) `make test` rc 1 is still the 2 pre-existing T16a, my filter now shows totals. - **Anomalies**: (1) three sub-agents wrote to `/tmp` outside the scratchpad then could not `rm -rf` (refused, correctly) — the brief must name the scratchpad path; (2) executors' self-justified deviations were plausible each time and wrong twice → blind verifier stays mandatory; (3) analyzer reports at ~120-180 words per skill were the right grain, two of them fit my context; (4) `make test` rc 1 is still the 2 pre-existing T16a, my filter now shows totals.
- **Action**: brief template line "scratch only under <scratchpad>"; count claims in CHANGELOG/journal cite the list they count; keep the analyzer-first pattern for any pack > 5 skills. - **Action**: brief template line "scratch only under <scratchpad>"; count claims in CHANGELOG/journal cite the list they count; keep the analyzer-first pattern for any pack > 5 skills.
## EVAL-034 — two /feat runs by hand: challengers earned their cost, my artefacts were the weak link
- **Date**: 2026-09-28
- **Method**: 5 analyzers (4 clusters + docs guide) → user decisions (4 questions ×2 batches) → contract 18 criteria → plan r1→r4 with 3 blind challengers + 1 confirmation → 4 feater in parallel (disjoint scopes) → gates.sh → fresh verifier → security. Second run (21st gate): same chain, 1 executor.
- **Result**: prune — challengers closed 8 MAJOR at r3, the confirmation pass still found 1 BLOCKER (nested SKILL.md in browser-skills/openclaw/node_modules) + 3 MAJOR (setup's global symlink, update-all 3rd copy, fixture cp lists); executors 4/4 DONE first pass; GATE 0 UNMET(4) = my heredoc CHECKs ([[LRN-176]]); verifier ECARTS(1) = floor-guard false positive ([[BLK-023]]), CONFORME at iteration 2; security PASS. 21st gate — three lenses: my shared-helper reflex = BLOCKER ×2 ([[LRN-178]]), my `export TWENTYFIRST_TOKEN` remedy = MAJOR (env does not persist); confirmation pass pinned the diagnostic format; executor DONE first pass, CONFORME 7/7, PASS.
- **Anomalies**: (1) both times the confirmation pass found real defects after "all MAJOR closed" → r3 is not a stopping point; (2) every gate failure of the day was mine (ledger format, tool pattern), none the executors'; (3) verifier and challengers each re-ran the live oracles themselves (link.sh, `set full`, the gate) — cheap, decisive; (4) the user's rule ("full ⊇ every profile") arrived at pass B and inverted a settled plan step: pass B before challenge is the right order.
- **Action**: keep the single confirmation pass mandatory when a plan changed materially; contract CHECKs one line, files under `.oracles/`; grep fixture `cp` lists before any new `source`; run the live oracle once by hand before dispatching the verifier.
+3
View File
@@ -535,3 +535,6 @@ rules:
- User go: feature/mengto-site-motion merged into develop via `gitflow finish` → d3633db, no conflict (develop had not moved), pushed, local + origin copies removed by the lib. develop == origin/develop, no working branch anywhere. The whole review is on develop: cases 1-5 (yesterday) + case 7 (today). Open for the user: `make link` + `bash lib/profile.sh apply full` (8 vendored externals to symlink), `rm -rf /tmp/mengto-verify /tmp/tmp.AAyJzvufO6`. - User go: feature/mengto-site-motion merged into develop via `gitflow finish` → d3633db, no conflict (develop had not moved), pushed, local + origin copies removed by the lib. develop == origin/develop, no working branch anywhere. The whole review is on develop: cases 1-5 (yesterday) + case 7 (today). Open for the user: `make link` + `bash lib/profile.sh apply full` (8 vendored externals to symlink), `rm -rf /tmp/mengto-verify /tmp/tmp.AAyJzvufO6`.
- User: "tout cela s'installe et se met à jour comme le reste ?" → traced: install/plugin/update/link all cover the 8 vendored skills; only `make doctor` was blind to curl-vendored externals (since emil). User go → /feat by hand: `lib/doctor-vendored.sh` + doctor section + README (6394fa7); gates MET, verifier CONFORME ×2, security PASS ×2 after one re-dispatch (MEDIUM traceback leak on malformed lock, LOW allowlists). 37 suites green minus 2 pre-existing T16a. feature/doctor-vendored-skills UNMERGED — human gate. Live: 129 skills in the census, 11 externals ✓ in doctor. - User: "tout cela s'installe et se met à jour comme le reste ?" → traced: install/plugin/update/link all cover the 8 vendored skills; only `make doctor` was blind to curl-vendored externals (since emil). User go → /feat by hand: `lib/doctor-vendored.sh` + doctor section + README (6394fa7); gates MET, verifier CONFORME ×2, security PASS ×2 after one re-dispatch (MEDIUM traceback leak on malformed lock, LOW allowlists). 37 suites green minus 2 pre-existing T16a. feature/doctor-vendored-skills UNMERGED — human gate. Live: 129 skills in the census, 11 externals ✓ in doctor.
- User go: feature/doctor-vendored-skills merged into develop via `gitflow finish` → 2c94a0c, no conflict, pushed, local + origin copies removed by the lib. develop == origin/develop, no working branch anywhere. `make doctor` now covers the 11 vendored externals. - User go: feature/doctor-vendored-skills merged into develop via `gitflow finish` → 2c94a0c, no conflict, pushed, local + origin copies removed by the lib. develop == origin/develop, no working branch anywhere. `make doctor` now covers the 11 vendored externals.
- Skill-catalog audit (user: "tour des skills, doublons, économiser tokens"): 5 analyzers over 150 skills / 53.5k chars desc; 78 listed name-only this session (listing budget ≈1 % ctx, least-invoked lose desc → gain = routing quality + no broken 100 KB body invoked, not listing chars). Found: frontend-design plugin byte-dup of managed copy; brightdata 21 skills keyless + hostile WebFetch routing; gstack ship trunk-based (origin/HEAD=main), land-and-deploy auto-merge+deploy, autoplan/make-pdf/diagram/careful/guard/freeze dead paths (only bin + browse/dist linked); security-guidance = Opus call per code turn + agentic commit review, 0 findings/6 days; doctor.sh undercount ×6. User go: tier 1, superpowers vendor-7 (tier 2 later), 21st trio parked (CLI `Not logged in`), rule "full ⊇ every profile, max = everything". Live: brightdata disabled, frontend-design plugin uninstalled, `set full` → 75 skills (was 89).
- /feat by hand on feature/skill-catalog-prune: contract 18 criteria; plan r1→r4 (3 challengers, confirmation FATAL(4): nested SKILL.md in browser-skills/openclaw/node_modules, ./setup global symlink, update-all 3rd copy); 4 feater parallel DONE; GATE 0 UNMET(4) = MY heredoc CHECKs (gates.sh single-line) → oracles to `<contract>.oracles/*.py` → MET; verifier ECARTS(1) = floor-guard `xit(` false-positive on `sys.exit(` → restructure → CONFORME; security PASS. 41 suites green minus 2 pre-existing T16a, shellcheck clean. UNMERGED — human gate. Registries pending user approval.
- User: "quand on détecte qu'on a besoin de 21st, on demande de log si c'est pas fait et on attend". /feat by hand on the same branch: design gate gains exit 12 `SIGN-IN REQUIRED` (three-state whoami probe, unknown → 11 with diagnostic, explicit "proceed without 21st" only skip); challenge round dropped my shared-helper idea (would break 4 fixture suites + change installer semantics) and my in-session `export TWENTYFIRST_TOKEN` remedy (env does not persist across tool calls). Executor DONE first pass, GATE 0 MET, verifier CONFORME 7/7, security PASS, 8/8 hermetic. Gate now exits 12 live here until `21st login`.
+20
View File
@@ -194,6 +194,10 @@ rules:
| LRN-172 | 2026-09-27 | TF-IDF cosine on a 2-doc corpus is identically 0: similarity self-tests need N ≥ 4, a same-corpus positive control and a sensitivity re-run | fixtures for any corpus-normalised statistic (idf, z-score, ranking), "distinct pair passes" tests | | LRN-172 | 2026-09-27 | TF-IDF cosine on a 2-doc corpus is identically 0: similarity self-tests need N ≥ 4, a same-corpus positive control and a sensitivity re-run | fixtures for any corpus-normalised statistic (idf, z-score, ranking), "distinct pair passes" tests |
| LRN-173 | 2026-09-27 | contract oracles written from memory failed twice: run the CHECK on the precedent files first, census greps via `git grep` (tracked only), `EVIDENCE: pending` mandatory for gates.sh | contract CHECK lines, precedent-mirroring criteria, gates.sh ledgers | | LRN-173 | 2026-09-27 | contract oracles written from memory failed twice: run the CHECK on the precedent files first, census greps via `git grep` (tracked only), `EVIDENCE: pending` mandatory for gates.sh | contract CHECK lines, precedent-mirroring criteria, gates.sh ledgers |
| LRN-174 | 2026-09-28 | a coverage census must grep plugin DATA files (CSV/JSON search DBs), not only SKILL.md prose; and a registry sample is not the upstream catalog, list the tree | before claiming a gap in installed skills; before scoping an external-repo evaluation | | LRN-174 | 2026-09-28 | a coverage census must grep plugin DATA files (CSV/JSON search DBs), not only SKILL.md prose; and a registry sample is not the upstream catalog, list the tree | before claiming a gap in installed skills; before scoping an external-repo evaluation |
| LRN-175 | 2026-09-28 | skill listing budget ≈1 % ctx, least-invoked skills lose their description (78/150 name-only); pruning under the cap buys routing + no broken-body invocation, not listing chars; doctor undercount = find w/o -L + single-line desc grep | before any "save tokens by removing skills" claim; doctor token section |
| LRN-176 | 2026-09-28 | gates.sh `CHECK:` is single-line: a heredoc body reads as prose, the oracle runs `python3 -` on empty stdin and lands NOT-MET "marker absent", never ERROR; multi-line oracle → `<contract>.oracles/*.py` | writing contract oracles longer than one line |
| LRN-177 | 2026-09-28 | gstack skills hardcode `~/.claude/skills/gstack/<path>` (83 paths: bin, scripts, ETHOS.md, */sections, review/specialists, make-pdf/dist, freeze/bin…); only bin + browse/dist were linked → dead skills and vacuous hooks (exit 127); ./setup plants a global symlink; whole-dir link exposes nested SKILL.md; `apply` is additive, `set` parks | any gstack wiring change, any "gstack skill fails" report |
| LRN-178 | 2026-09-28 | a top-level `source` added to a lib breaks every hermetic suite that copies that lib alone into a fixture; grep the `cp` lists before adding one, or source lazily inside the branch that needs it | adding `source` to profile.sh / toggle-external.sh / any lib the suites copy |
--- ---
@@ -1620,3 +1624,19 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
## LRN-174 — a coverage census greps plugin data files too; a registry sample is not the catalog ## LRN-174 — a coverage census greps plugin data files too; a registry sample is not the catalog
- **Context**: I told the user Astro View Transitions had zero local mentions. ui-ux-pro-max's `data/stacks/astro.csv` rows 28-31 carry ClientRouter, `transition:name`, no-JS fallback; `motion.csv` carries GSAP pin/scrub, SplitText, parallax. My grep covered SKILL.md prose and archetypes, not the plugin's CSV search DB. Same day: the ui-skills registry showed 11 MengTo skills; the repo tree has 88 web-design skills, and the substantive ones were outside the sample. - **Context**: I told the user Astro View Transitions had zero local mentions. ui-ux-pro-max's `data/stacks/astro.csv` rows 28-31 carry ClientRouter, `transition:name`, no-JS fallback; `motion.csv` carries GSAP pin/scrub, SplitText, parallax. My grep covered SKILL.md prose and archetypes, not the plugin's CSV search DB. Same day: the ui-skills registry showed 11 MengTo skills; the repo tree has 88 web-design skills, and the substantive ones were outside the sample.
- **Apply**: census = `grep -rl` over the plugin cache including data dirs, then say "row in a search DB" vs "workflow"; evaluating an upstream = `git/trees?recursive=1` first, sample never. Correct the user the moment the miss is found ([[BDR-104]]). - **Apply**: census = `grep -rl` over the plugin cache including data dirs, then say "row in a search DB" vs "workflow"; evaluating an upstream = `git/trees?recursive=1` first, sample never. Correct the user the moment the miss is found ([[BDR-104]]).
## LRN-175 — the skill listing has a char budget; pruning under it buys routing, not context
- **Context**: 150 skills, 53.5k chars of descriptions, but the harness listed ~19k with a description and 78 name-only (profile, seo, tour, all ui-ux-pro-max, 14/15 superpowers, brightdata, synced). Budget ≈1 % of context (`SLASH_COMMAND_TOOL_CHAR_BUDGET`, community-documented), least-invoked skills lose theirs first. doctor.sh said 34 skills / 3.4k t: `find -maxdepth 2` without `-L` skipped every symlinked skill, `grep '^description:' | head -1` counted 0 for block scalars.
- **Apply**: removing skills below the cap frees no context (the cap refills); the gain is descriptions back for kept skills + no accidental 50-100 KB broken body load (autoplan = 25k t to fail). Real context levers = session-start injections (superpowers 3.6 KB) and name-only lines. Measure with the census parser through symlinks ([[BDR-105]]).
## LRN-176 — a multi-line CHECK is silently truncated by gates.sh
- **Context**: four `CHECK: python3 - <<'PY' … PY` oracles parsed as one line each; the ledger validated (RUNNABLE 13/18), GATE 0 ran `python3 -` on an empty heredoc → rc 0, no marker → NOT-MET ×4 while every executor had self-checked green. Cause: `_set_attr` takes the rest of the `CHECK:` line only; the body lines are criterion prose.
- **Apply**: one line per CHECK; anything longer lives in `<contract>.oracles/c<n>.py` next to the contract (committed with it) and the CHECK calls the file. Follow-up: gates.sh could refuse a CHECK containing `<<`. Sibling of [[LRN-173]] (run the CHECK on precedent files first).
## LRN-177 — the gstack helper-tree class: one lib, no SKILL.md exposed, dst never a symlink
- **Context**: make-pdf MAKE_PDF_NOT_AVAILABLE, diagram BUNDLE_MISSING, careful/guard/freeze hooks exit 127, cso and plan-*-review unable to read `sections/` — all one class: skills hardcode `~/.claude/skills/gstack/<path>`, link.sh (and two copies in install-plugins.sh / update-all.sh) linked only `bin` and `browse/dist`. Traps found by the challengers: gstack `./setup` plants `~/.claude/skills/gstack -> submodule` when absent (a helper writing into dst then nests links inside the submodule); `browser-skills/`, `openclaw/`, `node_modules/` are non-skill dirs holding nested SKILL.md (a whole-dir link would double-list skills); `profile.sh apply` only enables, `set`/`reset` park.
- **Apply**: `lib/gstack-links.sh` is the single writer (skill dirs → children minus SKILL.md; non-skill dirs skipped when they hold a SKILL.md; `.git*`/`node_modules` by name; dst symlink removed, dst inside src refused); after a profile edit run `set full`, never `apply`; a gstack skill "failing" → check the census oracle first ([[BDR-105]]).
## LRN-178 — before a new top-level `source`, grep the fixture `cp` lists
- **Context**: twice in one day. E1b's `source gstack-removed.sh` in profile.sh/toggle-external.sh needed a `cp` line in three suites (profile-default, profile-set-managed, toggle-external-repo-resolution) — caught by the confirmation challenger, fixed in scope. My 21st helper plan would have added a second top-level `source` to toggle-external.sh with no fixture update → four suites red under `set -euo pipefail`; two challengers flagged it as BLOCKER, the helper was dropped.
- **Apply**: `grep -n "cp .*lib/<file>" lib/tests/*.sh` before adding a `source` to a lib; either widen every fixture copy in the same change or source lazily inside the one branch that needs it. Prefer the inline predicate when only one caller needs the new semantics ([[BDR-105]]).
+69
View File
@@ -1,5 +1,74 @@
# TODO # TODO
## 2026-09-28 — design gate asks for `21st login` and waits (feature/skill-catalog-prune)
User: "si on veut l'utiliser, on demande à l'utilisateur de se log, plus simple que
dire c'est pas logged on utilise pas… on demande de log si c'est pas fait et on
attend". Contract `.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md`.
- [x] S1 three-state probe `twentyfirst_auth_state` INLINE in design-tool-gate.sh
(challenge r2 dropped the shared helper: install-plugins/toggle-external keep
their own semantics); `in` (TWENTYFIRST_TOKEN / API_KEY_21ST, or whoami
"Logged in as") / `out` (exact "Not logged in") / `unknown:whoami: rc=…`
→ exit 11 with a CLI-specific remedy; exit 12 `SIGN-IN REQUIRED`;
`DESIGN_GATE_REPO_OVERRIDE`; hermetic suite 8/8 (stub control, in, out,
token, absent, INCOMPLETE wins, unknown ×2).
- [x] S2 design-gate.md §3 branch 12: STOP, ask `! 21st login` (or any terminal
on this machine), END THE TURN, re-run on reply; explicit "proceed without
21st" = the only skip, stated visibly, not re-asked in the run; no in-session
token export; §4 resume path; feat/bugfix STEP 0.5 name SIGN-IN REQUIRED.
- [x] S3 plan r1→r3 (3 challengers + confirmation), executor DONE, GATE 0 MET,
verifier CONFORME 7/7, security PASS. Live on this machine: gate exits 12
until `21st login`. Committed in place on feature/skill-catalog-prune.
## 2026-09-28 — skill-catalog prune, tier 1 (feature/skill-catalog-prune)
User go after the 5-agent duplicate audit (150 skills, 53.5k chars of descriptions,
78 listed name-only in session = listing budget exceeded). Contract
`.claude/tasks/contracts/2026-09-28-skill-catalog-prune-0554.md`, plan
`.claude/tasks/plans/2026-09-28-skill-catalog-prune-0554.md`. Live already done:
`claude plugin disable brightdata-plugin@synced`, `claude plugin uninstall
frontend-design@claude-plugins-official` (byte-identical to the managed copy).
- [x] K1 profiles: the 9 broken/doctrine-breaking gstack out of every profile
(ship trunk-based, land-and-deploy auto-merge+deploy, setup-deploy, autoplan
dead paths, context-save orphan, learn unused, careful/guard vacuous hooks,
design-shotgun needs OPENAI_API_KEY); make-pdf + diagram + 21st-ai/
ui-explore/ui-review parked out of `full` (trio out of web/web-full/design
too); user rule: full ⊇ every other profile, `max` (`# SUPERSET-OF: full`)
= full + parked; profile docs (SKILL.md, README, USAGE); hermetic
`lib/tests/profile-census.test.sh` (removed / parked / union invariants).
- [x] K2 wiring: link.sh helper links make-pdf/dist + lib/diagram-render/dist
(+ freeze/bin if gated); doctor.sh counts symlinked skills + block-scalar
descriptions + synced bucket info line, plugin constants re-based.
- [x] K3 docs/config: settings.json env `ENABLE_STOP_REVIEW=0` (security-guidance
Stop LLM review off, commit/push review kept); CLAUDE.global.md routing
(Ship/PR → ship-feature, gstack-off list); deploy/SKILL.md rows;
install-plugins.sh notes + summary "0 tokens" fix; plugin-advisor.md cost
text; CHANGELOG.
- [x] K4 plan r1→r4 (3 challengers + 1 confirmation pass, FATAL(4) closed by
named changes), 4 feater parallel DONE, GATE 0 MET after moving 4 heredoc
oracles to `<contract>.oracles/*.py` (gates.sh CHECK is single-line),
verifier CONFORME at iteration 2 (floor-guard `xit(` false positive on
`sys.exit(` → restructure), security PASS, make test 41 suites green minus
2 pre-existing T16a, shellcheck clean. Live: `set full` applied, 75 skills
listed (was 89), 16 parked, doctor 75 / ~5.4k t.
- [x] K5 registries written on user go (BDR-105, LRN-175..178, BLK-023, EVAL-034), merged
to develop on "merge le tout". Was: registries on user approval (BDR prune + full/max rule, LRN listing
budget, LRN gates.sh single-line CHECK, LRN gstack helper-tree class, BLK
floor-guard `xit(` pattern, EVAL challenge round), journal. UNMERGED — human
gate. After merge on any other machine: `make link` + `bash lib/profile.sh
set full` (NOT `apply`: additive). Follow-ups: floor-guard `xit(` → word
boundary (hotfix); gates.sh could refuse a CHECK holding `<<`; optional
doctor info line for the claude.ai synced bucket; `21st login`; claude.ai
skills useless in CLI off (built-in-browser, chrome-browser, computer-use,
skill-creator, import-memory). Tier 2 superpowers vendoring next.
Tier 2 (decided, not started): vendor brainstorming, writing-plans,
subagent-driven-development, test-driven-development, requesting-code-review,
using-git-worktrees, writing-skills from obra/superpowers at 5bf4e78 via
lib/vendor-skills.sh; drop the plugin (PROTECTED_PLUGINS, STEP 5, detect, banner,
doctor constants); rename `superpowers:` citers (ship-feature ×4, init-project ×4,
tour, deploy, audit-delta, lib/analyze-before-plan, lib/capitalize-commit,
plugin-advisor). User side: `21st login` (CLI reports Not logged in); claude.ai
skills useless in CLI (built-in-browser, chrome-browser, computer-use,
skill-creator, import-memory) to switch off in claude.ai settings.
## 2026-09-28 — make doctor checks the vendored externals (feature/doctor-vendored-skills) ## 2026-09-28 — make doctor checks the vendored externals (feature/doctor-vendored-skills)
User go "ok ajoute le check doctor" after the install/update/link trace: doctor.sh only User go "ok ajoute le check doctor" after the install/update/link trace: doctor.sh only
checked the gstack submodule; emil, frontend-design, motion and the 8 curl-vendored checked the gstack submodule; emil, frontend-design, motion and the 8 curl-vendored
@@ -0,0 +1,49 @@
# CONTRACT — 21st-signin-gate
- date: 2026-09-28 | flow: feat (ad-hoc dispatch, /feat gates replayed by the orchestrator) | branch: feature/skill-catalog-prune (working branch, commit in place)
- status: active
## REQUEST (verbatim — IMMUTABLE)
> Il faudrait pour 21st. Que, si on veut l'utiliser. Alors on demande à l'utilisateur de se log. Plus simple que de dire ah bah c'est pas logged on utilise pas. Donc ajoute ça quelque part, quand on detect qu'on a besoin de 21st, on demande de log si c'est pas fait et on attend
## CLARIFICATIONS
- Pass A: none — request complete. "Detect we need 21st" = the design gate (lib/design-gate.md → lib/design-tool-gate.sh), the single place the 21st CLI is required (GATE-BLOCK of design.profile); the 21st skills themselves are machine-owned (`21st skills install`) and are not edited.
- Pass B: no visible / public-name / scope choice left open — the gate message wording follows the gate's existing style, the exit code and the helper file are internal. Proceeds silently.
- [challenge 2026-09-28, 3 lenses: simplicity CONCERNS(1), correctness FATAL(2), robustness FATAL(4); every BLOCKER/MAJOR closed by a named plan change, r2] (a) NO shared helper: the predicate lives inline in lib/design-tool-gate.sh, toggle-external.sh and install-plugins.sh are untouched (their inline checks keep their own semantics); (b) three-state predicate `in` / `out` (exact "Not logged in" sentence) / `unknown` (rc≠0, timeout, unexpected line) → `unknown` surfaces as exit 11 with the raw diagnostic, never as the sign-in remedy; (c) no in-session `export TWENTYFIRST_TOKEN` remedy (env does not persist across tool calls, secrets stay out of the transcript) — the env var is honored when already present; (d) explicit user opt-out "proceed without 21st", stated visibly, scoped to the run; silent skip forbidden.
- [confirmation pass 2026-09-28, robustness CONCERNS(1), all closed by named changes, r3] unknown diagnostic pinned to `whoami: rc=<rc> <line>` with a CLI-specific remedy in the 11 block; stdout-only classification, `</dev/null`, rc captured under pipefail (test proves rc≠0 beats the sentence); hermeticity precondition on the sanitized PATH; MIRROR note at both sites; doc offers any terminal on this machine and does not re-ask after an explicit opt-out; `API_KEY_21ST` honored next to `TWENTYFIRST_TOKEN` (the CLI's second token env).
- Sign-in predicate = the CLI's own auth paths: `TWENTYFIRST_TOKEN` or `API_KEY_21ST` non-empty, or `21st whoami` first line starting with `Logged in as ` (local token read, no network; same sentence lib/toggle-external.sh:245 and install-plugins.sh:1059 test today). `whoami` wrapped in `timeout 15`; any other answer = `unknown`.
- Waiting = the orchestrator asks the user to run `! 21st login` in the session (browser flow) and ENDS THE TURN; on the user's reply it re-runs the gate before continuing. The agent never runs `21st login` itself (opens a browser, needs the human). A signed-out 21st is never treated as absent and its steps are never skipped.
- Functions ≤ 25 logic lines, 80-char lines; shellcheck clean; hermetic tests neutralize the real machine (`HOME` and `PATH` point into the fixture so `ensure_21st_on_path` cannot find the real CLI).
- Executors never run `21st login`, `profile.sh set|apply|reset`, `claude plugin …`, never commit.
## ACCEPTANCE CRITERIA
1. The three-state predicate lives in the gate script only; toggle-external.sh and install-plugins.sh are byte-identical to HEAD. [challenge r2]
CHECK: grep -q '^twentyfirst_auth_state()' lib/design-tool-gate.sh && grep -q 'DESIGN_GATE_REPO_OVERRIDE' lib/design-tool-gate.sh && git diff --quiet HEAD -- lib/toggle-external.sh install-plugins.sh && [ ! -e lib/twentyfirst-auth.sh ] && echo GATE_ONLY
EXPECT: GATE_ONLY
EVIDENCE: MET exit=0 marker-found :: GATE_ONLY
2. Live gate on this machine (21st installed, not signed in, no TWENTYFIRST_TOKEN): exit 12, output names `21st login`, and does NOT claim INCOMPLETE nor READY.
CHECK: env -u TWENTYFIRST_TOKEN bash lib/design-tool-gate.sh >/tmp/dtg.out 2>&1; rc=$?; cat /tmp/dtg.out; [ "$rc" = 12 ] && grep -q '21st login' /tmp/dtg.out && grep -q 'SIGN-IN REQUIRED' /tmp/dtg.out && ! grep -q 'INCOMPLETE' /tmp/dtg.out && ! grep -qE 'toolchain: READY' /tmp/dtg.out && echo LIVE_SIGNIN_12
EXPECT: LIVE_SIGNIN_12
EVIDENCE: MET exit=0 marker-found :: design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in ask the user to run in this session: ! 21st login (browser flow, save…
3. Hermetic suite green: stub control; signed-in → 0 READY; signed-out → 12 with `21st login`; TWENTYFIRST_TOKEN or API_KEY_21ST set → 0; CLI absent → 10 INCOMPLETE; INCOMPLETE wins over signed-out; unknown whoami answer (garbage rc 0, or the signed-out sentence with rc 3) → 11 with `whoami: rc=` diagnostic, without the sign-in remedy and without the claude-unreachable remedy. [challenge r2, r3]
CHECK: out=$(make test suite=lib/tests/design-tool-gate.test.sh 2>&1); echo "$out" | grep -qE 'FAIL=[1-9]' && { echo "$out" | tail -15; exit 1; }; for k in STUB_CONTROL SIGNED_IN_READY SIGNED_OUT_12 TOKEN_READY CLI_ABSENT_10 INCOMPLETE_WINS UNKNOWN_11; do echo "$out" | grep -q "PASS $k" || { echo "missing PASS $k"; exit 1; }; done; echo "$out" | grep -qE 'PASS=[1-9]' && echo SUITE_GREEN
EXPECT: SUITE_GREEN
EVIDENCE: MET exit=0 marker-found :: SUITE_GREEN
4. Gate doc and its two citers carry the new branch: design-gate.md documents exit 12 / SIGN-IN REQUIRED with `! 21st login`, "end the turn", re-run, the explicit opt-out "proceed without 21st", and never an in-session `export TWENTYFIRST_TOKEN`; feat and bugfix STEP 0.5 name SIGN-IN REQUIRED. [challenge r2]
CHECK: grep -q 'SIGN-IN REQUIRED' lib/design-gate.md && grep -q '! 21st login' lib/design-gate.md && grep -qi 'end the turn' lib/design-gate.md && grep -qi 'proceed without 21st' lib/design-gate.md && ! grep -qiE 'export TWENTYFIRST_TOKEN' lib/design-gate.md lib/design-tool-gate.sh && grep -q 'SIGN-IN REQUIRED' skills/feat/SKILL.md && grep -q 'SIGN-IN REQUIRED' skills/bugfix/SKILL.md && echo DOC_WIRED
EXPECT: DOC_WIRED
EVIDENCE: MET exit=0 marker-found :: DOC_WIRED
5. shellcheck clean on the two touched shell files; doctrine-citers and design-toolchain-reminder suites still green.
CHECK: shellcheck lib/design-tool-gate.sh lib/tests/design-tool-gate.test.sh && for s in doctrine-citers design-toolchain-reminder; do out=$(make test suite=lib/tests/$s.test.sh 2>&1) || { echo "$s rc"; exit 1; }; echo "$out" | grep -qE 'FAIL=[1-9]' && { echo "$s FAIL"; exit 1; }; done; echo SHELL_SUITES_OK
EXPECT: SHELL_SUITES_OK
EVIDENCE: MET exit=0 marker-found :: SHELL_SUITES_OK
6. When the gate is also INCOMPLETE (a blocking tool missing), the INCOMPLETE verdict (exit 10) wins; the sign-in state surfaces on the re-run after `/profile design` (hermetic case `INCOMPLETE_WINS`). [challenge r2: no extra line]
CHECK: out=$(make test suite=lib/tests/design-tool-gate.test.sh 2>&1); echo "$out" | grep -q 'PASS INCOMPLETE_WINS' && echo PRECEDENCE_OK
EXPECT: PRECEDENCE_OK
EVIDENCE: MET exit=0 marker-found :: PRECEDENCE_OK
7. CHANGELOG `[Unreleased]` names the new gate state and the remedy.
## FILE SCOPE
- lib/design-tool-gate.sh
- lib/design-gate.md, skills/feat/SKILL.md, skills/bugfix/SKILL.md (STEP 0.5 bullet only), CHANGELOG.md
- lib/tests/design-tool-gate.test.sh (new)
- Orchestrator-only: .claude/tasks/**, .claude/memory/**
@@ -0,0 +1,104 @@
# CONTRACT — skill-catalog-prune
- date: 2026-09-28 | flow: feat (ad-hoc dispatch, /feat gates replayed by the orchestrator, 3 parallel feater executors) | branch: feature/skill-catalog-prune
- status: active
## REQUEST (verbatim — IMMUTABLE)
> j'aimerias que tu fasse le tour des skills perso et installe, gstack compris, superpowers compris, que tu vois si il y a des doublons, S'il y en as, supprime le moins performant de l'installation auto, update etc (on supprimera le skill / agent en question apres installation du plugin si necesaire) on va faire en sorte d'economiser le plus de token possible comme ca. Fais le tour d'analyse, vois les doublons, vois les quels supprimer et retirer de la config car inutile
User answers to the decision batch (verbatim):
> Tier 1 → "Go, tout le tier 1 (Recommended)"
> Superpowers → "Vendoriser 7, retirer le plugin (Recommended)" [tier 2, separate branch, NOT this contract]
> 21st → "21st est en cli, et j'ai connecté le cli, n'est-ce pas ? On a retiré le mcp sinon 1" [CLI answers `Not logged in` → option 1: park 21st-ai, 21st-ui-explore, 21st-ui-review]
> gstack reste → "Parquer les 10 redondants hors full, Réparer make-pdf et diagram dans link.sh, Parquer make-pdf et diagram aussi, Avoir la possibilité de choisir un profil avec si on en a besoin, du style full + parked"
Tier 1 as presented and approved: disable brightdata (synced), remove the duplicate
frontend-design plugin, take the 9 broken/doctrine-breaking gstack skills (ship,
land-and-deploy, setup-deploy, autoplan, context-save, learn, careful, guard,
design-shotgun) out of the profiles with the routing lines corrected, switch the
security-guidance Stop layer off, fix doctor.sh and the "0 tokens" claims.
## CLARIFICATIONS
- Pass A: none — request complete (outcome, scope and constraints derivable from the audit).
- Live state already changed by the orchestrator before dispatch (user go): `claude plugin disable brightdata-plugin@synced` wrote `"brightdata-plugin@synced": false` into settings.json; `claude plugin uninstall frontend-design@claude-plugins-official` removed its enabledPlugins entry and cache. The executor keeps both states.
- The superset profile carries a header line `# SUPERSET-OF: full` so oracles find it by content, not by name (internal choice).
- "Parked" = kept installed, out of the default `full` profile, listed only in the superset profile (and in the specialized profiles that already carry them, see pass B). The 9 removed gstack skills go in NO profile, superset included: they are broken (autoplan, careful/guard hooks exit 127, context-save without restore) or break doctrine (ship base = origin/HEAD = main, land-and-deploy auto-merges and deploys), or need an absent key (design-shotgun → OPENAI_API_KEY).
- security-guidance stays installed and enabled (PROTECTED); only the Stop-hook LLM review is switched off through the plugin's own env switch `ENABLE_STOP_REVIEW=0` in settings.json `env`; commit/push agentic review and the regex layer stay on.
- [gated 2026-09-28] Q: superset profile name? / A: `max`.
- [gated 2026-09-28] Q: 11 redundant gstack out of the specialized profiles too? / A: NO — user rule: "full must already carry what every other profile has; max has everything; these gstack matter in full because full must do what each profile does". So the 11 redundant gstack STAY in full (and in their specialized profiles). Parked = only what no specialized profile carries: make-pdf, diagram, and the 21st trio. `full` ⊇ union of every non-max profile, minus the 9 removed names and the 21st trio, minus an explicit exception allowlist written in the census test with its reason (pr-review-toolkit: plugin deliberately out of full, audit 2026-07-02 #12, ~2.2k tokens; measured 2026-09-28: it is the ONLY name any specialized profile carries that full lacks).
- [gated 2026-09-28] Q: 21st-ai / 21st-ui-explore / 21st-ui-review scope? / A: out of all four design-bearing profiles (full, web, web-full, design), kept in `max`; CLAUDE.global.md "Review / audit" line drops "+ 21st-ui-review"; GATE-BLOCK untouched.
- [gated 2026-09-28] Q: add the `freeze/bin` helper link too? / A: yes, same fix as make-pdf/diagram.
- [challenge 2026-09-28, 3 blind challengers, no BLOCKER, 8 MAJOR adopted] (a) the broken-wiring class is every `~/.claude/skills/gstack/<path>` the gstack skills hardcode (bin, scripts, ETHOS.md, lib, design/dist, extension, */sections, review/checklist+specialists, make-pdf/dist, freeze/bin…), fixed by one shared `lib/gstack-links.sh` used by link.sh AND install-plugins.sh, exposing no SKILL.md; (b) `profile.sh apply` is additive, only `set`/`reset` park: the live tree is proven by criterion 16 (`set full`); (c) `gstack on` / `toggle-external enable gstack` honor the `GSTACK_REMOVED` denylist (`lib/gstack-removed.sh`, single source); (d) `max` ⊇ union of every profile − GSTACK_REMOVED (so it carries pr-review-toolkit); (e) census test = passing baseline then one mutant per invariant; (f) doctor reuses lib/skill-routing-census.py's parser through `lib/doctor-skills.sh`; (g) no synced-bucket line in doctor (scope). These refine criteria 2, 3, 8, 9 and add 16-18 below; they are reported to the human at the merge gate.
- [confirmation pass 2026-09-28, robustness FATAL(4), every finding closed by a named plan change, r4] helper lib skips nested-SKILL.md dirs and removes/refuses a dst symlink; update-all.sh joins the shared lib (criterion 18); the three existing profile/toggle suites copy lib/gstack-removed.sh into their fixtures; `gstack on` reports the real restored count; doctor stats warn on fallback.
- Functions ≤ 25 logic lines, 80-char lines, ≤ 5 params, ≤ 5 locals (CLAUDE.global.md). Shell edits shellcheck-clean.
- Executors never run `gitflow`, never commit, never run `claude plugin …`, never touch `skills/`, `skills-external/`, `~/.claude` outside link.sh's own effect, never delete anything.
## ACCEPTANCE CRITERIA
1. The 9 removed gstack skills appear as an entry in no profile (positive control first).
CHECK: pat='^(ship|land-and-deploy|setup-deploy|autoplan|context-save|learn|careful|guard|design-shotgun)([[:space:]]|$)'; printf 'ship\n' | grep -qE "$pat" || { echo control-failed; exit 1; }; if grep -lE "$pat" lib/profiles/*.profile; then echo listed-somewhere; exit 1; fi; echo PROFILES_CLEAN
EXPECT: PROFILES_CLEAN
EVIDENCE: MET exit=0 marker-found :: PROFILES_CLEAN
2. `full` lists none of the 5 parked names; exactly one profile carries `# SUPERSET-OF: full` (it is `max`), it lists every entry of full, every parked name, and every name any profile carries (max ⊇ union − removed). [gated 2026-09-28: parked set = 5; challenge: union]
CHECK: python3 .claude/tasks/contracts/2026-09-28-skill-catalog-prune-0554.oracles/c2.py
EXPECT: SUPERSET_OK
EVIDENCE: MET exit=0 marker-found :: SUPERSET_OK
3. After link.sh, every helper path the gstack skills hardcode under `~/.claude/skills/gstack/` and that exists in the submodule resolves (recomputed from a grep census of the skills; `<skill>/SKILL.md` cross-reads and `.git` excluded), and no SKILL.md is exposed anywhere under the helper tree. [challenge 2026-09-28: whole class, was 3 paths]
CHECK: bash link.sh >/dev/null 2>&1; python3 .claude/tasks/contracts/2026-09-28-skill-catalog-prune-0554.oracles/c3.py
EXPECT: LINKS_OK
EVIDENCE: MET exit=0 marker-found :: LINKS_OK
4. doctor.sh counts every skill reachable through `~/.claude/skills/*/SKILL.md` (symlinks included) and sums block-scalar descriptions too (recomputed independently, ±5 %).
CHECK: python3 .claude/tasks/contracts/2026-09-28-skill-catalog-prune-0554.oracles/c4.py
EXPECT: DOCTOR_COUNTS
EVIDENCE: MET exit=0 marker-found :: DOCTOR_COUNTS
5. settings.json: Stop review off, brightdata off, frontend-design plugin gone.
CHECK: python3 -c "import json;d=json.load(open('settings.json'));e=d['enabledPlugins'];assert d['env']['ENABLE_STOP_REVIEW']=='0';assert e['brightdata-plugin@synced'] is False;assert 'frontend-design@claude-plugins-official' not in e;print('SETTINGS_OK')"
EXPECT: SETTINGS_OK
EVIDENCE: MET exit=0 marker-found :: SETTINGS_OK
6. No repo doc still claims security-guidance costs 0 tokens (positive control first).
CHECK: pat='security-guidance.*0 tokens|0 tokens.*security-guidance'; echo 'security-guidance (0 tokens)' | grep -qE "$pat" || exit 1; if grep -rnE "$pat" install-plugins.sh agents/plugin-advisor.md; then exit 1; fi; echo DOCS_OK
EXPECT: DOCS_OK
EVIDENCE: MET exit=0 marker-found :: DOCS_OK
7. Routing: Ship/PR routes to ship-feature, the gstack-off list no longer names ship/context-save, deploy's table no longer routes to land-and-deploy/setup-deploy; the 21st trio is out of full/web/web-full/design and off the Design Review line. [gated 2026-09-28]
CHECK: grep -qE '^- Ship / PR → ship-feature' CLAUDE.global.md && ! grep -qE 'Ship / PR → ship \(' CLAUDE.global.md && ! grep -q 'context-save' CLAUDE.global.md && ! grep -qE 'land-and-deploy|setup-deploy' skills/deploy/SKILL.md && ! grep -q '21st-ui-review' CLAUDE.global.md && ! grep -lE '^21st-(ai|ui-explore|ui-review)([[:space:]]|$)' lib/profiles/full.profile lib/profiles/web.profile lib/profiles/web-full.profile lib/profiles/design.profile && echo ROUTING_OK
EXPECT: ROUTING_OK
EVIDENCE: MET exit=0 marker-found :: ROUTING_OK
8. Hermetic profile census suite green: baseline fixture passes, each of the three mutants is detected for its own reason. [challenge 2026-09-28]
CHECK: out=$(make test suite=lib/tests/profile-census.test.sh 2>&1); echo "$out" | grep -qE 'FAIL=[1-9]' && { echo "$out" | tail -15; exit 1; }; for k in FIXTURE_BASELINE_OK FIXTURE_REMOVED_DETECTED FIXTURE_SUPERSET_DETECTED FIXTURE_FULLGAP_DETECTED; do echo "$out" | grep -q "$k" || { echo "missing $k"; exit 1; }; done; echo "$out" | grep -qE 'PASS=[1-9]' && echo SUITE_GREEN
EXPECT: SUITE_GREEN
EVIDENCE: MET exit=0 marker-found :: SUITE_GREEN
9. shellcheck clean on every touched shell file; doctrine-citers census and the four new hermetic suites green, existing profile/toggle suites still green. [challenge 2026-09-28]
CHECK: shellcheck link.sh doctor.sh install-plugins.sh update-all.sh lib/profile.sh lib/toggle-external.sh lib/gstack-links.sh lib/gstack-removed.sh lib/doctor-skills.sh lib/tests/profile-census.test.sh lib/tests/gstack-removed.test.sh lib/tests/gstack-links.test.sh lib/tests/doctor-skills.test.sh && for s in doctrine-citers gstack-removed gstack-links doctor-skills profile-default profile-set-managed toggle-external-repo-resolution; do out=$(make test suite=lib/tests/$s.test.sh 2>&1) || { echo "$s rc"; exit 1; }; echo "$out" | grep -qE 'FAIL=[1-9]' && { echo "$s FAIL"; exit 1; }; done; echo SHELL_DOCTRINE_OK
EXPECT: SHELL_DOCTRINE_OK
EVIDENCE: MET exit=0 marker-found :: SHELL_DOCTRINE_OK
10. Profile docs name the superset profile and full's new meaning: skills/profile/SKILL.md table, README.md (`/profile` row and `make profile` lines), USAGE.md `/profile` row.
11. install-plugins.sh STEP 5 carries two notes (frontend-design@claude-plugins-official never installed: byte-identical duplicate of the managed skills-external copy; brightdata-plugin@synced kept disabled: account-synced, keyless-useless, its bright-data-mcp skill would hijack WebFetch/WebSearch) and the summary line describes security-guidance truthfully (hooks + out-of-band LLM reviews, quota not context).
12. agents/plugin-advisor.md describes security-guidance's real mechanics (regex on Edit/Write, agentic review on commit/push, Stop review disabled by env) and drops the "Hook-only / 0 tokens" wording.
13. CHANGELOG.md `[Unreleased]` entry describing the prune (Removed / Changed / Fixed as fits Keep a Changelog).
14. The 9 removed gstack skills are removed from every profile that listed them (dev, backend, web, web-full, design, full), not only from full.
15. `full` carries every entry of every other non-max profile, minus the 9 removed names, the 21st trio and the allowlisted exceptions (each with a reason in the census test). [gated 2026-09-28 — user rule "full does what each profile does"]
CHECK: python3 .claude/tasks/contracts/2026-09-28-skill-catalog-prune-0554.oracles/c15.py
EXPECT: FULL_UNION_OK
EVIDENCE: MET exit=0 marker-found :: FULL_UNION_OK
16. Live tree after `bash lib/profile.sh set full`: none of the 9 removed nor the 5 parked names resolves under `~/.claude/skills/`, and `profile current` names full. [challenge 2026-09-28: apply is additive, set parks]
CHECK: bash lib/profile.sh set full >/dev/null 2>&1; bad=""; for n in ship land-and-deploy setup-deploy autoplan context-save learn careful guard design-shotgun make-pdf diagram 21st-ai 21st-ui-explore 21st-ui-review; do [ -e "$HOME/.claude/skills/$n" ] && bad="$bad $n"; done; [ -z "$bad" ] || { echo "live:$bad"; exit 1; }; [ -e "$HOME/.claude/skills/browse" ] || { echo browse-missing; exit 1; }; [ "$(bash lib/profile.sh current 2>/dev/null | awk '{print $1}')" = full ] && echo LIVE_CLEAN
EXPECT: LIVE_CLEAN
EVIDENCE: MET exit=0 marker-found :: LIVE_CLEAN
17. `gstack on` and `toggle-external.sh enable gstack` skip the removed names (hermetic suite, see criterion 9), and both scripts source lib/gstack-removed.sh.
CHECK: grep -q 'gstack-removed.sh' lib/profile.sh && grep -q 'gstack-removed.sh' lib/toggle-external.sh && grep -q 'gstack_is_removed' lib/profile.sh && grep -q 'gstack_is_removed' lib/toggle-external.sh && echo DENYLIST_WIRED
EXPECT: DENYLIST_WIRED
EVIDENCE: MET exit=0 marker-found :: DENYLIST_WIRED
18. Neither install-plugins.sh nor update-all.sh carries its own copy of the helper-link block; link.sh and both installers go through lib/gstack-links.sh. [confirmation pass 2026-09-28]
CHECK: grep -q 'gstack-links.sh' link.sh && grep -q 'gstack-links.sh' install-plugins.sh && grep -q 'gstack-links.sh' update-all.sh && ! grep -q 'ln -sf "$GSTACK_DIR/browse/dist"' install-plugins.sh && ! grep -q 'ln -sf "$GSTACK_SRC/browse/dist"' link.sh && ! grep -q 'ln -sf "$GSTACK_DIR/bin"' update-all.sh && echo LINKS_SHARED
EXPECT: LINKS_SHARED
EVIDENCE: MET exit=0 marker-found :: LINKS_SHARED
## FILE SCOPE
- lib/profiles/full.profile, lib/profiles/max.profile (new), lib/profiles/{dev,backend,web,web-full,design}.profile
- lib/tests/profile-census.test.sh, lib/tests/gstack-removed.test.sh, lib/tests/gstack-links.test.sh, lib/tests/doctor-skills.test.sh (new)
- lib/gstack-removed.sh (orchestrator-written), lib/gstack-links.sh, lib/doctor-skills.sh (new); lib/profile.sh, lib/toggle-external.sh (denylist)
- link.sh, doctor.sh, update-all.sh (helper-link block), install-plugins.sh (STEP 2 helper-link block, STEP 5 comments, summary lines)
- lib/tests/{profile-default,profile-set-managed,toggle-external-repo-resolution}.test.sh (fixture copy of lib/gstack-removed.sh only)
- settings.json (env block + enabledPlugins only)
- agents/plugin-advisor.md
- CLAUDE.global.md (Skill routing lines + Design work Review line only), skills/deploy/SKILL.md (routing table rows only)
- skills/profile/SKILL.md, README.md, USAGE.md, CHANGELOG.md
- Orchestrator-only: .claude/tasks/**, .claude/memory/**
@@ -0,0 +1,16 @@
import glob,os,re
def entries(p): return {l.split()[0] for l in open(p) if l.strip() and not l.lstrip().startswith('#')}
full=entries('lib/profiles/full.profile')
removed={'ship','land-and-deploy','setup-deploy','autoplan','context-save','learn','careful','guard','design-shotgun'}
trio={'21st-ai','21st-ui-explore','21st-ui-review'}
test=open('lib/tests/profile-census.test.sh').read()
m=re.search(r'^\s*FULL_EXCEPTIONS=\(([^)]*)\)',test,re.M)
allow=set(m.group(1).split()) if m else set()
gap=set()
for p in glob.glob('lib/profiles/*.profile'):
if os.path.basename(p) in ('full.profile','max.profile'): continue
gap|=entries(p)-full
gap-=removed|trio|allow
assert not gap, sorted(gap)
assert 'pr-review-toolkit' in allow, 'allowlist must name pr-review-toolkit'
print('FULL_UNION_OK')
@@ -0,0 +1,15 @@
import glob,re,sys
def entries(p): return {l.split()[0] for l in open(p) if l.strip() and not l.lstrip().startswith('#')}
sup=[p for p in glob.glob('lib/profiles/*.profile') if re.search(r'^# SUPERSET-OF: full\s*$',open(p).read(),re.M)]
assert len(sup)==1, sup
full=entries('lib/profiles/full.profile'); S=entries(sup[0])
parked={'make-pdf','diagram','21st-ai','21st-ui-explore','21st-ui-review'}
assert not (parked & full), parked & full
assert full <= S, full - S
assert parked <= S, parked - S
removed={'ship','land-and-deploy','setup-deploy','autoplan','context-save','learn','careful','guard','design-shotgun'}
U=set()
for p in glob.glob('lib/profiles/*.profile'): U|=entries(p)
assert not (removed & U), removed & U
assert (U-removed) <= S, (U-removed)-S
print('SUPERSET_OK')
@@ -0,0 +1,14 @@
import glob,os,re,subprocess
H=os.path.expanduser('~'); SRC='skills-external/gstack'; DST=H+'/.claude/skills/gstack'
txt=''.join(open(f,errors='ignore').read() for f in glob.glob(SRC+'/*/SKILL.md'))
paths=set(re.findall(r'(?:~|\$HOME)/\.claude/skills/gstack/([A-Za-z0-9_./-]+)',txt))
paths={p.rstrip('.') for p in paths}
want=[p for p in sorted(paths) if os.path.exists(os.path.join(SRC,p)) and not p.endswith('SKILL.md') and not p.startswith('.git') and not p.startswith('.feature-prompted')]
assert len(want)>=20, want
missing=[p for p in want if not os.path.exists(os.path.join(DST,p))]
assert not missing, missing
for p in ('make-pdf/dist/pdf','lib/diagram-render/dist/diagram-render.html','freeze/bin/check-freeze.sh','scripts/jargon-list.json','ETHOS.md'):
assert os.path.exists(os.path.join(DST,p)), p
out=subprocess.run(['find','-L',DST,'-name','SKILL.md'],capture_output=True,text=True).stdout.strip()
assert out=='', out
print('LINKS_OK')
@@ -0,0 +1,15 @@
import glob,re,subprocess,os
H=os.path.expanduser('~')
files=glob.glob(H+'/.claude/skills/*/SKILL.md')
def desc(p):
t=open(p,encoding='utf-8',errors='ignore').read(); m=re.match(r'^---\n(.*?)\n---',t,re.S)
if not m: return 0
d=re.search(r'^description:\s*(\|[-+]?|>[-+]?)?\s*(.*?)(?=^\S|\Z)',m.group(1),re.S|re.M)
return len(d.group(2).strip()) if d else 0
exp_chars=sum(desc(p) for p in files); exp_n=len(files)
out=subprocess.run(['bash','doctor.sh'],capture_output=True,text=True).stdout
m=re.search(r'Skill descriptions:\s+~(\d+)t\s+\((\d+) skills\)',out); assert m, 'no skill line'
tok,n=int(m.group(1)),int(m.group(2))
assert n==exp_n,(n,exp_n)
assert abs(tok*4-exp_chars)<=exp_chars*0.05,(tok*4,exp_chars)
print('DOCTOR_COUNTS')
@@ -0,0 +1,190 @@
# PLAN — 21st-signin-gate (feat, ad-hoc dispatch) — r3 (after confirmation pass)
- r3 closes the confirmation pass (robustness CONCERNS(1)): MAJOR 1 — the
unknown diagnostic format is pinned to `unknown:whoami: rc=<rc> <line>`
(rendered `21st (whoami: rc=3 …)`) and the 11 block prints a CLI-specific
remedy for 21st instead of `claude plugin list`; MINOR 2 — classify on
stdout only (`2>/dev/null`), stderr never enters the match; MINOR 3 — rc
captured through `if line="$(…)"` under pipefail, and the `fail` stub prints
the signed-out sentence AND exits 3 so the test proves rc≠0 wins; MINOR 4 —
`</dev/null` on the whoami call (the gate loop reads the profile on stdin);
MINOR 5 — hermeticity precondition = `! PATH=/usr/bin:/bin command -v 21st`
and no `/usr/local/bin/21st`; MINOR 6 — MIRROR note at both sites: the auth
state is gate-only, `profile.sh:skill_status()` has no counterpart; MINOR 7
— doc offers "or run `21st login` in any terminal on this machine, then
reply"; MINOR 8 — a 12 after an explicit opt-out in the same run is
reported once, not re-asked. Also honors `API_KEY_21ST` next to
`TWENTYFIRST_TOKEN` (the CLI's second token env, per its getToken).
- date: 2026-09-28 | contract: contracts/2026-09-28-21st-signin-gate-1215.md
- branch: feature/skill-catalog-prune (working branch → commit in place)
- executor: 1 feater (sonnet-pinned)
- r2 closes: simplicity MAJOR 1 (no shared helper — predicate inline in the
gate, toggle-external.sh and install-plugins.sh untouched, which also
voids correctness BLOCKER 1 / MAJOR 2 / MINOR 3 and robustness BLOCKER 1 /
MINOR 5-6), robustness MAJOR 2 (three-state predicate: `in` / `out` on the
exact "Not logged in" sentence / `unknown` → exit 11 with the raw
diagnostic, never the sign-in remedy), MAJOR 3 (no in-session
`export TWENTYFIRST_TOKEN` remedy; token path documented as shell profile +
restart), MAJOR 4 (explicit user opt-out "proceed without 21st", scoped to
the run, stated visibly; silent skip stays forbidden), correctness MINOR 4
(fake profile.sh executable, per-case output), MINOR 5 / robustness MINOR 7
(`also unverified` line in the 12 block), MINOR 6 (design-gate.md §4 names
the resume-after-sign-in path), robustness MINOR 8 (test asserts no
system-wide 21st first), simplicity MINOR 2 (no extra INCOMPLETE line, the
precedence test case stays), MINOR 4 (helper cases dropped), MINOR 5
(feat/bugfix bullets point at design-gate.md §3, no restated remedy).
## Ground truth (verified 2026-09-28)
- `lib/design-tool-gate.sh` (`set -euo pipefail`) checks the `21st` cli entry
with `command -v` only (`tool_active`, case `cli`). `ensure_21st_on_path`
probes `~/.local/bin`, `/usr/local/bin` and `~/.nvm/versions/node/*/bin`.
Exit codes: 0 ready · 11 ready-but-unverified · 10 incomplete · 2 error.
`REPO` is derived from the script path (no override); `PROFILE_SH` has
`DESIGN_GATE_PROFILE_SH`; `[ -x "$PROFILE_SH" ]` is required.
- `21st whoami` is a local token read, rc 0 both ways: `Logged in as <user>
(saved …).` or `Not logged in. Run \`21st login\`, or set TWENTYFIRST_TOKEN.`
The CLI is `#!/usr/bin/env node` under nvm: with a sanitized PATH it can
fail (rc≠0, "env: node: No such file") — that is NOT "signed out".
On this machine: installed, not signed in; the live gate today returns 0.
- Consumers: lib/design-gate.md §3 (verdict branches) and §4 (resume list);
skills/feat and skills/bugfix STEP 0.5 bullets; hotfix skips the gate.
- No hermetic test covers design-tool-gate.sh today. Existing suites copy
toggle-external.sh / profile.sh into fixtures — NOT touched by this plan.
## Approach
1. `lib/design-tool-gate.sh`:
- `REPO="${DESIGN_GATE_REPO_OVERRIDE:-$(cd -P … && pwd)}"` (fixture seam,
same idiom as PROFILE_REPO_OVERRIDE). Nothing new is sourced.
- New function `twentyfirst_auth_state` (≤ 25 logic lines): echoes `in`
when `${TWENTYFIRST_TOKEN:-}` or `${API_KEY_21ST:-}` is non-empty; else
`if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)";
then rc=0; else rc=$?; fi` (pipefail is set: rc is 21st's rc, 124 on
timeout; stdout only, stderr never enters the match; stdin closed so a
CLI reading stdin cannot eat the gate's profile loop). `in` when rc=0
and the line starts with `Logged in as `; `out` when rc=0 and the line
starts with `Not logged in`; otherwise exactly
`unknown:whoami: rc=<rc> <first 60 chars of line, or "no output">`.
Comment: the token envs are honored when already present (a shell-
profile export), never requested in-session.
- `tool_active` case `cli`: `command -v` fails → `inactive`; name `21st` →
`case "$(twentyfirst_auth_state)"` in `in` → `active`, `out` →
`signedout`, `unknown:*` → `unknown:<diag>`; other cli names → `active`.
- Main loop: state `signedout` → `signedout+=("$name")`; state `unknown:*`
→ `unverified_cli+=("$name (${state#unknown:})")`, rendered
`21st (whoami: rc=3 Something unexpected)`; the existing bare `unknown`
(claude unreachable) keeps filling `unverified`.
- Verdict order: blocking/manual → INCOMPLETE exit 10 (block unchanged).
Else signedout non-empty → print
`design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in`
` ask the user to run in this session: ! 21st login (browser flow, saves a local token)`
` then re-run this gate before any 21st step — never skip 21st silently`
plus the `also unverified` line(s) when either unverified array is
non-empty; exit 12.
Else unverified or unverified_cli non-empty → 11: one helper
`print_unverified` (≤ 25 logic lines) prints, for `unverified`, the
existing claude-unreachable block (`claude plugin list` remedy) and, for
`unverified_cli`, ` 21st could not answer: <diag> — a CLI runtime/PATH
problem (node under nvm?), not a sign-in problem; fix it, then re-run`.
The 10 and 12 blocks reuse the same helper for their `also unverified`
lines so no block ever says "claude CLI unreachable" about 21st. Else 0.
- Header comment: exit codes line gains `12 = sign-in required (21st)`;
the `required-manual` paragraph gets two lines on the three auth states;
the MIRROR sentence ("tool_active MIRRORS profile.sh:skill_status()")
gains "except the 21st auth state, gate-only, no skill_status
counterpart".
2. `lib/design-gate.md`:
- Exit-codes line: add `12 = sign-in required (21st installed, signed out)`.
- §3 new branch **12 / `SIGN-IN REQUIRED`** → STOP. Relay the script's
block. Ask the user to run `! 21st login` (the `!` prefix runs it in this
session, browser flow, saves a local token). END THE TURN and wait. On
the user's reply, re-run `design-tool-gate.sh` before any 21st step:
READY → continue; still 12 → ask again once, then offer the opt-out.
Explicit refusal — the user answers "proceed without 21st" (or words to
that effect) → say visibly `21st skipped for this run at your request`
and continue with the rest of the toolchain, 21st steps left out. Never
skip silently ("not logged in, so we don't use it" is the failure this
branch closes). Never run `21st login` yourself. `TWENTYFIRST_TOKEN` is
a shell-profile setting followed by a session restart, never an
in-session `export` (tool calls do not share a shell, and a secret does
not belong in the transcript).
- §3 **11** bullet: a `21st (whoami: rc=… …)` entry means the CLI could
not answer (runtime/PATH problem, node under nvm), so the remedy is the
diagnostic, not a sign-in; relay the script's own line.
- §3 **12** bullet also says: "or run `21st login` in any terminal on this
machine, then reply" (the token is a local file, any terminal works;
`! …` in-session is the convenient form, not the only one); and: after
an explicit opt-out, a later 12 in the same run is reported in one line,
never re-asked.
- §4 first paragraph: "(READY, after the user ran `/profile design`, or
after the sign-in re-run returns READY)".
- §IMPORTANT 21st bullet: add "signed out → exit 12: ask `! 21st login`,
wait, re-run; explicit opt-out only". §IMPORTANT MIRROR bullet: add
"except the 21st auth state: gate-only, no skill_status counterpart".
3. `skills/feat/SKILL.md` and `skills/bugfix/SKILL.md` STEP 0.5 bullet →
"If signals found → run `design-tool-gate.sh`; INCOMPLETE → tell the user
to run `/profile design`; SIGN-IN REQUIRED → design-gate.md §3 (ask
`! 21st login`, wait) before proceeding." No restated remedy beyond that.
4. `lib/tests/design-tool-gate.test.sh` (hermetic, `set -u`, `check` helper,
`trap 'rm -rf "$WORK"' EXIT`, style of lib/tests/skill-routing-census.test.sh):
- Precondition, loud: `! PATH=/usr/bin:/bin command -v 21st` and
`[ ! -e /usr/local/bin/21st ]` (the two places the sanitized test PATH
and `ensure_21st_on_path` could still find a real CLI) else print
`FAIL precondition: system-wide 21st present, CLI_ABSENT case not
hermetic` and count a FAIL.
- Fixture `$WORK/repo`: `lib/profiles/design.profile` holding
`# GATE-BLOCK: 21st ghost-skill` and entries `21st cli`,
`ghost-skill external`; `lib/profile.sh` = an executable stub that
`cat`s `$WORK/plain.txt` for `show design --plain` (per case the test
writes `cli\t21st` alone, or `cli\t21st` + `external\tghost-skill`);
`skills/` empty. `$WORK/bin/21st` = executable stub: `whoami` prints per
`$FAKE_21ST_MODE`: `in` → `Logged in as tester (saved locally).`,
`out` → `Not logged in. Run \`21st login\`, or set TWENTYFIRST_TOKEN.`,
`garbage` → `Something unexpected` (rc 0), `fail` → prints the exact
signed-out sentence AND exits 3 (proves rc≠0 overrides the sentence).
- Every gate run: `env -u TWENTYFIRST_TOKEN HOME=$WORK/home
PATH=$WORK/bin:/usr/bin:/bin DESIGN_GATE_REPO_OVERRIDE=$WORK/repo
DESIGN_GATE_PROFILE_SH=$WORK/repo/lib/profile.sh FAKE_21ST_MODE=<mode>
bash "$ROOT/lib/design-tool-gate.sh"` (CLAUDE_BIN irrelevant: no
plugin/mcp entry in the fixture).
- Stub positive control first: the stub prints the expected sentence for
`in` and `out` (`PASS STUB_CONTROL`).
- Cases, each `PASS <NAME>`: `SIGNED_IN_READY` (rc 0, `READY`);
`SIGNED_OUT_12` (rc 12, `SIGN-IN REQUIRED`, `21st login`, no
`INCOMPLETE`); `TOKEN_READY` (mode out + `TWENTYFIRST_TOKEN=x` → rc 0);
`CLI_ABSENT_10` (PATH without `$WORK/bin` → rc 10, `INCOMPLETE`);
`INCOMPLETE_WINS` (plain adds ghost-skill, mode out → rc 10,
`INCOMPLETE`, no `SIGN-IN REQUIRED` line); `UNKNOWN_11` (mode garbage →
rc 11, output has `whoami: rc=0` and `Something unexpected`, lacks
`21st login` and lacks `claude CLI unreachable`; mode fail → rc 11 with
`whoami: rc=3`). `TOKEN_READY` also checks `API_KEY_21ST=x` alone → rc 0.
Summary `PASS=n FAIL=m`, rc 1 on any FAIL.
5. CHANGELOG `[Unreleased]` → Added: design gate `SIGN-IN REQUIRED` (exit 12)
when the 21st CLI is installed but signed out — the agent asks for
`! 21st login` and waits, explicit opt-out only; unknown whoami answers
surface as unverified with the diagnostic.
## Edge cases
- `21st whoami` hang: `timeout 15` → rc 124 → `unknown`, exit 11 with the
diagnostic (not a sign-in loop).
- `TWENTYFIRST_TOKEN` set but invalid: the CLI decides at call time; the gate
honors the env var as `in` (documented).
- INCOMPLETE and signed out at once: 10 wins by construction (the re-run
after `/profile design` returns 12); no extra line.
- The fixture never sees the real `~/.nvm` (HOME redirected) and the test
fails loudly if a system-wide 21st exists.
- `set -euo pipefail`: the `whoami` capture must not abort the script on a
nonzero rc (run inside `if`, or `|| true`).
## Tests
- lib/tests/design-tool-gate.test.sh (new); `make test suite=` for
doctrine-citers, design-toolchain-reminder (unchanged suites, stay green).
- shellcheck lib/design-tool-gate.sh lib/tests/design-tool-gate.test.sh.
## Disposition (RELATED MEMORY)
- honors BDR-025 — GATE-BLOCK single source untouched; a state is added, not
a scope.
- honors BDR-093 — 21st auth is `21st login` / TWENTYFIRST_TOKEN, no key, no MCP.
- honors LRN-102 — the STOP asks in the turn's final text and ends the turn.
- honors "ask, don't guess" — a signed-out tool becomes a question to the
human, and an explicit refusal is an answer, never a silent skip.
- honors LRN-096 (vacuous guard class) — an unknown answer is surfaced, not
swallowed as "signed out".
@@ -0,0 +1,375 @@
# PLAN — skill-catalog-prune (feat, ad-hoc dispatch) — r4 (after confirmation pass)
- r4 closes the confirmation pass (robustness FATAL(4)): BLOCKER 1 — the
helper lib skips any non-skill dir holding a nested SKILL.md
(browser-skills/, node_modules/, openclaw/) and `.git*`/`node_modules`
by name, fixture `other/deep/SKILL.md`; MAJOR 2 — the three existing
profile/toggle suites copy lib/gstack-removed.sh into their fixture
(E1b scope); MAJOR 3 — the lib removes a stale `dst` symlink (gstack
./setup plants `~/.claude/skills/gstack -> submodule`) and refuses a dst
that resolves inside src, fixture case added; MAJOR 4 — update-all.sh's
third copy of the block goes through the lib too (criterion 18); MINOR
5-8 — real restored count after the skip, policy message instead of the
setup hint, `len(x or '')` + stderr warn on fallback, E2 owns every
install-plugins.sh edit (E3 no longer touches it).
- date: 2026-09-28 | contract: contracts/2026-09-28-skill-catalog-prune-0554.md
- branch: feature/skill-catalog-prune
- executors: 4 feater (sonnet-pinned), parallel, disjoint file sets, same tree
- r3 closes: correctness MAJOR 1-3 / MINOR 4-9, robustness MAJOR 1-4 / MINOR
5-9, simplicity MAJOR 1 / MINOR 2-5. Named changes: whole-class gstack
helper links through one shared lib (E2), `GSTACK_REMOVED` denylist that
`gstack on` / `enable gstack` honor (E1b), `max` = union of every profile
(E1a), live `set full` as an oracle (criterion 16), census test with a
passing baseline then one mutant per invariant (E1a), description parser
reused from lib/skill-routing-census.py (E2), synced info line dropped,
stale r1 wording swept.
- r2: superset = `max`; full keeps the 11 redundant gstack (user rule: full
⊇ every other profile, max = everything); parked = make-pdf, diagram,
21st trio; 21st trio out of full/web/web-full/design; freeze/bin link.
## Ground truth (verified on the live tree, 2026-09-28)
- Catalog: 150 skills, 53.5k chars of descriptions; the harness lists only
~19k chars of them with a description (least-invoked skills lose theirs).
78 skills are name-only in the current session.
- gstack skills are symlinked per profile (BDR-030), `full` is the default
(BDR-101). `profile.sh apply` is ADDITIVE (enables only); `set` and `reset`
park what the profile does not list (`disable_gstack_not_in`,
`disable_externals_not_in`, `disable_plugins_not_in`) then apply. So a
name dropped from full leaves the live tree only at the next `set full`
or `reset`; nothing runs that automatically. `gstack on`
(`enable_all_gstack`) and `lib/toggle-external.sh enable gstack` move
EVERY `skills-disabled/gstack__*` back, with no denylist.
- gstack skills hardcode `~/.claude/skills/gstack/<path>` for shared
assets, but link.sh (and a duplicate block in install-plugins.sh
STEP 2) only create `gstack/bin` and `gstack/browse/dist`. Census of the
hardcoded paths (`grep -rhoE '(~|\$HOME)/\.claude/skills/gstack/[A-Za-z0-9_./-]+'
skills-external/gstack/*/SKILL.md`): bin/* (dozens), scripts/jargon-list.json,
ETHOS.md, browse/bin/remote-slug, browse/dist/browse, design/dist/design,
extension/, lib/diagram-render/dist/diagram-render.html, make-pdf/dist/pdf,
freeze/bin/check-freeze.sh, careful/bin/check-careful.sh, */sections/*.md
(plan-*-review, cso, office-hours, design-consultation, document-release),
review/checklist.md, review/specialists/*.md, and other skills' SKILL.md
(office-hours/SKILL.md, gstack-upgrade/SKILL.md). Everything but `bin` and
`browse/dist` is unreachable today: make-pdf returns MAKE_PDF_NOT_AVAILABLE,
diagram BUNDLE_MISSING, the careful/guard/freeze hooks exit 127 and never
fire (LRN-096 class), cso and plan-*-review cannot read their sections.
- A global `skills/gstack -> skills-external/gstack` symlink is forbidden
(link.sh comment): the top-level gstack SKILL.md then lists as a duplicate
skill. Skill discovery reads `~/.claude/skills/<name>/SKILL.md`; a
`SKILL.md` must therefore never sit at `~/.claude/skills/gstack/SKILL.md`,
and no `<skill>/SKILL.md` is linked below `gstack/` either (only their
non-SKILL.md children), so the helper tree exposes no skill file.
- gstack `ship` resolves its base from `origin/HEAD` on Gitea → main; it
diffs and PRs against main, skipping develop. `land-and-deploy` runs
`gh pr merge --squash --delete-branch` then waits for the deploy.
- security-guidance 2.0.0: SessionStart venv, UserPromptSubmit baseline,
PostToolUse regex, Stop = direct POST /v1/messages (opus-4-7, thinking
10000) on every turn that changed source, commit/push = Agent SDK review
up to 18 turns. Log 2026-09-22→28: 0 findings, 1 recorded false positive
(EVAL-024). `ENABLE_STOP_REVIEW=0` is the plugin's own switch.
- settings.json has NO top-level `env` key today (create it). It is the
live `~/.claude/settings.json`: validate JSON right after the edit.
- doctor.sh: `find -maxdepth 2` without `-L` → 34 skills; `grep
'^description:' | head -1` → block scalars count 0 chars. doctor.sh runs
under `set -euo pipefail`. `lib/skill-routing-census.py` already has a
tested `extract_description()` handling `|`/`>` blocks (hyphenated file
name → load with importlib, not `import`).
- Specialized profiles carry exactly ONE name full lacks: pr-review-toolkit
(audit.profile; plugin deliberately out of full, audit 2026-07-02 #12,
~2.2k tokens when enabled; MANAGED_PLUGINS so `set` toggles it).
- Already done live (user go): brightdata disabled (`false` in
settings.json), frontend-design@claude-plugins-official uninstalled
(entry removed). `lib/gstack-removed.sh` written by the orchestrator:
`GSTACK_REMOVED=(…9…)` + `gstack_is_removed <name>`.
## Approach
### E1a — profiles + census suite + profile docs
Files: lib/profiles/{full,dev,backend,web,web-full,design}.profile,
lib/profiles/max.profile (new), lib/tests/profile-census.test.sh (new),
skills/profile/SKILL.md, README.md, USAGE.md.
1. Remove the 9 `GSTACK_REMOVED` entries (`ship land-and-deploy setup-deploy
autoplan context-save learn careful guard design-shotgun`) from EVERY
profile that lists them (dev, backend, web, web-full, design, full).
Comment lines describing only them go too. Keep `freeze` and `unfreeze`.
2. `full.profile`: additionally remove the 5 parked entries `make-pdf
diagram 21st-ai 21st-ui-explore 21st-ui-review`. The 11 redundant gstack
(plan-ceo/design/devex-review, spec, review, retro, investigate, canary,
qa, open-gstack-browser, setup-browser-cookies) STAY. Rewrite `# DESC:`:
default profile; carries everything every other profile carries (user
rule 2026-09-28, one exception: pr-review-toolkit); no broken or
doctrine-breaking gstack (see lib/gstack-removed.sh); parked tools live
in `max`. The pr-review-toolkit comment block stays.
3. `web`, `web-full`, `design`: also remove the 21st trio lines.
4. New `lib/profiles/max.profile`: header `# DESC: Everything — full + the
parked tools (make-pdf, diagram, 21st-ai/ui-explore/ui-review) +
pr-review-toolkit; switch here when one of them is wanted` and the marker
line `# SUPERSET-OF: full`. Content = new full + the 5 parked names in
their original sections + `pr-review-toolkit plugin@claude-code-plugins`
(the audit.profile line). NEVER a `GSTACK_REMOVED` name. Invariant: max ⊇
union(every profile) − GSTACK_REMOVED.
5. `lib/tests/profile-census.test.sh` (hermetic, `set -u`, `check` helper
and `trap 'rm -rf "$WORK"' EXIT` like lib/tests/skill-routing-census.test.sh):
- Top of file: `source "$ROOT/lib/gstack-removed.sh"` (REMOVED comes
from the single source), `PARKED=(make-pdf diagram 21st-ai
21st-ui-explore 21st-ui-review)`, and `FULL_EXCEPTIONS=(pr-review-toolkit)`
with the reason on comment lines ABOVE the array (names only inside it:
contract criterion 15 parses the parentheses).
- ONE assertion function `census_check <profiles_dir>` that runs an
inline python3 heredoc taking the dir and the three lists as argv.
Entry = first whitespace token of a non-blank line whose first non-blank
char is not `#` (what `read_profile` links). It prints ONE reason code
per violation on stdout — `REMOVED_LISTED:<profile>:<name>`,
`NO_SUPERSET` / `MANY_SUPERSETS`, `SUPERSET_GAP:<name>` (full or a
parked/exception name missing from the `# SUPERSET-OF: full` profile),
`MAX_GAP:<name>` (a name some profile carries that max lacks),
`FULL_GAP:<name>` (a name a non-max profile carries that full lacks,
minus REMOVED, the 21st trio and FULL_EXCEPTIONS) — and returns 0 iff
no violation.
- Live part: `census_check "$ROOT/lib/profiles"` must return 0
(`check T1-live-clean`).
- Fixture part under `mktemp -d`: `baseline/` = a minimal profiles dir
(full.profile with `alpha`, `beta`; qa.profile with `alpha`;
max.profile with the marker + `alpha`, `beta`, `parked-x`; PARKED and
FULL_EXCEPTIONS overridden for the fixture through the argv lists).
The baseline MUST return 0 → print `FIXTURE_BASELINE_OK`. Then three
mutants, each a copy of baseline with ONE change, each MUST return
non-zero AND print its own code: `ship` added to qa.profile →
`REMOVED_LISTED:qa:ship` → `FIXTURE_REMOVED_DETECTED`; `beta` deleted
from max.profile → `SUPERSET_GAP:beta` → `FIXTURE_SUPERSET_DETECTED`;
`gamma` added to qa.profile only → `FULL_GAP:gamma` →
`FIXTURE_FULLGAP_DETECTED`. A detected mutant counts as a PASS of the
test (it is the positive control); a mutant that returns 0 is a FAIL.
- Summary `PASS=n FAIL=m`, rc 1 on any FAIL. Shell helpers ≤ 25 logic
lines; the python heredoc is one function per invariant.
6. Docs: skills/profile/SKILL.md table gains the `max` row ("Everything —
full + parked tools (make-pdf, diagram, 21st generation trio) +
pr-review-toolkit") and full's row reads "Default — everything the other
profiles carry, minus broken or doctrine-breaking gstack"; README line
175 and 357-360 list `max`; README line 179 drops `context-save,
context-restore` from its example list; USAGE line 166 lists `max`.
### E1b — denylist honored by every "gstack back" path
Files: lib/profile.sh, lib/toggle-external.sh, lib/tests/gstack-removed.test.sh (new),
lib/tests/{profile-default,profile-set-managed,toggle-external-repo-resolution}.test.sh
(fixture copy line only).
1. lib/profile.sh: `source "$(dirname "${BASH_SOURCE[0]}")/gstack-removed.sh"`
next to the other top-level definitions. `enable_all_gstack`: skip a
parked entry whose name `gstack_is_removed` (leave it parked, `info
"skipped (removed by policy, lib/gstack-removed.sh): $name"`).
`enable_skill` gstack branch: refuse a removed name with `warn` and
return 0 (a profile listing one is a census failure, not a crash).
2. lib/toggle-external.sh `enable gstack` loop (the `for entry in
"$DISABLED_DIR"/gstack__*` block): same skip, same source line
(`source "$(dirname "$0")/gstack-removed.sh"`; note toggle-external
resolves REPO from `$0`, keep that idiom).
3. `enable_all_gstack` echoes the REAL restored count on stdout (skipped
names excluded); `cmd_gstack on` prints that count, not the pre-computed
`parked_gstack_count` (profile.sh ~651-655). toggle-external.sh: when
the loop skipped ≥ 1 removed name and moved 0, print "only policy-removed
skills remain parked (lib/gstack-removed.sh)" and NOT the "re-run gstack
setup" hint (setup would relink the 9 removed skills).
4. The three existing suites copy only profile.sh / toggle-external.sh into
their fixture `lib/` and would die on the new `source`: add
`cp "$ROOT/lib/gstack-removed.sh" "$FX/lib/"` (same for `$SANDBOX/repo/lib/`)
in lib/tests/profile-default.test.sh (:27), lib/tests/profile-set-managed.test.sh
(:22), lib/tests/toggle-external-repo-resolution.test.sh (:19). No other
change to those suites.
5. `lib/tests/gstack-removed.test.sh`: fixture repo under mktemp (the
`PROFILE_REPO_OVERRIDE` / `TOGGLE_EXTERNAL_REPO_OVERRIDE` harness as
lib/tests/profile-default.test.sh and toggle-external-repo-resolution.test.sh
use it — read them first): `skills-disabled/gstack__ship` and
`gstack__browse` present → `profile.sh gstack on` restores browse only,
ship stays parked, output names the skip; same for `toggle-external.sh
enable gstack`; `gstack_is_removed` positive + negative. `PASS=n FAIL=m`.
### E2 — wiring: shared gstack helper links + doctor catalog stats
Files: lib/gstack-links.sh (new), link.sh, install-plugins.sh (STEP 2 helper
block, STEP 5 comment blocks, summary lines — E2 owns EVERY edit of this
file), update-all.sh (its helper-link block), lib/doctor-skills.sh (new),
doctor.sh, lib/tests/gstack-links.test.sh (new), lib/tests/doctor-skills.test.sh (new).
1. `lib/gstack-links.sh` — `link_gstack_helpers <src> <dst>` (split into
small helpers, each ≤ 25 logic lines, no `set -e`, fallback ok/warn/info
like lib/vendor-skills.sh):
a. Guard: if `$dst` is a symlink → `rm -f "$dst"` + info (gstack ./setup
plants `~/.claude/skills/gstack -> skills-external/gstack` when the
dir is absent; link.sh's old stale-link removal moves here). Then if
`realpath -m "$dst"` is inside `realpath "$src"` → warn, return 1
(never write into the submodule). `mkdir -p "$dst"`.
b. For every top-level entry E of `$src`, skip by name `.git*`,
`node_modules`, `SKILL.md`. If E is a dir holding its own `SKILL.md`
(a gstack skill) → `mkdir -p "$dst/E"` and `ln -sfn` each child of E
except `SKILL.md`. Else if E is a dir and `find -L "$src/E" -name
SKILL.md -print -quit` is non-empty (browser-skills/, openclaw/ …) →
skip with info (would expose a nested skill). Else (file, or a clean
non-skill dir such as bin, scripts, lib, design, extension, ETHOS.md)
→ `ln -sfn "$src/E" "$dst/E"`.
c. Idempotent (`ln -sfn`), removes nothing but the stale dst symlink,
echoes the number of links created THIS run on stdout (callers add it
to CHANGED) and prints one `ok` summary on stderr. Rationale comment:
the census above + why no SKILL.md is ever exposed under `<dst>`.
2. link.sh: replace the stale-global-link removal AND the `bin` /
`browse/dist` blocks (lines ~55-91) with `source "$REPO/lib/gstack-links.sh"`
+ one call `n=$(link_gstack_helpers "$REPO/skills-external/gstack"
"$CLAUDE/skills/gstack")` guarded by `[ -d "$REPO/skills-external/gstack" ]`,
`CHANGED=$((CHANGED + n))`; keep the "submodule not found" warning; the
comment says the helper tree replaces the hand-made links and exposes no
SKILL.md.
3. install-plugins.sh STEP 2, the duplicate `GSTACK_DST` block (lines
~375-390): replace with the same source + call (after ./setup, so the
lib's guard removes the global link setup may have planted).
update-all.sh, its helper-link block (~106-116, `ln -sf` without `-n`
→ would nest `src/bin/bin` on a re-run): same source + call, right after
the submodule update. STEP 5 comment blocks + summary lines: E2 does
them (moved from E3, see E3.2 text below — same wording).
4. `lib/doctor-skills.sh` — `skill_catalog_stats <skills_dir>`: prints
`<count> <desc_chars>` on stdout, ALWAYS exits 0 (prints `0 0` when the
dir is absent or python fails). Implementation: one `python3 -` call that
loads `lib/skill-routing-census.py` via `importlib.util.spec_from_file_location`
(hyphenated name), globs `<dir>/*/SKILL.md` (glob follows symlinks),
sums `len(extract_description(path) or '')` (the function takes a PATH
and returns None when there is no description). On any python failure
print `0 0` to stdout AND one `warn` line to stderr (doctor shows the
failure instead of a healthy-looking zero). No awk parser.
5. doctor.sh token block: replace the loop + `find` with
`read -r SKILL_COUNT SKILL_DESC_CHARS < <(skill_catalog_stats "$HOME/.claude/skills")`
after `source "$REPO/lib/doctor-skills.sh"`. Constants: DELETE the
gstack (2750), context7 (200) and graphifyy (300) lines — their skills
sit in `~/.claude/skills` and are counted by the stats (one comment line
says so); superpowers 800 → 1500 (~900 t session-start injection + ~600 t
of 15 descriptions, measured 2026-09-28); ui-ux-pro-max 400 → 670 (7
descriptions, 2 669 chars, measured 2026-09-28). The "measured ~11.4k
post-audit, LRN-088" note → "(re-measure after a catalog change;
LRN-088)". NO synced-bucket line (dropped: out of the request's scope;
noted as a TODO follow-up by the orchestrator).
6. Tests. `lib/tests/gstack-links.test.sh`: fixture src under mktemp with
`bin/x`, `ETHOS.md`, `SKILL.md`, `browse/{SKILL.md,dist/browse}`,
`review/{SKILL.md,checklist.md,specialists/a.md}`, `.git/HEAD`,
`other/deep/SKILL.md`, `node_modules/pkg/SKILL.md` → after
`link_gstack_helpers`, `dst/bin`, `dst/ETHOS.md`, `dst/browse/dist`,
`dst/review/checklist.md`, `dst/review/specialists` resolve;
`find -L dst -name SKILL.md` is EMPTY (so no `dst/SKILL.md`,
`dst/browse/SKILL.md`, `dst/other`, `dst/node_modules`), `dst/.git`
absent; second run echoes 0 and changes nothing (idempotent); a dst
that is a symlink to src → the symlink is removed, dst becomes a real
dir, and `find src -type l` stays EMPTY (nothing written into src); a
dst path inside src (`src/helpers`) → warn + rc 1, nothing created. `lib/tests/doctor-skills.test.sh`:
fixture skills dir with an inline description, a `|` block scalar, a
`>-` block, a SKILL.md with no description, a symlinked skill dir →
count and chars equal the hand-computed sum; absent dir → `0 0` rc 0.
`PASS=n FAIL=m` summaries.
### E3 — config + doctrine + docs
Files: settings.json, agents/plugin-advisor.md, CLAUDE.global.md,
skills/deploy/SKILL.md, CHANGELOG.md. (install-plugins.sh is E2's: the
E3.2 wording below is what E2 writes there.)
1. settings.json: CREATE the top-level key `"env": {"ENABLE_STOP_REVIEW": "0"}`
(it does not exist), keep the two enabledPlugins states already written
live. Immediately validate: `python3 -c 'import json;json.load(open("settings.json"))'`.
2. [DONE BY E2, wording kept here] install-plugins.sh STEP 5: after the ui-ux-pro-max block, a comment block
"frontend-design@claude-plugins-official — NEVER installed: byte-identical
to the skills-external copy Step 8b syncs from the example-skills cache;
uninstalled 2026-09-28 (skill-catalog prune)" and "brightdata-plugin@synced
— account-synced from claude.ai, kept `false` in settings.json: every skill
needs a Bright Data account and its bright-data-mcp skill orders WebFetch/
WebSearch replaced 'no exceptions' (would hijack /seo /geo /harden)".
Summary line "security-guidance — PreToolUse security hook (0 tokens)" →
"security-guidance — regex hints on Edit/Write + out-of-band LLM reviews
on commit/push (Stop review off via ENABLE_STOP_REVIEW=0; quota, not
context) [claude-code-plugins]". The frontend-design summary line stays
(the managed copy stays).
3. agents/plugin-advisor.md: the `security-guidance ↔ any` row → "Hooks +
out-of-band LLM reviews (agentic review on commit/push; Stop diff review
disabled by ENABLE_STOP_REVIEW=0). No context injection unless a regex
hits."; the "> security-guidance and rtk are ALWAYS ON (0 tokens)" note →
"> rtk is always on at 0 context tokens; security-guidance is always on
and costs quota out of band (LLM reviews), not context — both omitted
from the estimates".
4. CLAUDE.global.md Skill routing: "- Ship / PR → ship (ship-feature if
gstack off); deploy → deploy (runbook, the user runs it)" → "- Ship / PR →
ship-feature (never gstack ship: it takes `origin/HEAD` = main as base
and skips develop); deploy → deploy (runbook, the user runs it)". The
gstack-OFF line lists "(investigate, qa, review, health, retro,
office-hours…)". Design work "Review / audit" line: drop "+ 21st-ui-review"
and add, at the end of the 21st sentence in that section, "21st-ai /
ui-explore / ui-review are `max`-profile only." Keep every line ≤ 80 chars.
5. skills/deploy/SKILL.md table: drop the `/land-and-deploy` and
`/setup-deploy` rows; add one row "Merge a finished branch | `gitflow
finish` on an explicit human signal (skills/gitflow)".
6. CHANGELOG `[Unreleased]`: Removed (brightdata synced plugin disabled,
frontend-design official plugin uninstalled, 9 gstack skills out of every
profile with reasons, `GSTACK_REMOVED` denylist honored by `gstack on` /
`enable gstack`), Changed (full = everything the other profiles carry,
`max` = full + parked + pr-review-toolkit, 21st trio max-only,
security-guidance Stop review off, doctor constants), Fixed (gstack helper
tree: make-pdf, diagram, sections, jargon list, ETHOS, freeze hook now
fires — `/unfreeze` clears `~/.gstack/freeze-dir.txt`; doctor.sh
undercount; "0 tokens" claims; Ship/PR routing), Known residual (kept
gstack skills still carry upstream prose routing to /ship,
/land-and-deploy, /context-save, /autoplan, /design-shotgun; 21st-ui-build
and 21st-cli-use point to the max-only trio — a Skill call on a parked
name fails and the doctrine routing applies).
## Orchestrator steps after the executors
- Criterion 16 runs `bash lib/profile.sh set full` live (parks the 14 names,
re-applies full) and checks none of them resolves under `~/.claude/skills`.
- Post-merge (user): `make link` (helper tree) and `bash lib/profile.sh set
full` on any other machine.
## Edge cases
- A profile line may carry a trailing label column (`personal`, `external`,
`# gstack`); entries match on the first token only.
- `design.profile` `# GATE-BLOCK:` lines name frontend-design, ui-ux-pro-max,
emil-design-eng, design-html, design-motion-principles, design-review,
design-consultation, the 21st CLI, 21st-ui-build: none of the removed or
parked names → no gate edit (grep before editing).
- link.sh / install-plugins.sh run on machines without the gstack
submodule: the call is guarded by `[ -d skills-external/gstack ]`.
- `skill_catalog_stats` never breaks doctor.sh's `set -euo pipefail`: it
always exits 0 and always prints two integers.
- The helper tree never contains a `SKILL.md` at any depth: skill dirs
expose only their non-SKILL.md children, non-skill dirs with a nested
SKILL.md (browser-skills/, node_modules/, openclaw/) are skipped
(asserted by the gstack-links test and criterion 3).
- `~/.claude/skills/gstack` may be a symlink to the submodule (planted by
gstack ./setup on a fresh machine): the lib removes it before linking and
never writes when dst resolves inside src.
- Kept gstack skills still name removed/parked skills in their upstream
prose: documented as a known residual (CHANGELOG), not patched (machine-
owned submodule files).
## Tests
- lib/tests/profile-census.test.sh, gstack-removed.test.sh,
gstack-links.test.sh, doctor-skills.test.sh (new, hermetic; SUITES glob
picks `lib/tests/*.test.sh` up automatically).
- make test suite=lib/tests/doctrine-citers.test.sh (routing text changed);
existing profile-default / profile-set-managed / toggle-external suites
must stay green (profile.sh and toggle-external.sh changed).
- shellcheck link.sh doctor.sh install-plugins.sh update-all.sh lib/*.sh lib/tests/*.test.sh.
- Full `make test` by the orchestrator at the end (2 pre-existing T16a
gitleaks failures are known).
## Disposition (RELATED MEMORY, read-before)
- honors BDR-030 / BDR-101 — gstack via profiles, full default: every drop
is a profile edit; the live tree follows through `set full`.
- honors BDR-025 — GATE-BLOCK single source untouched.
- honors BDR-093 — 21st pack stays installed; its generation/review trio
leaves the four design-bearing profiles and lives in max; BDR amendment
noted in registries.
- honors BDR-023 — close alias untouched.
- honors BDR-080 — investigate stays explicit-only and stays in full/dev/
backend (user rule).
- honors BDR-095 — static deny beats `ask`: careful/guard removal loses no
live protection (their hooks never fired).
- honors LRN-088 — measured before cutting: the gain is routing quality and
no broken 100 KB body invoked, not listing chars.
- honors LRN-022 / BLK-005 — profiles audited with the skill change (census).
- honors LRN-096 — vacuous guard class: helper tree makes the freeze hook
real; careful/guard are removed rather than left vacuous.
- honors BDR-070 — no rival SEO tooling: brightdata seo-audit stays off.
- does NOT touch BDR-104 (MengTo pack) nor the 2026-07-05 frontend-design +
impeccable "both" decision (the managed copy stays).
+68
View File
@@ -7,6 +7,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- **Design gate asks the user to sign in to 21st instead of skipping it**:
`lib/design-tool-gate.sh` adds a three-state 21st auth predicate
(`twentyfirst_auth_state`, honors `TWENTYFIRST_TOKEN`/`API_KEY_21ST` or a
local `21st whoami` read). Signed out now trips a new `SIGN-IN REQUIRED`
state (exit 12) instead of silently proceeding or reporting a plain
INCOMPLETE. The agent asks the user to run `! 21st login` in-session and
waits, re-running the gate on reply; an explicit "proceed without 21st"
opt-out is honored and never re-asked. A `whoami` answer that can't be
classified (unexpected line, nonzero rc, timeout) surfaces as unverified
with the raw diagnostic (`21st (whoami: rc=… …)`), never guessed as
signed-in or signed-out. `lib/design-gate.md` and the
`skills/feat`/`skills/bugfix` STEP 0.5 design-gate bullets document the
new branch. Hermetic suite `lib/tests/design-tool-gate.test.sh`, 7 named
cases.
- **`make doctor` checks the vendored externals** — new - **`make doctor` checks the vendored externals** — new
`lib/doctor-vendored.sh` (`check_vendored_skills`), wired into doctor.sh `lib/doctor-vendored.sh` (`check_vendored_skills`), wired into doctor.sh
after the gstack section: every curl-pinned entry of plugins.lock.json after the gstack section: every curl-pinned entry of plugins.lock.json
@@ -201,6 +215,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
seeded like a real tree (gstack off, nothing linked). seeded like a real tree (gstack off, nothing linked).
### Changed ### Changed
- **`full` = everything the other profiles carry** (user rule: full does
what every specialized profile does), minus the 9 removed gstack
skills, the 21st generation/review trio and one named exception
(`pr-review-toolkit`, deliberately out of full since audit 2026-07-02
#12). New `max` profile (`# SUPERSET-OF: full` marker) is `full` plus
the parked tools (`make-pdf`, `diagram`, `21st-ai`, `21st-ui-explore`,
`21st-ui-review`) plus `pr-review-toolkit` — switch here when one of
them is needed. The 21st generation/review trio leaves `full`, `web`,
`web-full` and `design`; `CLAUDE.global.md`'s Design work line drops
`21st-ui-review` and notes the trio is `max`-profile only.
`security-guidance`'s Stop-hook LLM review is off
(`ENABLE_STOP_REVIEW=0` in `settings.json`'s `env`, the plugin's own
switch); its regex layer and the commit/push agentic review stay on.
`doctor.sh`'s skill-catalog token constants are recomputed from a real
count instead of a stale estimate.
- **CLAUDE.global.md § Code style** — the ordered YAGNI decision ladder - **CLAUDE.global.md § Code style** — the ordered YAGNI decision ladder
(not needed → reuse → stdlib → platform → installed dependency → one line (not needed → reuse → stdlib → platform → installed dependency → one line
→ the minimum that works, after understanding the problem) and a → the minimum that works, after understanding the problem) and a
@@ -381,6 +410,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
traced by reading, never by running, whatever the brief says. traced by reading, never by running, whatever the brief says.
### Removed ### Removed
- **Skill-catalog prune**: `brightdata-plugin@synced` disabled
(account-synced, keyless-useless, its `bright-data-mcp` skill would
hijack WebFetch/WebSearch), `frontend-design@claude-plugins-official`
uninstalled (byte-identical duplicate of the managed `skills-external`
copy). The 9 broken or doctrine-breaking gstack skills — `ship`,
`land-and-deploy`, `setup-deploy`, `autoplan`, `context-save`, `learn`,
`careful`, `guard`, `design-shotgun` — are out of every profile that
listed them (`dev`, `backend`, `web`, `web-full`, `design`, `full`),
each with its reason in the new `lib/gstack-removed.sh` (exit-127 hooks,
an absent `OPENAI_API_KEY`, `ship`/`land-and-deploy` skipping develop,
`context-save` with no restore). The new `GSTACK_REMOVED` denylist is
honored by `profile.sh gstack on` and `toggle-external.sh enable
gstack`: both now skip a removed name instead of silently restoring it.
- `deploy` `push_deploy_tags` knob (the STATE.json commit's hook pushes the tag - `deploy` `push_deploy_tags` knob (the STATE.json commit's hook pushes the tag
with `--follow-tags`); `/onboard add gsd` and `/onboard continue` mentions with `--follow-tags`); `/onboard add gsd` and `/onboard continue` mentions
(never had a handler). (never had a handler).
@@ -393,6 +435,24 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
and the dead `MAGIC_API_KEY=abc123` gitleaks allowlist regex. and the dead `MAGIC_API_KEY=abc123` gitleaks allowlist regex.
### Fixed ### Fixed
- **gstack's shared helper tree was mostly unreachable.** gstack skills
hardcode `~/.claude/skills/gstack/<path>` for shared assets, but
`link.sh` and `install-plugins.sh` only ever linked `bin` and
`browse/dist`. A shared `lib/gstack-links.sh` (used by `link.sh`,
`install-plugins.sh` and `update-all.sh`) now links every non-skill
child of the gstack submodule, so `make-pdf`, `diagram`, the `freeze`
hook, the `*/sections/*.md` files, `scripts/jargon-list.json` and
`ETHOS.md` resolve; `/unfreeze` now actually clears
`~/.gstack/freeze-dir.txt`. `doctor.sh` counted skills with `find
-maxdepth 2` (no `-L`, missed symlinked skills) and truncated
block-scalar (`|`/`>`) descriptions to 0 chars; it now reuses
`lib/skill-routing-census.py`'s description parser through
`lib/doctor-skills.sh`. Dropped the stale "security-guidance … 0
tokens" claim from `install-plugins.sh` and `agents/plugin-advisor.md`:
the Stop review costs out-of-band quota, not context.
`CLAUDE.global.md`'s Ship/PR routing pointed at gstack's `ship`, which
bases off `origin/HEAD` (= main) and skips develop; it now routes
straight to `ship-feature`.
- **`gitflow init` on an existing repo under the machine-wide hooks** — the - **`gitflow init` on an existing repo under the machine-wide hooks** — the
socle commit (`.gitignore` + `.githooks/`) landed directly on `main` socle commit (`.gitignore` + `.githooks/`) landed directly on `main`
"while the hook is inactive"; since the global `core.hooksPath` the "while the hook is inactive"; since the global `core.hooksPath` the
@@ -438,6 +498,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
pin rerun (no false warning), parked copy plus rotted pin (fallback, then pin rerun (no false warning), parked copy plus rotted pin (fallback, then
returned to `skills-disabled/`). returned to `skills-disabled/`).
### Known residual
- The kept gstack skills still carry upstream prose routing to `/ship`,
`/land-and-deploy`, `/context-save`, `/autoplan` and `/design-shotgun`
(their own text, machine-owned submodule files, not ours to patch);
`21st-ui-build` and `21st-cli-use` still point at the now-`max`-only
21st trio. A Skill call on a parked name fails, and the doctrine
routing in `CLAUDE.global.md` applies instead.
## [1.5.0] — 2026-09-13 ## [1.5.0] — 2026-09-13
### Added ### Added
+8 -6
View File
@@ -249,8 +249,9 @@ cryptic names.
gates, registries). investigate only on explicit ask for the gstack gates, registries). investigate only on explicit ask for the gstack
ecosystem (cross-project learnings, /freeze, long open-ended investigation) ecosystem (cross-project learnings, /freeze, long open-ended investigation)
- feat / hotfix / bugfix distinguished by file count → see descriptions - feat / hotfix / bugfix distinguished by file count → see descriptions
- Ship / PR → ship (ship-feature if gstack off); deploy → deploy (runbook, - Ship / PR → ship-feature (never gstack ship: it takes `origin/HEAD` =
the user runs it) main as base and skips develop); deploy → deploy (runbook, the user
runs it)
- Docs post-ship → document-release (doc if gstack off); stale-doc audit → doc - Docs post-ship → document-release (doc if gstack off); stale-doc audit → doc
- Grouped all-axes sweep ("tir groupé", fix + loop until clean) → tour - Grouped all-axes sweep ("tir groupé", fix + loop until clean) → tour
- Open-work inventory / "queue empty?" / stale TODO vs git → reconcile - Open-work inventory / "queue empty?" / stale TODO vs git → reconcile
@@ -259,8 +260,8 @@ cryptic names.
- Before /clear or /compact → capitalize; end-of-session ritual → close - Before /clear or /compact → capitalize; end-of-session ritual → close
- SEO+GEO → seo (GEO only → geo); W3C + WCAG a11y → web-validate; - SEO+GEO → seo (GEO only → geo); W3C + WCAG a11y → web-validate;
security audit (secrets, CVE, OWASP) → cso security audit (secrets, CVE, OWASP) → cso
gstack OFF → its skills (investigate, ship, qa, review, health, retro, gstack OFF → its skills (investigate, qa, review, health, retro,
office-hours, context-save…) are gone: use the fallback above, else say so. office-hours…) are gone: use the fallback above, else say so.
## Design work — full toolchain (tiered by scope) ## Design work — full toolchain (tiered by scope)
Trigger = UI work: editing a component/style file (.tsx/.vue/.svelte/.css…) Trigger = UI work: editing a component/style file (.tsx/.vue/.svelte/.css…)
@@ -275,11 +276,12 @@ design routing; the design-toolchain hook reinforces it.
<files>` (45 deterministic anti-slop rules, exit 2 = findings). <files>` (45 deterministic anti-slop rules, exit 2 = findings).
- 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
+ 21st-ui-review + /impeccable audit|critique + `impeccable detect` floor. + /impeccable audit|critique + `impeccable detect` floor.
Scope doubt → ask or default to Build, never silently skip. Gate: light Scope doubt → ask or default to Build, never silently skip. Gate: light
skills run `~/.claude/lib/design-gate.md`, orchestrators plugin-check. 21st = skills run `~/.claude/lib/design-gate.md`, orchestrators plugin-check. 21st =
CLI (`npm i -g @21st-dev/cli`, `21st login`), no MCP, no key; search free, CLI (`npm i -g @21st-dev/cli`, `21st login`), no MCP, no key; search free,
`21st get`/`generate` metered → generation, not micro-tweaks. `21st get`/`generate` metered → generation, not micro-tweaks. 21st-ai /
ui-explore / ui-review are `max`-profile only.
## graphify ## graphify
+4 -4
View File
@@ -172,12 +172,12 @@ a different package, ships its own conflicting `graphify` bin) — see
| `/web-validate` | W3C HTML/CSS validity + WCAG 2.1 accessibility audit | | `/web-validate` | W3C HTML/CSS validity + WCAG 2.1 accessibility audit |
| `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) | | `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) |
| `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) | | `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) |
| `/profile` | Activate a skill profile (web / seo / web-full / full / backend / design / dev / qa / audit / minimal) (default: full) | | `/profile` | Activate a skill profile (web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal) (default: full) |
| `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean | | `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean |
> This table lists personal skills. Gstack skills (investigate, review, retro, > This table lists personal skills. Gstack skills (investigate, review, retro,
> office-hours, context-save, context-restore, cso…) and marketplace plugins add > office-hours, cso…) and marketplace plugins add many more — run
> many more — run `/skills-perso` to list your hand-written skills, or browse `skills/`. > `/skills-perso` to list your hand-written skills, or browse `skills/`.
--- ---
@@ -354,7 +354,7 @@ make update # update Claude Code, config, submodules, plugins, a
make test # run deterministic tests (lib/tests/*.test.sh + lib/seo-data/*.test.sh + lib/gitflow-test.sh + lib/tests/run-*.sh) make test # run deterministic tests (lib/tests/*.test.sh + lib/seo-data/*.test.sh + lib/gitflow-test.sh + lib/tests/run-*.sh)
make onboard # onboard an existing project (run from its dir) make onboard # onboard an existing project (run from its dir)
make seo-connect # connect a Google account for /seo FULL (OAuth consent) make seo-connect # connect a Google account for /seo FULL (OAuth consent)
make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/backend/design/dev/qa/audit/minimal) make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/max/backend/design/dev/qa/audit/minimal)
make profile-list # list skill profiles make profile-list # list skill profiles
make profile-current # show the active profile (full when none selected) make profile-current # show the active profile (full when none selected)
make profile-reset # go to the default profile (full) make profile-reset # go to the default profile (full)
+1 -1
View File
@@ -163,7 +163,7 @@ Tu veux...
| `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) | | `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) |
| `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) | | `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) |
| `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre | | `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre |
| `/profile` | Changer le profil de skills | web / seo / web-full / full / backend / design / dev / qa / audit / minimal | | `/profile` | Changer le profil de skills | web / seo / web-full / full / max / backend / design / dev / qa / audit / minimal |
> Cette table couvre les skills personnels principaux. Les plugins (gstack, > Cette table couvre les skills personnels principaux. Les plugins (gstack,
> pr-review-toolkit…) et marketplaces externes en ajoutent beaucoup d'autres — > pr-review-toolkit…) et marketplaces externes en ajoutent beaucoup d'autres —
+4 -2
View File
@@ -181,7 +181,7 @@ When the plugin-advisor detects a `simple` or `hotfix` signal, suggest the appro
| ui-ux-pro-max ↔ gstack | ✅ Complementary | GStack = deploy/QA layer; ui-ux-pro-max = UI quality layer. Different concerns. | | ui-ux-pro-max ↔ gstack | ✅ Complementary | GStack = deploy/QA layer; ui-ux-pro-max = UI quality layer. Different concerns. |
| pr-review-toolkit ↔ superpowers | ✅ Complementary | superpowers:requesting-code-review and /pr-review-toolkit:review-pr cover different review styles. | | pr-review-toolkit ↔ superpowers | ✅ Complementary | superpowers:requesting-code-review and /pr-review-toolkit:review-pr cover different review styles. |
| rtk ↔ any | ✅ Independent | Hook-only token compression. Zero interaction with any plugin. | | rtk ↔ any | ✅ Independent | Hook-only token compression. Zero interaction with any plugin. |
| security-guidance ↔ any | ✅ Independent | Hook-only security rules. Zero interaction. | | security-guidance ↔ any | ✅ Independent | Hooks + out-of-band LLM reviews (agentic review on commit/push; Stop diff review disabled by ENABLE_STOP_REVIEW=0). No context injection unless a regex hits. |
### Recommended sets by project type ### Recommended sets by project type
@@ -197,7 +197,9 @@ When the plugin-advisor detects a `simple` or `hotfix` signal, suggest the appro
| Fast-evolving libs (Next.js etc.) | superpowers, context7 | — | ~1000t | | Fast-evolving libs (Next.js etc.) | superpowers, context7 | — | ~1000t |
| Enterprise multi-agent orchestration | superpowers + gsd v2 (external) | plugin-dev | ~800t CC | | Enterprise multi-agent orchestration | superpowers + gsd v2 (external) | plugin-dev | ~800t CC |
> security-guidance and rtk are ALWAYS ON (0 tokens) — omitted from cost estimates for clarity. > rtk is always on at 0 context tokens; security-guidance is always on and
> costs quota out of band (LLM reviews), not context — both omitted from
> the estimates
### Conditional rules ### Conditional rules
+17 -15
View File
@@ -24,6 +24,8 @@ source "$REPO/lib/detect-plugins.sh"
source "$REPO/lib/gstack-playwright.sh" source "$REPO/lib/gstack-playwright.sh"
# shellcheck source=lib/doctor-vendored.sh disable=SC1091 # shellcheck source=lib/doctor-vendored.sh disable=SC1091
source "$REPO/lib/doctor-vendored.sh" source "$REPO/lib/doctor-vendored.sh"
# shellcheck source=lib/doctor-skills.sh disable=SC1091
source "$REPO/lib/doctor-skills.sh"
echo "" echo ""
echo "═══ claude-config doctor (v${VERSION}) ═══" echo "═══ claude-config doctor (v${VERSION}) ═══"
@@ -403,23 +405,23 @@ echo "── Token budget estimate ──"
CLAUDE_MD_CHARS=$(wc -c < "$REPO/CLAUDE.global.md" 2>/dev/null || echo 0) CLAUDE_MD_CHARS=$(wc -c < "$REPO/CLAUDE.global.md" 2>/dev/null || echo 0)
CLAUDE_MD_TOKENS=$((CLAUDE_MD_CHARS / 4)) CLAUDE_MD_TOKENS=$((CLAUDE_MD_CHARS / 4))
# Skill descriptions only (frontmatter description field — loaded passively at startup) # Skill descriptions across the whole live catalog — every SKILL.md
SKILL_DESC_CHARS=0 # reachable through ~/.claude/skills/*/SKILL.md, symlinks included
for f in "$HOME/.claude/skills/"*/SKILL.md; do # (lib/doctor-skills.sh; catches a `|`/`>` block-scalar description that
[ -f "$f" ] || continue # the old `grep '^description:' | head -1` counted as 0 chars, and a
desc=$(grep "^description:" "$f" 2>/dev/null | head -1 | sed 's/^description: *//' ) # symlinked skill dir that the old `find -maxdepth 2` without `-L` missed).
SKILL_DESC_CHARS=$((SKILL_DESC_CHARS + ${#desc})) read -r SKILL_COUNT SKILL_DESC_CHARS \
done < <(skill_catalog_stats "$HOME/.claude/skills")
SKILL_DESC_TOKENS=$((SKILL_DESC_CHARS / 4)) SKILL_DESC_TOKENS=$((SKILL_DESC_CHARS / 4))
SKILL_COUNT=$(find "$HOME/.claude/skills/" -maxdepth 2 -name "SKILL.md" 2>/dev/null | wc -l | tr -d ' ')
# Plugin passive cost estimates (tokens) # Plugin passive cost estimates (tokens) — session-start injections and
# hook prompts that never show up as a skill description above. gstack,
# context7 (find-docs) and graphifyy dropped 2026-09-28 (skill-catalog
# prune): their skills sit under ~/.claude/skills and are already counted
# by the stats above — a separate constant here double-counted them.
PLUGIN_TOKENS=0 PLUGIN_TOKENS=0
if detect_superpowers 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 800)); fi if detect_superpowers 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 1500)); fi
if detect_gstack 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 2750)); fi if detect_uiux_pro_max 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 670)); fi
if detect_uiux_pro_max 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 400)); fi
if detect_context7 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 200)); fi
if detect_graphifyy 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 300)); fi
TOTAL_TOKENS=$((CLAUDE_MD_TOKENS + SKILL_DESC_TOKENS + PLUGIN_TOKENS)) TOTAL_TOKENS=$((CLAUDE_MD_TOKENS + SKILL_DESC_TOKENS + PLUGIN_TOKENS))
CONTEXT_WINDOW=200000 # Claude Code default context window (conservative; 1M is opt-in) CONTEXT_WINDOW=200000 # Claude Code default context window (conservative; 1M is opt-in)
@@ -430,7 +432,7 @@ echo " CLAUDE.global.md: ~${CLAUDE_MD_TOKENS}t"
echo " Skill descriptions: ~${SKILL_DESC_TOKENS}t (${SKILL_COUNT} skills)" echo " Skill descriptions: ~${SKILL_DESC_TOKENS}t (${SKILL_COUNT} skills)"
echo " Plugin passive cost: ~${PLUGIN_TOKENS}t (active plugins)" echo " Plugin passive cost: ~${PLUGIN_TOKENS}t (active plugins)"
echo " ─────────────────────────────────────────" echo " ─────────────────────────────────────────"
info " Total: ~${TOTAL_TOKENS}t (measured ~11.4k post-audit, LRN-088)" info " Total: ~${TOTAL_TOKENS}t (re-measure after a catalog change; LRN-088)"
info " Context window: ${CONTEXT_WINDOW}t (default; 1M opt-in)" info " Context window: ${CONTEXT_WINDOW}t (default; 1M opt-in)"
info " Usage: ~${PCT}% of context" info " Usage: ~${PCT}% of context"
echo "" echo ""
+22 -13
View File
@@ -30,6 +30,8 @@ fi
source "$REPO/lib/detect-plugins.sh" source "$REPO/lib/detect-plugins.sh"
# shellcheck source=lib/gstack-playwright.sh disable=SC1091 # shellcheck source=lib/gstack-playwright.sh disable=SC1091
source "$REPO/lib/gstack-playwright.sh" source "$REPO/lib/gstack-playwright.sh"
# shellcheck source=lib/gstack-links.sh disable=SC1091
source "$REPO/lib/gstack-links.sh"
# ── Guard hand-curated config against installer drift ──────── # ── Guard hand-curated config against installer drift ────────
# graphify's installer (Step 7) rewrites CLAUDE.md + .claude/settings.json # graphify's installer (Step 7) rewrites CLAUDE.md + .claude/settings.json
@@ -372,19 +374,17 @@ if [ -d "$GSTACK_DIR" ]; then
warn "GStack NOT ready — ./setup did not complete (see warnings above)" warn "GStack NOT ready — ./setup did not complete (see warnings above)"
fi fi
# GStack shared infrastructure: bin/ (CLI tools) and browse/dist/ (compiled binary). # GStack shared helper tree: every asset the skills hardcode under
# Per-skill SKILL.md symlinks don't expose these, but multiple skills hardcode # ~/.claude/skills/gstack/ (bin/, browse/dist/, ETHOS.md, …) that a
# ~/.claude/skills/gstack/bin/ and gstack/browse/dist/. # per-skill SKILL.md symlink never exposes — see lib/gstack-links.sh.
# Run AFTER ./setup so the lib's own stale-symlink guard removes any
# `skills/gstack -> skills-external/gstack` link setup may have planted.
GSTACK_DST="$HOME/.claude/skills/gstack" GSTACK_DST="$HOME/.claude/skills/gstack"
if [ -d "$GSTACK_DIR/bin" ]; then _n_gstack_links=$(link_gstack_helpers "$GSTACK_DIR" "$GSTACK_DST")
mkdir -p "$GSTACK_DST" if [ "$_n_gstack_links" -gt 0 ]; then
[ -L "$GSTACK_DST/bin" ] || ln -sf "$GSTACK_DIR/bin" "$GSTACK_DST/bin" ok "gstack helper tree linked ($_n_gstack_links new)"
ok "gstack/bin/ symlink OK" else
fi ok "gstack helper tree up to date"
if [ -d "$GSTACK_DIR/browse/dist" ]; then
mkdir -p "$GSTACK_DST/browse"
[ -L "$GSTACK_DST/browse/dist" ] || ln -sf "$GSTACK_DIR/browse/dist" "$GSTACK_DST/browse/dist"
ok "gstack/browse/dist/ symlink OK"
fi fi
else else
warn "GStack submodule directory not found after init — check .gitmodules" warn "GStack submodule directory not found after init — check .gitmodules"
@@ -546,6 +546,15 @@ install_plugin "ui-ux-pro-max" "ui-ux-pro-max-skill"
echo "" echo ""
# frontend-design@claude-plugins-official — NEVER installed: byte-identical
# to the skills-external copy Step 8b syncs from the example-skills cache;
# uninstalled 2026-09-28 (skill-catalog prune).
# brightdata-plugin@synced — account-synced from claude.ai, kept `false` in
# settings.json: every skill needs a Bright Data account and its
# bright-data-mcp skill orders WebFetch/WebSearch replaced "no exceptions"
# (would hijack /seo /geo /harden).
# Caveman plugin removed (cleanup/caveman-always-on, v3.5.0): on a # Caveman plugin removed (cleanup/caveman-always-on, v3.5.0): on a
# subscription plan its ~75% output-token compression has no cost benefit, # subscription plan its ~75% output-token compression has no cost benefit,
# and the plugin's always-on SessionStart/UserPromptSubmit hooks added # and the plugin's always-on SessionStart/UserPromptSubmit hooks added
@@ -1196,7 +1205,7 @@ echo "║ Install Summary ║"
echo "╚══════════════════════════════════════════════════════════╝" echo "╚══════════════════════════════════════════════════════════╝"
echo "" echo ""
echo " ALWAYS ON (installed at user scope):" echo " ALWAYS ON (installed at user scope):"
echo " ✅ security-guidance — PreToolUse security hook (0 tokens) [claude-code-plugins]" echo " ✅ security-guidance — regex hints on Edit/Write + out-of-band LLM reviews on commit/push (Stop review off via ENABLE_STOP_REVIEW=0; quota, not context) [claude-code-plugins]"
echo " ✅ rtk — token compression hook (0 tokens)" echo " ✅ rtk — token compression hook (0 tokens)"
echo " ✅ superpowers — brainstorm/plan/implement/debug workflow" echo " ✅ superpowers — brainstorm/plan/implement/debug workflow"
echo "" echo ""
+30 -6
View File
@@ -61,7 +61,7 @@ skill symlink, `claude plugin list`, `claude mcp list`, `command -v`. It never
reads `disabledMcpServers` (unreliable for bi-modal servers like context7). 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) · `12` = sign-in required (21st installed, signed out) · `2` = error.
### 3. Branch on the result ### 3. Branch on the result
@@ -82,18 +82,39 @@ Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it)
but the CLI they shell out to is a global npm install: tell the user to run 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). `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.
- **12 / `SIGN-IN REQUIRED`** → STOP. The 21st CLI is installed but `21st
whoami` reports signed out. Relay the script's block, then ask the user to
run `! 21st login` (the `!` prefix runs it in this session, browser flow,
saves a local token) — or run `21st login` in any terminal on this
machine, then reply (the token is a local file; any terminal works, `!`
in-session is just the convenient form). END THE TURN and wait. On the
user's reply, re-run `design-tool-gate.sh` before any 21st step: `READY` →
continue; still `12` → ask again once, then offer the opt-out. Explicit
refusal — the user answers "proceed without 21st" (or words to that
effect) → say visibly `21st skipped for this run at your request` and
continue with the rest of the toolchain, 21st steps left out; after that,
a later `12` in the same run is reported in one line, never re-asked.
Never skip silently ("not logged in, so we don't use it" is the failure
this branch closes). Never run `21st login` yourself — it opens a browser
and needs the human. `TWENTYFIRST_TOKEN` is a shell-profile setting
followed by a session restart, never an in-session `export` (tool calls
don't share a shell, and a secret doesn't belong in the transcript).
- **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design - **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design
plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready": plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready":
proceed only after telling the user that N tool(s) went unverified and having proceed only after telling the user that N tool(s) went unverified and having
them confirm with `claude plugin list`. Fail-visible, not fail-silent. them confirm with `claude plugin list`. Fail-visible, not fail-silent. A
`21st (whoami: rc=… …)` entry in this block means the CLI itself could not
answer (a runtime/PATH problem, e.g. node under nvm) — the remedy is the
diagnostic the script prints, never a sign-in prompt; relay its own line.
### 4. Animation library — suggest-only (fires only on a real motion signal) ### 4. Animation library — suggest-only (fires only on a real motion signal)
Orthogonal to the toolchain check above: §2-3 are about Claude's design TOOLS; Orthogonal to the toolchain check above: §2-3 are about Claude's design TOOLS;
this is about the PROJECT's runtime dep. Evaluate it only once the toolchain is this is about the PROJECT's runtime dep. Evaluate it only once the toolchain is
resolved and you're actually proceeding with the build (READY, or after the user resolved and you're actually proceeding with the build (READY, after the user
ran `/profile design`). Never on the INCOMPLETE stop path — that path has one ran `/profile design`, or after the sign-in re-run returns READY). Never on
action only (`/profile design`); don't stack an optional note on it. the INCOMPLETE stop path — that path has one action only (`/profile
design`); don't stack an optional note on it.
**Fires only when ALL THREE hold** — drop any one → no suggestion, stay silent: **Fires only when ALL THREE hold** — drop any one → no suggestion, stay silent:
@@ -182,11 +203,14 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
the profile system is the single source of truth for what's active. the profile system is the single source of truth for what's active.
- the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot - the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot
install it — the gate names the two commands; surface them to the user. install it — the gate names the two commands; surface them to the user.
Signed out → exit 12: ask `! 21st login`, wait, re-run; explicit opt-out
only, never a silent skip.
- The design-core set (what trips the gate) is declared in `design.profile` on - The 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.
- The state check shells out to `claude` (plugin/mcp list): a few seconds. - The state check shells out to `claude` (plugin/mcp list): a few seconds.
Trivial / non-design tasks skip it entirely (no signal, or trivial tier). Trivial / non-design tasks skip it entirely (no signal, or trivial tier).
- `design-tool-gate.sh`'s per-type state checks MIRROR - `design-tool-gate.sh`'s per-type state checks MIRROR
`profile.sh:skill_status()` — change one, sync the other. `profile.sh:skill_status()` — change one, sync the other, except the 21st
auth state: gate-only, no skill_status counterpart.
- Do NOT run this gate on pure backend/API/CLI tasks (no signals = no gate). - Do NOT run this gate on pure backend/API/CLI tasks (no signals = no gate).
+96 -17
View File
@@ -17,7 +17,8 @@
# -> fall back to every skill/plugin/mcp entry (coarse). # -> fall back to every skill/plugin/mcp entry (coarse).
# #
# State (active or not) is checked per channel, by type. These per-type # State (active or not) is checked per channel, by type. These per-type
# checks MIRROR profile.sh:skill_status() — change one, sync the other. # checks MIRROR profile.sh:skill_status() — change one, sync the other,
# except the 21st auth state: gate-only, no skill_status counterpart.
# #
# type channel class # type channel class
# gstack|external|personal skill symlink in skills/ blocking # gstack|external|personal skill symlink in skills/ blocking
@@ -31,18 +32,21 @@
# it, and the remedy is `/profile design` + a manual step. # it, and the remedy is `/profile design` + a manual step.
# This is where the `21st` CLI lands: required, never # This is where the `21st` CLI lands: required, never
# silent (npm i -g @21st-dev/cli, then 21st login). # silent (npm i -g @21st-dev/cli, then 21st login).
# 21st's sign-in state is three-valued: in (active),
# out (exit 12, ask to sign in), unknown (exit 11).
# Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are # 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
# (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) · 12 = sign-in required (21st) · 2 = error.
# Usage: design-tool-gate.sh [profile] (default profile: design) # Usage: design-tool-gate.sh [profile] (default profile: design)
# ============================================================ # ============================================================
set -euo pipefail set -euo pipefail
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO="${DESIGN_GATE_REPO_OVERRIDE:-$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
PROFILE_SH="${DESIGN_GATE_PROFILE_SH:-$REPO/lib/profile.sh}" PROFILE_SH="${DESIGN_GATE_PROFILE_SH:-$REPO/lib/profile.sh}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}" CLAUDE_BIN="${CLAUDE_BIN:-claude}"
PROFILES_DIR="$REPO/lib/profiles" PROFILES_DIR="$REPO/lib/profiles"
@@ -104,6 +108,35 @@ ensure_21st_on_path() {
} }
ensure_21st_on_path ensure_21st_on_path
# 21st's sign-in state, three-valued. `whoami` is a local token read (no
# network), rc 0 either way — so rc alone can't tell signed-in from signed-
# out; the FIRST LINE of stdout does. A token env, when already exported by
# the user's shell profile, wins without a CLI call (never requested here:
# tool calls don't share a shell, and a secret doesn't belong in a comment
# or the transcript). `timeout 15` bounds a hung CLI; stdin is closed so a
# CLI that reads stdin can't eat the gate's own `read` loop; stderr never
# enters the match (stdout only). Echoes: in | out | unknown:<diagnostic>.
twentyfirst_auth_state() {
if [ -n "${TWENTYFIRST_TOKEN:-}" ] || [ -n "${API_KEY_21ST:-}" ]; then
echo in
return
fi
local line rc
if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)"; then
rc=0
else
rc=$?
fi
if [ "$rc" -eq 0 ]; then
case "$line" in
"Logged in as "*) echo in; return ;;
"Not logged in"*) echo out; return ;;
esac
fi
[ -n "$line" ] || line="no output"
echo "unknown:whoami: rc=$rc ${line:0:60}"
}
# Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated). # 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 \
@@ -115,8 +148,10 @@ in_scope() {
case " $core_set " in *" $1 "*) return 0 ;; *) return 1 ;; esac case " $core_set " in *" $1 "*) return 0 ;; *) return 1 ;; esac
} }
# State of one tool, by type. Mirrors profile.sh:skill_status() — keep in sync. # State of one tool, by type. Mirrors profile.sh:skill_status() — keep in
# Echoes: active | inactive | unknown (unknown = can't verify, claude absent) # sync, except the 21st auth state (gate-only, no skill_status counterpart).
# Echoes: active | inactive | unknown (can't verify, claude absent) |
# signedout | unknown:<diagnostic> (last two: 21st CLI only)
tool_active() { tool_active() {
local name="$1" type="$2" local name="$1" type="$2"
case "$type" in case "$type" in
@@ -135,7 +170,15 @@ tool_active() {
if "$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${name}"; then echo active; else echo inactive; fi if "$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${name}"; then echo active; else echo inactive; fi
;; ;;
cli) cli)
if command -v "$name" >/dev/null 2>&1; then echo active; else echo inactive; fi command -v "$name" >/dev/null 2>&1 || { echo inactive; return; }
[ "$name" = "21st" ] || { echo active; return; }
local auth
auth="$(twentyfirst_auth_state)"
case "$auth" in
in) echo active ;;
out) echo signedout ;;
unknown:*) echo "$auth" ;;
esac
;; ;;
*) echo inactive ;; *) echo inactive ;;
esac esac
@@ -150,12 +193,17 @@ plain="$("$PROFILE_SH" show "$PROFILE" --plain 2>/dev/null)" \
blocking=() # inactive, /profile design activates it (skill/plugin) blocking=() # inactive, /profile design activates it (skill/plugin)
manual=() # inactive, required but needs a manual step (mcp key / cli install) manual=() # inactive, required but needs a manual step (mcp key / cli install)
unverified=() # can't check (claude CLI absent) unverified=() # can't check (claude CLI absent)
signedout=() # 21st CLI installed, not signed in
unverified_cli=() # 21st CLI: whoami answered something unexpected
while IFS=$'\t' read -r type name; do while IFS=$'\t' read -r type name; do
[ -n "$type" ] || continue [ -n "$type" ] || continue
in_scope "$name" || continue # ignore non-core tooling (browser, plan-*, graphify) in_scope "$name" || continue # ignore non-core tooling (browser, plan-*, graphify)
case "$(tool_active "$name" "$type")" in state="$(tool_active "$name" "$type")"
case "$state" in
active) ;; active) ;;
signedout) signedout+=("$name") ;;
unknown) unverified+=("$name") ;; unknown) unverified+=("$name") ;;
unknown:*) unverified_cli+=("$name (${state#unknown:})") ;;
*) *)
case "$type" in case "$type" in
gstack|external|personal|plugin) blocking+=("$name") ;; gstack|external|personal|plugin) blocking+=("$name") ;;
@@ -165,11 +213,31 @@ while IFS=$'\t' read -r type name; do
esac esac
done <<< "$plain" done <<< "$plain"
# Verdict — three outcomes: # print_unverified — the "also unverified" lines shared by the 10, 11 and 12
# blocks: a claude-unreachable tool keeps its existing remedy; a 21st CLI
# that answered whoami with something unexpected gets its own — the two are
# never merged, so no block blames 21st for a claude problem or vice versa.
print_unverified() {
if [ "${#unverified[@]}" -gt 0 ]; then
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
fi
local entry name diag
for entry in "${unverified_cli[@]}"; do
name="${entry%% (*}"
diag="${entry#*\(}"; diag="${diag%\)}"
echo " $name could not answer: $diag —" \
"a CLI runtime/PATH problem (node under nvm?)," \
"not a sign-in problem; fix it, then re-run"
done
}
# Verdict — four outcomes, checked in order:
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips. # blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE. # else signedout non-empty -> SIGN-IN REQUIRED (exit 12): ask the user to
# claude was unreachable, so the plugin channel (ui-ux-pro-max) could not # run `21st login`, end the turn, wait, re-run — never a silent skip.
# be checked. Never pass this as a silent READY — proceed, but say so. # else unverified/unverified_cli non-empty -> READY BUT UNVERIFIED (exit
# 11): fail-VISIBLE. claude unreachable and/or 21st couldn't answer
# whoami — never pass either as a silent READY.
# nothing pending -> READY (exit 0). # 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"
@@ -182,19 +250,30 @@ if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
*" 21st "*) echo " 21st needs the CLI: npm i -g @21st-dev/cli then 21st login" ;; *" 21st "*) echo " 21st needs the CLI: npm i -g @21st-dev/cli then 21st login" ;;
esac esac
fi fi
if [ "${#unverified[@]}" -gt 0 ]; then print_unverified
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
fi
echo " → run: /profile $PROFILE" echo " → run: /profile $PROFILE"
exit 10 exit 10
fi fi
if [ "${#unverified[@]}" -gt 0 ]; then if [ "${#signedout[@]}" -gt 0 ]; then
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked" echo "design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in"
echo " unverified (claude CLI unreachable): ${unverified[*]}" echo " ask the user to run in this session:" \
" ! 21st login" \
" (browser flow, saves a local token)"
echo " then re-run this gate before any 21st step — never skip 21st silently"
print_unverified
exit 12
fi
if [ "${#unverified[@]}" -gt 0 ] || [ "${#unverified_cli[@]}" -gt 0 ]; then
echo "design toolchain: READY BUT UNVERIFIED —" \
"$(( ${#unverified[@]} + ${#unverified_cli[@]} )) tool(s) not checked"
print_unverified
if [ "${#unverified[@]}" -gt 0 ]; then
echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is" echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
echo " active. Proceed only after checking manually:" echo " active. Proceed only after checking manually:"
echo " claude plugin list" echo " claude plugin list"
fi
exit 11 exit 11
fi fi
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# ============================================================
# lib/doctor-skills.sh — doctor.sh's skill-catalog stats
#
# `skill_catalog_stats <skills_dir>` counts every skill reachable
# through <skills_dir>/*/SKILL.md (symlinks included — Python's
# glob.glob follows them, verified: a symlinked skill dir matches the
# pattern the same as a real one) and sums their description length,
# reusing lib/skill-routing-census.py's extract_description() (handles
# a plain scalar AND a `|`/`>` YAML block scalar) instead of doctor.sh's
# old `grep '^description:' | head -1` (0 chars on every block-scalar
# description) and its `find -maxdepth 2` skill count (missed
# symlinked skill dirs without `-L`).
#
# Prints "<count> <desc_chars>" on stdout and ALWAYS exits 0 — an
# absent <skills_dir> naturally globs to nothing (0 0, no error); a
# python failure also prints "0 0" so doctor.sh (which runs under
# `set -euo pipefail`) never aborts on this check, PLUS one warn line on
# stderr so a real failure still shows instead of reading as a healthy
# empty catalog. The warn goes to stderr explicitly (not just via the
# caller's own warn() convention) because the caller reads this
# function's stdout with `read -r … < <(skill_catalog_stats …)` — any
# extra stdout line would corrupt that capture.
#
# No `set -euo pipefail` here (mirrors lib/vendor-skills.sh): a sourced
# lib must not change the caller's shell options.
# ============================================================
DOCTOR_SKILLS_LIB_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! declare -F warn >/dev/null 2>&1; then
YELLOW='\033[1;33m'; NC='\033[0m'
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
fi
# skill_catalog_stats <skills_dir> — see file header.
skill_catalog_stats() {
local dir="$1" census="$DOCTOR_SKILLS_LIB_DIR/skill-routing-census.py"
local out rc
out=$(python3 - "$dir" "$census" <<'PY'
import glob, importlib.util, sys
skills_dir, census_path = sys.argv[1], sys.argv[2]
spec = importlib.util.spec_from_file_location(
"skill_routing_census", census_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
paths = glob.glob(skills_dir + "/*/SKILL.md")
chars = sum(len(module.extract_description(p) or "") for p in paths)
print(len(paths), chars)
PY
)
rc=$?
if [ "$rc" -eq 0 ]; then
echo "$out"
return 0
fi
warn "skill_catalog_stats: python failed (rc=$rc) — showing 0 0" >&2
echo "0 0"
return 0
}
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# ============================================================
# lib/gstack-links.sh — shared gstack helper-tree linker
#
# gstack skills hardcode `~/.claude/skills/gstack/<path>` for shared
# assets (bin/, browse/dist/, design/dist/, lib/diagram-render/dist/,
# ETHOS.md, scripts/jargon-list.json, freeze/bin/, */sections/*.md,
# review/checklist.md + specialists/, …) that per-skill SKILL.md
# symlinks never expose (BDR-030 links gstack skills individually).
# `link_gstack_helpers()` walks the submodule ONCE and mirrors every one
# of those assets under <dst>, so make-pdf, diagram, the freeze hook,
# cso/plan-*-review sections etc. actually resolve — before this, only
# bin/ and browse/dist/ were hand-linked and everything else returned
# *_NOT_AVAILABLE or exited 127 (LRN-096 class).
#
# A skill dir (one holding its own SKILL.md, e.g. review/, careful/) is
# mirrored child-by-child with SKILL.md excluded — <dst> must never
# expose a SKILL.md at ANY depth, or skill discovery lists gstack/<name>
# as a duplicate entry alongside the individually-linked skill. A
# non-skill dir that HOLDS a nested SKILL.md somewhere below it
# (browser-skills/, openclaw/ — vendored/generated content, not a gstack
# asset) is skipped whole: mirroring it would expose that nested
# SKILL.md through <dst> too. `.git*` and `node_modules` are skipped by
# name (vcs metadata / vendored deps, never worth walking).
#
# Sourced by link.sh, install-plugins.sh (Step 2) and update-all.sh — the
# same block used to be hand-duplicated in all three (three divergent
# copies, one of them `ln -sf` without `-n` — nests src/bin/bin on a
# re-run — criterion 18 forbids the duplication now).
#
# No `set -e` (mirrors lib/vendor-skills.sh): a sourced lib must not
# change the caller's shell options.
# ============================================================
# Fallback color helpers when sourced standalone (hermetic test suite) —
# every diagnostic call below is explicitly redirected to stderr (>&2) so
# `n=$(link_gstack_helpers …)` captures ONLY the final link count,
# whichever ok/warn/info implementation (caller's or this fallback) runs.
if ! declare -F ok >/dev/null 2>&1; then
GREEN='\033[0;32m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $1"; }
fi
if ! declare -F warn >/dev/null 2>&1; then
YELLOW='\033[1;33m'; NC='\033[0m'
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
fi
if ! declare -F info >/dev/null 2>&1; then
BLUE='\033[0;34m'; NC='\033[0m'
info() { echo -e "${BLUE}→${NC} $1"; }
fi
# _gstack_links_guard_dst <src> <dst> — removes a stale <dst> symlink
# (gstack ./setup plants `skills/gstack -> skills-external/gstack` when
# the dir is absent), refuses ever writing INTO <src> (dst resolving
# inside src), then ensures <dst> exists as a real dir. rc 1 on the
# write-into-src guard; nothing is created in that case.
_gstack_links_guard_dst() {
local src="$1" dst="$2" real_src real_dst
if [ -L "$dst" ]; then
info "removing stale gstack symlink: $dst" >&2
rm -f "$dst"
fi
real_src="$(realpath "$src")"
real_dst="$(realpath -m "$dst")"
case "$real_dst" in
"$real_src"/*|"$real_src")
warn "refusing to write into the gstack submodule: $dst" >&2
return 1
;;
esac
mkdir -p "$dst"
}
# _gstack_links_skip_entry <name> — true iff a top-level entry is never
# mirrored by name alone (vcs metadata, vendored deps, the top-level
# SKILL.md itself).
_gstack_links_skip_entry() {
case "$1" in
.git*|node_modules|SKILL.md) return 0 ;;
*) return 1 ;;
esac
}
# _gstack_links_skill_dir <src_entry> <dst_dir> — mirrors a gstack skill
# dir (one that holds its own SKILL.md) child-by-child, SKILL.md
# excluded. Echoes the count of links freshly created (idempotent on a
# re-run: an already-correct symlink is not recounted).
_gstack_links_skill_dir() {
local entry="$1" dst_dir="$2" child base n=0
mkdir -p "$dst_dir"
for child in "$entry"/*; do
[ -e "$child" ] || continue
base="$(basename "$child")"
[ "$base" = "SKILL.md" ] && continue
if [ ! -L "$dst_dir/$base" ] \
|| [ "$(readlink "$dst_dir/$base")" != "$child" ]; then
n=$((n + 1))
fi
ln -sfn "$child" "$dst_dir/$base"
done
echo "$n"
}
# _gstack_links_top_entry <src_entry> <dst_entry> — links ONE top-level
# src entry into dst: mirror child-by-child if it is a skill dir, skip
# whole if it is a non-skill dir hiding a nested SKILL.md, else a single
# whole-entry symlink (file or clean non-skill dir). Echoes the count of
# links freshly created.
_gstack_links_top_entry() {
local src_entry="$1" dst_entry="$2" n=0
if [ -d "$src_entry" ] && [ -f "$src_entry/SKILL.md" ]; then
n=$(_gstack_links_skill_dir "$src_entry" "$dst_entry")
elif [ -d "$src_entry" ] \
&& [ -n "$(find -L "$src_entry" -name SKILL.md -print -quit)" ]; then
info "skipped $(basename "$src_entry") (nested SKILL.md, not a \
gstack asset)" >&2
else
if [ ! -L "$dst_entry" ] \
|| [ "$(readlink "$dst_entry")" != "$src_entry" ]; then
n=1
fi
ln -sfn "$src_entry" "$dst_entry"
fi
echo "$n"
}
# link_gstack_helpers <src> <dst> — mirrors every gstack shared asset
# under <src> (the skills-external/gstack submodule) into <dst>
# (normally ~/.claude/skills/gstack), idempotent (ln -sfn), never
# exposing a SKILL.md at any depth under <dst>. Echoes the total link
# count freshly created THIS run on stdout (add it to the caller's
# CHANGED counter); prints one ok/warn summary on stderr. rc 1 (echoing
# 0) iff <dst> resolves inside <src> — nothing is created in that case.
link_gstack_helpers() {
local src="$1" dst="$2" entry base total=0 n
_gstack_links_guard_dst "$src" "$dst" || { echo 0; return 1; }
for entry in "$src"/*; do
[ -e "$entry" ] || continue
base="$(basename "$entry")"
_gstack_links_skip_entry "$base" && continue
n=$(_gstack_links_top_entry "$entry" "$dst/$base")
total=$((total + n))
done
if [ "$total" -gt 0 ]; then
ok "gstack helper tree: $total link(s) created under $dst" >&2
else
ok "gstack helper tree up to date ($dst)" >&2
fi
echo "$total"
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# ============================================================
# lib/gstack-removed.sh — the gstack skills this config never exposes
#
# Single source for the denylist. Sourced by lib/profile.sh
# (enable_all_gstack, enable_skill) and lib/toggle-external.sh
# (`enable gstack`), read by lib/tests/profile-census.test.sh. A name
# listed here is in no profile, `max` included, and every "bring gstack
# back" path skips it. Decided 2026-09-28 (skill-catalog prune, user go):
#
# ship base = origin/HEAD = main on Gitea, skips develop
# land-and-deploy `gh pr merge --squash --delete-branch` then deploys
# setup-deploy companion of land-and-deploy (Claude never deploys)
# autoplan reads ~/.claude/skills/gstack/plan-*/ paths that do
# not exist in this install
# context-save its pair context-restore is not linked; never used
# learn parallel JSONL store outside .claude/memory, unused
# careful, guard hooks exit 127 (missing bin path) — vacuous; the
# house permissions.deny is stricter (BDR-095)
# design-shotgun mockups need OPENAI_API_KEY, absent
#
# No `set -euo pipefail` here (sourced lib, mirrors lib/detect-plugins.sh).
# ============================================================
GSTACK_REMOVED=(ship land-and-deploy setup-deploy autoplan context-save
learn careful guard design-shotgun)
# gstack_is_removed <name> — exit 0 when <name> is on the denylist.
gstack_is_removed() {
local name="$1" entry
for entry in "${GSTACK_REMOVED[@]}"; do
[ "$entry" = "$name" ] && return 0
done
return 1
}
+27 -9
View File
@@ -55,6 +55,11 @@ PROFILES_DIR="$REPO/lib/profiles"
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)
DEFAULT_PROFILE="full" # profile in force when none is selected (cache absent, empty, or legacy "none") DEFAULT_PROFILE="full" # profile in force when none is selected (cache absent, empty, or legacy "none")
# GSTACK_REMOVED + gstack_is_removed() — single source, honored by every
# "bring gstack back" path below (enable_all_gstack, enable_skill).
# shellcheck source=lib/gstack-removed.sh disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/gstack-removed.sh"
# 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
# never auto-disabled — protects always-on plugins (security-guidance, # never auto-disabled — protects always-on plugins (security-guidance,
# superpowers) and unrelated user plugins. Add a plugin here only when its # superpowers) and unrelated user plugins. Add a plugin here only when its
@@ -313,7 +318,11 @@ enable_skill() {
local skill="$1" type="$2" local skill="$1" type="$2"
case "$type" in case "$type" in
gstack) gstack)
if [ -e "$DISABLED_DIR/gstack__$skill" ]; then if gstack_is_removed "$skill"; then
warn "refusing to enable removed skill: $skill (lib/gstack-removed.sh \
— a profile census failure, not a crash)"
return 0
elif [ -e "$DISABLED_DIR/gstack__$skill" ]; then
rm -rf "${SKILLS_DIR:?}/${skill:?}" rm -rf "${SKILLS_DIR:?}/${skill:?}"
mv "$DISABLED_DIR/gstack__$skill" "$SKILLS_DIR/$skill" mv "$DISABLED_DIR/gstack__$skill" "$SKILLS_DIR/$skill"
ok "enabled: $skill" ok "enabled: $skill"
@@ -454,18 +463,27 @@ disable_skill() {
# ── Shared gstack operations ────────────────────────────── # ── Shared gstack operations ──────────────────────────────
# Re-enable every gstack skill parked in skills-disabled/ (move gstack__* # Re-enable every gstack skill parked in skills-disabled/ (move gstack__*
# back into skills/). Shared by cmd_reset and `gstack on`. Side effects # back into skills/), skipping a name lib/gstack-removed.sh denies (left
# only; prints one confirmation per restored skill. # parked — the policy line goes to stderr). Shared by cmd_reset and
# `gstack on`. Echoes the REAL restored count (skipped names excluded) on
# stdout so the caller can report it accurately; per-skill confirmations
# go to stderr so that count is the only thing captured with `$(...)`.
enable_all_gstack() { enable_all_gstack() {
local entry name local entry name restored=0
[ -d "$DISABLED_DIR" ] || return 0 [ -d "$DISABLED_DIR" ] || { echo 0; return 0; }
for entry in "$DISABLED_DIR"/gstack__*; do for entry in "$DISABLED_DIR"/gstack__*; do
[ -e "$entry" ] || continue [ -e "$entry" ] || continue
name="$(basename "$entry" | sed 's/^gstack__//')" name="$(basename "$entry" | sed 's/^gstack__//')"
if gstack_is_removed "$name"; then
info "skipped (removed by policy, lib/gstack-removed.sh): $name" >&2
continue
fi
rm -rf "${SKILLS_DIR:?}/${name:?}" rm -rf "${SKILLS_DIR:?}/${name:?}"
mv "$entry" "$SKILLS_DIR/$name" mv "$entry" "$SKILLS_DIR/$name"
ok "re-enabled: $name" ok "re-enabled: $name" >&2
restored=$((restored + 1))
done done
echo "$restored"
} }
# Disable gstack-origin skills not listed in the given profile. Shared by # Disable gstack-origin skills not listed in the given profile. Shared by
@@ -648,13 +666,13 @@ cmd_gstack() {
on) on)
# Restore whatever is parked, but DON'T touch active-profile — the # Restore whatever is parked, but DON'T touch active-profile — the
# user is adding gstack on top of their current profile, not clearing it. # user is adding gstack on top of their current profile, not clearing it.
local parked local parked restored
parked="$(parked_gstack_count)" parked="$(parked_gstack_count)"
if [ "$parked" -eq 0 ]; then if [ "$parked" -eq 0 ]; then
info "nothing parked — gstack skills are linked per profile (set/apply/reset)" info "nothing parked — gstack skills are linked per profile (set/apply/reset)"
else else
enable_all_gstack restored="$(enable_all_gstack)"
ok "$parked parked gstack skills restored" ok "$restored parked gstack skills restored"
fi fi
;; ;;
off) off)
+1 -7
View File
@@ -18,11 +18,8 @@ observability-and-instrumentation external
deprecation-and-migration external deprecation-and-migration external
ci-cd-and-automation external ci-cd-and-automation external
# Ship + review + land # Review
ship
review review
context-save
land-and-deploy
# Second opinion for hard problems # Second opinion for hard problems
codex codex
@@ -32,11 +29,8 @@ cso
health health
# Session hygiene # Session hygiene
careful
freeze freeze
unfreeze unfreeze
guard
learn
retro retro
# pr-review-toolkit removed (audit 2026-07-02 #12 — ~2.2k tokens, PR-only): # pr-review-toolkit removed (audit 2026-07-02 #12 — ~2.2k tokens, PR-only):
+1 -5
View File
@@ -11,13 +11,12 @@
# GATE-BLOCK: 21st 21st-ui-build # GATE-BLOCK: 21st 21st-ui-build
# Core design skills (gstack) # Core design skills (gstack)
design-shotgun
design-review design-review
design-consultation design-consultation
design-html design-html
plan-design-review plan-design-review
# Browser tooling — design-review and design-shotgun rely on it # Browser tooling — design-review relies on it
browse browse
open-gstack-browser open-gstack-browser
setup-browser-cookies setup-browser-cookies
@@ -44,10 +43,7 @@ site-motion personal
# External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry # External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry
# and 21st-design-sync are publishing flows; installed but left parked. # and 21st-design-sync are publishing flows; installed but left parked.
21st-ui-build external 21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use 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
-5
View File
@@ -6,7 +6,6 @@
# Implementation # Implementation
feat personal feat personal
ship-feature personal ship-feature personal
ship
# Bug fixing # Bug fixing
hotfix personal hotfix personal
@@ -23,7 +22,3 @@ commit-change personal
observability-and-instrumentation external observability-and-instrumentation external
deprecation-and-migration external deprecation-and-migration external
ci-cd-and-automation external ci-cd-and-automation external
# Session hygiene
context-save
land-and-deploy
+6 -19
View File
@@ -1,7 +1,8 @@
# DESC: Maximum mode — web-full + plan + dev for end-to-end MVP via /init-project # DESC: Default profile — carries everything every other profile carries
# Activate when: scaffolding new project with /init-project and need # (user rule 2026-09-28: full does what each profile does), minus the
# brainstorm → design → architecture review → scaffold → implement → ship → audit # broken or doctrine-breaking gstack skills (lib/gstack-removed.sh) and
# pipeline available in one session. Superset of web-full + dev. # the parked tools (make-pdf, diagram, 21st-ai/ui-explore/ui-review) that
# live in `max`. One named exception: pr-review-toolkit (see below).
# === Brainstorm + plan-mode reviews ================================== # === Brainstorm + plan-mode reviews ==================================
office-hours office-hours
@@ -9,11 +10,9 @@ plan-ceo-review
plan-eng-review plan-eng-review
plan-design-review plan-design-review
plan-devex-review plan-devex-review
autoplan
spec spec
# === Design pipeline ================================================= # === Design pipeline =================================================
design-shotgun
design-review design-review
design-consultation design-consultation
design-html design-html
@@ -35,12 +34,8 @@ refactor personal
code-clean personal code-clean personal
commit-change personal commit-change personal
# === Ship + review + land ============================================ # === Review ===========================================================
ship
review review
context-save
land-and-deploy
setup-deploy
# === Second opinion ================================================== # === Second opinion ==================================================
codex codex
@@ -63,20 +58,15 @@ qa-only
# === Docs + translation ============================================== # === Docs + translation ==============================================
doc personal doc personal
document-release document-release
diagram
make-pdf
pdf-translate personal pdf-translate personal
# === Session hygiene + memory ======================================== # === Session hygiene + memory ========================================
close personal close personal
prune-memory personal prune-memory personal
status personal status personal
learn
retro retro
careful
freeze freeze
unfreeze unfreeze
guard
# === External + plugin + MCP ========================================= # === External + plugin + MCP =========================================
emil-design-eng external emil-design-eng external
@@ -99,10 +89,7 @@ ui-ux-pro-max plugin@ui-ux-pro-max-skill
# 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).
21st-ui-build external 21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use external 21st-cli-use external
21st-ai external
# Personal: motion implementation companion (skills/site-motion) # Personal: motion implementation companion (skills/site-motion)
site-motion personal site-motion personal
+102
View File
@@ -0,0 +1,102 @@
# DESC: Everything — full + the parked generation/review tools + PR review
# SUPERSET-OF: full
# Activate when: you need make-pdf, diagram, the 21st-ai/ui-explore/
# ui-review generation trio, or pr-review-toolkit, on top of everything
# full carries. Never a broken or doctrine-breaking gstack skill
# (lib/gstack-removed.sh) — max is full's superset, not the raw catalog.
# === Brainstorm + plan-mode reviews ==================================
office-hours
plan-ceo-review
plan-eng-review
plan-design-review
plan-devex-review
spec
# === Design pipeline =================================================
design-review
design-consultation
design-html
# === Browser + dogfooding ============================================
browse
open-gstack-browser
setup-browser-cookies
scrape
skillify
# === Code work — implementation ======================================
feat personal
ship-feature personal
hotfix personal
bugfix personal
investigate
refactor personal
code-clean personal
commit-change personal
# === Review ===========================================================
review
# === Second opinion ==================================================
codex
# === SEO / GEO / standards / security ================================
seo personal
geo personal
web-validate personal
harden personal
analyze personal
cso
# === Perf + canary + QA ==============================================
health
benchmark
canary
qa
qa-only
# === Docs + translation ==============================================
doc personal
document-release
diagram
make-pdf
pdf-translate personal
# === Session hygiene + memory ========================================
close personal
prune-memory personal
status personal
retro
freeze
unfreeze
# === External + plugin + MCP =========================================
emil-design-eng external
frontend-design external
design-motion-principles external
impeccable external
observability-and-instrumentation external
deprecation-and-migration external
ci-cd-and-automation external
scroll-world-storytelling external
build-threejs-scroll-worlds external
scroll-scrubbed-visual-sequence external
scroll-scrubbed-word-reveal external
scroll-progress-timeline external
ui-ux-pro-max plugin@ui-ux-pro-max-skill
pr-review-toolkit plugin@claude-code-plugins
21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use external
21st-ai external
# Personal: motion implementation companion (skills/site-motion)
site-motion personal
# === CLIs (advisory) =================================================
21st cli
ctx7 cli
graphify cli
gsd cli
-6
View File
@@ -4,7 +4,6 @@
# polish, audit, and verify. # polish, audit, and verify.
# === Design =========================================================== # === Design ===========================================================
design-shotgun
design-review design-review
design-consultation design-consultation
design-html design-html
@@ -25,9 +24,7 @@ feat personal
ship-feature personal ship-feature personal
hotfix personal hotfix personal
bugfix personal bugfix personal
ship
review review
context-save
commit-change personal commit-change personal
refactor personal refactor personal
@@ -55,10 +52,7 @@ scroll-scrubbed-visual-sequence external
scroll-scrubbed-word-reveal external scroll-scrubbed-word-reveal external
scroll-progress-timeline external scroll-progress-timeline external
21st-ui-build external 21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use 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
# Personal: motion implementation companion (skills/site-motion) # Personal: motion implementation companion (skills/site-motion)
-6
View File
@@ -4,7 +4,6 @@
# For SEO/GEO audit on top, use web-full or apply seo afterwards. # For SEO/GEO audit on top, use web-full or apply seo afterwards.
# Design skills (gstack) — full design pipeline # Design skills (gstack) — full design pipeline
design-shotgun
design-review design-review
design-consultation design-consultation
design-html design-html
@@ -23,9 +22,7 @@ plan-eng-review
feat personal feat personal
ship-feature personal ship-feature personal
hotfix personal hotfix personal
ship # gstack
review # gstack review # gstack
context-save # gstack
commit-change personal commit-change personal
refactor personal refactor personal
@@ -51,10 +48,7 @@ site-motion personal
# External: 21st.dev pack (publishing flows 21st-registry / -design-sync # External: 21st.dev pack (publishing flows 21st-registry / -design-sync
# stay parked) # stay parked)
21st-ui-build external 21st-ui-build external
21st-ui-explore external
21st-ui-review external
21st-cli-use 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
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# lib/tests/design-tool-gate.test.sh — hermetic suite for the 21st sign-in
# state added to lib/design-tool-gate.sh (contract 2026-09-28-21st-signin-
# gate-1215): a fake `21st` CLI on a fixture PATH drives every whoami answer
# (signed in / signed out / garbage / nonzero rc) through the real gate
# script, with HOME and PATH redirected into the fixture so the machine's
# real CLI is never reachable. A loud precondition proves that redirection
# actually holds before any case runs.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
GATE="$ROOT/lib/design-tool-gate.sh"
PASS=0; FAIL=0
ok() { echo "PASS $1"; PASS=$((PASS + 1)); }
bad() { echo "FAIL $1 — $2"; FAIL=$((FAIL + 1)); }
# Precondition, loud: the sanitized PATH below and ensure_21st_on_path()'s
# own probes must never resolve a REAL 21st — else CLI_ABSENT_10 (and every
# other case) would silently exercise this machine's CLI instead of the stub.
if PATH=/usr/bin:/bin command -v 21st >/dev/null 2>&1 \
|| [ -e /usr/local/bin/21st ]; then
echo "FAIL precondition: system-wide 21st present," \
"CLI_ABSENT case not hermetic"
FAIL=$((FAIL + 1))
echo "PASS=$PASS FAIL=$FAIL"
exit 1
fi
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/repo/lib/profiles" "$WORK/repo/skills" "$WORK/bin" "$WORK/home"
# GATE-BLOCK allowlist: 21st (cli) always, ghost-skill (external) only used
# by INCOMPLETE_WINS to prove a blocking miss still trips the gate.
cat > "$WORK/repo/lib/profiles/design.profile" <<'EOF'
# GATE-BLOCK: 21st ghost-skill
21st cli
ghost-skill external
EOF
# Fake profile.sh: `show design --plain` cats whatever the case wrote to
# plain.txt. $WORK is read from the environment at run time (exported
# below), never baked in here — the heredoc is quoted on purpose.
cat > "$WORK/repo/lib/profile.sh" <<'EOF'
#!/usr/bin/env bash
[ "$1" = show ] && [ "$3" = --plain ] && { cat "$WORK/plain.txt"; exit 0; }
exit 1
EOF
chmod +x "$WORK/repo/lib/profile.sh"
export WORK
# Fake 21st CLI: `whoami` answers per $FAKE_21ST_MODE, the real CLI's exact
# sentences for in/out (proven by STUB_CONTROL below), a garbage line at
# rc 0, or the signed-out sentence at a nonzero rc (proves rc wins).
cat > "$WORK/bin/21st" <<'EOF'
#!/usr/bin/env bash
[ "${1:-}" = whoami ] || exit 1
signedout='Not logged in. Run `21st login`, or set TWENTYFIRST_TOKEN.'
case "${FAKE_21ST_MODE:-in}" in
in) echo "Logged in as tester (saved locally)."; exit 0 ;;
out) echo "$signedout"; exit 0 ;;
garbage) echo "Something unexpected"; exit 0 ;;
fail) echo "$signedout"; exit 3 ;;
esac
EOF
chmod +x "$WORK/bin/21st"
# gate_run <FAKE_21ST_MODE> <plain, \t and \n escapes> [PATH override]
# -> sets $GATE_OUT / $GATE_RC. TWENTYFIRST_TOKEN and API_KEY_21ST are
# always unset here; TOKEN_READY below sets them explicitly instead.
gate_run() {
local mode="$1" plain="$2" gate_path="${3:-$WORK/bin:/usr/bin:/bin}"
printf '%b\n' "$plain" > "$WORK/plain.txt"
GATE_OUT="$(env -u TWENTYFIRST_TOKEN -u API_KEY_21ST \
HOME="$WORK/home" PATH="$gate_path" \
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
FAKE_21ST_MODE="$mode" bash "$GATE" 2>&1)"
GATE_RC=$?
}
# ── stub positive control ────────────────────────────────────────────────
if FAKE_21ST_MODE=in "$WORK/bin/21st" whoami | grep -q '^Logged in as ' \
&& FAKE_21ST_MODE=out "$WORK/bin/21st" whoami | grep -q '^Not logged in'
then ok STUB_CONTROL; else bad STUB_CONTROL "stub sentences wrong"; fi
# ── SIGNED_IN_READY: whoami says logged in -> exit 0, READY ────────────────
gate_run in 'cli\t21st'
if [ "$GATE_RC" -eq 0 ] && echo "$GATE_OUT" | grep -q 'toolchain: READY'; then
ok SIGNED_IN_READY
else
bad SIGNED_IN_READY "rc=$GATE_RC out=$GATE_OUT"
fi
# ── SIGNED_OUT_12: whoami says not logged in -> exit 12, ask to sign in ────
gate_run out 'cli\t21st'
if [ "$GATE_RC" -eq 12 ] && echo "$GATE_OUT" | grep -q 'SIGN-IN REQUIRED' \
&& echo "$GATE_OUT" | grep -q '21st login' \
&& ! echo "$GATE_OUT" | grep -q 'INCOMPLETE'; then
ok SIGNED_OUT_12
else
bad SIGNED_OUT_12 "rc=$GATE_RC out=$GATE_OUT"
fi
# ── TOKEN_READY: a token env wins even while whoami reports signed out ─────
printf 'cli\t21st\n' > "$WORK/plain.txt"
out_t="$(env HOME="$WORK/home" PATH="$WORK/bin:/usr/bin:/bin" \
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
FAKE_21ST_MODE=out TWENTYFIRST_TOKEN=x bash "$GATE" 2>&1)"; rc_t=$?
out_k="$(env HOME="$WORK/home" PATH="$WORK/bin:/usr/bin:/bin" \
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
FAKE_21ST_MODE=out API_KEY_21ST=x bash "$GATE" 2>&1)"; rc_k=$?
if [ "$rc_t" -eq 0 ] && [ "$rc_k" -eq 0 ]; then
ok TOKEN_READY
else
bad TOKEN_READY "TOKEN rc=$rc_t ($out_t) | API_KEY rc=$rc_k ($out_k)"
fi
# ── CLI_ABSENT_10: 21st off PATH -> exit 10, INCOMPLETE (not sign-in) ──────
gate_run in 'cli\t21st' /usr/bin:/bin
if [ "$GATE_RC" -eq 10 ] && echo "$GATE_OUT" | grep -q 'INCOMPLETE'; then
ok CLI_ABSENT_10
else
bad CLI_ABSENT_10 "rc=$GATE_RC out=$GATE_OUT"
fi
# ── INCOMPLETE_WINS: a blocking miss outranks a signed-out 21st ────────────
gate_run out 'cli\t21st\nexternal\tghost-skill'
if [ "$GATE_RC" -eq 10 ] && echo "$GATE_OUT" | grep -q 'INCOMPLETE' \
&& ! echo "$GATE_OUT" | grep -q 'SIGN-IN REQUIRED'; then
ok INCOMPLETE_WINS
else
bad INCOMPLETE_WINS "rc=$GATE_RC out=$GATE_OUT"
fi
# ── UNKNOWN_11: whoami answers something else -> surfaced, never guessed ───
gate_run garbage 'cli\t21st'
if [ "$GATE_RC" -eq 11 ] && echo "$GATE_OUT" | grep -q 'whoami: rc=0' \
&& echo "$GATE_OUT" | grep -q 'Something unexpected' \
&& ! echo "$GATE_OUT" | grep -q '21st login' \
&& ! echo "$GATE_OUT" | grep -q 'claude CLI unreachable'; then
ok UNKNOWN_11
else
bad UNKNOWN_11 "garbage: rc=$GATE_RC out=$GATE_OUT"
fi
gate_run fail 'cli\t21st'
if [ "$GATE_RC" -eq 11 ] && echo "$GATE_OUT" | grep -q 'whoami: rc=3'; then
ok UNKNOWN_11
else
bad UNKNOWN_11 "fail: rc=$GATE_RC out=$GATE_OUT"
fi
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# lib/tests/doctor-skills.test.sh — lib/doctor-skills.sh's
# skill_catalog_stats(): an inline scalar description, a `|` block
# scalar, a `>-` folded block, a SKILL.md with no description, a
# symlinked skill dir (counted, matching Python glob.glob's symlink
# behavior), and an absent skills dir ("0 0", rc 0 — doctor.sh runs
# under `set -euo pipefail` and must never abort on this check).
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LIB="$ROOT/lib/doctor-skills.sh"
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
SKILLS="$WORK/skills"
REAL="$WORK/real-skill"
mkdir -p "$SKILLS/inline" "$SKILLS/pipe-block" "$SKILLS/fold-block" \
"$SKILLS/no-desc" "$REAL"
cat > "$SKILLS/inline/SKILL.md" <<'EOF'
---
name: inline
description: "Inline description, sixteen."
---
body
EOF
cat > "$SKILLS/pipe-block/SKILL.md" <<'EOF'
---
name: pipe-block
description: |
Block scalar description
spanning two lines.
---
body
EOF
cat > "$SKILLS/fold-block/SKILL.md" <<'EOF'
---
name: fold-block
description: >-
Folded block description
on two lines too.
---
body
EOF
cat > "$SKILLS/no-desc/SKILL.md" <<'EOF'
---
name: no-desc
---
body
EOF
cat > "$REAL/SKILL.md" <<'EOF'
---
name: symlinked
description: "Symlinked skill description."
---
body
EOF
ln -s "$REAL" "$SKILLS/symlinked"
# ── hand-computed expectations (mirrors extract_description()'s scalar
# and block-scalar handling) ──
INLINE_DESC="Inline description, sixteen."
PIPE_DESC="Block scalar description spanning two lines."
FOLD_DESC="Folded block description on two lines too."
SYM_DESC="Symlinked skill description."
EXP_COUNT=5
EXP_CHARS=$((${#INLINE_DESC} + ${#PIPE_DESC} + ${#FOLD_DESC} + ${#SYM_DESC}))
# shellcheck source=../doctor-skills.sh disable=SC1091
source "$LIB"
out="$(skill_catalog_stats "$SKILLS")"
rc=$?
got_count="${out%% *}"
got_chars="${out##* }"
check T1-rc "$rc" 0
check T1-count "$got_count" "$EXP_COUNT"
check T1-chars "$got_chars" "$EXP_CHARS"
# ── T2: absent dir — "0 0", rc 0 ──
out2="$(skill_catalog_stats "$WORK/does-not-exist")"
rc2=$?
check T2-rc "$rc2" 0
check T2-zeroes "$out2" "0 0"
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# lib/tests/gstack-links.test.sh — lib/gstack-links.sh's
# link_gstack_helpers(): whole-class mirroring of a gstack skill dir
# (SKILL.md excluded), whole-dir symlink for a clean non-skill dir/file,
# skip-whole for a non-skill dir hiding a nested SKILL.md
# (browser-skills/, openclaw/-style), skip-by-name for `.git*` and
# `node_modules`, idempotent re-run (echoes 0, nothing changes), a stale
# dst-is-symlink-to-src planted by gstack ./setup (removed, dst becomes
# a real dir, nothing written into src), and a dst path that would
# resolve inside src (refused, rc 1, nothing created). Covers contract
# criterion 3 (whole class) and the r4 confirmation-pass fixtures.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LIB="$ROOT/lib/gstack-links.sh"
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
SRC="$WORK/src"
DST="$WORK/dst"
# ── fixture src: one skill dir (browse), one nested skill-dir tree
# (review, with a sub-directory of its own), plain shared assets
# (bin/, ETHOS.md), the top-level gstack SKILL.md itself, vcs metadata,
# and the two non-skill-dir-hiding-a-nested-SKILL.md cases (other/deep,
# node_modules/pkg) ──
mkdir -p "$SRC/bin" "$SRC/browse/dist" "$SRC/review/specialists" \
"$SRC/.git" "$SRC/other/deep" "$SRC/node_modules/pkg"
echo x > "$SRC/bin/x"
echo ethos > "$SRC/ETHOS.md"
echo skill > "$SRC/SKILL.md"
echo browse-skill > "$SRC/browse/SKILL.md"
echo browse-bin > "$SRC/browse/dist/browse"
echo review-skill > "$SRC/review/SKILL.md"
echo checklist > "$SRC/review/checklist.md"
echo spec-a > "$SRC/review/specialists/a.md"
echo head > "$SRC/.git/HEAD"
echo nested > "$SRC/other/deep/SKILL.md"
echo pkg-skill > "$SRC/node_modules/pkg/SKILL.md"
# shellcheck source=../gstack-links.sh disable=SC1091
source "$LIB"
# ── T1: first run mirrors the whole class, exposes no SKILL.md ──
n1=$(link_gstack_helpers "$SRC" "$DST" 2>/dev/null)
check T1-bin-resolves \
"$([ -f "$DST/bin/x" ] && cat "$DST/bin/x" || echo missing)" x
check T1-ethos-resolves \
"$([ -f "$DST/ETHOS.md" ] && cat "$DST/ETHOS.md" || echo missing)" ethos
check T1-browse-dist-resolves \
"$([ -f "$DST/browse/dist/browse" ] && cat "$DST/browse/dist/browse" \
|| echo missing)" browse-bin
check T1-review-checklist-resolves \
"$([ -f "$DST/review/checklist.md" ] && cat "$DST/review/checklist.md" \
|| echo missing)" checklist
check T1-review-specialists-resolves \
"$([ -f "$DST/review/specialists/a.md" ] \
&& cat "$DST/review/specialists/a.md" || echo missing)" spec-a
check T1-no-skillmd-anywhere \
"$(find -L "$DST" -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')" 0
check T1-no-dotgit "$([ -e "$DST/.git" ] && echo present || echo absent)" \
absent
check T1-no-other "$([ -e "$DST/other" ] && echo present || echo absent)" \
absent
check T1-no-node-modules \
"$([ -e "$DST/node_modules" ] && echo present || echo absent)" absent
check T1-count-positive "$([ "$n1" -gt 0 ] && echo yes || echo no)" yes
# ── T2: idempotent re-run — echoes 0, tree unchanged ──
n2=$(link_gstack_helpers "$SRC" "$DST" 2>/dev/null)
check T2-echoes-zero "$n2" 0
check T2-bin-still-resolves \
"$([ -f "$DST/bin/x" ] && cat "$DST/bin/x" || echo missing)" x
# ── T3: dst is a symlink to src (gstack ./setup's stale-link case) —
# removed, dst becomes a real dir, nothing written into src ──
DST3="$WORK/dst-symlinked"
ln -s "$SRC" "$DST3"
n3=$(link_gstack_helpers "$SRC" "$DST3" 2>/dev/null)
check T3-dst-is-real-dir "$([ -d "$DST3" ] && [ ! -L "$DST3" ] \
&& echo yes || echo no)" yes
check T3-bin-resolves \
"$([ -f "$DST3/bin/x" ] && cat "$DST3/bin/x" || echo missing)" x
check T3-nothing-written-in-src \
"$(find "$SRC" -type l 2>/dev/null | wc -l | tr -d ' ')" 0
check T3-count-positive "$([ "$n3" -gt 0 ] && echo yes || echo no)" yes
# ── T4: dst path resolves inside src — refused, rc 1, nothing created ──
DST4="$SRC/helpers"
out4=$(link_gstack_helpers "$SRC" "$DST4" 2>&1 >/dev/null)
rc4=$?
n4=$(link_gstack_helpers "$SRC" "$DST4" 2>/dev/null)
check T4-rc "$rc4" 1
check T4-echoes-zero "$n4" 0
check T4-nothing-created "$([ -e "$DST4" ] && echo present || echo absent)" \
absent
check T4-warns "$(printf '%s' "$out4" | grep -qi 'refusing' \
&& echo yes || echo no)" yes
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# lib/tests/gstack-removed.test.sh — GSTACK_REMOVED denylist honored by
# every "bring gstack back" path: profile.sh `gstack on` and
# toggle-external.sh `enable gstack` both skip a policy-removed name and
# leave it parked, restoring only what policy allows; gstack_is_removed()
# itself, positive + negative. Covers contract criterion 17. Hermetic:
# fixture repo via PROFILE_REPO_OVERRIDE / TOGGLE_EXTERNAL_REPO_OVERRIDE —
# same harness as lib/tests/profile-default.test.sh and
# lib/tests/toggle-external-repo-resolution.test.sh.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
check_has() { case "$2" in *"$3"*) pass=$((pass+1));; *) fail=$((fail+1));
printf 'FAIL %s: [%s] does not contain [%s]\n' "$1" "$2" "$3";; esac; }
check_not() { case "$2" in *"$3"*) fail=$((fail+1));
printf 'FAIL %s: [%s] unexpectedly contains [%s]\n' "$1" "$2" "$3";; *) pass=$((pass+1));; esac; }
# mk_fixture <dir> — minimal repo: profile.sh + toggle-external.sh +
# gstack-removed.sh under lib/, one removed name (ship) and one kept name
# (browse) parked in skills-disabled/.
mk_fixture() {
local fx="$1"
mkdir -p "$fx/skills" "$fx/skills-disabled/gstack__ship" \
"$fx/skills-disabled/gstack__browse" "$fx/lib"
cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" \
"$ROOT/lib/gstack-removed.sh" "$fx/lib/"
}
# --- T1-T5: profile.sh gstack on restores browse, skips + parks ship ---
FX1="$(mktemp -d)"; mk_fixture "$FX1"
out="$(PROFILE_REPO_OVERRIDE="$FX1" bash "$FX1/lib/profile.sh" gstack on 2>&1)"
check T1-browse-restored "$([ -e "$FX1/skills/browse" ] && echo on || echo off)" on
check T2-ship-parked "$([ -e "$FX1/skills-disabled/gstack__ship" ] && echo p || echo n)" p
check T3-ship-not-live "$([ -e "$FX1/skills/ship" ] && echo on || echo off)" off
check_has T4-skip-names-ship "$out" "ship"
check_has T5-real-count "$out" "1 parked gstack skills restored"
rm -rf "$FX1"
# --- T6-T9: toggle-external.sh enable gstack — same skip + restore ---
FX2="$(mktemp -d)"; mk_fixture "$FX2"
out="$(TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX2" bash "$FX2/lib/toggle-external.sh" enable gstack 2>&1)"
check T6-browse-restored "$([ -e "$FX2/skills/browse" ] && echo on || echo off)" on
check T7-ship-parked "$([ -e "$FX2/skills-disabled/gstack__ship" ] && echo p || echo n)" p
check T8-ship-not-live "$([ -e "$FX2/skills/ship" ] && echo on || echo off)" off
check_has T9-skip-names-ship "$out" "ship"
rm -rf "$FX2"
# --- T10-T11: toggle-external.sh, only removed names parked — 0 restored,
# the policy message fires instead of the "re-run gstack setup" hint ---
FX3="$(mktemp -d)"
mkdir -p "$FX3/skills" "$FX3/skills-disabled/gstack__ship" "$FX3/lib"
cp "$ROOT/lib/toggle-external.sh" "$ROOT/lib/gstack-removed.sh" "$FX3/lib/"
out="$(TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX3" bash "$FX3/lib/toggle-external.sh" enable gstack 2>&1)"
check_has T10-policy-msg "$out" "policy-removed skills remain parked"
check_not T11-no-setup-hint "$out" "re-run gstack setup"
rm -rf "$FX3"
# --- T12-T13: gstack_is_removed() itself, positive + negative ---
# shellcheck source=lib/gstack-removed.sh disable=SC1091
source "$ROOT/lib/gstack-removed.sh"
gstack_is_removed ship; check T12-removed-positive "$?" 0
gstack_is_removed browse; check T13-removed-negative "$?" 1
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env bash
# lib/tests/profile-census.test.sh — profile catalog invariants (skill-
# catalog prune, 2026-09-28): no GSTACK_REMOVED name listed in any
# profile, exactly one profile carries the `# SUPERSET-OF: full` marker
# and it is a superset of full + the parked tools + every name any
# profile carries, and `full` carries every name any other (non-max)
# profile carries save one named exception (pr-review-toolkit).
#
# Hermetic: a passing baseline fixture, then one single-change mutant per
# invariant (each mutant is a positive control — it MUST be detected).
# Live part runs against this repo's real lib/profiles/ (a violation
# there fails the suite for real, same style as
# lib/tests/skill-routing-census.test.sh).
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
# shellcheck source=lib/gstack-removed.sh disable=SC1091
source "$ROOT/lib/gstack-removed.sh"
# Parked = kept installed, out of `full`, listed only in the superset
# profile (`max`). Names come from the single denylist above for REMOVED;
# PARKED has no such shared source (it is a positive allowlist, not a
# denylist), so it is spelled out here.
PARKED=(make-pdf diagram 21st-ai 21st-ui-explore 21st-ui-review)
# full carries every name any other (non-max) profile carries, with one
# named exception: pr-review-toolkit is deliberately out of full (audit
# 2026-07-02 #12, heaviest single plugin, ~2.2k tokens/session, PR-only
# use) — measured 2026-09-28: it is the only name any specialized
# profile carries that full lacks (audit.profile).
FULL_EXCEPTIONS=(pr-review-toolkit)
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
# census_check <profiles_dir> [parked_csv] [exceptions_csv] — one
# violation code per line on stdout, rc 0 iff none. REMOVED always comes
# from the sourced denylist (single source, never overridden);
# PARKED/FULL_EXCEPTIONS default to the real production sets and can be
# overridden per call (fixture runs use a small standalone catalog).
census_check() {
local dir="$1"
# Unset (arg omitted, live call) falls back to the real production
# set; an explicitly EMPTY string (fixture calls) means "none" and
# must stay empty — hence "-" defaults, never ":-" (which would treat
# empty the same as unset and silently pull the real set back in).
local parked_csv="${2-$(IFS=,; echo "${PARKED[*]}")}"
local exc_csv="${3-$(IFS=,; echo "${FULL_EXCEPTIONS[*]}")}"
local removed_csv out
removed_csv="$(IFS=,; echo "${GSTACK_REMOVED[*]}")"
out=$(python3 - "$dir" "$removed_csv" "$parked_csv" "$exc_csv" <<'PY'
import glob, os, re, sys
def entries(path):
"""Entry = first whitespace token of a non-blank, non-comment line."""
names = set()
for line in open(path, encoding="utf-8"):
s = line.strip()
if s and not s.startswith("#"):
names.add(s.split()[0])
return names
def has_marker(path):
text = open(path, encoding="utf-8").read()
return re.search(r'^# SUPERSET-OF: full\s*$', text, re.M) is not None
def check_removed(by_name, removed):
return [f"REMOVED_LISTED:{n}:{e}" for n, ents in by_name.items()
for e in sorted(ents & removed)]
def find_superset(files):
hits = [p for p in files if has_marker(p)]
if not hits:
return None, ["NO_SUPERSET"]
if len(hits) > 1:
return None, ["MANY_SUPERSETS"]
return os.path.basename(hits[0])[:-len(".profile")], []
def check_superset_gap(by_name, sname, parked, exceptions):
full = by_name.get("full", set())
target = full | parked | exceptions
return [f"SUPERSET_GAP:{n}" for n in sorted(target - by_name[sname])]
def check_max_gap(by_name, sname, removed):
union = set().union(*by_name.values())
gap = (union - removed) - by_name[sname]
return [f"MAX_GAP:{n}" for n in sorted(gap)]
def check_full_gap(by_name, sname, removed, exceptions):
trio = {"21st-ai", "21st-ui-explore", "21st-ui-review"}
src = set().union(*(e for n, e in by_name.items()
if n not in ("full", sname)))
full = by_name.get("full", set())
gap = src - full - removed - trio - exceptions
return [f"FULL_GAP:{n}" for n in sorted(gap)]
def main():
d, removed_csv, parked_csv, exc_csv = sys.argv[1:5]
removed = {x for x in removed_csv.split(",") if x}
parked = {x for x in parked_csv.split(",") if x}
exceptions = {x for x in exc_csv.split(",") if x}
files = sorted(glob.glob(os.path.join(d, "*.profile")))
by_name = {os.path.basename(p)[:-len(".profile")]: entries(p)
for p in files}
violations = check_removed(by_name, removed)
sname, sup_violations = find_superset(files)
violations += sup_violations
if sname:
violations += check_superset_gap(by_name, sname, parked, exceptions)
violations += check_max_gap(by_name, sname, removed)
violations += check_full_gap(by_name, sname, removed, exceptions)
# Reason codes only, one per line; no exit here — the bash caller
# derives pass/fail from whether this captured output is empty.
print("\n".join(violations))
main()
PY
)
if [ -n "$out" ]; then
printf '%s\n' "$out"
return 1
fi
return 0
}
# run_mutant <label> <dir> <code> <flag> — census_check on <dir> (fixture
# PARKED override, empty FULL_EXCEPTIONS), asserts a non-zero rc AND the
# presence of <code>; echoes <flag> when both hold (the positive-control
# marker the contract CHECK greps for).
run_mutant() {
local label="$1" dir="$2" code="$3" flag="$4"
local out rc hit nonzero
out=$(census_check "$dir" "parked-x" ""); rc=$?
[ -n "$out" ] && printf '%s\n' "$out"
hit=$(printf '%s\n' "$out" | grep -qxF "$code" && echo yes || echo no)
nonzero=$([ "$rc" -ne 0 ] && echo yes || echo no)
check "$label-code" "$hit" yes
check "$label-nonzero" "$nonzero" yes
[ "$hit" = yes ] && [ "$nonzero" = yes ] && echo "$flag"
}
# ── live: this repo's real profiles dir (a violation here → suite RED) ──
live_out=$(census_check "$ROOT/lib/profiles"); live_rc=$?
[ -n "$live_out" ] && printf '%s\n' "$live_out"
check T1-live-clean "$live_rc" 0
# ── fixtures: baseline + one single-change mutant per invariant ────────
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
BASE="$WORK/baseline"
mkdir -p "$BASE"
cat > "$BASE/full.profile" <<'EOF'
alpha
beta
EOF
cat > "$BASE/qa.profile" <<'EOF'
alpha
EOF
cat > "$BASE/max.profile" <<'EOF'
# SUPERSET-OF: full
alpha
beta
parked-x
EOF
base_out=$(census_check "$BASE" "parked-x" ""); base_rc=$?
[ -n "$base_out" ] && printf '%s\n' "$base_out"
check T2-fixture-baseline-clean "$base_rc" 0
[ "$base_rc" -eq 0 ] && echo FIXTURE_BASELINE_OK
# Mutant 1: a REMOVED name (ship) added to a profile other than full.
M1="$WORK/mutant-removed"; cp -r "$BASE" "$M1"
printf 'ship\n' >> "$M1/qa.profile"
run_mutant T3-mutant-removed "$M1" 'REMOVED_LISTED:qa:ship' \
FIXTURE_REMOVED_DETECTED
# Mutant 2: the superset profile drops a name full carries.
M2="$WORK/mutant-superset"; cp -r "$BASE" "$M2"
sed -i '/^beta$/d' "$M2/max.profile"
run_mutant T4-mutant-superset "$M2" 'SUPERSET_GAP:beta' \
FIXTURE_SUPERSET_DETECTED
# Mutant 3: a non-full, non-superset profile carries a name full lacks.
M3="$WORK/mutant-fullgap"; cp -r "$BASE" "$M3"
printf 'gamma\n' >> "$M3/qa.profile"
run_mutant T5-mutant-fullgap "$M3" 'FULL_GAP:gamma' \
FIXTURE_FULLGAP_DETECTED
echo "PASS=$pass FAIL=$fail"
[ "$fail" -eq 0 ]
+2 -1
View File
@@ -24,7 +24,8 @@ 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" \
"$ROOT/lib/gstack-removed.sh" "$FX/lib/"
cp "$ROOT/hooks/statusline.sh" "$FX/hooks/" cp "$ROOT/hooks/statusline.sh" "$FX/hooks/"
cat > "$FX/lib/profiles/full.profile" <<'EOF' cat > "$FX/lib/profiles/full.profile" <<'EOF'
+2 -1
View File
@@ -19,7 +19,8 @@ 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" \
"$ROOT/lib/gstack-removed.sh" "$FX/lib/"
# 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"
@@ -17,6 +17,7 @@ mkdir -p "$SANDBOX/repo/lib" "$SANDBOX/repo/skills-external/emil-design-eng" \
"$SANDBOX/repo/skills-external/observability-and-instrumentation" \ "$SANDBOX/repo/skills-external/observability-and-instrumentation" \
"$SANDBOX/repo/skills" "$SANDBOX/home/.claude" "$SANDBOX/repo/skills" "$SANDBOX/home/.claude"
cp "$HELPER_SRC" "$SANDBOX/repo/lib/toggle-external.sh" cp "$HELPER_SRC" "$SANDBOX/repo/lib/toggle-external.sh"
cp "$(dirname "$HELPER_SRC")/gstack-removed.sh" "$SANDBOX/repo/lib/"
# mark emil-design-eng ENABLED in the real (physical) repo tree # mark emil-design-eng ENABLED in the real (physical) repo tree
ln -s "$SANDBOX/repo/skills-external/emil-design-eng" "$SANDBOX/repo/skills/emil-design-eng" ln -s "$SANDBOX/repo/skills-external/emil-design-eng" "$SANDBOX/repo/skills/emil-design-eng"
# replicate the real ~/.claude/lib -> <repo>/lib symlink # replicate the real ~/.claude/lib -> <repo>/lib symlink
+15 -2
View File
@@ -40,6 +40,12 @@ REPO="${TOGGLE_EXTERNAL_REPO_OVERRIDE:-$(cd -P "$(dirname "$0")/.." && pwd)}"
SKILLS_DIR="$REPO/skills" SKILLS_DIR="$REPO/skills"
DISABLED_DIR="$REPO/skills-disabled" DISABLED_DIR="$REPO/skills-disabled"
# GSTACK_REMOVED + gstack_is_removed() — single source, honored by the
# `enable gstack` loop below. Resolved from $0 like REPO above, not from
# $REPO/lib — gstack-removed.sh sits next to this file wherever it runs.
# shellcheck source=lib/gstack-removed.sh disable=SC1091
source "$(dirname "$0")/gstack-removed.sh"
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $1"; } ok() { echo -e "${GREEN}✓${NC} $1"; }
warn() { echo -e "${YELLOW}⚠${NC} $1"; } warn() { echo -e "${YELLOW}⚠${NC} $1"; }
@@ -162,18 +168,25 @@ enable_tool() {
local tool="$1" local tool="$1"
case "$tool" in case "$tool" in
gstack) gstack)
local moved=0 local moved=0 skipped=0
if [ -d "$DISABLED_DIR" ]; then if [ -d "$DISABLED_DIR" ]; then
for entry in "$DISABLED_DIR"/gstack__*; do for entry in "$DISABLED_DIR"/gstack__*; do
[ -e "$entry" ] || continue [ -e "$entry" ] || continue
local name local name
name="$(basename "$entry" | sed 's/^gstack__//')" name="$(basename "$entry" | sed 's/^gstack__//')"
if gstack_is_removed "$name"; then
warn "skipped (removed by policy, lib/gstack-removed.sh): $name"
skipped=$((skipped + 1))
continue
fi
rm -rf "${SKILLS_DIR:?}/${name:?}" rm -rf "${SKILLS_DIR:?}/${name:?}"
mv "$entry" "$SKILLS_DIR/$name" mv "$entry" "$SKILLS_DIR/$name"
moved=$((moved + 1)) moved=$((moved + 1))
done done
fi fi
if [ "$moved" -eq 0 ]; then if [ "$moved" -eq 0 ] && [ "$skipped" -ge 1 ]; then
warn "only policy-removed skills remain parked (lib/gstack-removed.sh)"
elif [ "$moved" -eq 0 ]; then
warn "gstack was not disabled — re-run gstack setup to (re)create symlinks" warn "gstack was not disabled — re-run gstack setup to (re)create symlinks"
else else
ok "gstack enabled ($moved symlinks restored)" ok "gstack enabled ($moved symlinks restored)"
+14 -32
View File
@@ -55,41 +55,23 @@ for item in hooks githooks agents skills lib templates rules; do
done done
# GStack is exposed via per-skill symlinks under skills/ (browse, # GStack is exposed via per-skill symlinks under skills/ (browse,
# canary, autoplan, design-review, …) created by gstack's own # canary, autoplan, design-review, …) created by gstack's own `./setup`,
# `./setup`. A global `skills/gstack -> skills-external/gstack/` # PLUS a shared helper tree at skills/gstack/ mirroring every asset the
# symlink duplicated the top-level gstack SKILL.md alongside those # skills hardcode (bin/, browse/dist/, ETHOS.md, …) that a per-skill
# individual skills, producing two entries with the same description # symlink never exposes — see lib/gstack-links.sh. The helper tree
# ("Fast headless browser for QA testing…"). Remove any stale global # never contains a SKILL.md at any depth, so it never duplicates a
# link — only per-skill entries remain. # per-skill entry the way a flat `skills/gstack -> skills-external/gstack`
if [ -L "$REPO/skills/gstack" ] || [ -L "$CLAUDE/skills/gstack" ]; then # link used to (removed by the lib's own stale-symlink guard).
rm -f "$REPO/skills/gstack" "$CLAUDE/skills/gstack" # shellcheck source=lib/gstack-links.sh disable=SC1091
CHANGED=$((CHANGED + 1)) source "$REPO/lib/gstack-links.sh"
fi if [ -d "$REPO/skills-external/gstack" ]; then
if [ ! -d "$REPO/skills-external/gstack" ]; then n=$(link_gstack_helpers "$REPO/skills-external/gstack" \
"$CLAUDE/skills/gstack")
CHANGED=$((CHANGED + n))
else
echo "⚠️ GStack submodule not found — run: git submodule update --init" echo "⚠️ GStack submodule not found — run: git submodule update --init"
fi fi
# GStack shared infrastructure: bin/ (CLI tools, config, analytics) and
# browse/dist/ (compiled browse binary). Per-skill SKILL.md symlinks don't
# expose these, but multiple skills hardcode ~/.claude/skills/gstack/bin/
# and ~/.claude/skills/gstack/browse/dist/. Create targeted symlinks.
GSTACK_SRC="$REPO/skills-external/gstack"
GSTACK_DST="$CLAUDE/skills/gstack"
if [ -d "$GSTACK_SRC/bin" ]; then
mkdir -p "$GSTACK_DST"
if [ ! -L "$GSTACK_DST/bin" ]; then
ln -sf "$GSTACK_SRC/bin" "$GSTACK_DST/bin"
CHANGED=$((CHANGED + 1))
fi
fi
if [ -d "$GSTACK_SRC/browse/dist" ]; then
mkdir -p "$GSTACK_DST/browse"
if [ ! -L "$GSTACK_DST/browse/dist" ]; then
ln -sf "$GSTACK_SRC/browse/dist" "$GSTACK_DST/browse/dist"
CHANGED=$((CHANGED + 1))
fi
fi
# impeccable is NOT here: its installer writes the skill straight into # 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/ (and its agents into agents/) at --scope=global, so there is no
# skills-external/ copy to symlink. See install-plugins.sh Step 8d. # skills-external/ copy to symlink. See install-plugins.sh Step 8d.
+6 -3
View File
@@ -5,6 +5,9 @@
"pr": "", "pr": "",
"sessionUrl": false "sessionUrl": false
}, },
"env": {
"ENABLE_STOP_REVIEW": "0"
},
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(git status)", "Bash(git status)",
@@ -419,7 +422,7 @@
"security-guidance@claude-code-plugins": true, "security-guidance@claude-code-plugins": true,
"superpowers@superpowers-marketplace": true, "superpowers@superpowers-marketplace": true,
"pr-review-toolkit@claude-code-plugins": false, "pr-review-toolkit@claude-code-plugins": false,
"frontend-design@claude-plugins-official": true "brightdata-plugin@synced": false
}, },
"extraKnownMarketplaces": { "extraKnownMarketplaces": {
"claude-code-plugins": { "claude-code-plugins": {
@@ -447,6 +450,7 @@
} }
} }
}, },
"feedbackDrafts": "off",
"effortLevel": "xhigh", "effortLevel": "xhigh",
"remoteControlAtStartup": true, "remoteControlAtStartup": true,
"inputNeededNotifEnabled": true, "inputNeededNotifEnabled": true,
@@ -499,6 +503,5 @@
"**Sensitive remote targets**: any namespace, host, database or container whose name carries `prod` or `production` as a whole word or name segment.", "**Sensitive remote targets**: any namespace, host, database or container whose name carries `prod` or `production` as a whole word or name segment.",
"**Sensitive data locations & audiences**: per-project `.env` files (gitignored) hold database, deploy and API credentials; some web projects store customer-submitted form data under a retention policy. Both are personal or client data — never send either to an external service." "**Sensitive data locations & audiences**: per-project `.env` files (gitignored) hold database, deploy and API credentials; some web projects store customer-submitted form data under a retention policy. Both are personal or client data — never send either to an external service."
] ]
}, }
"feedbackDrafts": "off"
} }
+3 -2
View File
@@ -55,8 +55,9 @@ git log --oneline -20 --all -- <suspected files>
Follow `$HOME/.claude/lib/design-gate.md`: Follow `$HOME/.claude/lib/design-gate.md`:
- Scan $ARGUMENTS and target files for design/UI/style signals (CSS, component, layout, animation). - Scan $ARGUMENTS and target files for design/UI/style signals (CSS, component, layout, animation).
- If signals found → run `design-tool-gate.sh`; if it reports INCOMPLETE, - If signals found → run `design-tool-gate.sh`; INCOMPLETE → tell the user
tell the user to run `/profile design` before proceeding. to run `/profile design`; SIGN-IN REQUIRED → design-gate.md §3 (ask
`! 21st login`, wait) before proceeding.
- If no signals → skip (zero overhead). - If no signals → skip (zero overhead).
## STEP 2 — INVESTIGATE ## STEP 2 — INVESTIGATE
+1 -2
View File
@@ -58,8 +58,7 @@ jq dependency.
|-----------|-------| |-----------|-------|
| Run this project's deploy runbook, delta-instantiated, learning | **this skill** | | Run this project's deploy runbook, delta-instantiated, learning | **this skill** |
| Project has no `.claude/deploy/PROCEDURE.md` yet | this skill's **bootstrap** branch (see STEP 0) | | Project has no `.claude/deploy/PROCEDURE.md` yet | this skill's **bootstrap** branch (see STEP 0) |
| Merge a branch + trigger CI deploy (gstack) | `/land-and-deploy` | | Merge a finished branch | `gitflow finish` on an explicit human signal (skills/gitflow) |
| Configure deployment settings | `/setup-deploy` |
| Document a release after shipping | `/document-release`, `/doc` | | Document a release after shipping | `/document-release`, `/doc` |
## Artifacts — `.claude/deploy/` (four files) ## Artifacts — `.claude/deploy/` (four files)
+3 -2
View File
@@ -71,8 +71,9 @@ FEAT: <feature name> — rule <N>, ~<N> files, <brief approach>
Follow `$HOME/.claude/lib/design-gate.md`: Follow `$HOME/.claude/lib/design-gate.md`:
- Scan $ARGUMENTS and target files for design/UI/style signals. - Scan $ARGUMENTS and target files for design/UI/style signals.
- If signals found → run `design-tool-gate.sh`; if it reports INCOMPLETE, - If signals found → run `design-tool-gate.sh`; INCOMPLETE → tell the user
tell the user to run `/profile design` before proceeding. to run `/profile design`; SIGN-IN REQUIRED → design-gate.md §3 (ask
`! 21st login`, wait) before proceeding.
- If no signals → skip (zero overhead). - If no signals → skip (zero overhead).
## STEP 0.6 — MEMORY READ-BEFORE (decisions-first) ## STEP 0.6 — MEMORY READ-BEFORE (decisions-first)
+2 -1
View File
@@ -33,7 +33,8 @@ carrying every gstack + personal skill in every session.
| `web` | Public website work — frontend + content + light dev | | `web` | Public website work — frontend + content + light dev |
| `seo` | SEO + GEO + W3C audit — search/AI indexability + standards | | `seo` | SEO + GEO + W3C audit — search/AI indexability + standards |
| `web-full` | Production website end-to-end — `web` + `seo` combined | | `web-full` | Production website end-to-end — `web` + `seo` combined |
| `full` | Maximum — web-full + plan + dev for `/init-project` MVP pipeline | | `full` | Default — everything the other profiles carry, minus broken or doctrine-breaking gstack |
| `max` | Everything — full + parked tools (make-pdf, diagram, 21st generation trio) + pr-review-toolkit |
| `backend` | Backend / API / system dev — no design, no SEO | | `backend` | Backend / API / system dev — no design, no SEO |
| `design` | Visual QA, design systems, mockups, polish | | `design` | Visual QA, design systems, mockups, polish |
| `dev` | Daily code work — features, fixes, refactor, ship (any stack) | | `dev` | Daily code work — features, fixes, refactor, ship (any stack) |
+6 -8
View File
@@ -19,6 +19,8 @@ VERSION=$(cat "$REPO/version.txt" 2>/dev/null || echo "unknown")
source "$REPO/lib/detect-plugins.sh" source "$REPO/lib/detect-plugins.sh"
# shellcheck source=lib/gstack-playwright.sh disable=SC1091 # shellcheck source=lib/gstack-playwright.sh disable=SC1091
source "$REPO/lib/gstack-playwright.sh" source "$REPO/lib/gstack-playwright.sh"
# shellcheck source=lib/gstack-links.sh disable=SC1091
source "$REPO/lib/gstack-links.sh"
echo "" echo ""
echo "═══ claude-config update (v${VERSION}) ═══" echo "═══ claude-config update (v${VERSION}) ═══"
@@ -103,16 +105,12 @@ if [[ "$_gstack_confirm" =~ ^[Yy]$ ]]; then
warn "GStack submodule update failed — run: git submodule update --init" warn "GStack submodule update failed — run: git submodule update --init"
fi fi
# Refresh gstack shared infrastructure symlinks (bin/ + browse/dist/) # Refresh the gstack shared helper tree (bin/, browse/dist/, ETHOS.md,
# …) — see lib/gstack-links.sh.
GSTACK_DIR="$REPO/skills-external/gstack" GSTACK_DIR="$REPO/skills-external/gstack"
GSTACK_DST="$HOME/.claude/skills/gstack" GSTACK_DST="$HOME/.claude/skills/gstack"
if [ -d "$GSTACK_DIR/bin" ]; then if [ -d "$GSTACK_DIR" ]; then
mkdir -p "$GSTACK_DST" link_gstack_helpers "$GSTACK_DIR" "$GSTACK_DST" >/dev/null
ln -sf "$GSTACK_DIR/bin" "$GSTACK_DST/bin"
fi
if [ -d "$GSTACK_DIR/browse/dist" ]; then
mkdir -p "$GSTACK_DST/browse"
ln -sf "$GSTACK_DIR/browse/dist" "$GSTACK_DST/browse/dist"
fi fi
# Restore prior enabled/disabled state # Restore prior enabled/disabled state