feat(superpowers): vendor the 7 wired skills at v6.4.1, drop the plugin

plugins.lock.json gains a superpowers entry (obra/superpowers @ 5bf4e78,
path skills, per-skill file lists, always_on) that lib/vendor-skills.sh
fetches byte-for-byte: brainstorming, writing-plans,
subagent-driven-development, test-driven-development,
requesting-code-review, using-git-worktrees, writing-skills. install-plugins
STEP 8e vendors it, update-all refreshes it at the pin, link.sh links the
seven, .gitignore ignores them. The plugin is no longer installed or
protected: its 8 other skills duplicated personal flows and its
SessionStart injection cost ~900 tokens per start, clear and compact.
detect_superpowers is one file test on the linked skill; doctor and
session-start stop charging the injection. doctor-vendored gains an
always_on class (third lock column) so always-on externals are
link-checked instead of reported parked.
This commit is contained in:
bastien
2026-09-28 14:54:52 +02:00
parent c39c0e1045
commit 18f8c898f8
12 changed files with 170 additions and 68 deletions
+4 -8
View File
@@ -16,14 +16,10 @@ detect_rtk() {
}
detect_superpowers() {
# Fast check: filesystem (plugin cache)
local cache_dir="$HOME/.claude/plugins/cache"
if [ -d "$cache_dir" ]; then
compgen -G "$cache_dir"/*superpowers* &>/dev/null && return 0
fi
# Slow fallback: CLI (only if fast check fails)
claude plugin list 2>/dev/null | grep -qi "superpowers" && return 0
return 1
# superpowers = 7 vendored skills since 2026-09-28; the plugin is gone.
# One file test on the linked vendored skill: proves vendored AND
# linked in one shot — no plugin cache glob, no `claude plugin list`.
[ -f "$HOME/.claude/skills/brainstorming/SKILL.md" ]
}
+53 -20
View File
@@ -5,8 +5,8 @@
# EXTERNAL_SKILLS array). doctor.sh's "GStack submodule" section only
# covers the gstack submodule — this covers the OTHER external skill
# packs (emil-design-eng, the agent-skills trio, the five Mengto scroll
# skills, and any name link.sh links with no lock entry at all, e.g.
# frontend-design, design-motion-principles).
# skills, the seven superpowers skills, and any name link.sh links with
# no lock entry at all, e.g. frontend-design, design-motion-principles).
#
# One entry point, `check_vendored_skills <repo> <claude_home>
# [profile_file]`, sourced and called by doctor.sh. Two things checked
@@ -21,7 +21,9 @@
# is passed — the "could not resolve the active profile" case),
# the <claude_home>/skills/<name> symlink points at
# <repo>/skills-external/<name>. A name absent from the profile is
# reported parked, not failed.
# reported parked, not failed — unless its lock entry is
# "always_on": true (the superpowers entry is), in which case the
# symlink is checked regardless of the profile (see _dv_check_link).
#
# Lock parsing via python3 argv (never string-spliced) — same pattern as
# lib/vendor-skills.sh's _vendor_read_lock. link.sh's EXTERNAL_SKILLS
@@ -61,11 +63,14 @@ fi
# _dv_lock_expectations <lockfile> — prints "<name>\t<file>" for every
# skill named under a plugins.lock.json entry whose "managed_by" is
# "curl": a bare list defaults each name to ["SKILL.md"]; a dict names
# its own per-skill file list; an entry with neither (the
# emil-design-eng single-file "path" shape) is itself the skill name,
# file "SKILL.md" (the literal "path" value is upstream layout, not the
# local dest — never used here). Reads the lockfile via argv only.
# "curl", plus a THIRD column "\t1" when that entry is "always_on": true
# (the superpowers entry is) — read by _dv_is_always_on, ignored by the
# $1==n {print $2} awk in _dv_check_files: a bare list defaults each name
# to ["SKILL.md"]; a dict names its own per-skill file list; an entry
# with neither (the emil-design-eng single-file "path" shape) is itself
# the skill name, file "SKILL.md" (the literal "path" value is upstream
# layout, not the local dest — never used here). Reads the lockfile via
# argv only.
# Every curl-managed entry's shape is validated ("skills" null, a list
# of str, or a dict of str -> list of str; "path" a str when present)
# BEFORE it is used, so a malformed entry is the same clean failure as
@@ -118,12 +123,13 @@ for key, entry in data.items():
sys.exit(1)
if not valid_skills(skills):
sys.exit(1)
suffix = "\t1" if entry.get("always_on") is True else ""
if skills is None:
print(f"{key}\tSKILL.md")
print(f"{key}\tSKILL.md{suffix}")
continue
for name, files in skill_files(skills).items():
for file in files:
print(f"{name}\t{file}")
print(f"{name}\t{file}{suffix}")
PY
}
@@ -196,16 +202,30 @@ allowlist — skipped"
[ "$all_ok" -eq 1 ]
}
# _dv_check_link <claude_home> <repo> <name> <profile_file> — when
# <profile_file> is non-empty and does not list <name>, reports it
# parked (info), not failed. Otherwise (listed, or no <profile_file> was
# passed — active profile could not be resolved, every external is then
# expected linked) checks the <claude_home>/skills/<name> symlink points
# at <repo>/skills-external/<name>.
# _dv_is_always_on <name> <lock_out> — true when <lock_out> (the
# "<name>\t<file>[\t1]" lines from _dv_lock_expectations) carries the
# always_on third column for <name>'s lock entry.
_dv_is_always_on() {
local name="$1" lock_out="$2"
awk -F'\t' -v n="$name" '$1 == n && $3 == 1 { found=1 } \
END { exit !found }' <<< "$lock_out"
}
# _dv_check_link <claude_home> <repo> <name> <profile_file> <always_on> —
# when <always_on> is "1" (the name's lock entry is "always_on": true),
# the symlink is checked whatever <profile_file> says — never parked.
# Otherwise, when <profile_file> is non-empty and does not list <name>,
# reports it parked (info), not failed. Otherwise (listed, always_on, or
# no <profile_file> was passed — active profile could not be resolved,
# every external is then expected linked) checks the
# <claude_home>/skills/<name> symlink points at
# <repo>/skills-external/<name>.
_dv_check_link() {
local claude_home="$1" repo="$2" name="$3" profile_file="$4"
local claude_home="$1" repo="$2" name="$3" profile_file="$4" \
always_on="$5"
local link target label
if [ -n "$profile_file" ] && ! _dv_profile_has "$profile_file" "$name"; then
if [ "$always_on" != "1" ] && [ -n "$profile_file" ] \
&& ! _dv_profile_has "$profile_file" "$name"; then
label="$(basename "$profile_file" .profile)"
info "$name: parked by profile $label"
return
@@ -220,6 +240,20 @@ lib/profile.sh apply <profile>)"
fi
}
# _dv_check_name <repo> <claude_home> <name> <profile_file> <lock_out> —
# per-name dispatch for check_vendored_skills's loop: files first (the
# link check runs only when every expected file is present, same as
# before), then the symlink, passing _dv_is_always_on's verdict as
# _dv_check_link's 5th param.
_dv_check_name() {
local repo="$1" claude_home="$2" name="$3" profile_file="$4" lock_out="$5"
local always_on=""
_dv_is_always_on "$name" "$lock_out" && always_on=1
_dv_check_files "$repo" "$name" "$lock_out" \
&& _dv_check_link "$claude_home" "$repo" "$name" "$profile_file" \
"$always_on"
}
# check_vendored_skills <repo> <claude_home> [profile_file] — see the
# file header. Either the lock or link.sh being unreadable (or a
# malformed lock entry — _dv_lock_expectations rc 1) is a warn, never a
@@ -251,7 +285,6 @@ check skipped"
item-name allowlist — skipped"
continue
fi
_dv_check_files "$repo" "$name" "$lock_out" \
&& _dv_check_link "$claude_home" "$repo" "$name" "$profile_file"
_dv_check_name "$repo" "$claude_home" "$name" "$profile_file" "$lock_out"
done <<< "$names"
}
+8 -6
View File
@@ -18,8 +18,10 @@
# and MCPs in the MANAGED_* allowlists are disabled when the profile
# does not list them — nothing outside those lists is ever auto-toggled.
#
# Always-on plugins (never toggled by `set`): security-guidance,
# superpowers + rtk hook + .claude internal. The script refuses to disable
# Always-on plugins (never toggled by `set`): security-guidance + rtk
# hook + .claude internal. superpowers is vendored skills now, not a
# plugin (never in PROTECTED_PLUGINS, never in MANAGED_EXTERNALS — same
# always-on class as darwin-skill). The script refuses to disable
# anything in PROTECTED_PLUGINS.
#
# Usage:
@@ -61,9 +63,10 @@ DEFAULT_PROFILE="full" # profile in force when none is selected (cache absent,
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
# enabled state is meaningfully driven by task type.
# never auto-disabled — protects always-on plugins (security-guidance;
# superpowers is vendored skills now, not a plugin) 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"
@@ -106,7 +109,6 @@ MANAGED_MCPS=()
# MANAGED_PLUGINS allowlist.)
PROTECTED_PLUGINS=(
"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'
+26 -7
View File
@@ -17,9 +17,11 @@
# "skills" is neither null/list/dict degrading the same way with no
# Python traceback leaking (LOCK_MALFORMED_ENTRY, rc 0), the
# profile-name allowlist rejecting a path-traversal value
# (REJECTS_BAD_PROFILE_NAME), and the item-name allowlist rejecting a
# (REJECTS_BAD_PROFILE_NAME), the item-name allowlist rejecting a
# link.sh entry with a ".." segment — warned and skipped, not failed
# (REJECTS_BAD_NAME).
# (REJECTS_BAD_NAME), and an "always_on": true lock entry's name, absent
# from the profile and with no symlink, checked (and failed) instead of
# reported parked (ALWAYS_ON_LINK_CHECKED).
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LIB="$ROOT/lib/doctor-vendored.sh"
@@ -57,6 +59,11 @@ cat > "$REPO/plugins.lock.json" <<'JSON'
"dict-entry": {
"managed_by": "curl",
"skills": {"dict-skill": ["SKILL.md", "references/notes.md"]}
},
"always-on-entry": {
"managed_by": "curl",
"always_on": true,
"skills": ["always-on-skill"]
}
}
JSON
@@ -66,25 +73,27 @@ cat > "$REPO/link.sh" <<'SH'
#!/usr/bin/env bash
EXTERNAL_SKILLS=(ok-skill missing-skill dict-skill
active-nolink-skill active-wronglink-skill
parked-skill noprofile-skill)
parked-skill noprofile-skill always-on-skill)
SH
# ── skills-external/ tree: every name's SKILL.md present, except
# missing-skill (nothing at all) and dict-skill's references/notes.md.
# always-on-skill has its SKILL.md too — only its symlink is missing.
for n in ok-skill dict-skill active-nolink-skill active-wronglink-skill \
parked-skill noprofile-skill; do
parked-skill noprofile-skill always-on-skill; do
mkdir -p "$REPO/skills-external/$n"
echo "v1" > "$REPO/skills-external/$n/SKILL.md"
done
# ── claude_home symlinks: ok-skill correct, active-wronglink-skill
# points elsewhere, active-nolink-skill and noprofile-skill have none.
# points elsewhere, active-nolink-skill, noprofile-skill and
# always-on-skill have none.
ln -sf "$REPO/skills-external/ok-skill" "$CLAUDE_HOME/skills/ok-skill"
mkdir -p "$WORK/elsewhere"
ln -sf "$WORK/elsewhere" "$CLAUDE_HOME/skills/active-wronglink-skill"
# ── active.profile: lists everything EXCEPT parked-skill and
# noprofile-skill (both proven absent from it).
# ── active.profile: lists everything EXCEPT parked-skill,
# noprofile-skill and always-on-skill (all three proven absent from it).
cat > "$REPO/active.profile" <<'PROF'
# DESC: fixture profile
ok-skill external
@@ -132,6 +141,16 @@ check_bool SYMLINK_PARKED \
grep -qF 'parked-skill: symlink missing/wrong' \
&& echo 1 || echo 0)"
# ── always-on-skill: absent from active.profile (same as parked-skill)
# but its lock entry is "always_on": true — checked (and failed, no
# symlink) instead of reported parked.
check_bool ALWAYS_ON_LINK_CHECKED \
"$(printf '%s' "$out1" | \
grep -qF 'always-on-skill: symlink missing/wrong' \
&& ! printf '%s' "$out1" | \
grep -qF 'always-on-skill: parked by profile' \
&& echo 1 || echo 0)"
# ── No profile file passed at all: noprofile-skill (absent from
# active.profile, parked above) must now be treated as expected-linked.
out2="$(check_vendored_skills "$REPO" "$CLAUDE_HOME" 2>&1)"
+3
View File
@@ -18,6 +18,9 @@
# `skills` as a bare list defaults every named skill to `["SKILL.md"]` and
# `path` to "skills" (the agent-skills shape); `skills` as a dict carries an
# explicit per-skill file list (references/*, etc.) and `path` is required.
# `"always_on": true` (optional) is ignored by this helper (fetch is the
# same either way) — lib/doctor-vendored.sh reads it to expect the
# entry's skills linked regardless of the active profile.
#
# Raw URL: https://raw.githubusercontent.com/<owner>/<repo>/<sha>/<path>/
# <skill>/<file>. VENDOR_BASE_URL overrides the "https://…/<repo>" prefix