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:
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user