From 239d91db67df299ba5fbfc6544c0839d64c6d224 Mon Sep 17 00:00:00 2001 From: bastien Date: Tue, 5 May 2026 02:09:28 +0200 Subject: [PATCH] feat(profile): partition skills/plugins/MCPs/CLIs by usage profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship lib/profile.sh + 9 profiles in lib/profiles/. A profile is a plain-text file listing items + types (gstack | personal | external | plugin@ | mcp | cli). `profile set ` enables the listed items and disables the rest: - gstack/personal/external skills: symlink toggle skills/ ↔ skills-disabled/ (gstack__ prefix to avoid collisions; no prefix for personal/external). - plugins typed `plugin@`: actually toggled via `claude plugin enable|disable @`. Allowlist: MANAGED_PLUGINS = ui-ux-pro-max, plugin-dev, pr-review-toolkit. Denylist: PROTECTED_PLUGINS = caveman, security-guidance, superpowers (always-on, never disabled even if absent from a profile). - mcp magic: delegated to lib/toggle-external.sh which already handles the MAGIC_API_KEY env lookup. Other MCPs stay advisory. - cli (rtk, gsd, ctx7, graphify): status-only, never auto-installed. Profiles shipped: web public website work — frontend + content + light dev seo SEO + GEO + W3C audit (search/AI indexability + a11y) web-full production website end-to-end (web ∪ seo ∪ qa-only/canary) backend backend / API / system dev — no design, no SEO design visual QA, design systems, mockups, polish dev daily code work — features, fixes, refactor, ship qa site testing, perf, canary, validation audit comprehensive audit — security + SEO + perf + health minimal strip all gstack skills (quiet session) Commands: profile list / show / current / apply / set / reset / diff `current` heuristic returns "full" when nothing is disabled, otherwise picks the profile with the highest available-ratio (counts both "enabled" and "installed" — the latter for CLIs). Tiebreaker: larger profile total wins, so web-full beats web at a 100% tie. `reset` re-enables every gstack skill but does NOT touch plugins — the user re-enables a managed plugin manually or via `apply `. This is documented in the trailing info line. Integration: - skills/profile/SKILL.md — `/profile` slash command, lists profiles, documents the per-type mechanism, points at lib/profile.sh. - agents/plugin-advisor.md — DETECT phase calls `profile current`, OUTPUT adds a PROFILE line, and TOGGLING EXTERNAL TOOLS gains a "Skill profiles" section with a signal → profile recommendation table. - lib/toggle-external.sh — header pointer to profile.sh for fine- grained activation (toggle-external still owns whole-gstack and magic-MCP toggles). - Makefile — `make profile cmd="set "`, profile-list, profile-current, profile-reset. Tested end-to-end: `set web` enables ui-ux-pro-max + magic; `set seo` disables ui-ux-pro-max; `set minimal` disables ui-ux-pro-max but spares always-on plugins; `reset` restores all 64 skills; shellcheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 14 +- agents/plugin-advisor.md | 40 +++ lib/profile.sh | 525 ++++++++++++++++++++++++++++++++++ lib/profiles/audit.profile | 28 ++ lib/profiles/backend.profile | 43 +++ lib/profiles/design.profile | 30 ++ lib/profiles/dev.profile | 24 ++ lib/profiles/minimal.profile | 7 + lib/profiles/qa.profile | 11 + lib/profiles/seo.profile | 25 ++ lib/profiles/web-full.profile | 53 ++++ lib/profiles/web.profile | 46 +++ lib/toggle-external.sh | 6 + skills/profile/SKILL.md | 119 ++++++++ 14 files changed, 970 insertions(+), 1 deletion(-) create mode 100755 lib/profile.sh create mode 100644 lib/profiles/audit.profile create mode 100644 lib/profiles/backend.profile create mode 100644 lib/profiles/design.profile create mode 100644 lib/profiles/dev.profile create mode 100644 lib/profiles/minimal.profile create mode 100644 lib/profiles/qa.profile create mode 100644 lib/profiles/seo.profile create mode 100644 lib/profiles/web-full.profile create mode 100644 lib/profiles/web.profile create mode 100644 skills/profile/SKILL.md diff --git a/Makefile b/Makefile index a57b457..e86fd7a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install plugin link doctor update new-skill +.PHONY: help install plugin link doctor update new-skill profile profile-list profile-current profile-reset help: ## Show available commands @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " make %-14s %s\n", $$1, $$2}' @@ -22,6 +22,18 @@ onboard: link ## Onboard an existing project (run from the project directory) @echo "Open Claude Code in your project directory and run: /onboard" @echo "Or with hints: /onboard Python FastAPI monorepo" +profile: ## Run profile.sh (usage: make profile cmd="set design") + @bash lib/profile.sh $(cmd) + +profile-list: ## List skill profiles (design, dev, qa, audit, minimal) + @bash lib/profile.sh list + +profile-current: ## Detect which skill profile is currently active + @bash lib/profile.sh current + +profile-reset: ## Re-enable all gstack skills (undo any profile set) + @bash lib/profile.sh reset + new-skill: ## Create a new skill scaffold (usage: make new-skill name=myskill) @test -n "$(name)" || (echo "Usage: make new-skill name=myskill" && exit 1) @mkdir -p agents skills/$(name) diff --git a/agents/plugin-advisor.md b/agents/plugin-advisor.md index eacc2fd..2e814e7 100644 --- a/agents/plugin-advisor.md +++ b/agents/plugin-advisor.md @@ -23,6 +23,11 @@ claude plugin list 2>/dev/null || echo "plugin-list-unavailable" # `claude plugin enable|disable` does not apply to them. bash "$HOME/.claude/lib/toggle-external.sh" list 2>/dev/null || echo "toggle-external-unavailable" +# Active skill profile — design / dev / qa / audit / minimal / custom. +# Profiles partition gstack + personal skills by purpose. See +# lib/profile.sh and lib/profiles/*.profile. +bash "$HOME/.claude/lib/profile.sh" current 2>/dev/null || echo "profile-unavailable" + # Context7 CLI command -v ctx7 &>/dev/null && ctx7 --version 2>/dev/null | head -1 || echo "ctx7-not-installed" @@ -114,6 +119,7 @@ Output: `COMPLEXITY: % — compare two profiles +# +# Profile file format (lib/profiles/.profile): +# # DESC: +# # type defaults to "gstack" +# personal # personal skill (skills//SKILL.md is real) +# external # symlinked into skills-external/ +# plugin@ # Claude plugin — auto-toggle +# mcp # MCP — advisory or via toggle-external +# cli # standalone CLI — advisory only +# +# ============================================================ +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKILLS_DIR="$REPO/skills" +DISABLED_DIR="$REPO/skills-disabled" +PROFILES_DIR="$REPO/lib/profiles" +TOGGLE_EXTERNAL="$REPO/lib/toggle-external.sh" + +# Plugins that are toggle-managed by `set`. Anything NOT in this list is +# never auto-disabled — protects always-on plugins (caveman, security-guidance, +# superpowers) and unrelated user plugins. Add a plugin here only when its +# enabled state is meaningfully driven by task type. +MANAGED_PLUGINS=( + "ui-ux-pro-max@ui-ux-pro-max-skill" + "plugin-dev@claude-code-plugins" + "pr-review-toolkit@claude-code-plugins" +) + +# Plugins that MUST stay enabled — `set` will refuse to disable these even if +# they're not in the profile. (Defensive: belt-and-suspenders alongside +# MANAGED_PLUGINS allowlist.) +PROTECTED_PLUGINS=( + "caveman@caveman" + "security-guidance@claude-code-plugins" + "superpowers@superpowers-marketplace" +) + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BLUE='\033[0;34m'; NC='\033[0m' +ok() { echo -e "${GREEN}✓${NC} $1"; } +warn() { echo -e "${YELLOW}⚠${NC} $1"; } +err() { echo -e "${RED}✗${NC} $1" >&2; } +info() { echo -e "${BLUE}ℹ${NC} $1"; } + +# ── Profile parsing ──────────────────────────────────────── + +# Read a profile file. Output one line per entry: "\t" +# Comments (#…) and blank lines are stripped. Default type is "gstack". +read_profile() { + local prof="$1" + local file="$PROFILES_DIR/$prof.profile" + [ -f "$file" ] || { err "Profile not found: $prof (looked in $PROFILES_DIR)"; return 1; } + local skill type rest + while IFS= read -r line || [ -n "$line" ]; do + line="${line%%#*}" + # trim leading whitespace + tabs + while [[ "$line" =~ ^[[:space:]] ]]; do line="${line#?}"; done + # trim trailing whitespace + tabs + while [[ "$line" =~ [[:space:]]$ ]]; do line="${line%?}"; done + [ -z "$line" ] && continue + # split on first whitespace run + skill="${line%%[[:space:]]*}" + rest="${line#"$skill"}" + while [[ "$rest" =~ ^[[:space:]] ]]; do rest="${rest#?}"; done + type="${rest:-gstack}" + # Validate type. Accepted forms: + # gstack | external | personal — skill (symlink toggle) + # plugin@ — Claude plugin (auto-toggle) + # plugin — legacy/advisory (no marketplace known) + # mcp — MCP server (advisory or via toggle-external) + # cli — standalone CLI (advisory only) + case "$type" in + gstack|external|personal|plugin|mcp|cli) : ;; + plugin@*) : ;; + *) warn "unknown type '$type' for entry '$skill' in $prof — defaulting to gstack"; type=gstack ;; + esac + printf '%s\t%s\n' "$skill" "$type" + done < "$file" +} + +# All skills bundled in skills-external/gstack/ +gstack_skills() { + local src="$REPO/skills-external/gstack" + [ -d "$src" ] || return 0 + for d in "$src"/*/; do + [ -f "${d}SKILL.md" ] || continue + basename "$d" + done +} + +# Profile description (line starting with "# DESC: …") +profile_desc() { + local file="$1" + grep -m1 '^# DESC:' "$file" 2>/dev/null | sed 's/^# DESC:[[:space:]]*//' || true +} + +# ── Status detection ────────────────────────────────────── + +skill_status() { + local skill="$1" type="$2" + case "$type" in + gstack|external|personal) + if [ -e "$SKILLS_DIR/$skill" ]; then + echo "enabled" + elif [ -e "$DISABLED_DIR/gstack__$skill" ] || [ -e "$DISABLED_DIR/$skill" ]; then + echo "disabled" + else + echo "missing" + fi + ;; + plugin|plugin@*) + # `claude plugin list` is the source of truth — settings.json may be + # ahead of or behind reality if the user toggled outside this tool. + if command -v claude >/dev/null 2>&1; then + # Match the plugin block by name then check Status line + if claude plugin list 2>/dev/null \ + | awk -v p="$skill" ' + /^[[:space:]]*❯ '"$skill"'@/ { found=1; next } + found && /Status:/ { print; exit } + ' \ + | grep -q "✔ enabled"; then + echo "enabled" + else + echo "disabled" + fi + else + echo "unknown" + fi + ;; + mcp) + if command -v claude >/dev/null 2>&1 && \ + claude mcp list 2>/dev/null | grep -q "^${skill}"; then + echo "enabled" + else + echo "disabled" + fi + ;; + cli) + command -v "$skill" >/dev/null 2>&1 && echo "installed" || echo "not-installed" + ;; + *) echo "unknown" ;; + esac +} + +# ── Enable / disable ────────────────────────────────────── + +enable_skill() { + local skill="$1" type="$2" + case "$type" in + gstack) + if [ -e "$DISABLED_DIR/gstack__$skill" ]; then + rm -rf "${SKILLS_DIR:?}/${skill:?}" + mv "$DISABLED_DIR/gstack__$skill" "$SKILLS_DIR/$skill" + ok "enabled: $skill" + elif [ -e "$DISABLED_DIR/$skill" ]; then + rm -rf "${SKILLS_DIR:?}/${skill:?}" + mv "$DISABLED_DIR/$skill" "$SKILLS_DIR/$skill" + ok "enabled: $skill" + elif [ -e "$SKILLS_DIR/$skill" ]; then + : # already enabled — silent + else + warn "missing: $skill — try: bash link.sh" + fi + ;; + external|personal) + if [ -e "$DISABLED_DIR/$skill" ]; then + rm -rf "${SKILLS_DIR:?}/${skill:?}" + mv "$DISABLED_DIR/$skill" "$SKILLS_DIR/$skill" + ok "enabled: $skill ($type)" + elif [ -e "$SKILLS_DIR/$skill" ]; then + : + else + warn "missing: $skill ($type)" + fi + ;; + plugin@*) + # type holds the marketplace: plugin@ + local marketplace="${type#plugin@}" + if [ "$(skill_status "$skill" "$type")" = "enabled" ]; then + : # already on + elif command -v claude >/dev/null 2>&1; then + if claude plugin enable "${skill}@${marketplace}" 2>&1 | grep -qiE "enabled|already"; then + ok "enabled plugin: ${skill}@${marketplace}" + else + warn "could not enable plugin: ${skill}@${marketplace}" + fi + else + info "claude CLI not in PATH — manual: claude plugin enable ${skill}@${marketplace}" + fi + ;; + plugin) + # No marketplace specified — purely advisory. + if [ "$(skill_status "$skill" plugin)" = "enabled" ]; then + : # already on + else + info "plugin '$skill' not enabled — run: claude plugin enable $skill@" + fi + ;; + mcp) + if [ "$(skill_status "$skill" mcp)" = "enabled" ]; then + : # already on + elif [ "$skill" = "magic" ] && [ -x "$TOGGLE_EXTERNAL" ]; then + # Known MCP — delegate to lib/toggle-external.sh which handles env vars. + if bash "$TOGGLE_EXTERNAL" enable magic 2>&1 | grep -qE "enabled|already"; then + ok "enabled MCP: magic" + else + info "MCP 'magic' could not be enabled (check .env for MAGIC_API_KEY)" + fi + else + info "MCP '$skill' not registered — run: claude mcp add $skill -- " + fi + ;; + cli) + # CLIs install externally; we never auto-install. Just report status. + if command -v "$skill" >/dev/null 2>&1; then + : # installed — silent + else + info "CLI '$skill' not installed — install separately (npm/cargo/pipx)" + fi + ;; + esac +} + +disable_skill() { + local skill="$1" type="$2" + case "$type" in + gstack) + if [ -e "$SKILLS_DIR/$skill" ]; then + mkdir -p "$DISABLED_DIR" + rm -rf "$DISABLED_DIR/gstack__$skill" + mv "$SKILLS_DIR/$skill" "$DISABLED_DIR/gstack__$skill" + ok "disabled: $skill" + fi + ;; + external|personal) + if [ -e "$SKILLS_DIR/$skill" ]; then + mkdir -p "$DISABLED_DIR" + rm -rf "${DISABLED_DIR:?}/${skill:?}" + mv "$SKILLS_DIR/$skill" "$DISABLED_DIR/$skill" + ok "disabled: $skill ($type)" + fi + ;; + plugin@*) + local marketplace="${type#plugin@}" + local key="${skill}@${marketplace}" + # Defensive check against PROTECTED_PLUGINS (always-on). + local p + for p in "${PROTECTED_PLUGINS[@]}"; do + if [ "$key" = "$p" ]; then + warn "refusing to disable protected plugin: $key" + return 0 + fi + done + if [ "$(skill_status "$skill" "$type")" = "disabled" ]; then + : # already off + elif command -v claude >/dev/null 2>&1; then + if claude plugin disable "$key" 2>&1 | grep -qiE "disabled|already"; then + ok "disabled plugin: $key" + else + warn "could not disable plugin: $key" + fi + else + info "claude CLI not in PATH — manual: claude plugin disable $key" + fi + ;; + plugin) + info "plugin '$skill' — manual: claude plugin disable $skill@" + ;; + mcp) + if [ "$skill" = "magic" ] && [ -x "$TOGGLE_EXTERNAL" ]; then + if bash "$TOGGLE_EXTERNAL" disable magic 2>&1 | grep -qE "disabled|already"; then + ok "disabled MCP: magic" + else + info "MCP 'magic' — manual disable failed" + fi + else + info "MCP '$skill' — manual: claude mcp remove $skill" + fi + ;; + cli) + : # never auto-uninstall CLIs + ;; + esac +} + +# ── Commands ────────────────────────────────────────────── + +cmd_list() { + printf "%-12s %s\n" "PROFILE" "DESCRIPTION" + printf "%-12s %s\n" "-------" "-----------" + local f name desc + for f in "$PROFILES_DIR"/*.profile; do + [ -f "$f" ] || continue + name="$(basename "$f" .profile)" + desc="$(profile_desc "$f")" + printf "%-12s %s\n" "$name" "${desc:--}" + done +} + +cmd_show() { + local prof="$1" + local file="$PROFILES_DIR/$prof.profile" + [ -f "$file" ] || { err "Profile not found: $prof"; return 1; } + echo "Profile: $prof" + local desc + desc="$(profile_desc "$file")" + [ -n "$desc" ] && echo "Description: $desc" + echo "" + printf "%-25s %-30s %s\n" "ITEM" "TYPE" "STATUS" + printf "%-25s %-30s %s\n" "----" "----" "------" + local skill type status + while IFS=$'\t' read -r skill type; do + status="$(skill_status "$skill" "$type")" + printf "%-25s %-30s %s\n" "$skill" "$type" "$status" + done < <(read_profile "$prof") +} + +cmd_apply() { + local prof="$1" + info "Applying profile: $prof (additive — leaves other skills alone)" + local skill type + while IFS=$'\t' read -r skill type; do + enable_skill "$skill" "$type" + done < <(read_profile "$prof") +} + +cmd_set() { + local prof="$1" + info "Setting profile: $prof (exclusive — disables non-listed gstack skills + managed plugins)" + + # Index of items in profile (skill names + plugin keys "name@marketplace"). + local keep_file + keep_file="$(mktemp)" + # Skill names (col 1) — used to keep gstack skills. + read_profile "$prof" | cut -f1 | sort -u > "$keep_file" + # Plugin keys "name@marketplace" — used to keep managed plugins. + local plugin_keep_file + plugin_keep_file="$(mktemp)" + read_profile "$prof" | awk -F'\t' '$2 ~ /^plugin@/ { sub(/^plugin@/, "", $2); print $1"@"$2 }' | sort -u > "$plugin_keep_file" + + # Disable gstack-origin skills not in profile. + local name + while read -r name; do + [ -n "$name" ] || continue + if ! grep -qx "$name" "$keep_file"; then + disable_skill "$name" gstack + fi + done < <(gstack_skills | sort -u) + + # Disable managed plugins not in profile (PROTECTED_PLUGINS are excluded + # by disable_skill itself — belt and suspenders). + local p key plugin_name marketplace + for p in "${MANAGED_PLUGINS[@]}"; do + if ! grep -qx "$p" "$plugin_keep_file"; then + plugin_name="${p%@*}" + marketplace="${p#*@}" + disable_skill "$plugin_name" "plugin@${marketplace}" + fi + done + + rm -f "$keep_file" "$plugin_keep_file" + # Enable everything listed in the profile. + cmd_apply "$prof" +} + +cmd_reset() { + info "Re-enabling all gstack skills (move skills-disabled/gstack__* back)" + local entry name + if [ -d "$DISABLED_DIR" ]; then + for entry in "$DISABLED_DIR"/gstack__*; do + [ -e "$entry" ] || continue + name="$(basename "$entry" | sed 's/^gstack__//')" + rm -rf "${SKILLS_DIR:?}/${name:?}" + mv "$entry" "$SKILLS_DIR/$name" + ok "re-enabled: $name" + done + fi + info "Plugin state NOT touched. To re-enable a managed plugin disabled by 'set'," + info "run: claude plugin enable @ (or: profile apply )" +} + +cmd_current() { + # A profile is "active" only if (a) most of its skills are enabled AND + # (b) at least one non-listed gstack skill is currently disabled (i.e. a + # `set` has actually been applied). Without (b), every profile reports + # 100% trivially because the full gstack is on. + local disabled_count=0 + if [ -d "$DISABLED_DIR" ]; then + disabled_count=$(find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' ') + fi + if [ "$disabled_count" -eq 0 ]; then + echo "full (all gstack skills enabled — no profile set)" + return 0 + fi + # Pick the profile with the highest "available" ratio. An item counts as + # available when its status is "enabled" (skills, plugins, MCPs) or + # "installed" (CLIs). On ties, the profile with the larger total wins + # — superset profiles describe state more completely than subsets. + local f name total available score skill type status + local best="" best_score=0 best_total=0 + for f in "$PROFILES_DIR"/*.profile; do + [ -f "$f" ] || continue + name="$(basename "$f" .profile)" + total=0; available=0 + while IFS=$'\t' read -r skill type; do + total=$((total + 1)) + status="$(skill_status "$skill" "$type")" + case "$status" in + enabled|installed) available=$((available + 1)) ;; + esac + done < <(read_profile "$name") + [ "$total" -eq 0 ] && continue + score=$((available * 100 / total)) + if [ "$score" -gt "$best_score" ] || \ + { [ "$score" -eq "$best_score" ] && [ "$total" -gt "$best_total" ]; }; then + best_score="$score" + best_total="$total" + best="$name" + fi + done + if [ -n "$best" ] && [ "$best_score" -ge 80 ]; then + echo "$best (${best_score}% match, $disabled_count gstack skills disabled)" + else + echo "custom (best guess: ${best:-none} ${best_score}%, $disabled_count gstack skills disabled)" + fi +} + +cmd_diff() { + local a="$1" b="$2" + local fa="$PROFILES_DIR/$a.profile" fb="$PROFILES_DIR/$b.profile" + [ -f "$fa" ] || { err "Profile not found: $a"; return 1; } + [ -f "$fb" ] || { err "Profile not found: $b"; return 1; } + local list_a list_b + list_a="$(mktemp)"; list_b="$(mktemp)" + read_profile "$a" | cut -f1 | sort -u > "$list_a" + read_profile "$b" | cut -f1 | sort -u > "$list_b" + echo "Only in $a:"; comm -23 "$list_a" "$list_b" | sed 's/^/ - /' + echo "Only in $b:"; comm -13 "$list_a" "$list_b" | sed 's/^/ + /' + echo "Common:" ; comm -12 "$list_a" "$list_b" | sed 's/^/ = /' + rm -f "$list_a" "$list_b" +} + +usage() { + cat < show profile contents + per-skill status + profile current detect which profile is currently active + profile apply enable skills in profile (additive) + profile set enable only listed skills (disables rest of gstack) + profile reset re-enable all gstack skills + profile diff compare two profiles + +PROFILES (in $PROFILES_DIR): +EOF + local f name desc + for f in "$PROFILES_DIR"/*.profile; do + [ -f "$f" ] || continue + name="$(basename "$f" .profile)" + desc="$(profile_desc "$f")" + printf " %-10s %s\n" "$name" "${desc:--}" + done + cat < +# bash lib/profile.sh reset # ============================================================ set -euo pipefail diff --git a/skills/profile/SKILL.md b/skills/profile/SKILL.md new file mode 100644 index 0000000..fc275b1 --- /dev/null +++ b/skills/profile/SKILL.md @@ -0,0 +1,119 @@ +--- +name: profile +description: | + Partition Claude skills by purpose: design, dev, qa, audit, minimal. + Toggles symlinks between skills/ and skills-disabled/ to keep only + the skills relevant to the current kind of work. + Trigger: "profile", "skill profile", "design profile", "qa profile", + "switch to design", "set profile", "active profile", "quel profil", + "profil design", "active les skills design", "désactive gstack", + "réduire le bruit gstack". +argument-hint: list | show | current | apply | set | reset | diff +disable-model-invocation: false +allowed-tools: + - Bash + - Read +--- + +# profile + +Activate a curated subset of skills for a specific kind of work — instead of +carrying every gstack + personal skill in every session. + +## When to invoke + +- User asks to switch profile (`set design`, `profile dev`, `quel profil actif`). +- User wants to see what's in a profile (`profile show qa`). +- User wants to compare profiles (`profile diff design qa`). +- User asks to "reduce gstack noise" or "only design skills". + +## Profiles available + +| Profile | Use case | +|------------|----------| +| `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 | +| `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) | +| `qa` | Site testing, perf, canary, validation | +| `audit` | Comprehensive audit — security + SEO + GEO + W3C + perf + health | +| `minimal` | Strip all gstack skills (quiet session) | + +## Mechanism + +Each profile is a plain-text file under `lib/profiles/.profile` that +lists items + types: + +| Type | Toggle mechanism | +|-------------------------|------------------| +| `gstack` | symlink move skills/ ↔ skills-disabled/gstack__\ | +| `personal` | symlink move skills/ ↔ skills-disabled/\ (no prefix) | +| `external` | symlink move skills/ ↔ skills-disabled/\ | +| `plugin@` | `claude plugin enable\|disable @` (auto) | +| `mcp` (known: magic) | delegate to `lib/toggle-external.sh` (uses `.env`) | +| `mcp` (other) | advisory — prints manual `claude mcp add …` command | +| `cli` | advisory only — reports installed/not-installed | + +**Always-on plugins** (`caveman`, `security-guidance`, `superpowers`) are +protected — `set` will refuse to disable them even if the profile omits them. +**Managed plugins** that `set` may disable when not in profile: +`ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`, +`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled. + +## Commands + +```bash +# List available profiles +bash "$HOME/.claude/lib/profile.sh" list + +# Show profile contents + per-skill status +bash "$HOME/.claude/lib/profile.sh" show + +# Detect which profile is currently active +bash "$HOME/.claude/lib/profile.sh" current + +# Enable skills in profile (additive — keeps others enabled) +bash "$HOME/.claude/lib/profile.sh" apply + +# Enable only skills in profile (disables non-listed gstack skills) +bash "$HOME/.claude/lib/profile.sh" set + +# Re-enable every gstack skill (undo any set/apply) +bash "$HOME/.claude/lib/profile.sh" reset + +# Compare two profiles +bash "$HOME/.claude/lib/profile.sh" diff +``` + +## Execution + +Run `lib/profile.sh` with the user's arguments. If user passed nothing, default +to `list`. If user named a profile without a verb (e.g. "profile design"), +treat it as `set ` — but confirm first because `set` disables other gstack +skills. + +```bash +bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS +``` + +## Output policy + +- After `set` / `apply` / `reset`: show the count of skills moved + tell the + user to start a new Claude session to pick up the changes (Claude scans + `skills/` at session start). +- After `current`: report the active profile + match percentage. +- After `show`: render the table directly — no extra commentary unless the user + asks. + +## Tradeoffs to mention if asked + +- gstack skills still depend on `~/.claude/skills/gstack/bin/` for telemetry, + update-check, learnings — script doesn't touch that infra. Disabled skills + are just hidden from Claude Code's scanner; the gstack repo stays installed. +- Profile changes do NOT toggle Claude Code plugins (ui-ux-pro-max, etc.) or + MCP servers — those are advisory only. The user runs `claude plugin + enable|disable` and `claude mcp add|remove` manually. +- `set` is destructive in the sense that it disables non-listed gstack skills. + Use `apply` if the user wants additive behavior.