feat(profile): default profile = full; reset applies it, current is label-driven
No profile selected (.active-profile absent, empty or legacy "none") now means the `full` profile is in force: DEFAULT_PROFILE declared once in lib/profile.sh, resolved by active_profile(); the statusline reads the constant and shows `full` instead of `?`; `gstack off` trims to it instead of erroring. `reset` goes to the default profile (= `set full`: enables its list, parks any non-listed gstack or managed item). `current` names the active label and scores that profile only, saying `default — not applied yet` until a set/apply/reset wrote the cache; the "none" sentinel and the cross-profile best-guess scan are gone (they keyed on the parked-gstack count, which says nothing under BDR-030's gstack-off default). Hermetic suite lib/tests/profile-default.test.sh (29 checks) seeds gstack as OFF like a real tree. Citers updated: profile SKILL, Makefile help, plugin-advisor PROFILE line + reset paragraph, toggle-external header.
This commit is contained in:
@@ -62,10 +62,10 @@ profile: ## Run profile.sh (usage: make profile cmd="set design")
|
|||||||
profile-list: ## List skill profiles (design, dev, qa, audit, minimal)
|
profile-list: ## List skill profiles (design, dev, qa, audit, minimal)
|
||||||
@bash lib/profile.sh list
|
@bash lib/profile.sh list
|
||||||
|
|
||||||
profile-current: ## Detect which skill profile is currently active
|
profile-current: ## Show the active profile (label + match)
|
||||||
@bash lib/profile.sh current
|
@bash lib/profile.sh current
|
||||||
|
|
||||||
profile-reset: ## Re-enable all gstack skills (undo any profile set)
|
profile-reset: ## Go to the default profile (full)
|
||||||
@bash lib/profile.sh reset
|
@bash lib/profile.sh reset
|
||||||
|
|
||||||
new-skill: ## Create a new skill scaffold (usage: make new-skill name=myskill)
|
new-skill: ## Create a new skill scaffold (usage: make new-skill name=myskill)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Output: `COMPLEXITY: <score>% — <label>` with one-line justification.
|
|||||||
```
|
```
|
||||||
PLUGIN CHECK
|
PLUGIN CHECK
|
||||||
ACTIVE: [plugin — status, one line each]
|
ACTIVE: [plugin — status, one line each]
|
||||||
PROFILE: [active skill profile — name + match%, or "custom"]
|
PROFILE: [active skill profile — name + match%, or "<name> (default — not applied yet …)"]
|
||||||
SIGNALS: [detected signals]
|
SIGNALS: [detected signals]
|
||||||
COMPLEXITY: <score>% — <simple|moderate|complex|enterprise>
|
COMPLEXITY: <score>% — <simple|moderate|complex|enterprise>
|
||||||
PLAN: <Max|Pro|Free (echoed from REQUEST) | unknown (not provided)> (budget: ~<N>t | n/a)
|
PLAN: <Max|Pro|Free (echoed from REQUEST) | unknown (not provided)> (budget: ~<N>t | n/a)
|
||||||
@@ -313,10 +313,9 @@ matching `profile set` command:
|
|||||||
| comprehensive audit (security + SEO + perf) | `audit` | `bash $HOME/.claude/lib/profile.sh set audit` |
|
| 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` |
|
| 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`.
|
To go back to the default profile: `bash $HOME/.claude/lib/profile.sh reset`
|
||||||
Plugin state is NOT touched by reset — re-enable a managed plugin manually
|
(= `set full`: enables full's list, parks non-listed gstack/managed items,
|
||||||
or by applying a profile that lists it (e.g. `apply web` to restore
|
toggles the managed plugins like any `set`).
|
||||||
`ui-ux-pro-max`).
|
|
||||||
|
|
||||||
## BLOCK if
|
## BLOCK if
|
||||||
|
|
||||||
|
|||||||
+9
-4
@@ -20,12 +20,17 @@ BRANCH_STR="${BRANCH:+ ($BRANCH)}"
|
|||||||
|
|
||||||
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# Default profile — same constant as lib/profile.sh's active_profile(),
|
||||||
|
# read without sourcing the lib (speed). Unreadable lib → literal fallback.
|
||||||
|
DEFAULT_PROFILE=$(sed -n 's/^DEFAULT_PROFILE="\([^"]*\)".*/\1/p' "$REPO/lib/profile.sh" 2>/dev/null)
|
||||||
|
[ -n "$DEFAULT_PROFILE" ] || DEFAULT_PROFILE=full
|
||||||
|
|
||||||
# Active profile (written by lib/profile.sh set|apply|reset to <repo>/.active-profile).
|
# Active profile (written by lib/profile.sh set|apply|reset to <repo>/.active-profile).
|
||||||
# Read directly — `profile.sh current` is 12s+ and unusable in a statusline.
|
# Read directly — `profile.sh current` is 12s+ and unusable in a statusline.
|
||||||
PROFILE="?"
|
# Cache absent/empty/legacy "none" (same rule as active_profile()) → default.
|
||||||
if [ -f "$REPO/.active-profile" ]; then
|
PROFILE=$(head -n1 "$REPO/.active-profile" 2>/dev/null | tr -d '[:space:]')
|
||||||
PROFILE=$(head -n1 "$REPO/.active-profile" | tr -d '[:space:]')
|
if [ -z "$PROFILE" ] || [ "$PROFILE" = "none" ]; then
|
||||||
[ -z "$PROFILE" ] && PROFILE="?"
|
PROFILE="$DEFAULT_PROFILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Effort level from settings.json (.effortLevel — set by /effort or manual edit).
|
# Effort level from settings.json (.effortLevel — set by /effort or manual edit).
|
||||||
|
|||||||
+79
-60
@@ -26,10 +26,11 @@
|
|||||||
# profile.sh list list available profiles
|
# profile.sh list list available profiles
|
||||||
# profile.sh show <name> show contents of a profile (grouped by type)
|
# profile.sh show <name> show contents of a profile (grouped by type)
|
||||||
# profile.sh show <name> --plain parsable type+name list (no status, no claude)
|
# profile.sh show <name> --plain parsable type+name list (no status, no claude)
|
||||||
# profile.sh current detect which profile is active
|
# profile.sh current report the active profile (label + match)
|
||||||
# profile.sh apply <name> enable items in profile (additive)
|
# profile.sh apply <name> enable items in profile (additive)
|
||||||
# profile.sh set <name> enable only profile (disables rest)
|
# profile.sh set <name> enable only profile (disables rest)
|
||||||
# profile.sh reset re-enable all gstack skills + managed plugins
|
# profile.sh reset go to the default profile (full): enable its
|
||||||
|
# list, park non-listed gstack/managed items
|
||||||
# profile.sh gstack on|off toggle gstack, keeping active-profile label
|
# profile.sh gstack on|off toggle gstack, keeping active-profile label
|
||||||
# profile.sh diff <a> <b> compare two profiles
|
# profile.sh diff <a> <b> compare two profiles
|
||||||
#
|
#
|
||||||
@@ -52,6 +53,7 @@ DISABLED_DIR="$REPO/skills-disabled"
|
|||||||
GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills
|
GSTACK_SRC="$REPO/skills-external/gstack" # gstack submodule — source of truth for gstack skills
|
||||||
PROFILES_DIR="$REPO/lib/profiles"
|
PROFILES_DIR="$REPO/lib/profiles"
|
||||||
ACTIVE_CACHE="$REPO/.active-profile" # statusline reads this — keep fast (single-line file, profile name only)
|
ACTIVE_CACHE="$REPO/.active-profile" # statusline reads this — keep fast (single-line file, profile name only)
|
||||||
|
DEFAULT_PROFILE="full" # profile in force when none is selected (cache absent, empty, or legacy "none")
|
||||||
|
|
||||||
# Plugins that are toggle-managed by `set`. Anything NOT in this list is
|
# Plugins that are toggle-managed by `set`. Anything NOT in this list is
|
||||||
# never auto-disabled — protects always-on plugins (security-guidance,
|
# never auto-disabled — protects always-on plugins (security-guidance,
|
||||||
@@ -108,6 +110,26 @@ write_active() {
|
|||||||
printf '%s\n' "$name" > "$ACTIVE_CACHE" 2>/dev/null || true
|
printf '%s\n' "$name" > "$ACTIVE_CACHE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# First line of the cache, all whitespace stripped (tr -d, CRLF-proof).
|
||||||
|
# Empty string when the cache is missing or empty. Never trips
|
||||||
|
# `set -euo pipefail` (head's failure on a missing file is absorbed).
|
||||||
|
read_cache() {
|
||||||
|
head -n1 "$ACTIVE_CACHE" 2>/dev/null | tr -d '[:space:]' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# The profile actually in force: the cached label, or DEFAULT_PROFILE when
|
||||||
|
# the cache is absent, empty, or the legacy "none" sentinel. The ONE place
|
||||||
|
# that resolves "no profile selected" — every reader of the cache goes
|
||||||
|
# through this (or read_cache() when it needs the raw value).
|
||||||
|
active_profile() {
|
||||||
|
local cached; cached="$(read_cache)"
|
||||||
|
if [ -z "$cached" ] || [ "$cached" = "none" ]; then
|
||||||
|
printf '%s' "$DEFAULT_PROFILE"
|
||||||
|
else
|
||||||
|
printf '%s' "$cached"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Profile parsing ────────────────────────────────────────
|
# ── Profile parsing ────────────────────────────────────────
|
||||||
|
|
||||||
# Read a profile file. Output one line per entry: "<skill>\t<type>"
|
# Read a profile file. Output one line per entry: "<skill>\t<type>"
|
||||||
@@ -258,6 +280,25 @@ skill_status() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# How much of one profile is actually available right now. An item counts
|
||||||
|
# as available when its status is "enabled" (skills, plugins, MCPs) or
|
||||||
|
# "installed" (CLIs). Echo: "<available> <total>" — total 0 for an empty
|
||||||
|
# profile. Used by cmd_current to score the ACTIVE label only (BDR-030:
|
||||||
|
# scanning every profile for a best guess doesn't apply — gstack is off by
|
||||||
|
# default, so a parked count says nothing about which profile is on).
|
||||||
|
profile_match() {
|
||||||
|
local prof="$1" skill type status
|
||||||
|
local 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 "$prof")
|
||||||
|
printf '%d %d\n' "$available" "$total"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Enable / disable ──────────────────────────────────────
|
# ── Enable / disable ──────────────────────────────────────
|
||||||
|
|
||||||
enable_skill() {
|
enable_skill() {
|
||||||
@@ -585,39 +626,37 @@ cmd_set() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_reset() {
|
cmd_reset() {
|
||||||
info "Re-enabling all gstack skills (move skills-disabled/gstack__* back)"
|
info "Resetting to the default profile: $DEFAULT_PROFILE (exclusive — enables its list, parks any non-listed gstack or managed item currently on)"
|
||||||
enable_all_gstack
|
cmd_set "$DEFAULT_PROFILE"
|
||||||
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>)"
|
|
||||||
write_active "none"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# gstack on|off — focused gstack-only toggle that keeps the active-profile
|
# gstack on|off — focused gstack-only toggle that keeps the active-profile
|
||||||
# label intact (unlike reset, which clears it to "none"). Lets the user
|
# label intact (unlike reset, which switches to the default profile). Lets
|
||||||
# layer all gstack on top of their current profile, or trim it back down
|
# the user layer all gstack on top of their current profile, or trim it
|
||||||
# to just what the active profile needs.
|
# back down to just what the active profile needs.
|
||||||
cmd_gstack() {
|
cmd_gstack() {
|
||||||
local action="${1:-}"
|
local action="${1:-}"
|
||||||
case "$action" in
|
case "$action" in
|
||||||
on)
|
on)
|
||||||
# Re-enable ALL gstack skills, but DON'T touch active-profile — the
|
# Restore whatever is parked, but DON'T touch active-profile — the
|
||||||
# user is adding gstack on top of their current profile, not clearing it.
|
# user is adding gstack on top of their current profile, not clearing it.
|
||||||
local parked
|
local parked
|
||||||
parked="$(parked_gstack_count)"
|
parked="$(parked_gstack_count)"
|
||||||
if [ "$parked" -eq 0 ]; then
|
if [ "$parked" -eq 0 ]; then
|
||||||
info "all gstack skills already enabled"
|
info "nothing parked — gstack skills are linked per profile (set/apply/reset)"
|
||||||
else
|
else
|
||||||
enable_all_gstack
|
enable_all_gstack
|
||||||
ok "all gstack enabled ($parked skills restored)"
|
ok "$parked parked gstack skills restored"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
off)
|
off)
|
||||||
# Disable gstack skills not needed by the active profile. Needs a real
|
# Disable gstack skills not needed by the active profile. A cache
|
||||||
# active profile to know what to keep.
|
# naming a profile with no lib/profiles/<name>.profile still errors;
|
||||||
|
# absent/empty/"none" resolve to the default profile via
|
||||||
|
# active_profile() and no longer hit that error.
|
||||||
local active
|
local active
|
||||||
active="$(head -n1 "$ACTIVE_CACHE" 2>/dev/null || echo none)"
|
active="$(active_profile)"
|
||||||
[ -z "$active" ] && active="none"
|
if [ ! -f "$PROFILES_DIR/$active.profile" ]; then
|
||||||
if [ "$active" = "none" ] || [ ! -f "$PROFILES_DIR/$active.profile" ]; then
|
|
||||||
err "no active profile — 'gstack off' needs one to know what to keep"
|
err "no active profile — 'gstack off' needs one to know what to keep"
|
||||||
info "run: bash lib/profile.sh set <name> then: gstack off"
|
info "run: bash lib/profile.sh set <name> then: gstack off"
|
||||||
return 1
|
return 1
|
||||||
@@ -640,48 +679,27 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_current() {
|
cmd_current() {
|
||||||
# A profile is "active" only if (a) most of its skills are enabled AND
|
# Label-driven (BDR-030): name active_profile() and score ONLY that
|
||||||
# (b) at least one non-listed gstack skill is currently disabled (i.e. a
|
# profile — no cross-profile best-guess scan, no fast path keyed on the
|
||||||
# `set` has actually been applied). Without (b), every profile reports
|
# parked-gstack count (that count says nothing about which profile is on
|
||||||
# 100% trivially because the full gstack is on.
|
# when gstack starts off, as it does on a real tree).
|
||||||
local disabled_count=0
|
local label; label="$(active_profile)"
|
||||||
if [ -d "$DISABLED_DIR" ]; then
|
if [ ! -f "$PROFILES_DIR/$label.profile" ]; then
|
||||||
disabled_count=$(find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' ')
|
echo "$label (unknown profile — no lib/profiles/$label.profile; run: profile reset)"
|
||||||
fi
|
|
||||||
if [ "$disabled_count" -eq 0 ]; then
|
|
||||||
echo "none (all gstack skills enabled — no profile set)"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
# Pick the profile with the highest "available" ratio. An item counts as
|
|
||||||
# available when its status is "enabled" (skills, plugins, MCPs) or
|
local available total pct
|
||||||
# "installed" (CLIs). On ties, the profile with the larger total wins
|
read -r available total <<<"$(profile_match "$label")"
|
||||||
# — superset profiles describe state more completely than subsets.
|
pct=0
|
||||||
local f name total available score skill type status
|
[ "$total" -gt 0 ] && pct=$((available * 100 / total))
|
||||||
local best="" best_score=0 best_total=0
|
local parked; parked="$(parked_gstack_count)"
|
||||||
for f in "$PROFILES_DIR"/*.profile; do
|
|
||||||
[ -f "$f" ] || continue
|
local cached; cached="$(read_cache)"
|
||||||
name="$(basename "$f" .profile)"
|
if [ -z "$cached" ] || [ "$cached" = "none" ]; then
|
||||||
total=0; available=0
|
echo "$label (default — not applied yet, ${pct}% of its items enabled; run: profile reset)"
|
||||||
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
|
else
|
||||||
echo "custom (best guess: ${best:-none} ${best_score}%, $disabled_count gstack skills disabled)"
|
echo "$label (${pct}% match, $parked gstack skills disabled)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,10 +726,11 @@ USAGE:
|
|||||||
profile list list all available profiles
|
profile list list all available profiles
|
||||||
profile show <name> show profile contents grouped by type + status
|
profile show <name> show profile contents grouped by type + status
|
||||||
profile show <name> --plain parsable type+name list (no status, no claude)
|
profile show <name> --plain parsable type+name list (no status, no claude)
|
||||||
profile current detect which profile is currently active
|
profile current report the active profile (label + match)
|
||||||
profile apply <name> enable skills in profile (additive)
|
profile apply <name> enable skills in profile (additive)
|
||||||
profile set <name> enable only listed skills (disables rest of gstack)
|
profile set <name> enable only listed skills (disables rest of gstack)
|
||||||
profile reset re-enable all gstack skills
|
profile reset go to the default profile (full): enable its list,
|
||||||
|
park non-listed gstack/managed items
|
||||||
profile gstack on|off toggle gstack only, keep active-profile label
|
profile gstack on|off toggle gstack only, keep active-profile label
|
||||||
profile diff <a> <b> compare two profiles
|
profile diff <a> <b> compare two profiles
|
||||||
|
|
||||||
@@ -731,7 +750,7 @@ EXAMPLES:
|
|||||||
bash lib/profile.sh show design
|
bash lib/profile.sh show design
|
||||||
bash lib/profile.sh set design # only design skills active
|
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 apply qa # add QA skills on top
|
||||||
bash lib/profile.sh reset # restore everything
|
bash lib/profile.sh reset # back to the default profile (full)
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
"set" toggles the MANAGED items automatically, both ways: plugins
|
"set" toggles the MANAGED items automatically, both ways: plugins
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/tests/profile-default.test.sh — default profile (full) when nothing is
|
||||||
|
# selected: `current` is label-driven (BDR-030 — gstack starts off, so a
|
||||||
|
# parked count says nothing about which profile is on), `reset` lands on
|
||||||
|
# DEFAULT_PROFILE, `gstack off` resolves the default through active_profile(),
|
||||||
|
# and hooks/statusline.sh falls back to the same constant. Covers contract
|
||||||
|
# criteria 2/3/4/5. Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude`
|
||||||
|
# on PATH — same harness as profile-set-managed.test.sh.
|
||||||
|
set -u
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
pass=0; fail=0
|
||||||
|
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
|
||||||
|
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
|
||||||
|
check_has() { case "$2" in *"$3"*) pass=$((pass+1));; *) fail=$((fail+1));
|
||||||
|
printf 'FAIL %s: [%s] does not contain [%s]\n' "$1" "$2" "$3";; esac; }
|
||||||
|
check_not() { case "$2" in *"$3"*) fail=$((fail+1));
|
||||||
|
printf 'FAIL %s: [%s] unexpectedly contains [%s]\n' "$1" "$2" "$3";; *) pass=$((pass+1));; esac; }
|
||||||
|
|
||||||
|
FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT
|
||||||
|
mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \
|
||||||
|
"$FX/hooks" "$FX/skills-external/emil-design-eng"
|
||||||
|
for g in gs-a gs-b gs-c; do
|
||||||
|
mkdir -p "$FX/skills-external/gstack/$g"
|
||||||
|
touch "$FX/skills-external/gstack/$g/SKILL.md"
|
||||||
|
done
|
||||||
|
cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" "$FX/lib/"
|
||||||
|
cp "$ROOT/hooks/statusline.sh" "$FX/hooks/"
|
||||||
|
|
||||||
|
cat > "$FX/lib/profiles/full.profile" <<'EOF'
|
||||||
|
gs-a
|
||||||
|
gs-b
|
||||||
|
emil-design-eng external
|
||||||
|
EOF
|
||||||
|
cat > "$FX/lib/profiles/otherish.profile" <<'EOF'
|
||||||
|
gs-c
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Fake claude: logs every call, prints nothing — skill_status greps for
|
||||||
|
# "✔ enabled" and finds nothing, so every managed plugin reads back
|
||||||
|
# "disabled" and set/reset never touch real plugin state.
|
||||||
|
cat > "$FX/bin/claude" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
FX="$FX"
|
||||||
|
echo "\$*" >> "\$FX/claude-calls.log"
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$FX/bin/claude"
|
||||||
|
|
||||||
|
run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
|
||||||
|
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" "$@"; }
|
||||||
|
statusline() { echo '{}' | bash "$FX/hooks/statusline.sh"; }
|
||||||
|
first_word() { printf '%s' "${1%% *}"; }
|
||||||
|
|
||||||
|
# --- T1: clean seed, no cache — current names the default, "not applied" ---
|
||||||
|
out="$(run current)"
|
||||||
|
check T1-first-word "$(first_word "$out")" full
|
||||||
|
check_has T1-not-applied "$out" "default — not applied yet"
|
||||||
|
check_not T1-no-all-gstack "$out" "all gstack"
|
||||||
|
|
||||||
|
# --- T2: clean seed, no cache — gstack off is a clean no-op (rc 0) ---
|
||||||
|
run gstack off >/dev/null 2>&1; rc=$?
|
||||||
|
check T2-rc "$rc" 0
|
||||||
|
|
||||||
|
# --- T2b: one gstack skill manually linked, no cache — off parks just it ---
|
||||||
|
ln -s "$FX/skills-external/gstack/gs-c" "$FX/skills/gs-c"
|
||||||
|
run gstack off >/dev/null 2>&1; rc=$?
|
||||||
|
check T2b-rc "$rc" 0
|
||||||
|
check T2b-gsc-parked "$([ -e "$FX/skills-disabled/gstack__gs-c" ] && echo p || echo n)" p
|
||||||
|
check T2b-gsa-absent "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" off
|
||||||
|
rm -rf "$FX/skills-disabled/gstack__gs-c" # back to the clean seed
|
||||||
|
|
||||||
|
# --- T3: cache "none" (CRLF) — off is still a clean no-op ---
|
||||||
|
printf 'none\r\n' > "$FX/.active-profile"
|
||||||
|
run gstack off >/dev/null 2>&1; rc=$?
|
||||||
|
check T3-rc "$rc" 0
|
||||||
|
|
||||||
|
# --- T4: cache names an unknown profile — off errors, current says so ---
|
||||||
|
printf 'ghost\n' > "$FX/.active-profile"
|
||||||
|
run gstack off >/dev/null 2>&1; rc=$?
|
||||||
|
check T4-rc "$rc" 1
|
||||||
|
out="$(run current)"
|
||||||
|
check T4-first-word "$(first_word "$out")" ghost
|
||||||
|
check_has T4-unknown "$out" "unknown profile"
|
||||||
|
|
||||||
|
# --- T5: reset lands on the default profile, exclusively ---
|
||||||
|
run reset >/dev/null 2>&1
|
||||||
|
check T5-cache "$(cat "$FX/.active-profile" 2>/dev/null)" full
|
||||||
|
check T5-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
|
||||||
|
check T5-gsb-on "$([ -e "$FX/skills/gs-b" ] && echo on || echo off)" on
|
||||||
|
check T5-gsc-off "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" off
|
||||||
|
check T5-emil-on "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
|
||||||
|
out="$(run current)"
|
||||||
|
check T5-first-word "$(first_word "$out")" full
|
||||||
|
check_has T5-match "$out" "100% match"
|
||||||
|
check_not T5-not-applied "$out" "not applied"
|
||||||
|
|
||||||
|
# --- T6: current is label-driven even for a gstack-only profile ---
|
||||||
|
run set otherish >/dev/null 2>&1
|
||||||
|
out="$(run current)"
|
||||||
|
check T6-first-word "$(first_word "$out")" otherish
|
||||||
|
run reset >/dev/null 2>&1
|
||||||
|
check T6-gsc-parked "$([ -e "$FX/skills-disabled/gstack__gs-c" ] && echo p || echo n)" p
|
||||||
|
check T6-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
|
||||||
|
check T6-cache "$(cat "$FX/.active-profile" 2>/dev/null)" full
|
||||||
|
|
||||||
|
# --- T7: gstack on restores parked skills, keeps the profile label ---
|
||||||
|
run set otherish >/dev/null 2>&1
|
||||||
|
run gstack on >/dev/null 2>&1
|
||||||
|
out="$(run current)"
|
||||||
|
check T7-first-word "$(first_word "$out")" otherish
|
||||||
|
check_not T7-no-default "$out" "default"
|
||||||
|
|
||||||
|
# --- T8-T11: statusline fallback + constant read ---
|
||||||
|
rm -f "$FX/.active-profile"
|
||||||
|
out="$(statusline)"
|
||||||
|
check_has T8-full "$out" "profile: full"
|
||||||
|
|
||||||
|
printf 'otherish\n' > "$FX/.active-profile"
|
||||||
|
out="$(statusline)"
|
||||||
|
check_has T9-otherish "$out" "profile: otherish"
|
||||||
|
|
||||||
|
printf ' none \r' > "$FX/.active-profile"
|
||||||
|
out="$(statusline)"
|
||||||
|
check_has T10-full "$out" "profile: full"
|
||||||
|
|
||||||
|
sed -i 's/^DEFAULT_PROFILE="full"/DEFAULT_PROFILE="otherish"/' "$FX/lib/profile.sh"
|
||||||
|
rm -f "$FX/.active-profile"
|
||||||
|
out="$(statusline)"
|
||||||
|
check_has T11-otherish "$out" "profile: otherish"
|
||||||
|
|
||||||
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
|
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
|
||||||
# bash lib/profile.sh list
|
# bash lib/profile.sh list
|
||||||
# bash lib/profile.sh set <design|dev|qa|audit|minimal>
|
# bash lib/profile.sh set <design|dev|qa|audit|minimal>
|
||||||
# bash lib/profile.sh reset
|
# bash lib/profile.sh reset # back to the default profile (full)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
+14
-6
@@ -72,6 +72,12 @@ auto-touched. gstack works the same
|
|||||||
all the way down: a profile listing gstack skills while the whole pack is
|
all the way down: a profile listing gstack skills while the whole pack is
|
||||||
off (via `toggle-external.sh`) re-enables JUST those skills on demand.
|
off (via `toggle-external.sh`) re-enables JUST those skills on demand.
|
||||||
|
|
||||||
|
**Default profile**: `full` is in force whenever `.active-profile` is
|
||||||
|
absent, empty, or the legacy `none` (statusline, `current`, `gstack off`,
|
||||||
|
`reset` all resolve it the same way). `reset` applies it (`set full`,
|
||||||
|
exclusive); a fresh `make plugin` applies it too when nothing has ever
|
||||||
|
been selected.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -81,7 +87,7 @@ bash "$HOME/.claude/lib/profile.sh" list
|
|||||||
# Show profile contents + per-skill status
|
# Show profile contents + per-skill status
|
||||||
bash "$HOME/.claude/lib/profile.sh" show <name>
|
bash "$HOME/.claude/lib/profile.sh" show <name>
|
||||||
|
|
||||||
# Detect which profile is currently active
|
# Report the active profile (label + match %)
|
||||||
bash "$HOME/.claude/lib/profile.sh" current
|
bash "$HOME/.claude/lib/profile.sh" current
|
||||||
|
|
||||||
# Enable skills in profile (additive — keeps others enabled)
|
# Enable skills in profile (additive — keeps others enabled)
|
||||||
@@ -90,11 +96,11 @@ bash "$HOME/.claude/lib/profile.sh" apply <name>
|
|||||||
# Enable only skills in profile (disables non-listed gstack skills)
|
# Enable only skills in profile (disables non-listed gstack skills)
|
||||||
bash "$HOME/.claude/lib/profile.sh" set <name>
|
bash "$HOME/.claude/lib/profile.sh" set <name>
|
||||||
|
|
||||||
# Re-enable every gstack skill (undo any set/apply) — resets active label to "none"
|
# Go to the default profile (full) — exclusive, same as `set full`
|
||||||
bash "$HOME/.claude/lib/profile.sh" reset
|
bash "$HOME/.claude/lib/profile.sh" reset
|
||||||
|
|
||||||
# Toggle gstack only, keeping the active-profile label intact
|
# Toggle gstack only, keeping the active-profile label intact
|
||||||
bash "$HOME/.claude/lib/profile.sh" gstack on # re-enable ALL gstack on top of current profile
|
bash "$HOME/.claude/lib/profile.sh" gstack on # restore parked gstack skills on top of the current profile
|
||||||
bash "$HOME/.claude/lib/profile.sh" gstack off # disable gstack skills not in the active profile
|
bash "$HOME/.claude/lib/profile.sh" gstack off # disable gstack skills not in the active profile
|
||||||
|
|
||||||
# Compare two profiles
|
# Compare two profiles
|
||||||
@@ -119,16 +125,18 @@ bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS
|
|||||||
| `lib/profile.sh` absent (foreign machine, links broken) | `test -f "$HOME/.claude/lib/profile.sh"` before any verb; missing → propose `bash link.sh` from the config repo | STOP — never hand-move symlinks to emulate the script |
|
| `lib/profile.sh` absent (foreign machine, links broken) | `test -f "$HOME/.claude/lib/profile.sh"` before any verb; missing → propose `bash link.sh` from the config repo | STOP — never hand-move symlinks to emulate the script |
|
||||||
| Unknown profile name (rc=1, `✗ Profile not found`) | Show `list` output + the closest existing name ("`desing` → did you mean `design`?") | Let the user pick — never guess-and-`set` |
|
| Unknown profile name (rc=1, `✗ Profile not found`) | Show `list` output + the closest existing name ("`desing` → did you mean `design`?") | Let the user pick — never guess-and-`set` |
|
||||||
| Unknown verb (rc=1 + usage) | Re-map the request to the argument-hint verbs, retry once | Show usage, ask |
|
| Unknown verb (rc=1 + usage) | Re-map the request to the argument-hint verbs, retry once | Show usage, ask |
|
||||||
| `set`/`apply` exits nonzero MID-TOGGLE (permission, plugin CLI failure) | State may be PARTIAL. Run `current` to show what actually took; name the failed item from the script's output | Offer `reset` as recovery to a known state; never blind-rerun `set` on top of partial state |
|
| `set`/`apply` exits nonzero MID-TOGGLE (permission, plugin CLI failure) | State may be PARTIAL. Run `current` to show what actually took; name the failed item from the script's output | run `current`, then re-run `set <name>` or `reset` (both are full exclusive applies, not an always-safe undo) |
|
||||||
| Plugin/MCP leg fails (marketplace/network) while symlink leg succeeded | Report the split state explicitly + print the manual `claude plugin`/`claude mcp` command for the failed leg | — |
|
| Plugin/MCP leg fails (marketplace/network) while symlink leg succeeded | Report the split state explicitly + print the manual `claude plugin`/`claude mcp` command for the failed leg | — |
|
||||||
| `current` says `none` right after a successful `set <name>` | Contradiction — do not trust either; show the raw script output to the user | Known failure family (BLK: symlink resolution in `cmd_current`) — report, don't hand-patch |
|
| `current` names a profile other than the one just set | Cache (`.active-profile`) was written by another tool or hand-edited — show the raw script output to the user | Never hand-patch `.active-profile` — re-run `set <name>` (or `reset`) to force it back |
|
||||||
|
|
||||||
## Output policy
|
## Output policy
|
||||||
|
|
||||||
- After `set` / `apply` / `reset` / `gstack on|off`: show the count of skills
|
- After `set` / `apply` / `reset` / `gstack on|off`: show the count of skills
|
||||||
moved + tell the user to start a new Claude session to pick up the changes
|
moved + tell the user to start a new Claude session to pick up the changes
|
||||||
(Claude scans `skills/` at session start).
|
(Claude scans `skills/` at session start).
|
||||||
- After `current`: report the active profile + match percentage.
|
- After `current`: report the active profile + match %; `default — not
|
||||||
|
applied yet` means the default profile is in force but not yet applied —
|
||||||
|
point the user at `reset`.
|
||||||
- After `show`: render the grouped output directly — no extra commentary unless
|
- After `show`: render the grouped output directly — no extra commentary unless
|
||||||
the user asks.
|
the user asks.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user