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)
|
||||
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.
|
||||
|
||||
### 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
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
## 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)
|
||||
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.
|
||||
|
||||
@@ -6,6 +6,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
+80
-15
@@ -14,6 +14,9 @@
|
||||
# - MCPs: delegated to lib/toggle-external.sh for known servers (magic),
|
||||
# advisory otherwise
|
||||
# - 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,
|
||||
# superpowers + rtk hook + .claude internal. The script refuses to disable
|
||||
@@ -61,6 +64,23 @@ MANAGED_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
|
||||
# they're not in the profile. (Defensive: belt-and-suspenders alongside
|
||||
# MANAGED_PLUGINS allowlist.)
|
||||
@@ -271,6 +291,11 @@ enable_skill() {
|
||||
ok "enabled: $skill ($type)"
|
||||
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
|
||||
warn "missing: $skill ($type)"
|
||||
fi
|
||||
@@ -422,6 +447,48 @@ parked_gstack_count() {
|
||||
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 ──────────────────────────────────────────────
|
||||
|
||||
cmd_list() {
|
||||
@@ -506,24 +573,20 @@ cmd_apply() {
|
||||
|
||||
cmd_set() {
|
||||
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_not_in "$prof"
|
||||
|
||||
# Disable managed plugins not in profile (PROTECTED_PLUGINS are excluded
|
||||
# by disable_skill itself — belt and suspenders).
|
||||
local plugin_keep_file p plugin_name marketplace
|
||||
plugin_keep_file="$(mktemp)"
|
||||
read_profile "$prof" | awk -F'\t' '$2 ~ /^plugin@/ { sub(/^plugin@/, "", $2); print $1"@"$2 }' | sort -u > "$plugin_keep_file"
|
||||
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 "$plugin_keep_file"
|
||||
disable_plugins_not_in "$prof"
|
||||
|
||||
# Symmetry (BDR-079): a profile switch also parks the managed external
|
||||
# packs and unregisters the managed MCPs the new profile does not need —
|
||||
# design leftovers (emil, magic…) no longer survive a `set backend`.
|
||||
disable_externals_not_in "$prof"
|
||||
disable_mcps_not_in "$prof"
|
||||
|
||||
# Enable everything listed in the profile.
|
||||
cmd_apply "$prof"
|
||||
@@ -679,9 +742,11 @@ EXAMPLES:
|
||||
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.
|
||||
"set" toggles the MANAGED items automatically, both ways: plugins
|
||||
(ui-ux-pro-max, plugin-dev, pr-review-toolkit), external packs
|
||||
(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
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
`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.
|
||||
**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
|
||||
|
||||
@@ -117,8 +126,11 @@ bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS
|
||||
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 toggle the managed Claude Code plugins (ui-ux-pro-max,
|
||||
plugin-dev, pr-review-toolkit) and the `magic` MCP — see the Mechanism table
|
||||
above (BDR-008). Anything outside that managed set stays manual:
|
||||
`claude plugin enable|disable`, `claude mcp add|remove`.
|
||||
plugin-dev, pr-review-toolkit), the managed external packs (emil-design-eng,
|
||||
frontend-design, design-motion-principles, impeccable) and the `magic` MCP —
|
||||
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.
|
||||
Use `apply` if the user wants additive behavior.
|
||||
|
||||
Reference in New Issue
Block a user