Parcourir la source

feat(profile): partition skills/plugins/MCPs/CLIs by usage profile

Ship lib/profile.sh + 9 profiles in lib/profiles/. A profile is a
plain-text file listing items + types (gstack | personal | external |
plugin@<marketplace> | mcp | cli). `profile set <name>` enables the
listed items and disables the rest:

  - gstack/personal/external skills: symlink toggle skills/ ↔
    skills-disabled/ (gstack__<name> prefix to avoid collisions; no
    prefix for personal/external).
  - plugins typed `plugin@<marketplace>`: actually toggled via
    `claude plugin enable|disable <name>@<marketplace>`. 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 <name> / current / apply <name> / set <name> /
  reset / diff <a> <b>

`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 <profile>`.
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 <name>"`, 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) <noreply@anthropic.com>
bastien il y a 1 semaine
Parent
commit
239d91db67

+ 13 - 1
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)

+ 40 - 0
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: <score>% — <label>` with one-line justification.
 ```
 PLUGIN CHECK
 ACTIVE: [plugin — status, one line each]
+PROFILE: [active skill profile — name + match%, or "custom"]
 SIGNALS: [detected signals]
 COMPLEXITY: <score>% — <simple|moderate|complex|enterprise>
 PLAN: <Max|Pro|Free> (budget: ~<N>t passive tokens)
@@ -308,6 +314,40 @@ bash $HOME/.claude/lib/toggle-external.sh enable gstack
 bash $HOME/.claude/lib/toggle-external.sh disable darwin-skill
 ```
 
+### Skill profiles (fine-grained partitioning, with plugin + MCP toggle)
+
+For task-shaped activation (web only, seo only, backend only, design only,
+etc.) prefer `lib/profile.sh` over toggling all of gstack at once. Profiles
+activate a curated subset of skills + plugins + MCPs and disable the rest of
+gstack + managed plugins — sessions stay focused and passive token cost drops.
+
+`profile set <name>` actually toggles plugins (`claude plugin enable|disable`)
+and MCPs (delegates to `lib/toggle-external.sh` for `magic`) — not just
+advisory. Always-on plugins (`caveman`, `security-guidance`, `superpowers`)
+are protected. Managed plugins that `set` may toggle:
+`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.
+
+When the project signal matches one of the canonical profiles, recommend the
+matching `profile set` command:
+
+| Signal | Recommended profile | Command |
+|---|---|---|
+| `frontend` public website (no backend, no SEO focus) | `web` | `bash $HOME/.claude/lib/profile.sh set web` |
+| audit-only: SEO + GEO + W3C + WCAG | `seo` | `bash $HOME/.claude/lib/profile.sh set seo` |
+| public website end-to-end (build + audit) | `web-full` | `bash $HOME/.claude/lib/profile.sh set web-full` |
+| backend / API / system / library (no UI, no SEO) | `backend` | `bash $HOME/.claude/lib/profile.sh set backend` |
+| `design-system` (heavy UI work, no dev) | `design` | `bash $HOME/.claude/lib/profile.sh set design` |
+| `simple` / hotfix / typical dev session | `dev` | `bash $HOME/.claude/lib/profile.sh set dev` |
+| `browser-qa` (e2e tests, no design work) | `qa` | `bash $HOME/.claude/lib/profile.sh set qa` |
+| comprehensive audit (security + SEO + perf) | `audit` | `bash $HOME/.claude/lib/profile.sh set audit` |
+| narrow session, minimal noise | `minimal` | `bash $HOME/.claude/lib/profile.sh set minimal` |
+
+To restore the full skill set: `bash $HOME/.claude/lib/profile.sh reset`.
+Plugin state is NOT touched by reset — re-enable a managed plugin manually
+or by applying a profile that lists it (e.g. `apply web` to restore
+`ui-ux-pro-max`).
+
 ## BLOCK if
 
 - Superpowers not active → install: `claude plugin marketplace add obra/superpowers-marketplace && claude plugin install --scope user superpowers@superpowers-marketplace`

+ 525 - 0
lib/profile.sh

