forked from bchanot/claude
feat(profile): set symmetric on managed externals + MCPs (BDR-079)
- MANAGED_EXTERNALS (emil-design-eng, frontend-design, design-motion-principles, impeccable) + MANAGED_MCPS (magic): cmd_set now trims both when the profile does not list them — design leftovers no longer survive a 'set backend' - cmd_set refactored to 4 symmetric trim helpers; nothing outside the MANAGED_* allowlists is ever auto-toggled (darwin-skill manual) - enable_skill external: from-source fallback (ln -sf skills-external/<name>), mirrors toggle-external.sh - stale usage() NOTE + SKILL.md updated to the both-ways reality - hermetic test: 16 checks, fixture repo + fake claude shim (gstack on-demand, from-source, park/restore, magic add/remove, non-managed untouched); shellcheck + full make test green
This commit is contained in:
@@ -1065,3 +1065,6 @@ Supersedes BDR-076 scope + amends BDR-066. Doctrine: session model (Fable) = mai
|
|||||||
### BDR-078 — ctx7 coverage: central fast-libs list + once-per-session reminder hook; every code path covered [accepted] (2026-07-20)
|
### BDR-078 — ctx7 coverage: central fast-libs list + once-per-session reminder hook; every code path covered [accepted] (2026-07-20)
|
||||||
Refines BDR-053 (single surface). Audit 2026-07-20: coverage PARTIAL — find-docs fired on user doc-questions only; ship-feature 0c / init-project 5c pre-fetched; /feat //bugfix executors + ad-hoc coding NEVER consulted ctx7; fast-libs list hardcoded 3× (drift risk). 4 closures shipped: (a) find-docs description += BEFORE-writing-code trigger (fast-moving lib, even without doc question, unless fresh cache) + cache-first rule in body (tee fetched docs to .ctx7-cache/); (b) feater+bugfixer briefs += fast-lib docs rule — read fresh `.ctx7-cache/<lib>*.md`, else `npx ctx7@latest` fetch max 2 topics, else `ctx7 cache miss: <lib>` in NOTES + proceed (executors lack Skill tool → Bash path); (c) hooks/ctx7-reminder.sh UserPromptSubmit — ONE fire/session (sentinel on session_id), only when project manifest carries fast-libs; reports cache state; skips <task-notification> turns; always exit 0; (d) lib/fast-libs.sh = SINGLE SOURCE (detect / cache-status verbs, JS package.json anchored full-key match + Python requirements/pyproject, 7-day freshness, LC_ALL=C sort locale-independent) consumed by hook + 3 pipeline skills + 2 briefs. 2nd session surface DELIBERATE, not a BDR-053 reversal: 053 killed a 490-tok ALWAYS-ON rule duplicate; hook costs ~0 quiet, 1 line once when fast-libs present. Alternatives rejected: PreToolUse Edit/Write gate (fires per-edit = noise); description-only fix (probabilistic, executors unreachable). Tests: lib/tests/fast-libs.test.sh 11 checks (anchored/near-miss/py/none, cache fresh/stale/missing, hook fire/sentinel/quiet×2); shellcheck + full make test green. Branch feature/ctx7-coverage, unmerged (human gate).
|
Refines BDR-053 (single surface). Audit 2026-07-20: coverage PARTIAL — find-docs fired on user doc-questions only; ship-feature 0c / init-project 5c pre-fetched; /feat //bugfix executors + ad-hoc coding NEVER consulted ctx7; fast-libs list hardcoded 3× (drift risk). 4 closures shipped: (a) find-docs description += BEFORE-writing-code trigger (fast-moving lib, even without doc question, unless fresh cache) + cache-first rule in body (tee fetched docs to .ctx7-cache/); (b) feater+bugfixer briefs += fast-lib docs rule — read fresh `.ctx7-cache/<lib>*.md`, else `npx ctx7@latest` fetch max 2 topics, else `ctx7 cache miss: <lib>` in NOTES + proceed (executors lack Skill tool → Bash path); (c) hooks/ctx7-reminder.sh UserPromptSubmit — ONE fire/session (sentinel on session_id), only when project manifest carries fast-libs; reports cache state; skips <task-notification> turns; always exit 0; (d) lib/fast-libs.sh = SINGLE SOURCE (detect / cache-status verbs, JS package.json anchored full-key match + Python requirements/pyproject, 7-day freshness, LC_ALL=C sort locale-independent) consumed by hook + 3 pipeline skills + 2 briefs. 2nd session surface DELIBERATE, not a BDR-053 reversal: 053 killed a 490-tok ALWAYS-ON rule duplicate; hook costs ~0 quiet, 1 line once when fast-libs present. Alternatives rejected: PreToolUse Edit/Write gate (fires per-edit = noise); description-only fix (probabilistic, executors unreachable). Tests: lib/tests/fast-libs.test.sh 11 checks (anchored/near-miss/py/none, cache fresh/stale/missing, hook fire/sentinel/quiet×2); shellcheck + full make test green. Branch feature/ctx7-coverage, unmerged (human gate).
|
||||||
Amendment (same session): skills/find-docs = machine-owned dist (gitignored, ctx7 regenerates on fresh clone) → durable copy of closure (a) lives in install-plugins.sh STEP ctx7 (idempotent grep-guarded python patch, fixture-verified); live SKILL.md carries the same edit uncommitted by design.
|
Amendment (same session): skills/find-docs = machine-owned dist (gitignored, ctx7 regenerates on fresh clone) → durable copy of closure (a) lives in install-plugins.sh STEP ctx7 (idempotent grep-guarded python patch, fixture-verified); live SKILL.md carries the same edit uncommitted by design.
|
||||||
|
|
||||||
|
### BDR-079 — profile `set` symmetric on managed externals + MCPs [accepted] (2026-07-20)
|
||||||
|
Audit (user ask "profile toggles externals both ways?"): ASYMMETRIC. Enable side OK — gstack on-demand from submodule when pack off (shared `skills-disabled/gstack__*` convention with toggle-external.sh, interoperable), externals restored from parked, magic delegated to toggle-external. Disable side MISSING: `cmd_set` trimmed only gstack + MANAGED_PLUGINS → `set backend` left emil/frontend-design/design-motion/impeccable active + magic registered; SKILL.md claimed both-ways toggle (true only at enable). Shipped: (1) `MANAGED_EXTERNALS` (emil-design-eng, frontend-design, design-motion-principles, impeccable = exact union of profile `external` usage; darwin-skill excluded — not task-type-driven) + `MANAGED_MCPS` (magic) allowlists, same doctrine as MANAGED_PLUGINS; (2) cmd_set refactored to 4 trim helpers (`disable_{gstack,plugins,externals,mcps}_not_in`) — symmetric, nothing outside allowlists ever auto-touched; (3) enable_skill external += from-source fallback (`ln -sf skills-external/<name>`, mirrors toggle-external) — closes the "missing symlink" warn; (4) stale usage() NOTE ("NOT toggled automatically") + SKILL.md fixed. Hermetic test profile-set-managed.test.sh 16 checks: fixture repo (both *_REPO_OVERRIDE), fake `claude` shim on PATH logging calls + flat-file MCP registry — gstack on-demand, external from-source, park/restore round-trip, magic add/remove calls, non-managed untouched. shellcheck + make test green. Branch feature/profile-managed-externals, unmerged (human gate).
|
||||||
|
|||||||
@@ -417,3 +417,4 @@ rules:
|
|||||||
## 2026-07-20
|
## 2026-07-20
|
||||||
- ctx7 coverage audit (user ask "ctx7 appelé à chaque techno ?") → verdict PARTIAL. 4 gaps: find-docs question-only, /feat //bugfix executors blind, ad-hoc coding uncovered, fast-libs hardcoded 3×. All 4 closed → BDR-078 (fast-libs.sh single source + ctx7-reminder hook + description trigger + executor-brief rule). fast-libs test 11/0, make test + review-guards green. feature/ctx7-coverage, UNMERGED.
|
- ctx7 coverage audit (user ask "ctx7 appelé à chaque techno ?") → verdict PARTIAL. 4 gaps: find-docs question-only, /feat //bugfix executors blind, ad-hoc coding uncovered, fast-libs hardcoded 3×. All 4 closed → BDR-078 (fast-libs.sh single source + ctx7-reminder hook + description trigger + executor-brief rule). fast-libs test 11/0, make test + review-guards green. feature/ctx7-coverage, UNMERGED.
|
||||||
- v1.2.0 cut + pushed (release-candidate flow: prep/finish via release-executor, tag on main 51b6572). CHANGELOG backfilled at prep: 10 entries added to Unreleased (plan-challenge, seo-data verbs, model-tiering v2, integrity pass, safe_fetch/url-guard) — was ctx7-only. /doc full post-release: README model-routing table v1→v2 reframe + ctx7 two-surface wording, chore/doc-sync-v1.2.0 merged. All pushed on explicit go.
|
- v1.2.0 cut + pushed (release-candidate flow: prep/finish via release-executor, tag on main 51b6572). CHANGELOG backfilled at prep: 10 entries added to Unreleased (plan-challenge, seo-data verbs, model-tiering v2, integrity pass, safe_fetch/url-guard) — was ctx7-only. /doc full post-release: README model-routing table v1→v2 reframe + ctx7 two-surface wording, chore/doc-sync-v1.2.0 merged. All pushed on explicit go.
|
||||||
|
- profile↔toggle-external audit (user) → enable side already symmetric (gstack on-demand LIVE), disable side missing → BDR-079: MANAGED_EXTERNALS+MANAGED_MCPS trim at set, external from-source fallback, 16-check hermetic test (claude shim). feature/profile-managed-externals, UNMERGED.
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
## 2026-07-20 — profile ↔ toggle-external symmetry (feature/profile-managed-externals, BDR-079)
|
||||||
|
Audit verdict: gstack on-demand + design enable already work; DISABLE side
|
||||||
|
missing — `set backend` leaves emil/frontend-design/design-motion/impeccable
|
||||||
|
active + magic registered. Doc claims auto-toggle both ways (only enable true).
|
||||||
|
- [x] profile.sh: `MANAGED_EXTERNALS` (emil-design-eng, frontend-design,
|
||||||
|
design-motion-principles, impeccable — union of profile usage) +
|
||||||
|
`MANAGED_MCPS` (magic) allowlists; cmd_set refactored to 4 trim
|
||||||
|
helpers (disable_{gstack,plugins,externals,mcps}_not_in).
|
||||||
|
- [x] profile.sh enable_skill external: from-source fallback
|
||||||
|
(`ln -sf skills-external/<name>`) mirroring toggle-external.
|
||||||
|
- [x] Texts: cmd_set info line, usage() NOTE (stale "NOT toggled
|
||||||
|
automatically"), header; skills/profile/SKILL.md Mechanism+tradeoffs.
|
||||||
|
- [x] Hermetic test lib/tests/profile-set-managed.test.sh — 16/0: gstack
|
||||||
|
on-demand, external from-source, park/restore round-trip, magic
|
||||||
|
add/remove via claude shim, non-managed untouched.
|
||||||
|
- [x] Gate: shellcheck OK + make test exit 0 (review-guards 5/0). BDR-079 +
|
||||||
|
journal + CHANGELOG done. Committed on branch, NO merge (human gate).
|
||||||
|
|
||||||
## 2026-07-20 — ctx7 coverage extension (feature/ctx7-coverage, BDR-078)
|
## 2026-07-20 — ctx7 coverage extension (feature/ctx7-coverage, BDR-078)
|
||||||
Close the 4 gaps from the ctx7 coverage audit: /feat //bugfix + ad-hoc coding
|
Close the 4 gaps from the ctx7 coverage audit: /feat //bugfix + ad-hoc coding
|
||||||
never consult ctx7; fast-libs list hardcoded 3×; zero deterministic backstop.
|
never consult ctx7; fast-libs list hardcoded 3×; zero deterministic backstop.
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Profile switches now toggle external packs and MCPs both ways (BDR-079)** — `profile.sh set` was asymmetric: it enabled what a profile listed (including gstack skills on demand when the whole pack is off, and the `magic` MCP) but never disabled the managed leftovers, so `set backend` after design work kept emil-design-eng / frontend-design / design-motion-principles / impeccable active and magic registered. `set` now trims managed externals (`MANAGED_EXTERNALS`) and managed MCPs (`MANAGED_MCPS`, delegated to `toggle-external.sh`) not listed in the profile — same allowlist doctrine as `MANAGED_PLUGINS`, nothing outside the allowlists is ever auto-touched (darwin-skill stays manual). Also: an `external` entry whose symlink never existed is now created from `skills-external/` (mirroring toggle-external's from-source path), and the stale "NOT toggled automatically" note in `profile.sh` usage was corrected. Covered by a hermetic 16-check test (`lib/tests/profile-set-managed.test.sh`) with a fake `claude` shim.
|
||||||
|
|
||||||
## [1.2.1] — 2026-07-20
|
## [1.2.1] — 2026-07-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+80
-15
@@ -14,6 +14,9 @@
|
|||||||
# - MCPs: delegated to lib/toggle-external.sh for known servers (magic),
|
# - MCPs: delegated to lib/toggle-external.sh for known servers (magic),
|
||||||
# advisory otherwise
|
# advisory otherwise
|
||||||
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
|
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
|
||||||
|
# - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs
|
||||||
|
# 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,
|
# Always-on plugins (never toggled by `set`): security-guidance,
|
||||||
# superpowers + rtk hook + .claude internal. The script refuses to disable
|
# superpowers + rtk hook + .claude internal. The script refuses to disable
|
||||||
@@ -61,6 +64,23 @@ MANAGED_PLUGINS=(
|
|||||||
"pr-review-toolkit@claude-code-plugins"
|
"pr-review-toolkit@claude-code-plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# External skill packs that are toggle-managed by `set` — same allowlist
|
||||||
|
# doctrine as MANAGED_PLUGINS: listed here only when the enabled state is
|
||||||
|
# task-type-driven. `set` disables these when the profile does not list
|
||||||
|
# them; anything else external (e.g. darwin-skill) is never auto-touched.
|
||||||
|
MANAGED_EXTERNALS=(
|
||||||
|
emil-design-eng
|
||||||
|
frontend-design
|
||||||
|
design-motion-principles
|
||||||
|
impeccable
|
||||||
|
)
|
||||||
|
|
||||||
|
# MCP servers that are toggle-managed by `set`, both ways (enable AND
|
||||||
|
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
|
||||||
|
MANAGED_MCPS=(
|
||||||
|
magic
|
||||||
|
)
|
||||||
|
|
||||||
# Plugins that MUST stay enabled — `set` will refuse to disable these even if
|
# Plugins that MUST stay enabled — `set` will refuse to disable these even if
|
||||||
# they're not in the profile. (Defensive: belt-and-suspenders alongside
|
# they're not in the profile. (Defensive: belt-and-suspenders alongside
|
||||||
# MANAGED_PLUGINS allowlist.)
|
# MANAGED_PLUGINS allowlist.)
|
||||||
@@ -271,6 +291,11 @@ enable_skill() {
|
|||||||
ok "enabled: $skill ($type)"
|
ok "enabled: $skill ($type)"
|
||||||
elif [ -e "$SKILLS_DIR/$skill" ]; then
|
elif [ -e "$SKILLS_DIR/$skill" ]; then
|
||||||
:
|
:
|
||||||
|
elif [ "$type" = external ] && [ -d "$REPO/skills-external/$skill" ]; then
|
||||||
|
# Symlink never created (or hand-removed): recreate it from the
|
||||||
|
# vendored pack — mirrors toggle-external.sh's from-source path.
|
||||||
|
ln -sf "$REPO/skills-external/$skill" "$SKILLS_DIR/$skill"
|
||||||
|
ok "enabled: $skill (external, symlink created)"
|
||||||
else
|
else
|
||||||
warn "missing: $skill ($type)"
|
warn "missing: $skill ($type)"
|
||||||
fi
|
fi
|
||||||
@@ -422,6 +447,48 @@ parked_gstack_count() {
|
|||||||
find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' '
|
find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── `set` trim helpers — one per managed category ─────────────
|
||||||
|
# Each disables the managed items NOT listed in the given profile. Allowlist
|
||||||
|
# doctrine: only MANAGED_* entries are ever auto-disabled.
|
||||||
|
|
||||||
|
disable_plugins_not_in() {
|
||||||
|
local prof="$1" keep_file p plugin_name marketplace
|
||||||
|
keep_file="$(mktemp)"
|
||||||
|
read_profile "$prof" \
|
||||||
|
| awk -F'\t' '$2 ~ /^plugin@/ { sub(/^plugin@/, "", $2); print $1"@"$2 }' \
|
||||||
|
| sort -u > "$keep_file"
|
||||||
|
for p in "${MANAGED_PLUGINS[@]}"; do
|
||||||
|
if ! grep -qx "$p" "$keep_file"; then
|
||||||
|
plugin_name="${p%@*}"
|
||||||
|
marketplace="${p#*@}"
|
||||||
|
disable_skill "$plugin_name" "plugin@${marketplace}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
rm -f "$keep_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_externals_not_in() {
|
||||||
|
local prof="$1" keep_file x
|
||||||
|
keep_file="$(mktemp)"
|
||||||
|
read_profile "$prof" | awk -F'\t' '$2 == "external" { print $1 }' \
|
||||||
|
| sort -u > "$keep_file"
|
||||||
|
for x in "${MANAGED_EXTERNALS[@]}"; do
|
||||||
|
grep -qx "$x" "$keep_file" || disable_skill "$x" external
|
||||||
|
done
|
||||||
|
rm -f "$keep_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_mcps_not_in() {
|
||||||
|
local prof="$1" keep_file s
|
||||||
|
keep_file="$(mktemp)"
|
||||||
|
read_profile "$prof" | awk -F'\t' '$2 == "mcp" { print $1 }' \
|
||||||
|
| sort -u > "$keep_file"
|
||||||
|
for s in "${MANAGED_MCPS[@]}"; do
|
||||||
|
grep -qx "$s" "$keep_file" || disable_skill "$s" mcp
|
||||||
|
done
|
||||||
|
rm -f "$keep_file"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Commands ──────────────────────────────────────────────
|
# ── Commands ──────────────────────────────────────────────
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
@@ -506,24 +573,20 @@ cmd_apply() {
|
|||||||
|
|
||||||
cmd_set() {
|
cmd_set() {
|
||||||
local prof="$1"
|
local prof="$1"
|
||||||
info "Setting profile: $prof (exclusive — disables non-listed gstack skills + managed plugins)"
|
info "Setting profile: $prof (exclusive — disables non-listed gstack skills + managed plugins/externals/MCPs)"
|
||||||
|
|
||||||
# Disable gstack-origin skills not in profile.
|
# Disable gstack-origin skills not in profile.
|
||||||
disable_gstack_not_in "$prof"
|
disable_gstack_not_in "$prof"
|
||||||
|
|
||||||
# Disable managed plugins not in profile (PROTECTED_PLUGINS are excluded
|
# Disable managed plugins not in profile (PROTECTED_PLUGINS are excluded
|
||||||
# by disable_skill itself — belt and suspenders).
|
# by disable_skill itself — belt and suspenders).
|
||||||
local plugin_keep_file p plugin_name marketplace
|
disable_plugins_not_in "$prof"
|
||||||
plugin_keep_file="$(mktemp)"
|
|
||||||
read_profile "$prof" | awk -F'\t' '$2 ~ /^plugin@/ { sub(/^plugin@/, "", $2); print $1"@"$2 }' | sort -u > "$plugin_keep_file"
|
# Symmetry (BDR-079): a profile switch also parks the managed external
|
||||||
for p in "${MANAGED_PLUGINS[@]}"; do
|
# packs and unregisters the managed MCPs the new profile does not need —
|
||||||
if ! grep -qx "$p" "$plugin_keep_file"; then
|
# design leftovers (emil, magic…) no longer survive a `set backend`.
|
||||||
plugin_name="${p%@*}"
|
disable_externals_not_in "$prof"
|
||||||
marketplace="${p#*@}"
|
disable_mcps_not_in "$prof"
|
||||||
disable_skill "$plugin_name" "plugin@${marketplace}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
rm -f "$plugin_keep_file"
|
|
||||||
|
|
||||||
# Enable everything listed in the profile.
|
# Enable everything listed in the profile.
|
||||||
cmd_apply "$prof"
|
cmd_apply "$prof"
|
||||||
@@ -679,9 +742,11 @@ EXAMPLES:
|
|||||||
bash lib/profile.sh reset # restore everything
|
bash lib/profile.sh reset # restore everything
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
Plugin and MCP entries print advisory commands — they are NOT toggled
|
"set" toggles the MANAGED items automatically, both ways: plugins
|
||||||
automatically. Run "claude plugin enable|disable" or "claude mcp add|remove"
|
(ui-ux-pro-max, plugin-dev, pr-review-toolkit), external packs
|
||||||
yourself for those.
|
(emil-design-eng, frontend-design, design-motion-principles, impeccable)
|
||||||
|
and the magic MCP. Anything outside those allowlists stays advisory —
|
||||||
|
run "claude plugin enable|disable" or "claude mcp add|remove" yourself.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/tests/profile-set-managed.test.sh — `set` symmetry on managed
|
||||||
|
# externals + MCPs, gstack on-demand, external from-source (BDR-079).
|
||||||
|
# Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH.
|
||||||
|
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; }
|
||||||
|
|
||||||
|
FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT
|
||||||
|
mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \
|
||||||
|
"$FX/skills-external/emil-design-eng" "$FX/skills-external/other-ext"
|
||||||
|
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/"
|
||||||
|
printf 'MAGIC_API_KEY=test-secret-000\n' > "$FX/.env"
|
||||||
|
|
||||||
|
# Non-managed external, enabled from the start — must never be touched.
|
||||||
|
ln -s "$FX/skills-external/other-ext" "$FX/skills/other-ext"
|
||||||
|
|
||||||
|
cat > "$FX/lib/profiles/designish.profile" <<'EOF'
|
||||||
|
gs-a
|
||||||
|
gs-b
|
||||||
|
emil-design-eng external
|
||||||
|
magic mcp
|
||||||
|
EOF
|
||||||
|
cat > "$FX/lib/profiles/backendish.profile" <<'EOF'
|
||||||
|
gs-c
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Fake claude: logs every call; keeps MCP registry state in a flat file.
|
||||||
|
cat > "$FX/bin/claude" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
FX="$FX"
|
||||||
|
echo "\$*" >> "\$FX/claude-calls.log"
|
||||||
|
case "\$1 \${2:-}" in
|
||||||
|
"mcp list") cat "\$FX/mcp-state" 2>/dev/null ;;
|
||||||
|
"mcp add") echo "magic: stub" > "\$FX/mcp-state" ;;
|
||||||
|
"mcp remove") : > "\$FX/mcp-state" ;;
|
||||||
|
esac
|
||||||
|
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" "$@"; }
|
||||||
|
|
||||||
|
# --- set designish: gstack on-demand + external from-source + magic on ---
|
||||||
|
run set designish >/dev/null 2>&1
|
||||||
|
check T1-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
|
||||||
|
check T2-gsb-on "$([ -e "$FX/skills/gs-b" ] && echo on || echo off)" on
|
||||||
|
check T3-gsc-off "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" off
|
||||||
|
check T4-emil-src "$([ -L "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
|
||||||
|
check T5-magic-on "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1
|
||||||
|
check T6-add-call "$(grep -c '^mcp add magic' "$FX/claude-calls.log")" 1
|
||||||
|
|
||||||
|
# --- set backendish: managed leftovers parked/unregistered ---
|
||||||
|
run set backendish >/dev/null 2>&1
|
||||||
|
check T7-gsc-on "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" on
|
||||||
|
check T8-gsa-park "$([ -e "$FX/skills-disabled/gstack__gs-a" ] && echo p || echo n)" p
|
||||||
|
check T9-emil-off "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" off
|
||||||
|
check T10-emil-park "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" p
|
||||||
|
check T11-magic-off "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null || true)" 0
|
||||||
|
check T12-rm-call "$(grep -c '^mcp remove magic' "$FX/claude-calls.log")" 1
|
||||||
|
check T13-other-untouched "$([ -e "$FX/skills/other-ext" ] && echo on || echo off)" on
|
||||||
|
|
||||||
|
# --- back to designish: parked external restored (not re-sourced) ---
|
||||||
|
run set designish >/dev/null 2>&1
|
||||||
|
check T14-emil-back "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
|
||||||
|
check T15-park-gone "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" n
|
||||||
|
check T16-magic-back "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1
|
||||||
|
|
||||||
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
+15
-3
@@ -61,6 +61,15 @@ protected — `set` will refuse to disable them even if the profile omits them.
|
|||||||
**Managed plugins** that `set` may disable when not in profile:
|
**Managed plugins** that `set` may disable when not in profile:
|
||||||
`ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`,
|
`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.
|
`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled.
|
||||||
|
**Managed externals** (`emil-design-eng`, `frontend-design`,
|
||||||
|
`design-motion-principles`, `impeccable`) and **managed MCPs** (`magic`)
|
||||||
|
follow the same symmetry (BDR-079): `set` enables them when the profile
|
||||||
|
lists them (from parked state, or from `skills-external/` if the symlink
|
||||||
|
never existed) and parks/unregisters them when it does not — e.g. `set
|
||||||
|
backend` after design work turns emil and magic off. `darwin-skill` and any
|
||||||
|
other unlisted external are never auto-touched. gstack works the same
|
||||||
|
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.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -117,8 +126,11 @@ bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS
|
|||||||
update-check, learnings — script doesn't touch that infra. Disabled skills
|
update-check, learnings — script doesn't touch that infra. Disabled skills
|
||||||
are just hidden from Claude Code's scanner; the gstack repo stays installed.
|
are just hidden from Claude Code's scanner; the gstack repo stays installed.
|
||||||
- Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max,
|
- Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max,
|
||||||
plugin-dev, pr-review-toolkit) and the `magic` MCP — see the Mechanism table
|
plugin-dev, pr-review-toolkit), the managed external packs (emil-design-eng,
|
||||||
above (BDR-008). Anything outside that managed set stays manual:
|
frontend-design, design-motion-principles, impeccable) and the `magic` MCP —
|
||||||
`claude plugin enable|disable`, `claude mcp add|remove`.
|
in BOTH directions: `set` enables what the profile lists and disables the
|
||||||
|
managed leftovers it doesn't (BDR-008, BDR-079). Anything outside those
|
||||||
|
allowlists stays manual: `claude plugin enable|disable`, `claude mcp
|
||||||
|
add|remove`.
|
||||||
- `set` is destructive in the sense that it disables non-listed gstack skills.
|
- `set` is destructive in the sense that it disables non-listed gstack skills.
|
||||||
Use `apply` if the user wants additive behavior.
|
Use `apply` if the user wants additive behavior.
|
||||||
|
|||||||
Reference in New Issue
Block a user