feat(profiles): prune the gstack catalog, add max, honor a removed denylist
Nine gstack skills leave every profile (ship is trunk-based on Gitea, land-and-deploy auto-merges and deploys, setup-deploy, autoplan reads paths that do not exist here, context-save has no restore, learn is an unused parallel store, careful and guard hooks never fired, design-shotgun needs an absent OpenAI key). lib/gstack-removed.sh is the single denylist; profile.sh gstack on and toggle-external.sh enable gstack skip it. full now carries everything every other profile carries (user rule), minus the parked make-pdf, diagram and 21st-ai/ui-explore/ui-review, which live in the new max profile together with pr-review-toolkit. The 21st trio also leaves web, web-full and design (redundant with impeccable + ui-ux-pro-max). lib/tests/profile-census.test.sh asserts the invariants live and on a baseline fixture plus one mutant per invariant; gstack-removed.test.sh covers both restore paths.
This commit is contained in:
@@ -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 |
|
||||
| `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) |
|
||||
| `/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 |
|
||||
|
||||
> This table lists personal skills. Gstack skills (investigate, review, retro,
|
||||
> office-hours, context-save, context-restore, cso…) and marketplace plugins add
|
||||
> many more — run `/skills-perso` to list your hand-written skills, or browse `skills/`.
|
||||
> office-hours, cso…) and marketplace plugins add many more — run
|
||||
> `/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 onboard # onboard an existing project (run from its dir)
|
||||
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-current # show the active profile (full when none selected)
|
||||
make profile-reset # go to the default profile (full)
|
||||
|
||||
@@ -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) |
|
||||
| `/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 |
|
||||
| `/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,
|
||||
> pr-review-toolkit…) et marketplaces externes en ajoutent beaucoup d'autres —
|
||||
|
||||
@@ -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
@@ -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)
|
||||
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
|
||||
# never auto-disabled — protects always-on plugins (security-guidance,
|
||||
# superpowers) and unrelated user plugins. Add a plugin here only when its
|
||||
@@ -313,7 +318,11 @@ enable_skill() {
|
||||
local skill="$1" type="$2"
|
||||
case "$type" in
|
||||
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:?}"
|
||||
mv "$DISABLED_DIR/gstack__$skill" "$SKILLS_DIR/$skill"
|
||||
ok "enabled: $skill"
|
||||
@@ -454,18 +463,27 @@ disable_skill() {
|
||||
# ── Shared gstack operations ──────────────────────────────
|
||||
|
||||
# Re-enable every gstack skill parked in skills-disabled/ (move gstack__*
|
||||
# back into skills/). Shared by cmd_reset and `gstack on`. Side effects
|
||||
# only; prints one confirmation per restored skill.
|
||||
# back into skills/), skipping a name lib/gstack-removed.sh denies (left
|
||||
# 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() {
|
||||
local entry name
|
||||
[ -d "$DISABLED_DIR" ] || return 0
|
||||
local entry name restored=0
|
||||
[ -d "$DISABLED_DIR" ] || { echo 0; return 0; }
|
||||
for entry in "$DISABLED_DIR"/gstack__*; do
|
||||
[ -e "$entry" ] || continue
|
||||
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:?}"
|
||||
mv "$entry" "$SKILLS_DIR/$name"
|
||||
ok "re-enabled: $name"
|
||||
ok "re-enabled: $name" >&2
|
||||
restored=$((restored + 1))
|
||||
done
|
||||
echo "$restored"
|
||||
}
|
||||
|
||||
# Disable gstack-origin skills not listed in the given profile. Shared by
|
||||
@@ -648,13 +666,13 @@ cmd_gstack() {
|
||||
on)
|
||||
# Restore whatever is parked, but DON'T touch active-profile — the
|
||||
# user is adding gstack on top of their current profile, not clearing it.
|
||||
local parked
|
||||
local parked restored
|
||||
parked="$(parked_gstack_count)"
|
||||
if [ "$parked" -eq 0 ]; then
|
||||
info "nothing parked — gstack skills are linked per profile (set/apply/reset)"
|
||||
else
|
||||
enable_all_gstack
|
||||
ok "$parked parked gstack skills restored"
|
||||
restored="$(enable_all_gstack)"
|
||||
ok "$restored parked gstack skills restored"
|
||||
fi
|
||||
;;
|
||||
off)
|
||||
|
||||
@@ -18,11 +18,8 @@ observability-and-instrumentation external
|
||||
deprecation-and-migration external
|
||||
ci-cd-and-automation external
|
||||
|
||||
# Ship + review + land
|
||||
ship
|
||||
# Review
|
||||
review
|
||||
context-save
|
||||
land-and-deploy
|
||||
|
||||
# Second opinion for hard problems
|
||||
codex
|
||||
@@ -32,11 +29,8 @@ cso
|
||||
health
|
||||
|
||||
# Session hygiene
|
||||
careful
|
||||
freeze
|
||||
unfreeze
|
||||
guard
|
||||
learn
|
||||
retro
|
||||
|
||||
# pr-review-toolkit removed (audit 2026-07-02 #12 — ~2.2k tokens, PR-only):
|
||||
|
||||
@@ -11,13 +11,12 @@
|
||||
# GATE-BLOCK: 21st 21st-ui-build
|
||||
|
||||
# Core design skills (gstack)
|
||||
design-shotgun
|
||||
design-review
|
||||
design-consultation
|
||||
design-html
|
||||
plan-design-review
|
||||
|
||||
# Browser tooling — design-review and design-shotgun rely on it
|
||||
# Browser tooling — design-review relies on it
|
||||
browse
|
||||
open-gstack-browser
|
||||
setup-browser-cookies
|
||||
@@ -44,10 +43,7 @@ site-motion personal
|
||||
# External: 21st.dev pack — CLI-driven (no MCP, no API key). 21st-registry
|
||||
# and 21st-design-sync are publishing flows; installed but left parked.
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
|
||||
# Plugin (auto-toggle)
|
||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
# Implementation
|
||||
feat personal
|
||||
ship-feature personal
|
||||
ship
|
||||
|
||||
# Bug fixing
|
||||
hotfix personal
|
||||
@@ -23,7 +22,3 @@ commit-change personal
|
||||
observability-and-instrumentation external
|
||||
deprecation-and-migration external
|
||||
ci-cd-and-automation external
|
||||
|
||||
# Session hygiene
|
||||
context-save
|
||||
land-and-deploy
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# DESC: Maximum mode — web-full + plan + dev for end-to-end MVP via /init-project
|
||||
# Activate when: scaffolding new project with /init-project and need
|
||||
# brainstorm → design → architecture review → scaffold → implement → ship → audit
|
||||
# pipeline available in one session. Superset of web-full + dev.
|
||||
# DESC: Default profile — carries everything every other profile carries
|
||||
# (user rule 2026-09-28: full does what each profile does), minus the
|
||||
# broken or doctrine-breaking gstack skills (lib/gstack-removed.sh) and
|
||||
# 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 ==================================
|
||||
office-hours
|
||||
@@ -9,11 +10,9 @@ plan-ceo-review
|
||||
plan-eng-review
|
||||
plan-design-review
|
||||
plan-devex-review
|
||||
autoplan
|
||||
spec
|
||||
|
||||
# === Design pipeline =================================================
|
||||
design-shotgun
|
||||
design-review
|
||||
design-consultation
|
||||
design-html
|
||||
@@ -35,12 +34,8 @@ refactor personal
|
||||
code-clean personal
|
||||
commit-change personal
|
||||
|
||||
# === Ship + review + land ============================================
|
||||
ship
|
||||
# === Review ===========================================================
|
||||
review
|
||||
context-save
|
||||
land-and-deploy
|
||||
setup-deploy
|
||||
|
||||
# === Second opinion ==================================================
|
||||
codex
|
||||
@@ -63,20 +58,15 @@ qa-only
|
||||
# === Docs + translation ==============================================
|
||||
doc personal
|
||||
document-release
|
||||
diagram
|
||||
make-pdf
|
||||
pdf-translate personal
|
||||
|
||||
# === Session hygiene + memory ========================================
|
||||
close personal
|
||||
prune-memory personal
|
||||
status personal
|
||||
learn
|
||||
retro
|
||||
careful
|
||||
freeze
|
||||
unfreeze
|
||||
guard
|
||||
|
||||
# === External + plugin + MCP =========================================
|
||||
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;
|
||||
# a later `set full` re-disables it — MANAGED_PLUGINS lifecycle).
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -4,7 +4,6 @@
|
||||
# polish, audit, and verify.
|
||||
|
||||
# === Design ===========================================================
|
||||
design-shotgun
|
||||
design-review
|
||||
design-consultation
|
||||
design-html
|
||||
@@ -25,9 +24,7 @@ feat personal
|
||||
ship-feature personal
|
||||
hotfix personal
|
||||
bugfix personal
|
||||
ship
|
||||
review
|
||||
context-save
|
||||
commit-change personal
|
||||
refactor personal
|
||||
|
||||
@@ -55,10 +52,7 @@ scroll-scrubbed-visual-sequence external
|
||||
scroll-scrubbed-word-reveal external
|
||||
scroll-progress-timeline external
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
|
||||
# Personal: motion implementation companion (skills/site-motion)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# For SEO/GEO audit on top, use web-full or apply seo afterwards.
|
||||
|
||||
# Design skills (gstack) — full design pipeline
|
||||
design-shotgun
|
||||
design-review
|
||||
design-consultation
|
||||
design-html
|
||||
@@ -23,9 +22,7 @@ plan-eng-review
|
||||
feat personal
|
||||
ship-feature personal
|
||||
hotfix personal
|
||||
ship # gstack
|
||||
review # gstack
|
||||
context-save # gstack
|
||||
commit-change personal
|
||||
refactor personal
|
||||
|
||||
@@ -51,10 +48,7 @@ site-motion personal
|
||||
# External: 21st.dev pack (publishing flows 21st-registry / -design-sync
|
||||
# stay parked)
|
||||
21st-ui-build external
|
||||
21st-ui-explore external
|
||||
21st-ui-review external
|
||||
21st-cli-use external
|
||||
21st-ai external
|
||||
|
||||
# Plugin: UI/UX intelligence (auto-toggle)
|
||||
ui-ux-pro-max plugin@ui-ux-pro-max-skill
|
||||
|
||||
Executable
+66
@@ -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 ]
|
||||
Executable
+192
@@ -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 ]
|
||||
@@ -24,7 +24,8 @@ for g in gs-a gs-b gs-c; do
|
||||
mkdir -p "$FX/skills-external/gstack/$g"
|
||||
touch "$FX/skills-external/gstack/$g/SKILL.md"
|
||||
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/"
|
||||
|
||||
cat > "$FX/lib/profiles/full.profile" <<'EOF'
|
||||
|
||||
@@ -19,7 +19,8 @@ for g in gs-a gs-b gs-c; do
|
||||
mkdir -p "$FX/skills-external/gstack/$g"
|
||||
touch "$FX/skills-external/gstack/$g/SKILL.md"
|
||||
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.
|
||||
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" "$SANDBOX/home/.claude"
|
||||
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
|
||||
ln -s "$SANDBOX/repo/skills-external/emil-design-eng" "$SANDBOX/repo/skills/emil-design-eng"
|
||||
# replicate the real ~/.claude/lib -> <repo>/lib symlink
|
||||
|
||||
+15
-2
@@ -40,6 +40,12 @@ REPO="${TOGGLE_EXTERNAL_REPO_OVERRIDE:-$(cd -P "$(dirname "$0")/.." && pwd)}"
|
||||
SKILLS_DIR="$REPO/skills"
|
||||
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'
|
||||
ok() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
@@ -162,18 +168,25 @@ enable_tool() {
|
||||
local tool="$1"
|
||||
case "$tool" in
|
||||
gstack)
|
||||
local moved=0
|
||||
local moved=0 skipped=0
|
||||
if [ -d "$DISABLED_DIR" ]; then
|
||||
for entry in "$DISABLED_DIR"/gstack__*; do
|
||||
[ -e "$entry" ] || continue
|
||||
local name
|
||||
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:?}"
|
||||
mv "$entry" "$SKILLS_DIR/$name"
|
||||
moved=$((moved + 1))
|
||||
done
|
||||
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"
|
||||
else
|
||||
ok "gstack enabled ($moved symlinks restored)"
|
||||
|
||||
@@ -33,7 +33,8 @@ carrying every gstack + personal skill in every session.
|
||||
| `web` | Public website work — frontend + content + light dev |
|
||||
| `seo` | SEO + GEO + W3C audit — search/AI indexability + standards |
|
||||
| `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 |
|
||||
| `design` | Visual QA, design systems, mockups, polish |
|
||||
| `dev` | Daily code work — features, fixes, refactor, ship (any stack) |
|
||||
|
||||
Reference in New Issue
Block a user