@@ -0,0 +1,525 @@
+#!/usr/bin/env bash
+# ============================================================
+# lib/profile.sh — Partition Claude skills + plugins + MCPs by purpose
+#
+# Profiles group skills (gstack + external + personal), plugins, MCPs,
+# and CLIs for a specific kind of work: web, seo, web-full, backend,
+# design, dev, qa, audit, minimal. Apply a profile to enable just the
+# relevant tools and disable the rest, instead of carrying every gstack
+# skill + every plugin in every session.
+#
+# Mechanism:
+#   - Skills (gstack/external/personal): symlink toggle skills/ ↔ skills-disabled/
+#   - Plugins: `claude plugin enable|disable <name>@<marketplace>`
+#   - MCPs: delegated to lib/toggle-external.sh for known servers (magic),
+#           advisory otherwise
+#   - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
+#
+# Always-on plugins (never toggled by `set`): caveman, security-guidance,
+# superpowers + rtk hook + .claude internal. The script refuses to disable
+# anything in PROTECTED_PLUGINS.
+#
+# Usage:
+#   profile.sh list                  list available profiles
+#   profile.sh show <name>           show contents of a profile
+#   profile.sh current               detect which profile is active
+#   profile.sh apply <name>          enable items in profile (additive)
+#   profile.sh set <name>            enable only profile (disables rest)
+#   profile.sh reset                 re-enable all gstack skills + managed plugins
+#   profile.sh diff <a> <b>          compare two profiles
+#
+# Profile file format (lib/profiles/<name>.profile):
+#   # DESC: <one-line description>
+#   <skill-name>                          # type defaults to "gstack"
+#   <skill-name>     personal             # personal skill (skills/<x>/SKILL.md is real)
+#   <skill-name>     external             # symlinked into skills-external/
+#   <plugin-name>    plugin@<marketplace> # Claude plugin — auto-toggle
+#   <mcp-name>       mcp                  # MCP — advisory or via toggle-external
+#   <cli-name>       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: "<skill>\t<type>"
+# 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@<marketplace>               — 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@<marketplace>
+      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@<marketplace>"
+      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 -- <command>"
+      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@<marketplace>"
+      ;;
+    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 <name>@<marketplace>  (or: profile apply <profile>)"
+}
+
+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 <<EOF
+profile.sh — partition Claude skills by purpose
+
+USAGE:
+  profile list              list all available profiles
+  profile show <name>       show profile contents + per-skill status
+  profile current           detect which profile is currently active
+  profile apply <name>      enable skills in profile (additive)
+  profile set <name>        enable only listed skills (disables rest of gstack)
+  profile reset             re-enable all gstack skills
+  profile diff <a> <b>      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 <<EOF
+
+EXAMPLES:
+  bash lib/profile.sh list
+  bash lib/profile.sh show design
+  bash lib/profile.sh set design       # only design skills active
+  bash lib/profile.sh apply qa         # add QA skills on top
+  bash lib/profile.sh reset            # restore everything
+
+NOTE:
+  Plugin and MCP entries print advisory commands — they are NOT toggled
+  automatically. Run "claude plugin enable|disable" or "claude mcp add|remove"
+  yourself for those.
+EOF
+}
+
+main() {
+  local cmd="${1:-}"
+  case "$cmd" in
+    list)    cmd_list ;;
+    show)    [ $# -ge 2 ] || { usage; exit 1; }; cmd_show "$2" ;;
+    current) cmd_current ;;
+    apply)   [ $# -ge 2 ] || { usage; exit 1; }; cmd_apply "$2" ;;
+    set)     [ $# -ge 2 ] || { usage; exit 1; }; cmd_set "$2" ;;
+    reset)   cmd_reset ;;
+    diff)    [ $# -ge 3 ] || { usage; exit 1; }; cmd_diff "$2" "$3" ;;
+    ""|-h|--help|help) usage ;;
+    *) err "Unknown command: $cmd"; usage; exit 1 ;;
+  esac
+}
+
+main "$@"

+ 28 - 0
lib/profiles/audit.profile

@@ -0,0 +1,28 @@
+# DESC: Comprehensive audit — security + SEO + GEO + W3C + perf + health
+# Activate when: doing a top-to-bottom audit pass on an existing project.
+# Wider than `seo` (adds security + dependency review).
+
+# Security
+cso
+harden                            personal
+analyze                           personal
+
+# SEO / GEO / web standards
+seo                               personal
+geo                               personal
+validate                          personal
+
+# Code + perf health
+health
+benchmark
+review
+
+# Browser tooling for live audits
+browse
+open-gstack-browser
+
+# Plugin: PR review toolkit (audit context for diffs)
+pr-review-toolkit                 plugin@claude-code-plugins
+
+# CLI: graphify for code structure
+graphify                          cli

+ 43 - 0
lib/profiles/backend.profile

@@ -0,0 +1,43 @@
+# DESC: Backend / API / system dev — no design, no SEO, focused on logic
+# Activate when: building backend services, APIs, CLIs, libraries, system
+# code, data pipelines. UI/visual work is out of scope. SEO/GEO out of scope.
+
+# Code work — primary
+feat                              personal
+ship-feature                      personal
+hotfix                            personal
+bugfix                            personal
+investigate
+refactor                          personal
+code-clean                        personal
+commit-change                     personal
+analyze                           personal
+
+# Ship + review + land
+ship
+review
+checkpoint
+land-and-deploy
+
+# Second opinion for hard problems
+codex
+
+# Security + health (always relevant for backend)
+cso
+health
+
+# Session hygiene
+careful
+freeze
+unfreeze
+guard
+learn
+retro
+
+# Plugin: PR review toolkit (pre-merge audit)
+pr-review-toolkit                 plugin@claude-code-plugins
+
+# CLIs (advisory)
+ctx7                              cli
+gsd                               cli
+graphify                          cli

+ 30 - 0
lib/profiles/design.profile

@@ -0,0 +1,30 @@
+# DESC: Design work — visual QA, design systems, mockups, polish
+# Activate when: building/reviewing UI, picking aesthetics, design tokens.
+# Companion CLIs (advisory): graphify (visual structure).
+
+# 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
+browse
+open-gstack-browser
+setup-browser-cookies
+
+# Plan-mode review companion (taste decisions before code)
+plan-ceo-review
+
+# External: Emil Kowalski's polish philosophy
+emil-design-eng                   external
+
+# Plugin (auto-toggle)
+ui-ux-pro-max                     plugin@ui-ux-pro-max-skill
+
+# MCP — auto-toggle via lib/toggle-external.sh (needs MAGIC_API_KEY in .env)
+magic                             mcp
+
+# CLIs (advisory only — installed/not-installed)
+graphify                          cli

+ 24 - 0
lib/profiles/dev.profile

@@ -0,0 +1,24 @@
+# DESC: Daily code work — small features, fixes, refactor, ship (any stack)
+# Activate when: implementing features, debugging, shipping PRs without a
+# specific frontend or backend bias. Lighter than `backend` (no security audit
+# baggage) and lighter than `web` (no design pipeline).
+
+# Implementation
+feat                              personal
+ship-feature                      personal
+ship
+
+# Bug fixing
+hotfix                            personal
+bugfix                            personal
+investigate
+
+# Code health
+review
+refactor                          personal
+code-clean                        personal
+commit-change                     personal
+
+# Session hygiene
+checkpoint
+land-and-deploy

+ 7 - 0
lib/profiles/minimal.profile

@@ -0,0 +1,7 @@
+# DESC: Strip all gstack skills (only essential personal skills + plugins remain)
+# Activate when: you want a quiet session with no gstack noise.
+#
+# Empty by design — `profile set minimal` disables every gstack skill,
+# leaves personal skills (analyze, doc, init-project, onboard, plugin-check,
+# refactor, seo, validate, harden, etc.) untouched, and makes no plugin/MCP
+# changes. To revert: `profile reset`.

+ 11 - 0
lib/profiles/qa.profile

@@ -0,0 +1,11 @@
+# DESC: QA + testing — site checks, perf, canary, validation
+# Activate when: running QA, checking deployed pages, perf regressions.
+
+qa
+qa-only
+browse
+benchmark
+canary
+validate                          personal
+open-gstack-browser
+setup-browser-cookies

+ 25 - 0
lib/profiles/seo.profile

@@ -0,0 +1,25 @@
+# DESC: SEO + GEO + W3C audit — search/AI indexability + standards
+# Activate when: auditing a site for Google/Bing indexability, AI search
+# (ChatGPT/Perplexity/Claude/Gemini), W3C HTML/CSS validity, WCAG a11y.
+# No design skills, no dev skills — pure audit pipeline.
+
+# SEO + GEO audits (personal)
+seo                               personal
+geo                               personal
+
+# W3C HTML/CSS validity + WCAG a11y
+validate                          personal
+
+# Web hardening (HSTS, CSP, redirects — affects ranking signals)
+harden                            personal
+
+# Code analysis (read structure for audit)
+analyze                           personal
+
+# Browser tooling — needed for live-page audits
+browse
+open-gstack-browser
+
+# Health check + benchmark — Core Web Vitals, perf signals
+health                                              # gstack
+benchmark                                           # gstack

+ 53 - 0
lib/profiles/web-full.profile

@@ -0,0 +1,53 @@
+# DESC: Production website — design + dev + SEO + GEO + W3C + perf
+# Activate when: shipping a public website end-to-end. Combines `web`
+# and `seo` profiles into a single set so the same session can build,
+# polish, audit, and verify.
+
+# === Design ===========================================================
+design-shotgun
+design-review
+design-consultation
+design-html
+plan-design-review
+
+# === Browser + dogfooding =============================================
+browse
+open-gstack-browser
+setup-browser-cookies
+
+# === Plan-mode reviews ================================================
+plan-ceo-review
+plan-eng-review
+
+# === Code work ========================================================
+feat                              personal
+ship-feature                      personal
+hotfix                            personal
+bugfix                            personal
+ship
+review
+checkpoint
+commit-change                     personal
+refactor                          personal
+
+# === SEO / GEO / standards ===========================================
+seo                               personal
+geo                               personal
+validate                          personal
+harden                            personal
+analyze                           personal
+
+# === Perf + canary ===================================================
+health
+benchmark
+canary
+qa-only
+
+# === External + plugin + MCP =========================================
+emil-design-eng                   external
+ui-ux-pro-max                     plugin@ui-ux-pro-max-skill
+magic                             mcp
+
+# === CLIs (advisory) =================================================
+ctx7                              cli
+graphify                          cli

+ 46 - 0
lib/profiles/web.profile

@@ -0,0 +1,46 @@
+# DESC: Public website work — frontend + content + light dev
+# Activate when: building/iterating a public-facing website (landing,
+# portfolio, marketing, blog) where SEO is NOT the immediate focus.
+# 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
+plan-design-review
+
+# Browser tooling for design + dogfooding
+browse
+open-gstack-browser
+setup-browser-cookies
+
+# Plan-mode reviews relevant to web work
+plan-ceo-review
+plan-eng-review
+
+# Code work skills — needed to actually build the site
+feat                              personal
+ship-feature                      personal
+hotfix                            personal
+ship                                                # gstack
+review                                              # gstack
+checkpoint                                          # gstack
+commit-change                     personal
+refactor                          personal
+
+# Validation companion (basic W3C/a11y check during build)
+validate                          personal
+
+# External: Emil Kowalski's polish philosophy
+emil-design-eng                   external
+
+# Plugin: UI/UX intelligence (auto-toggle)
+ui-ux-pro-max                     plugin@ui-ux-pro-max-skill
+
+# MCP: 21st-dev Magic component generator
+magic                             mcp
+
+# CLI: ctx7 (doc lookup for fast-evolving libs like Next.js)
+ctx7                              cli
+graphify                          cli

+ 6 - 0
lib/toggle-external.sh

@@ -22,6 +22,12 @@
 #   darwin-skill      — single symlink → ~/.agents/skills/darwin-skill
 #   find-skills       — single symlink → ~/.agents/skills/find-skills
 #   magic             — 21st-dev Magic MCP server (API key in .env)
+#
+# For fine-grained activation (only design skills, only qa skills, only
+# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
+#   bash lib/profile.sh list
+#   bash lib/profile.sh set <design|dev|qa|audit|minimal>
+#   bash lib/profile.sh reset
 # ============================================================
 set -euo pipefail
 

+ 119 - 0
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 <name> | current | apply <name> | set <name> | reset | diff <a> <b>
+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/<name>.profile` that
+lists items + types:
+
+| Type                    | Toggle mechanism |
+|-------------------------|------------------|
+| `gstack`                | symlink move skills/ ↔ skills-disabled/gstack__\<name\> |
+| `personal`              | symlink move skills/ ↔ skills-disabled/\<name\> (no prefix) |
+| `external`              | symlink move skills/ ↔ skills-disabled/\<name\> |
+| `plugin@<marketplace>`  | `claude plugin enable\|disable <name>@<marketplace>` (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 <name>
+
+# 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 <name>
+
+# Enable only skills in profile (disables non-listed gstack skills)
+bash "$HOME/.claude/lib/profile.sh" set <name>
+
+# 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 <a> <b>
+```
+
+## 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 <name>` — 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.