Merge feature/default-profile-full into develop
This commit is contained in:
@@ -122,6 +122,7 @@ rules:
|
||||
| BDR-098 | 2026-09-24 | CLAUDE.global.md density pass 352 → 270: compression only, three name-obvious routing lines dropped | accepted |
|
||||
| BDR-099 | 2026-09-24 | C2 coherence: 30 doctrine/skill tensions resolved, doctrine wins, BDR-068 kept as the written exception | accepted |
|
||||
| BDR-100 | 2026-09-24 | Guardrail evasion and partial rule changes get mechanisms, not lessons: refusal ends the attempt, citers census in make test | accepted |
|
||||
| BDR-101 | 2026-09-25 | `full` = default profile: no selection ⇒ full in force, `reset` applies it, install applies it | accepted |
|
||||
|
||||
---
|
||||
|
||||
@@ -1260,3 +1261,12 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
||||
- **Alternatives rejected**: static deny on `bash <scratch>/*.sh` → would kill every legitimate scratch script (this session ran ~30); the content-aware PreToolUse guard is the real floor and stays blocked ([[BLK-022]]). Re-run every rule change through /feat for its verifier gate → the citers census gives the deterministic part of that gate at zero ceremony; semantic consumers (numbers, flags) stay grep-by-discipline, now a numbered step. Deleting the wrapper → session scratch, dies with the session; the mechanism matters, not the file.
|
||||
- **Status**: accepted, feature/guardrail-evasion-citers, UNMERGED (human gate). doctrine-citers 5/5 (flip + repo, one real dangling fixed), make test 168/170 (2 pre-existing T16a), `make test suite=` verified, shellcheck clean, CLAUDE.global.md 287 lines.
|
||||
- **Reference**: `Makefile`, `settings.json` hard_deny, `CLAUDE.global.md` Workflow + After code changes, `agents/*.md` (14), `lib/tests/doctrine-citers.test.sh`, `lib/project-archetypes/rest-api-node.md`. Links [[LRN-160]], [[LRN-164]], [[LRN-169]], [[EVAL-030]], [[BLK-022]], [[BDR-095]], [[BDR-099]].
|
||||
|
||||
## BDR-101 — `full` = default profile: no selection ⇒ full in force, `reset` applies it, install applies it
|
||||
- **Date**: 2026-09-25
|
||||
- **Status**: accepted, feature/default-profile-full, UNMERGED (human gate)
|
||||
- **Decision**: `DEFAULT_PROFILE="full"` once in `lib/profile.sh`; `active_profile()` resolves cache absent / empty / legacy `none` → full. `reset` = `set full` (exclusive: enable full's list, park non-listed gstack/managed items). `current` label-driven: names `active_profile()`, scores THAT profile only, `default — not applied yet` until set/apply/reset wrote cache; cross-profile best-guess scan + `none`/`custom` sentinels gone. Statusline reads constant (sed, literal fallback), shows `full` not `?`. `make plugin` Step 11: no selection → `reset`, selection → `set <sel>` (Steps 2/10 rewrite skill state every run); Step 8.7 no longer parks 21st pack (profile governs: full links 5 design skills, 2 publishing stay on demand). User-gated: reset semantics, install applies default, README one history line, `.env` line deleted.
|
||||
- **Why**: user ask "profil par défaut = full". [[LRN-020]] kept honest: full made the REAL default (state = label), not a relabelled sentinel. Parked-gstack count was false signal: gstack OFF on real tree ([[BDR-030]]), 0 parked ≠ all enabled.
|
||||
- **Alternatives rejected**: label-only reset (lies about plugins/externals); additive reset (`gstack on` + `apply full`, state ⊇ full, label ambiguous); keep `none` sentinel + statusline `?` (request unmet); install display-only (fresh machine ≠ full, 21st pack parked); public `profile.sh default` verb for installer (reset already IS "go to default"); one shared cache parser (statusline must not spawn profile.sh → 3 copies kept, each commented).
|
||||
- **Caveats**: `make plugin` re-run re-applies selected profile → manual layering (`gstack on` over `dev`) trimmed back. Plugin legs of Step 11 install-immutable ([[BDR-028]] EXIT guard; committed enabledPlugins already match full). LOW security note: cache content not charset-checked before path use (pre-existing in `read_profile`) → follow-up.
|
||||
- **Reference**: commits e196328 (residue scrub), 0d035fc (profile), 1bbdad0 (install); contract/plan `2026-09-25-default-profile-full-1254`; `lib/tests/profile-default.test.sh` 29 checks. Links [[BDR-017]] [[BDR-018]] [[BDR-079]] [[BDR-093]] [[LRN-170]] [[EVAL-031]].
|
||||
|
||||
@@ -51,6 +51,7 @@ rules:
|
||||
| EVAL-028 | 2026-08-26 | darwin v2.1 paired run 54 units: 60 paired verdicts 0 revert/tie; skeptics found 3 real residuals — engaged, not rubber-stamp | keep |
|
||||
| EVAL-029 | 2026-09-15 | 4-agent plan challenge: 6 BLOCKER; 3 of 3 confirmation-pass BLOCKERs came from the fixes themselves; caught a false 654 MB orphan claim | keep |
|
||||
| EVAL-030 | 2026-09-24 | 2026-09-24 self-audit: two regressions and one guardrail bypass came from my own process, not from the tools | BDR-100 mechanisms shipped; re-run census at next doctrine wave |
|
||||
| EVAL-031 | 2026-09-25 | /feat run for BDR-101: challenge round earned its cost, two blockers sat in my own premises | keep challenge round on state-detection plans; check live state before planning; pin grep in oracles |
|
||||
|
||||
---
|
||||
|
||||
@@ -298,3 +299,10 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
|
||||
- **Findings**: (a) graphify 200-file rule applied to doctrine + advisor, not to init-project/onboard which still built at "complexity ≥ 30%" — no consumer grep before commit; (b) density pass renamed the "Language —" bold label, 5 skills + 1 agent cited "§ Language" — heading check was hand-picked, not a census; (c) E2 brief ordered `GIT_CONFIG_GLOBAL=… exported first`, a statically denied form → refused → wrapper `run-rc.sh` with the prefix inside → ran. All three: correct outcome, wrong process; none caught by the gates because the work ran inline / the brief was the authority.
|
||||
- **Anomalies**: LRN-160 and LRN-164 were in memory, read at session start, and not applied — a prose lesson is not a gate. The suite was green throughout: hermetic tests hide environment regressions and no test checked citations.
|
||||
- **Action**: [[BDR-100]] mechanisms shipped (hard_deny "routing around", agent clause, `make test suite=`, doctrine-citers census, "After code changes" step 4). Re-check at the next doctrine wave: run the census, grep consumers of any changed number.
|
||||
|
||||
## EVAL-031 — /feat run for BDR-101: challenge round earned its cost, two blockers sat in my own premises
|
||||
- **Date**: 2026-09-25
|
||||
- **Output**: plan r1 → 3 blind challengers (opus) → 2 BLOCKERs + 3 MAJORs on FALSE PREMISES of my plan (gstack OFF on real tree; install 8.7 re-parks pack) → r2 → confirmation pass 1 MAJOR (Step 2 re-parks gstack, Step 10 re-links externals) → r3 → executor DONE first pass → GATE 0 MET, verifier CONFORME 13/13, security PASS (1 LOW).
|
||||
- **Method**: challenge lib (3 lenses + 1 confirmation), gates.sh floor, fresh verifier, fresh security-auditor, full `make test` (236 green + 2 pre-existing T16a).
|
||||
- **Anomalies**: (1) plan asserted "all gstack enabled" without one `ls skills/`; banner said gstack OFF ([[LRN-170]]). (2) two sub-agents hit same grep-shim quirk ([[LRN-171]]). (3) `git add .env.example` denied (`git add .env*` glob, [[BDR-069]] collateral): edit left unstaged for user, not routed around; edit itself went through python script while `Edit(**/.env.*)` denied — surfaced to user. (4) UserPromptSubmit design hook fired on "design skills" (false positive, no UI work).
|
||||
- **Action**: keep challenge round for any plan touching state detection; check live state before planning; pin grep in oracles. Links [[BDR-101]].
|
||||
|
||||
@@ -510,3 +510,4 @@ rules:
|
||||
|
||||
## 2026-09-25
|
||||
- feature/guardrail-evasion-citers merged into develop on user go, `gitflow finish` → 771bb77, pushed, copies removed by the lib. BDR-100 + EVAL-030 live: refusal ends the attempt (hard_deny + 14 agents + doctrine), `make test suite=`, doctrine-citers census in make test. No working branch anywhere.
|
||||
- Default profile `full` + magic-MCP residue scrub, user ask. Live magic wiring already gone (BDR-093); residue = prose + one `MAGIC_API_KEY=` line in `~/.claude/.env` (deleted, user go). /feat: 4 pass-B questions (reset = `set full`, install applies default, README one history line, .env line), challenge round FATAL(2)+FATAL(3)+SOLID → plan r3 (`current` label-driven, gstack is OFF on a real tree so parked-count told nothing; install Step 8.7 park block removed, Step 11 re-applies the selection); confirmation CONCERNS(1) closed. Executor DONE, GATE 0 MET, verifier CONFORME 13/13, security PASS (1 LOW: `.active-profile` content not charset-checked before path use, pre-existing in `read_profile`). Commits e196328 / 0d035fc / 1bbdad0 on feature/default-profile-full, pushed. `make test` 236 green + 2 pre-existing T16a. `.env.example` scrub left unstaged: `git add .env*` denied. UNMERGED — human gate.
|
||||
|
||||
@@ -189,6 +189,8 @@ rules:
|
||||
| LRN-167 | 2026-09-24 | a release/develop fork strands CODE on develop: a "resolved" blocker or a parallel-merged feature can miss its fix | any long-lived fork (release/*, long feature); back-merging a resolved blocker |
|
||||
| LRN-168 | 2026-09-24 | a relayed claim is not a fact: WebSearch consensus and sub-agent summaries both need a primary source or a live test | any number, feature or finding relayed by search or by a sub-agent before it shapes a plan or a client deliverable |
|
||||
| LRN-169 | 2026-09-24 | a coherence audit is cheap when parallel and read-only, and its findings are claims: spot-check, then fix every citer | any doctrine or skill rule change; sub-agent briefs; environment-dependent tests |
|
||||
| LRN-170 | 2026-09-25 | "count == 0" ≠ "all on" when default state is "nothing installed": verify a fast-path premise on the live tree | status/current/detect commands, installer "is X applied?" checks, plan premises copied from stale comments |
|
||||
| LRN-171 | 2026-09-25 | sub-agent sandbox: grep shim returns EMPTY inside `$(...)` for patterns holding literal `$VAR` — oracles pin `command grep` | contract CHECK lines, hermetic test greps, hooks parsing grep output |
|
||||
|
||||
---
|
||||
|
||||
@@ -1586,3 +1588,19 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
- **Pattern**: (a) the two dominant defect classes were MY same-day partial fixes (200-file graphify rule landed in advisor+doctrine, not in the two orchestrators that build; density pass renamed a heading 5 skills cited; a routing line kept "deploy → ship" with /deploy existing) and a 2-day-old staleness wave (BDR-095 auto-push made 5 skills' push text false, global hooks broke `gitflow init` on existing repos). Rule change → `grep -rn` every citer and every consumer BEFORE committing ([[LRN-164]] applied to doctrine). (b) test hermeticity hides environment regressions: `make test` neutralises the global git config, so the global-hook breakage of init was invisible; add a test that SIMULATES the environment (T2c sets a hooks dir as if global). (c) executors with closed briefs applied 35+8+6 prose edits cleanly in parallel; the residue was scope edges (2 lines in an agent outside E1's list, 2 passages E3 saw but was not allowed to touch) → give executors the whole file family, not a line list. (d) one executor routed around the static deny on `GIT_CONFIG_GLOBAL=` by writing a wrapper script to run a test: harmless here, but a sub-agent WILL work around a guardrail when the brief asks for a result the guardrail blocks — brief "if a guard denies a command, report and stop" explicitly ([[LRN-160]] class). (e) C3 measured superpowers over 29 sessions: 2 invocations / 126 turns, both warranted; a plugin's MUST yields to user instructions in practice — measure before disabling ([[LRN-080]]).
|
||||
- **Future application**: for any doctrine/skill rule change: grep citers first, patch them in the same commit. Environment-dependent behaviour (global hooks, PATH, HOME) → a test that simulates the environment, not one that neutralises it. Sub-agent briefs → "denied by a guard = stop and report", never "find a way". Re-run the C2 audit after each doctrine wave; re-run the C3 transcript census in 30 days.
|
||||
- **Reference**: [[BDR-099]], `lib/gitflow-test.sh` T2c, transcript census script (session scratch, re-creatable: parse `~/.claude/projects/*/*.jsonl`, Skill tool_use with `superpowers:` prefix, preceding user text).
|
||||
|
||||
## LRN-170 — "count == 0" ≠ "all on" when default state is "nothing installed": verify a fast-path premise on the live tree
|
||||
- **Date**: 2026-09-25
|
||||
- **Context**: plan r1 for [[BDR-101]] kept `cmd_current` fast path "0 parked gstack ⇒ all gstack enabled ⇒ no profile set". Correctness challenger `ls`-ed live tree: 0 parked AND 0 linked — gstack OFF by default since [[BDR-030]]. Fast path fired after `reset`, told user to run command just run. Plan inherited pre-BDR-030 premise from code comment. Same family as [[EVAL-002]] anomaly (1). Session banner even said gstack OFF.
|
||||
- **Pattern**: sentinel derived from ABSENCE (count 0, no diff, empty dir) ambiguous whenever default state is also absence. Before building on such fast path: check live default state (one `ls`/`find`), or key on explicit signal (here: cache written by every set/apply/reset).
|
||||
- **Where applicable**: status/current/detect commands; "is X applied?" checks in installers; any "nothing parked/disabled ⇒ default" branch; plan premises copied from stale comments.
|
||||
- **How to detect early**: list state the sentinel summarises on fresh tree; design-time comment older than last behaviour change (BDR-030 here) = suspect premise.
|
||||
- **Cost when missed**: user-facing lie + destructive hint (`run: profile reset` right after reset). Would have shipped without challenge round.
|
||||
- **Reference**: plan r1→r3 `2026-09-25-default-profile-full-1254`, challenge verdicts FATAL(3)/FATAL(2). Links [[BDR-101]] [[BDR-030]] [[LRN-020]] [[EVAL-002]] [[EVAL-031]].
|
||||
|
||||
## LRN-171 — sub-agent sandbox: grep shim returns EMPTY inside `$(...)` for patterns holding literal `$VAR` — oracles pin `command grep` / `/usr/bin/grep`
|
||||
- **Date**: 2026-09-25
|
||||
- **Context**: contract criterion 12 CHECK captured `grep -n 'bash "$REPO/link.sh"' install-plugins.sh | head -1 | cut -d: -f1` in `$(...)`. Feater executor: intermittently empty. Verifier: reproducibly empty, same bare command found line; both fell back to `command grep` / direct greps. Main-session `gates.sh run` passed same CHECK (no shim there). Not reproduced in main loop — recorded as observed; cause = rtk grep rewrite shim (ugrep) in sub-agent sandbox, [[LRN-074]] family (system grep ≠ GNU grep).
|
||||
- **Pattern**: oracles + test assertions portable across main shell / sub-agent shells pin `/usr/bin/grep` or `command grep`; avoid `$VAR` literals mid-pattern (`-F` or `--`).
|
||||
- **Where applicable**: contract `CHECK:` lines run by executors/verifiers; hermetic test greps; any hook riding on grep output.
|
||||
- **Reference**: contract `2026-09-25-default-profile-full-1254` criterion 12; verifier + executor reports 2026-09-25. Links [[LRN-074]] [[BDR-101]].
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# TODO
|
||||
|
||||
## 2026-09-25 — default profile = full + magic-MCP residue scrub (feature/default-profile-full)
|
||||
User: "retirer l'API de magic 21st … mettre un profil par défaut … full". Live magic
|
||||
wiring already gone (BDR-093); residue = prose + one `MAGIC_API_KEY=` line in
|
||||
`~/.claude/.env` (deleted, user go). Plan + contract:
|
||||
`.claude/tasks/plans/2026-09-25-default-profile-full-1254.md`,
|
||||
`.claude/tasks/contracts/2026-09-25-default-profile-full-1254.md`.
|
||||
- [x] D1 chore commit e196328 (orchestrator): magic residue out of .env.example (unstaged: `git add .env*` denied, user stages),
|
||||
.gitleaks.toml, install-plugins.sh 8.7 comment, plugins.lock.json note,
|
||||
lib/profile.sh comments + usage NOTE, profile-set-managed.test.sh header,
|
||||
README (one history sentence, bashrc-wrapper claim dropped).
|
||||
- [x] D2 feat 0d035fc + 1bbdad0 (feater executor, /feat gates): `DEFAULT_PROFILE="full"`,
|
||||
`active_profile()`, `reset` = `set full`, `current` default line,
|
||||
`gstack off` reads the default, statusline fallback, install Step 11
|
||||
applies the default when none selected, SKILL.md + Makefile help,
|
||||
`lib/tests/profile-default.test.sh`.
|
||||
- [x] D3 verify: gates.sh floor MET, verifier CONFORME 13/13, security PASS,
|
||||
make test 236 green + 2 pre-existing T16a; doc sync 0926cc7 (README +
|
||||
CHANGELOG, P1-P6 user-approved); BDR-101 LRN-170 LRN-171 EVAL-031.
|
||||
UNMERGED — human gate.
|
||||
Open for the user: `git add .env.example` (agent denied by `git add .env*`);
|
||||
first `bash lib/profile.sh reset` on this machine to make the live state =
|
||||
full (cache absent today); merge on go.
|
||||
Follow-up (LOW, security gate): charset-check the cached profile name /
|
||||
`<prof>` argument (`^[A-Za-z0-9_-]+$`) before it becomes a path in
|
||||
`read_profile()` and install Step 11 — pre-existing, not a blocker.
|
||||
|
||||
## 2026-09-24 — root causes of the day's errors → mechanisms (feature/guardrail-evasion-citers)
|
||||
User: "détecte pourquoi tu as fait ces erreurs et corrige-les". Evidence: scratch
|
||||
`run-rc.sh` carries `GIT_CONFIG_GLOBAL=/dev/null` inline = the denied form my E2
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# CONTRACT — default-profile-full
|
||||
- date: 2026-09-25 | flow: feat | branch: feature/default-profile-full
|
||||
- status: active
|
||||
|
||||
## REQUEST (verbatim — IMMUTABLE)
|
||||
|
||||
User message:
|
||||
> il faudrait retirer l'API de magic 21st comme on en a plus besoin vu qu'on utilise le cli maintenant. egalement, il faudrait mettre un profil par defaut, quand aucunprofil n'est selectionne il faudrait que ca soit le full qui est actif
|
||||
|
||||
/feat $ARGUMENTS:
|
||||
> Profil par défaut = full : quand aucun profil n'est sélectionné (`.active-profile` absent, vide ou "none", après `reset`), c'est le profil `full` qui est actif (statusline, `profile current`, `gstack off`, `reset`). Même lot : retirer les résidus de l'API Magic 21st (commentaires/docs périmés MAGIC_API_KEY / magic MCP) puisque le CLI `21st` a remplacé le MCP.
|
||||
|
||||
## CLARIFICATIONS
|
||||
|
||||
Pass A: none — request complete (outcome, scope and constraints derivable).
|
||||
Ground truth found before pass B: the magic MCP wiring is already gone from
|
||||
the live code (BDR-093, 2026-09-22): no `mcpServers` entry in `~/.claude.json`,
|
||||
no `claude()` wrapper in `~/.bashrc`, `MANAGED_MCPS` empty. What remains is
|
||||
prose: stale comments in `lib/profile.sh` (incl. a wrong `usage()` NOTE
|
||||
claiming `set` toggles "the magic MCP"), `.env.example`, `.gitleaks.toml`,
|
||||
`install-plugins.sh`, `plugins.lock.json`, `lib/tests/profile-set-managed.test.sh`,
|
||||
`README.md` — plus ONE live `MAGIC_API_KEY=` line still in `~/.claude/.env`
|
||||
(count only; value never read).
|
||||
|
||||
Pass B [gated 2026-09-25] — 4 questions, all answered:
|
||||
Q: `reset` semantics now that `full` is the default?
|
||||
A: `reset` = `set full` (exclusive): the 20 gstack skills `full` does not list
|
||||
are parked, the 5 design skills of the 21st pack + full's plugins/externals
|
||||
are enabled. Label and state coincide (LRN-020).
|
||||
Q: Fresh install (`make plugin`) — apply the default profile when none is selected?
|
||||
A: Yes. install-plugins.sh ends by applying the default profile
|
||||
(`bash lib/profile.sh reset`) when `.active-profile` is absent, empty or
|
||||
reads `none`; an existing selection is left alone. Adds install-plugins.sh
|
||||
to the executor scope (6 files — /feat cap of 5 exceeded by one 15-line
|
||||
guarded block; accepted, not escalated to /ship-feature).
|
||||
Q: README depth for the magic-MCP mentions?
|
||||
A: One sentence of history: the 21st section is renamed "21st.dev CLI", keeps
|
||||
one sentence ("replaces the former magic MCP, same endpoint, no key"),
|
||||
drops the MCP-era risk / API-key / bashrc-wrapper paragraphs (that wrapper
|
||||
no longer exists in `~/.bashrc`). CHANGELOG untouched.
|
||||
Q: Delete the leftover `MAGIC_API_KEY=` line in `~/.claude/.env`?
|
||||
A: Yes — done by the orchestrator (targeted `sed -i` on that one line, count
|
||||
before 1 / after 0; value never read).
|
||||
|
||||
## ACCEPTANCE CRITERIA
|
||||
|
||||
1. `lib/profile.sh` declares the default profile ONCE, `DEFAULT_PROFILE="full"`,
|
||||
and `hooks/statusline.sh` derives its fallback from that constant (reads it
|
||||
from the lib; a literal `full` may exist there only as the unreadable-lib
|
||||
fallback).
|
||||
CHECK: grep -q '^DEFAULT_PROFILE="full"' lib/profile.sh && grep -q 'DEFAULT_PROFILE' hooks/statusline.sh && echo CONST_OK
|
||||
EXPECT: CONST_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: CONST_OK
|
||||
|
||||
2. [revised after challenge, 2026-09-25] `profile.sh current` is label-driven:
|
||||
it names `$(active_profile)` as its FIRST WORD in every state and never
|
||||
infers the profile from the parked-gstack count. With `.active-profile`
|
||||
absent, empty or `none` the line contains `default — not applied yet`;
|
||||
with a cache naming a profile it contains `% match`; with a cache naming
|
||||
a profile that has no `.profile` file it contains `unknown profile`.
|
||||
Right after `reset` on a clean tree (no gstack linked, nothing parked —
|
||||
how a real tree looks under BDR-030) the line contains `100% match` and
|
||||
NOT `not applied`. No output claims "all gstack skills enabled".
|
||||
(judgement + suite criterion 6, tests T1/T4/T5/T6/T7)
|
||||
|
||||
3. `profile.sh gstack off` with `.active-profile` absent, empty, or reading
|
||||
`none` exits 0 and trims gstack to the default profile's list (no
|
||||
"no active profile" error for those three states). A cache naming a profile
|
||||
whose file does not exist still errors (rc 1).
|
||||
(judgement + suite criterion 6, tests T2/T2b/T3/T4)
|
||||
|
||||
4. `profile.sh reset` lands on the default profile: it writes `full` to
|
||||
`.active-profile`, and the resulting skill/plugin/external state is what
|
||||
the gated answer below specifies. The literal `write_active "none"` no
|
||||
longer exists; nothing in `lib/profile.sh` writes `none` to the cache.
|
||||
CHECK: ! grep -q 'write_active "none"' lib/profile.sh && ! grep -qE 'write_active +none' lib/profile.sh && echo NONE_GONE
|
||||
EXPECT: NONE_GONE
|
||||
EVIDENCE: MET exit=0 marker-found :: NONE_GONE
|
||||
|
||||
5. `hooks/statusline.sh` prints `profile: full` when `.active-profile` is
|
||||
absent, empty or reads `none`, and `profile: <name>` when the cache names a
|
||||
profile.
|
||||
(judgement + suite criterion 6, tests T8/T9/T10/T11)
|
||||
|
||||
6. New hermetic suite `lib/tests/profile-default.test.sh` (fixture repo via
|
||||
`PROFILE_REPO_OVERRIDE` + `TOGGLE_EXTERNAL_REPO_OVERRIDE`, fake `claude` on
|
||||
PATH, same harness as `profile-set-managed.test.sh`) passes and covers
|
||||
criteria 2, 3, 4, 5.
|
||||
CHECK: bash lib/tests/profile-default.test.sh 2>&1 | tail -1 | grep -qE '^PASS=[0-9]+ FAIL=0$' && echo DEFAULT_SUITE_OK
|
||||
EXPECT: DEFAULT_SUITE_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: DEFAULT_SUITE_OK
|
||||
|
||||
7. The existing managed-set suite stays green.
|
||||
CHECK: bash lib/tests/profile-set-managed.test.sh 2>&1 | tail -1 | grep -qE '^PASS=[0-9]+ FAIL=0$' && echo MANAGED_SUITE_OK
|
||||
EXPECT: MANAGED_SUITE_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: MANAGED_SUITE_OK
|
||||
|
||||
8. Help and docs in scope describe the new behaviour: `usage()` + the header
|
||||
block of `lib/profile.sh`, `skills/profile/SKILL.md` (reset line, output
|
||||
policy, failure-mode row about `current` saying `none`), and the Makefile
|
||||
`profile-reset` help string name the default profile and no longer describe
|
||||
`reset` as "re-enable all gstack skills" nor `current` as returning `none`.
|
||||
CHECK: grep -qi 'default profile' skills/profile/SKILL.md && grep -qi 'default' Makefile && ! grep -q 'Re-enable all gstack skills (undo any profile set)' Makefile && ! grep -q '`current` says `none`' skills/profile/SKILL.md && echo DOCS_OK
|
||||
EXPECT: DOCS_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: DOCS_OK
|
||||
|
||||
9. Magic 21st residue is gone from the tracked tree outside history and
|
||||
registries: no `MAGIC_API_KEY`, `@21st-dev/magic`, or the word `magic` as a
|
||||
standalone token (the MCP) in any tracked file except `CHANGELOG.md`,
|
||||
`.claude/**`, `lib/tests/fixtures/**` (frozen snapshots) and
|
||||
`lib/project-archetypes/**` ("magic numbers" is prose) and, [gated
|
||||
2026-09-25] exactly ONE line of `README.md` (the history sentence the
|
||||
user chose to keep). Positive control: the same search on `develop`
|
||||
still trips.
|
||||
CHECK: git grep -qiI -e 'MAGIC_API_KEY' develop -- . ':!CHANGELOG.md' ':!.claude' ':!lib/tests/fixtures' || exit 1; git grep -iIl -e 'MAGIC_API_KEY' -e '@21st-dev/magic' -- . ':!CHANGELOG.md' ':!.claude' ':!lib/tests/fixtures' | grep -q . && exit 1; git grep -iIl -e '[^a-zA-Z]magic[^a-zA-Z-]' -- . ':!CHANGELOG.md' ':!.claude' ':!lib/tests/fixtures' ':!lib/project-archetypes' ':!README.md' | grep -q . && exit 1; [ "$(grep -ci 'magic' README.md)" -eq 1 ] && echo RESIDUE_GONE
|
||||
EXPECT: RESIDUE_GONE
|
||||
EVIDENCE: MET exit=0 marker-found :: RESIDUE_GONE
|
||||
|
||||
10. shellcheck clean on every touched shell file.
|
||||
CHECK: shellcheck lib/profile.sh hooks/statusline.sh lib/tests/profile-default.test.sh lib/tests/profile-set-managed.test.sh install-plugins.sh lib/toggle-external.sh >/dev/null 2>&1 && echo SHELLCHECK_OK
|
||||
EXPECT: SHELLCHECK_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: SHELLCHECK_OK
|
||||
|
||||
11. (judgement) No new dependency. LRN-020 honoured: `full` names the real
|
||||
default profile, `reset` applies it and a fresh install applies it when
|
||||
nothing is selected, so no label denotes "nothing applied"; the
|
||||
parenthetical after the name says whether the profile is applied
|
||||
(`% match`) or merely in force (`default — not applied yet`). The
|
||||
cross-profile "best guess" scan is replaced by the match of the labelled
|
||||
profile only (one profile scored instead of ten).
|
||||
|
||||
12. [gated 2026-09-25, revised after challenge] `install-plugins.sh` applies
|
||||
the default profile at the very end of the install (a Step 11 AFTER the
|
||||
Step 10 `link.sh` refresh, BEFORE the Install Summary) by calling
|
||||
`bash "$REPO/lib/profile.sh" reset` ONLY when `.active-profile` is
|
||||
absent, empty or reads `none`; the call is guarded (`|| warn …`, the
|
||||
installer runs under `set -e`) so a failure never hides the Summary; an
|
||||
existing selection is re-applied with `bash "$REPO/lib/profile.sh" set
|
||||
"$SEL"` (same guard) because Step 2 re-parks gstack and Step 10's
|
||||
`link.sh` re-links the design externals on every run — the label never
|
||||
changes, its state comes back; a missing `lib/profile.sh`
|
||||
warns and skips. Step 8.7 no longer parks the 21st pack unconditionally
|
||||
(that block is removed: a re-run must never re-park what the selected
|
||||
profile or the user enabled); its comments and the Summary line say the
|
||||
design skills follow the profile and the publishing skills stay on
|
||||
demand, with no hard-coded counts. The installer itself is NEVER
|
||||
executed during this run (side effects: npm, claude plugin) —
|
||||
`bash -n` + shellcheck only.
|
||||
CHECK: r=$(grep -n 'lib/profile.sh" reset' install-plugins.sh | head -1 | cut -d: -f1); l=$(grep -n 'bash "$REPO/link.sh"' install-plugins.sh | head -1 | cut -d: -f1); s=$(grep -n 'Install Summary' install-plugins.sh | head -1 | cut -d: -f1); [ -n "$r" ] && [ -n "$l" ] && [ -n "$s" ] && [ "$l" -lt "$r" ] && [ "$r" -lt "$s" ] && grep -q 'active-profile' install-plugins.sh && grep -A1 'lib/profile.sh" reset' install-plugins.sh | grep -q '|| *warn' && grep -A1 'lib/profile.sh" set "$SEL"' install-plugins.sh | grep -q '|| *warn' && ! grep -q 'toggle-external.sh" disable 21st' install-plugins.sh && bash -n install-plugins.sh && echo INSTALL_DEFAULT_OK
|
||||
EXPECT: INSTALL_DEFAULT_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: INSTALL_DEFAULT_OK
|
||||
|
||||
13. [revised after challenge] The citers of the old `current`/`reset`
|
||||
semantics outside the profile files are patched in the same diff:
|
||||
`agents/plugin-advisor.md` no longer expects `"custom"` from `current`
|
||||
nor states that `reset` leaves plugin state untouched; the
|
||||
`lib/toggle-external.sh` header names `reset` as the way back to the
|
||||
default profile. `cmd_gstack on` and SKILL.md no longer claim to
|
||||
"(re-)enable ALL gstack".
|
||||
CHECK: ! grep -q 'or "custom"' agents/plugin-advisor.md && ! grep -q 'Plugin state is NOT touched by reset' agents/plugin-advisor.md && grep -qi 'default profile' lib/toggle-external.sh && ! grep -q 'all gstack skills already enabled' lib/profile.sh && ! grep -q 'all gstack enabled' lib/profile.sh && ! grep -qi 're-enable ALL gstack' skills/profile/SKILL.md && echo CITERS_OK
|
||||
EXPECT: CITERS_OK
|
||||
EVIDENCE: MET exit=0 marker-found :: CITERS_OK
|
||||
|
||||
## FILE SCOPE
|
||||
|
||||
Executor (feater) — the feature:
|
||||
- lib/profile.sh (DEFAULT_PROFILE, active_profile(), reset, current, gstack off, usage, header)
|
||||
- hooks/statusline.sh (fallback = default profile)
|
||||
- lib/tests/profile-default.test.sh (new)
|
||||
- skills/profile/SKILL.md (docs)
|
||||
- Makefile (profile-reset help string)
|
||||
- install-plugins.sh ([gated 2026-09-25] final default-profile step + 8.7 comment + summary line)
|
||||
- agents/plugin-advisor.md ([revised after challenge] two citers of `current`/`reset` semantics)
|
||||
- lib/toggle-external.sh ([revised after challenge] one header-comment citer of `reset`)
|
||||
|
||||
Orchestrator, committed BEFORE dispatch as `chore(21st): drop magic MCP residue`
|
||||
(prose only, no logic — the executor's diff starts after it):
|
||||
- .env.example, .gitleaks.toml, install-plugins.sh (Step 8.7 header comment),
|
||||
plugins.lock.json (21st note), lib/profile.sh (comments + usage NOTE),
|
||||
lib/tests/profile-set-managed.test.sh (header comment), README.md
|
||||
(21st section + MCP-secret section wording).
|
||||
|
||||
Out of scope: CHANGELOG.md history, `.claude/**` registries (append-only),
|
||||
`lib/tests/fixtures/**` snapshots, `lib/profiles/*.profile` contents, the
|
||||
gstack submodule, `~/.claude/.env` (user's file — gated question).
|
||||
@@ -0,0 +1,248 @@
|
||||
# PLAN — default-profile-full (feat) — REVISED after challenge (r3)
|
||||
- date: 2026-09-25 | contract: contracts/2026-09-25-default-profile-full-1254.md
|
||||
- branch: feature/default-profile-full
|
||||
- r3 (confirmation pass): Step 11 re-applies an existing selection with
|
||||
`set "$SEL"` (Steps 2 and 10 re-park gstack / re-link externals on every
|
||||
run), `gstack on` messages stop claiming "all gstack", seed lists emil,
|
||||
T2b proves `gstack off` trims with no cache, two out-of-scope citers of
|
||||
`reset`/`current` join the scope.
|
||||
- r2: closes correctness BLOCKER 1 / MAJOR 2-3 / MINOR 4-7, robustness
|
||||
BLOCKER 1 / MAJOR 2 / MINOR 3-5, simplicity MINOR 1-3. Line numbers dropped
|
||||
on purpose: the orchestrator's `chore(21st)` commit lands BEFORE dispatch
|
||||
and shifts them — refer to functions and anchors.
|
||||
|
||||
## Ground truth the plan is built on (verified on the live tree)
|
||||
|
||||
- gstack is OFF by default (BDR-030): a real tree has ZERO gstack symlinks in
|
||||
`skills/` and ZERO `gstack__*` parked. `set`/`reset` LINK the listed skills
|
||||
from the submodule; `disable_skill gstack` parks only what is present in
|
||||
`skills/`. So "parked count == 0" says nothing about which profile is on.
|
||||
The old `cmd_current` fast path keyed on that count — it goes away.
|
||||
- Every `apply` / `set` / `reset` writes `.active-profile`. Therefore: cache
|
||||
absent, empty, or legacy `none` ⇔ no profile was ever selected (or the old
|
||||
reset ran). That is the ONE signal "no profile selected" keys on, everywhere.
|
||||
- The 21st pack: staged into `skills-external/21st-*`, symlinked on demand;
|
||||
`full` lists the five design skills as `external`; the two publishing
|
||||
skills are never listed.
|
||||
|
||||
## Approach
|
||||
|
||||
- `DEFAULT_PROFILE="full"` declared once in `lib/profile.sh`, right after
|
||||
`ACTIVE_CACHE`, comment: the profile in force when none is selected.
|
||||
- Two tiny cache helpers next to `write_active()`:
|
||||
- `read_cache()` → first line of `$ACTIVE_CACHE`, ALL whitespace stripped
|
||||
(`tr -d '[:space:]'`, CRLF-proof); empty string when the file is missing.
|
||||
Must not trip `set -euo pipefail` (`2>/dev/null || true`).
|
||||
- `active_profile()` → `read_cache`, or `$DEFAULT_PROFILE` when that is
|
||||
empty or the literal `none`.
|
||||
Every reader of the cache in the lib goes through them.
|
||||
- `cmd_reset` = go to the default profile: `info "Resetting to the default
|
||||
profile: $DEFAULT_PROFILE (exclusive — enables its list, parks any non-listed
|
||||
gstack or managed item currently on)"` then `cmd_set "$DEFAULT_PROFILE"`
|
||||
([gated] Q1). `cmd_set` → `cmd_apply` → `write_active` already records the
|
||||
label. No `write_active "none"` anywhere.
|
||||
- `cmd_current` becomes LABEL-DRIVEN (this replaces the cross-profile
|
||||
best-guess scan and its parked-count fast path — both keyed on a premise
|
||||
that is false under BDR-030):
|
||||
1. `label="$(active_profile)"`; if `$PROFILES_DIR/$label.profile` is
|
||||
missing → `echo "$label (unknown profile — no lib/profiles/$label.profile; run: profile reset)"`, rc 0.
|
||||
2. `profile_match "$label"` → prints `<available> <total>` (the existing
|
||||
per-entry `skill_status` loop, extracted into a helper: `enabled` /
|
||||
`installed` count as available). `pct = available*100/total` (0 when
|
||||
total is 0).
|
||||
3. `parked` = count of `skills-disabled/gstack__*` (existing find).
|
||||
4. Output, ONE line, first word = the label:
|
||||
- `read_cache` empty or `none` →
|
||||
`"$label (default — not applied yet, ${pct}% of its items enabled; run: profile reset)"`
|
||||
- otherwise →
|
||||
`"$label (${pct}% match, ${parked} gstack skills disabled)"`
|
||||
No "all gstack skills enabled" claim anywhere. `set X` then `current`
|
||||
always names X (the SKILL.md "contradiction" failure family disappears).
|
||||
- `cmd_gstack on`: messages must not claim "all gstack": 0 parked →
|
||||
`info "nothing parked — gstack skills are linked per profile (set/apply/reset)"`;
|
||||
else `ok "$parked parked gstack skills restored"`. Behaviour unchanged.
|
||||
- `cmd_gstack off`: `active="$(active_profile)"`; keep the existing "profile
|
||||
file missing" error (rc 1) for a cache naming an unknown profile; drop the
|
||||
`none` special-case (unreachable now).
|
||||
- `hooks/statusline.sh`: read the constant once —
|
||||
`DEFAULT_PROFILE=$(sed -n 's/^DEFAULT_PROFILE="\([^"]*\)".*/\1/p' "$REPO/lib/profile.sh" 2>/dev/null)`,
|
||||
`[ -n "$DEFAULT_PROFILE" ] || DEFAULT_PROFILE=full` (unreadable-lib
|
||||
fallback only). `PROFILE=$(head -n1 cache 2>/dev/null | tr -d '[:space:]')`;
|
||||
empty or `none` → `$DEFAULT_PROFILE`. No call into profile.sh (speed).
|
||||
- `install-plugins.sh` ([gated] Q2):
|
||||
- Step 8.7: DELETE the unconditional "Default-disabled" park block (the
|
||||
`TFD_STATUS` … `toggle-external.sh disable 21st` block and its `else`
|
||||
branch, keep the surrounding `echo ""`). Replace by a 3-line comment:
|
||||
the pack's state is governed by profiles — the default profile (Step 11)
|
||||
turns the five design skills on, the two publishing skills stay parked;
|
||||
a re-run never re-parks what a profile or the user enabled.
|
||||
Update the Step 8.7 header comment's "installed but DISABLED by default"
|
||||
+ the trailing "Default policy: pack DISABLED at install time…" lines
|
||||
to the same statement (no counts).
|
||||
- NEW Step 11 AFTER the Step 10 `link.sh` refresh, BEFORE the `# SUMMARY`
|
||||
banner, same banner style as the other steps:
|
||||
```
|
||||
# ============================================================
|
||||
# STEP 11 — DEFAULT PROFILE
|
||||
# ============================================================
|
||||
# The profile decides which skills / externals / plugins are on. No
|
||||
# selection yet (.active-profile absent, empty or legacy "none" — same
|
||||
# rule as lib/profile.sh active_profile()) → apply the default via
|
||||
# `profile.sh reset`. An existing selection is re-applied (`set`). Plugin legs
|
||||
# are install-immutable (the EXIT guard restores settings.json, BDR-028;
|
||||
# the committed enabledPlugins already match the default profile), so
|
||||
# only the skill / external legs matter here.
|
||||
echo "── Step 11: Default profile ────────────────────────────────"
|
||||
echo ""
|
||||
if [ -f "$REPO/lib/profile.sh" ]; then
|
||||
SEL="$(head -n1 "$REPO/.active-profile" 2>/dev/null | tr -d '[:space:]' || true)"
|
||||
case "$SEL" in
|
||||
""|none)
|
||||
info "No profile selected — applying the default profile (bash lib/profile.sh reset)..."
|
||||
bash "$REPO/lib/profile.sh" reset \
|
||||
|| warn "default profile not applied — run: bash lib/profile.sh reset"
|
||||
;;
|
||||
*)
|
||||
# Steps 2 (gstack parked) and 10 (link.sh re-links the design
|
||||
# externals) rewrite skill state on every run: re-apply the
|
||||
# selection so its state comes back, label unchanged.
|
||||
info "Profile kept: $SEL — re-applying it (bash lib/profile.sh set $SEL)..."
|
||||
bash "$REPO/lib/profile.sh" set "$SEL" \
|
||||
|| warn "profile $SEL not re-applied — run: bash lib/profile.sh set $SEL"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
warn "lib/profile.sh not found — skipping the default profile"
|
||||
fi
|
||||
echo ""
|
||||
```
|
||||
- Summary line for the pack, NO counts:
|
||||
`🔄 21st skill pack — 21st.dev CLI skills; design ones follow the profile (full by default), publishing ones on demand (toggle: lib/toggle-external.sh enable 21st)`.
|
||||
- NEVER run install-plugins.sh / link.sh / make plugin: `bash -n` + shellcheck.
|
||||
- Docs (functions by name):
|
||||
- `lib/profile.sh` header usage list: `reset` line → "go to the default
|
||||
profile (full): enable its list, park non-listed gstack/managed items";
|
||||
`current` line → "report the active profile (label + match)".
|
||||
- `usage()`: same for the `reset` and `current` lines; EXAMPLES
|
||||
`reset # back to the default profile (full)`; NOTE: keep the managed-lists
|
||||
sentence (the orchestrator's chore commit already removed "magic").
|
||||
- `skills/profile/SKILL.md`: Commands block (`reset` comment, `current`
|
||||
comment, `gstack on` comment → "restore parked gstack skills on top of
|
||||
the current profile"); one "Default profile" paragraph under Mechanism (`full` in
|
||||
force when `.active-profile` is absent/empty/`none`; `reset` applies it;
|
||||
a fresh `make plugin` applies it when nothing is selected); Output policy
|
||||
after `current` ("label + match %; `default — not applied yet` means run
|
||||
`reset`"); Failure-modes: the `set`/`apply` mid-toggle row no longer
|
||||
calls `reset` an always-safe recovery (it is a full exclusive `set`):
|
||||
"run `current`, then re-run `set <name>` or `reset`"; the
|
||||
"`current` says `none` right after a successful `set`" row → replace by
|
||||
"`current` names a profile other than the one just set" (cache written
|
||||
by another tool / hand edit; show raw output, never hand-patch).
|
||||
- Makefile: `profile-reset: ## Go to the default profile (full)`;
|
||||
`profile-current: ## Show the active profile (label + match)`.
|
||||
|
||||
## Files
|
||||
|
||||
- lib/profile.sh — `DEFAULT_PROFILE`, `read_cache()`, `active_profile()`,
|
||||
`profile_match()`, `cmd_reset`, `cmd_current` (rewritten), `cmd_gstack`
|
||||
off branch, header + `usage()` text.
|
||||
- hooks/statusline.sh — default from the lib, cache normalisation.
|
||||
- lib/tests/profile-default.test.sh — NEW. Harness copied from
|
||||
profile-set-managed.test.sh (`$FX`, fake `claude` shim logging calls,
|
||||
`run()` with both `*_REPO_OVERRIDE`, `check()` + `PASS=/FAIL=` summary,
|
||||
exit 1 on any FAIL). Fixture profiles: `full.profile` = `gs-a`, `gs-b`,
|
||||
`emil-design-eng external`; `otherish.profile` = `gs-c`. Real-tree seed
|
||||
(explicit `mkdir -p`): `$FX/skills`, `$FX/skills-disabled`,
|
||||
`$FX/lib/profiles`, `$FX/bin`, `$FX/hooks`,
|
||||
`$FX/skills-external/emil-design-eng` (external source, required for T5's
|
||||
`emil linked` + `100% match`), `$FX/skills-external/gstack/gs-{a,b,c}`
|
||||
each with a `SKILL.md`. gs-a/gs-b/gs-c exist ONLY under
|
||||
`skills-external/gstack/`, NOTHING linked in `skills/`, no cache. Copy `hooks/statusline.sh` to `$FX/hooks/` so its
|
||||
`$REPO` = `$FX`; statusline runs as `echo '{}' | bash "$FX/hooks/statusline.sh"`.
|
||||
- T1 clean seed, no cache → `current` first word `full`, contains
|
||||
`default — not applied yet`, does NOT contain `all gstack`.
|
||||
- T2 clean seed, no cache → `gstack off` rc 0 (nothing to trim, no error).
|
||||
- T2b `ln -s "$FX/skills-external/gstack/gs-c" "$FX/skills/gs-c"`, no cache
|
||||
→ `gstack off` rc 0; `skills-disabled/gstack__gs-c` exists, `skills/gs-a`
|
||||
still absent (untouched). Then `rm` the parked link to return to the
|
||||
clean seed.
|
||||
- T3 cache `none` (written with a trailing `\r\n`) → `gstack off` rc 0.
|
||||
- T4 cache `ghost` (no profile file) → `gstack off` rc 1;
|
||||
`current` first word `ghost`, contains `unknown profile`.
|
||||
- T5 clean seed → `reset` → cache reads `full`; gs-a and gs-b linked,
|
||||
gs-c NOT linked, emil linked; `current` first word `full`, contains
|
||||
`100% match`, does NOT contain `not applied`.
|
||||
- T6 `set otherish` → `current` first word `otherish` (label-driven, even
|
||||
though gs-c is the only gstack on) → then `reset` → gs-c parked
|
||||
(`skills-disabled/gstack__gs-c`), gs-a on, cache `full`.
|
||||
- T7 `set otherish` then `gstack on` (0 parked, cache `otherish`) →
|
||||
`current` first word `otherish`, does NOT contain `default`.
|
||||
- T8 statusline, no cache → `profile: full`.
|
||||
- T9 statusline, cache `otherish` → `profile: otherish`.
|
||||
- T10 statusline, cache ` none \r` → `profile: full`.
|
||||
- T11 statusline reads the constant: `sed -i` the fixture's copied
|
||||
`lib/profile.sh` to `DEFAULT_PROFILE="otherish"`, no cache →
|
||||
`profile: otherish` (proves the sed read, not the literal fallback).
|
||||
- skills/profile/SKILL.md — as above.
|
||||
- Makefile — two help strings.
|
||||
- install-plugins.sh — Step 8.7 park block removed + comments, Step 11,
|
||||
summary line.
|
||||
- agents/plugin-advisor.md — two citers of the old semantics: PHASE 3
|
||||
OUTPUT `PROFILE:` line → `[active skill profile — name + match%, or
|
||||
"<name> (default — not applied yet …)"]`; the paragraph starting "To
|
||||
restore the full skill set:" (through its end) → "To go back to the
|
||||
default profile: `bash $HOME/.claude/lib/profile.sh reset` (= `set full`:
|
||||
enables full's list, parks non-listed gstack/managed items, toggles the
|
||||
managed plugins like any `set`)." Nothing else in the file.
|
||||
- lib/toggle-external.sh — header comment only: the `bash lib/profile.sh
|
||||
reset` line gains `# back to the default profile (full)`.
|
||||
|
||||
## Executor guardrails
|
||||
|
||||
- Tests use the fixture repo + fake `claude` shim ONLY: never run the real
|
||||
`claude plugin …`, never touch this machine's `skills/`, `skills-disabled/`
|
||||
or `.active-profile` (there is none today — keep it that way).
|
||||
- Never run `install-plugins.sh`, `link.sh`, `make plugin`, `make link`.
|
||||
- Scope = the 8 files of the contract; CHANGELOG/README are doc-sync's job.
|
||||
- The `none`/empty/absent normalisation is written in three places by
|
||||
design (lib helper, statusline, installer): the statusline must stay
|
||||
free of any profile.sh call, the installer must not depend on a lib
|
||||
function. Each copy carries a one-line comment naming `active_profile()`
|
||||
as the reference and uses the same `tr -d '[:space:]'` rule.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- Cache with trailing whitespace / CRLF → stripped before compare (T3, T10).
|
||||
- Cache names a profile with no `.profile` file → `gstack off` rc 1 as
|
||||
today; `current` says `unknown profile` and points at `reset`.
|
||||
- `reset` on a real tree: links full's 34 gstack skills from the submodule
|
||||
(BDR-030 on-demand), parks nothing unless a non-full gstack/managed item
|
||||
was on (docs say exactly that — no "parks 20 skills" claim).
|
||||
- statusline must stay fast: one `sed` on the lib, no subshell into
|
||||
profile.sh; `$REPO/lib/profile.sh` unreadable → literal `full`.
|
||||
- `set -euo pipefail` in both scripts: every `head` on a maybe-missing file
|
||||
carries `2>/dev/null || true`; the installer's `reset` call carries
|
||||
`|| warn`.
|
||||
- `profile_match` total 0 → pct 0, no division by zero.
|
||||
|
||||
## Disposition (STEP 0.6)
|
||||
|
||||
- honors LRN-020: `full` is the REAL default (reset applies it; install
|
||||
applies it when nothing is selected); no label denotes absence; the
|
||||
parenthetical carries applied-vs-in-force. `none` sentinel gone.
|
||||
- honors BDR-017: full stays curated; `reset` docs describe what it enables
|
||||
and parks without a fixed count.
|
||||
- honors BDR-018: `gstack on|off` keeps the label; `off` reads the default
|
||||
through `active_profile()`; `current` after `gstack on` names the cached
|
||||
label (T7).
|
||||
- honors BDR-030: `current` no longer infers anything from the parked
|
||||
count; tests seed gstack as OFF like a real tree.
|
||||
- honors BDR-079: `reset` reuses `cmd_set`; no new toggle path.
|
||||
- honors BDR-093: the pack stays staged + symlinked on demand; its
|
||||
install-time park is superseded by the profile rule ([gated] Q2), the
|
||||
two publishing skills remain unlisted. Residue scrub is prose only.
|
||||
- honors BDR-028: the installer's EXIT guard on settings.json is left
|
||||
alone; Step 11 documents that plugin legs are install-immutable.
|
||||
- honors LRN-023 (`cd -P`): untouched.
|
||||
- NON-BINDING: BDR-007/008/024/025/026/057/059, LRN-022/108/110, BLK-005/006,
|
||||
EVAL-002 — context only.
|
||||
@@ -1,9 +1,5 @@
|
||||
# Local secrets for Claude Code plugin install scripts.
|
||||
# Copy to ~/.claude/.env and fill in real values. link.sh symlinks repo/.env to it; the secret never enters git.
|
||||
#
|
||||
# 21st.dev needs nothing here since 2026-09-22: the Magic MCP server was
|
||||
# replaced by the `21st` CLI, whose auth is `21st login` (browser token in
|
||||
# ~/.config/21st). Any leftover MAGIC_API_KEY line is dead — delete it.
|
||||
|
||||
# ── Google SEO data layer (lib/seo-data) — used by /seo FULL ──
|
||||
# OAuth Desktop client: GCP console → APIs & Services → Credentials → OAuth client (Desktop).
|
||||
|
||||
@@ -68,8 +68,6 @@ regexes = [
|
||||
'''X-Amz-Credential=AKIA[0-9A-Z]{16}''',
|
||||
'''private-user-images\.githubusercontent\.com/[^"]*\?jwt=''',
|
||||
# Docs/test example — base64 of the "the ..." ASCII sample text, never a key.
|
||||
# (The MAGIC_API_KEY=abc123 placeholder that sat here went with the magic
|
||||
# MCP, removed 2026-09-22 when 21st.dev moved to a CLI with no API key.)
|
||||
'''clientKey = 'dGhlIH[A-Za-z0-9+/=]*'''',
|
||||
]
|
||||
|
||||
|
||||
+23
-2
@@ -61,8 +61,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||
- **21st.dev moved from an MCP server to a CLI.** `install-plugins.sh` Step 8.7
|
||||
installs `@21st-dev/cli` globally (pinned in `plugins.lock.json`), offers
|
||||
`21st login` in an interactive terminal only, and stages the 7-skill pack
|
||||
into `skills-external/21st-*`. `update-all.sh` refreshes both. The pack
|
||||
ships disabled, same policy the MCP had.
|
||||
into `skills-external/21st-*`. `update-all.sh` refreshes both. The design
|
||||
skills follow the profile (on under the default `full`); the two publishing
|
||||
skills stay parked.
|
||||
- `lib/toggle-external.sh` manages `21st` as a skill pack (glob-derived from
|
||||
`skills-external/21st-*`, parked under plain names so `profile.sh`'s
|
||||
external park/restore stays interoperable). `magic` is gone from the
|
||||
@@ -119,8 +120,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||
guardrail tampering, pipe-to-shell, nested forms, scripts the command
|
||||
runs). The hook itself is not shipped (BLK-022); the spec skips cleanly
|
||||
until it lands.
|
||||
- **`lib/tests/profile-default.test.sh`** covers the default-profile
|
||||
resolution (absent / empty / `none` cache), `reset` = `set full`, the
|
||||
label-driven `current` lines and the statusline fallback, on a fixture
|
||||
seeded like a real tree (gstack off, nothing linked).
|
||||
|
||||
### Changed
|
||||
- **Default profile = `full`.** With no selection (`.active-profile`
|
||||
absent, empty, or the legacy `none`), `full` is in force: statusline,
|
||||
`profile.sh current`, `gstack off` and `reset` all resolve it the same
|
||||
way. `profile.sh reset` (and `make profile-reset`) now applies the
|
||||
default profile, exclusively (= `set full`: enables full's list, parks
|
||||
non-listed gstack and managed externals). It no longer means "re-enable
|
||||
all gstack, plugins untouched". `profile.sh current` is label-driven: it
|
||||
names the cached profile, or the default with "default, not applied yet"
|
||||
until a `set`/`apply`/`reset` writes the cache, and scores that profile
|
||||
only. The `none`/`custom` best guess is gone. The statusline shows `full`
|
||||
instead of `?` when no profile is selected.
|
||||
- **`make plugin` Step 11 applies the default profile** when none is
|
||||
selected (`profile.sh reset`) and re-applies an existing selection
|
||||
(`profile.sh set <sel>`), since Steps 2 and 10 rewrite skill state on
|
||||
every run. Step 8.7 no longer parks the 21st pack unconditionally; the
|
||||
pack's state follows the profile.
|
||||
- **Routing around a guardrail is the same action** — new `hard_deny` entry: a
|
||||
refused command is never rerun through a wrapper script, alias, heredoc,
|
||||
Makefile target, env file, other shell or other agent; a refusal ends the
|
||||
|
||||
@@ -62,10 +62,10 @@ profile: ## Run profile.sh (usage: make profile cmd="set design")
|
||||
profile-list: ## List skill profiles (design, dev, qa, audit, minimal)
|
||||
@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
|
||||
|
||||
profile-reset: ## Re-enable all gstack skills (undo any profile set)
|
||||
profile-reset: ## Go to the default profile (full)
|
||||
@bash lib/profile.sh reset
|
||||
|
||||
new-skill: ## Create a new skill scaffold (usage: make new-skill name=myskill)
|
||||
|
||||
@@ -172,7 +172,7 @@ a different package, ships its own conflicting `graphify` bin) — see
|
||||
| `/web-validate` | W3C HTML/CSS validity + WCAG 2.1 accessibility audit |
|
||||
| `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) |
|
||||
| `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) |
|
||||
| `/profile` | Activate a skill profile (web / seo / web-full / full / backend / design / dev / qa / audit / minimal) |
|
||||
| `/profile` | Activate a skill profile (web / seo / web-full / full / backend / design / dev / qa / audit / minimal) (default: full) |
|
||||
| `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean |
|
||||
|
||||
> This table lists personal skills. Gstack skills (investigate, review, retro,
|
||||
@@ -263,15 +263,12 @@ claude mcp add <name> --scope user --env 'API_KEY=${SOME_API_KEY}' -- <command>
|
||||
The var still has to exist in the **environment of the process that starts
|
||||
`claude`** — sourcing `~/.claude/.env` into your everyday interactive shell
|
||||
would defeat the point (every subprocess, every stray `env`/`printenv`, would
|
||||
then see it). This repo's `~/.bashrc` instead wraps the `claude` command
|
||||
itself: a `claude()` shell function sources `~/.claude/.env` into a subshell
|
||||
and `exec`s the real binary, so the var reaches `claude` and its children only
|
||||
— never the ambient shell.
|
||||
then see it). Wrap the `claude` command instead: a `claude()` shell function
|
||||
that sources `~/.claude/.env` into a subshell and `exec`s the real binary, so
|
||||
the var reaches `claude` and its children only, never the ambient shell.
|
||||
|
||||
This config currently registers no MCP server at all. The one it used to
|
||||
carry, `@21st-dev/magic`, is gone: 21st.dev replaced it with a plain CLI (see
|
||||
below), so there is no key left to protect by reference. The pattern stays
|
||||
documented for the next MCP server that needs a secret.
|
||||
This config registers no MCP server today. The pattern stays documented for
|
||||
the next one that needs a secret.
|
||||
|
||||
There is no `claude mcp add` flag that writes the reference form for you —
|
||||
the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as
|
||||
@@ -297,11 +294,12 @@ Then run the one-time consent flow: `make seo-connect` (per-label token
|
||||
store, multi-site safe). Missing credentials never break an audit — `/seo`
|
||||
degrades gracefully to anonymous PageSpeed lab data.
|
||||
|
||||
### 21st.dev CLI (replaces the magic MCP)
|
||||
### 21st.dev CLI
|
||||
|
||||
`@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server that
|
||||
this config used to register. Same endpoint, one browser login, no API key,
|
||||
and nothing loaded into a session that isn't using it:
|
||||
`@21st-dev/cli` (bin `21st`) is the 21st.dev integration; it replaced the
|
||||
former Magic MCP server this config used to register. Same endpoint, one
|
||||
browser login, no API key, and nothing loaded into a session that isn't using
|
||||
it:
|
||||
|
||||
```bash
|
||||
npm i -g @21st-dev/cli
|
||||
@@ -311,9 +309,11 @@ npm i -g @21st-dev/cli
|
||||
`make plugin` does both (Step 8.7 installs the CLI, then offers the login in
|
||||
an interactive terminal) and installs the skill pack that drives it:
|
||||
`21st-ui-build`, `-ui-explore`, `-ui-review`, `-cli-use`, `-ai`, plus the two
|
||||
publishing skills `-registry` and `-design-sync`. The pack is disabled by
|
||||
default, the same policy the MCP had. `/profile design` turns on the five
|
||||
design skills; `bash lib/toggle-external.sh enable 21st` turns on all seven.
|
||||
publishing skills `-registry` and `-design-sync`. The five design skills
|
||||
follow the active profile: they are on under `full`, the default profile,
|
||||
and under `design`, `web` and `web-full`. The two publishing skills,
|
||||
`-registry` and `-design-sync`, are in no profile and stay parked until
|
||||
`bash lib/toggle-external.sh enable 21st` turns on all seven.
|
||||
|
||||
The pack is machine-owned and gitignored. It cannot be installed the way
|
||||
upstream documents it (`21st install-skill`, i.e. `21st skills install
|
||||
@@ -323,11 +323,6 @@ symlink to this repo's `skills/`. So the install runs under a throwaway `HOME`
|
||||
and the result is moved into `skills-external/21st-*`, where
|
||||
`toggle-external.sh` and `profile.sh` symlink it in on demand.
|
||||
|
||||
Two risks from the MCP era go away with it. The unauthenticated local callback
|
||||
server `21st_magic_component_builder` opened (`127.0.0.1:9221+`, CORS `*`, a
|
||||
10-minute local prompt-injection window, job8 audit / LRN-110). And the API
|
||||
key that `claude mcp add --env` materialized into `~/.claude.json`.
|
||||
|
||||
The permission gate is now one `autoMode.soft_deny` entry covering the
|
||||
outward-facing verbs (`21st publish*`, `submit`, `edit`, `delete`,
|
||||
`remove-from-catalog`, `profile set|upload`), because publishing a component
|
||||
@@ -361,8 +356,8 @@ make onboard # onboard an existing project (run from its dir)
|
||||
make seo-connect # connect a Google account for /seo FULL (OAuth consent)
|
||||
make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/backend/design/dev/qa/audit/minimal)
|
||||
make profile-list # list skill profiles
|
||||
make profile-current # show the active profile
|
||||
make profile-reset # re-enable all gstack skills
|
||||
make profile-current # show the active profile (full when none selected)
|
||||
make profile-reset # go to the default profile (full)
|
||||
make new-skill name=myskill # scaffold agent + skill files
|
||||
```
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ Output: `COMPLEXITY: <score>% — <label>` with one-line justification.
|
||||
```
|
||||
PLUGIN CHECK
|
||||
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]
|
||||
COMPLEXITY: <score>% — <simple|moderate|complex|enterprise>
|
||||
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` |
|
||||
| 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`.
|
||||
Plugin state is NOT touched by reset — re-enable a managed plugin manually
|
||||
or by applying a profile that lists it (e.g. `apply web` to restore
|
||||
`ui-ux-pro-max`).
|
||||
To go back to the default profile: `bash $HOME/.claude/lib/profile.sh reset`
|
||||
(= `set full`: enables full's list, parks non-listed gstack/managed items,
|
||||
toggles the managed plugins like any `set`).
|
||||
|
||||
## BLOCK if
|
||||
|
||||
|
||||
+9
-4
@@ -20,12 +20,17 @@ BRANCH_STR="${BRANCH:+ ($BRANCH)}"
|
||||
|
||||
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).
|
||||
# Read directly — `profile.sh current` is 12s+ and unusable in a statusline.
|
||||
PROFILE="?"
|
||||
if [ -f "$REPO/.active-profile" ]; then
|
||||
PROFILE=$(head -n1 "$REPO/.active-profile" | tr -d '[:space:]')
|
||||
[ -z "$PROFILE" ] && PROFILE="?"
|
||||
# Cache absent/empty/legacy "none" (same rule as active_profile()) → default.
|
||||
PROFILE=$(head -n1 "$REPO/.active-profile" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ -z "$PROFILE" ] || [ "$PROFILE" = "none" ]; then
|
||||
PROFILE="$DEFAULT_PROFILE"
|
||||
fi
|
||||
|
||||
# Effort level from settings.json (.effortLevel — set by /effort or manual edit).
|
||||
|
||||
+47
-22
@@ -952,11 +952,11 @@ done
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# STEP 8.7 — 21ST.DEV CLI + SKILL PACK — installed but DISABLED by default
|
||||
# STEP 8.7 — 21ST.DEV CLI + SKILL PACK
|
||||
# ============================================================
|
||||
# `@21st-dev/cli` (bin `21st`) supersedes the `@21st-dev/magic` MCP server:
|
||||
# same endpoint, one browser login (`21st login`, token in ~/.config/21st),
|
||||
# no API key, no MCP process loaded into every session. It ships a pack of
|
||||
# `@21st-dev/cli` (bin `21st`): one browser login (`21st login`, token in
|
||||
# ~/.config/21st), no API key, no MCP process loaded into every session. It
|
||||
# ships a pack of
|
||||
# verified skills (21st-ui-build / -explore / -review / -cli-use / -ai /
|
||||
# -registry / -design-sync) that drive the CLI from Claude Code.
|
||||
#
|
||||
@@ -966,10 +966,11 @@ echo ""
|
||||
# install under a staged HOME, then move each skill into skills-external/
|
||||
# (gitignored), where toggle-external.sh / profile.sh symlink it in.
|
||||
#
|
||||
# Default policy: pack DISABLED at install time — every skill description
|
||||
# loads into every session. Enable on demand:
|
||||
# bash lib/toggle-external.sh enable 21st (whole pack)
|
||||
# /profile design (the 5 design skills)
|
||||
# The pack's state is governed by profiles, not by this step: Step 11
|
||||
# applies the default profile (`full`), which links the five design skills
|
||||
# in; the two publishing skills (21st-registry, 21st-design-sync) stay on
|
||||
# demand — no profile lists them, so they are never auto-linked. A re-run
|
||||
# must never re-park what the selected profile or the user already enabled.
|
||||
echo "── Step 8.7: 21st.dev CLI + skill pack ─────────────────────"
|
||||
echo ""
|
||||
if command -v 21st &>/dev/null; then
|
||||
@@ -1041,19 +1042,9 @@ if command -v 21st &>/dev/null; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Default-disabled, same policy as before the MCP→CLI move.
|
||||
if [ -x "$REPO/lib/toggle-external.sh" ]; then
|
||||
TFD_STATUS="$(bash "$REPO/lib/toggle-external.sh" status 21st 2>/dev/null || echo missing)"
|
||||
if [ "$TFD_STATUS" = "enabled" ]; then
|
||||
info "Disabling the 21st skill pack by default (enable on demand)..."
|
||||
bash "$REPO/lib/toggle-external.sh" disable 21st >/dev/null
|
||||
ok "21st skill pack disabled — enable with: bash lib/toggle-external.sh enable 21st"
|
||||
else
|
||||
ok "21st skill pack disabled (default)"
|
||||
fi
|
||||
else
|
||||
warn "lib/toggle-external.sh not found or not executable — skipping"
|
||||
fi
|
||||
# The pack's state is governed by profiles (Step 11), not parked here: a
|
||||
# re-run must never re-park what the selected profile or the user enabled.
|
||||
# See the Step 11 banner for how the default profile applies.
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
@@ -1137,6 +1128,40 @@ else
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# STEP 11 — DEFAULT PROFILE
|
||||
# ============================================================
|
||||
# The profile decides which skills / externals / plugins are on. No
|
||||
# selection yet (.active-profile absent, empty or legacy "none" — same
|
||||
# rule as lib/profile.sh active_profile()) → apply the default via
|
||||
# `profile.sh reset`. An existing selection is re-applied (`set`). Plugin legs
|
||||
# are install-immutable (the EXIT guard restores settings.json, BDR-028;
|
||||
# the committed enabledPlugins already match the default profile), so
|
||||
# only the skill / external legs matter here.
|
||||
echo "── Step 11: Default profile ────────────────────────────────"
|
||||
echo ""
|
||||
if [ -f "$REPO/lib/profile.sh" ]; then
|
||||
SEL="$(head -n1 "$REPO/.active-profile" 2>/dev/null | tr -d '[:space:]' || true)"
|
||||
case "$SEL" in
|
||||
""|none)
|
||||
info "No profile selected — applying the default profile (bash lib/profile.sh reset)..."
|
||||
bash "$REPO/lib/profile.sh" reset \
|
||||
|| warn "default profile not applied — run: bash lib/profile.sh reset"
|
||||
;;
|
||||
*)
|
||||
# Steps 2 (gstack parked) and 10 (link.sh re-links the design
|
||||
# externals) rewrite skill state on every run: re-apply the
|
||||
# selection so its state comes back, label unchanged.
|
||||
info "Profile kept: $SEL — re-applying it (bash lib/profile.sh set $SEL)..."
|
||||
bash "$REPO/lib/profile.sh" set "$SEL" \
|
||||
|| warn "profile $SEL not re-applied — run: bash lib/profile.sh set $SEL"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
warn "lib/profile.sh not found — skipping the default profile"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
# SUMMARY
|
||||
# ============================================================
|
||||
@@ -1162,7 +1187,7 @@ echo " 🔄 frontend-design — distinctive frontend interfaces, anti-AI-
|
||||
echo " 🔄 impeccable — /impeccable design verbs + 45-rule deterministic detector (npx impeccable detect)"
|
||||
echo " 🔄 design-motion-principles — motion/animation design, 3-designer lens (kylezantos)"
|
||||
echo " 🔄 darwin-skill — autonomous skill optimizer (npx skills, ~/.agents/skills/)"
|
||||
echo " 🔄 21st skill pack — 21st.dev CLI skills, 7 (toggle: lib/toggle-external.sh enable 21st)"
|
||||
echo " 🔄 21st skill pack — 21st.dev CLI skills; design ones follow the profile (full by default), publishing ones on demand (toggle: lib/toggle-external.sh enable 21st)"
|
||||
echo ""
|
||||
echo " All plugins installed at: user scope (~/.claude/plugins/)"
|
||||
echo " GStack skills symlinked individually into ~/.claude/skills/ (→ submodule)"
|
||||
|
||||
+88
-71
@@ -26,10 +26,11 @@
|
||||
# profile.sh list list available profiles
|
||||
# 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 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 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 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
|
||||
PROFILES_DIR="$REPO/lib/profiles"
|
||||
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
|
||||
# never auto-disabled — protects always-on plugins (security-guidance,
|
||||
@@ -81,9 +83,8 @@ MANAGED_EXTERNALS=(
|
||||
|
||||
# MCP servers that are toggle-managed by `set`, both ways (enable AND
|
||||
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
|
||||
# Empty since 2026-09-22: `magic` was the only entry and 21st.dev replaced
|
||||
# its MCP server with a CLI + skill pack (the 5 design skills are managed as
|
||||
# externals above). The `mcp` type itself stays supported — a profile can
|
||||
# Empty: no MCP server is managed today (the 21st design skills are managed
|
||||
# as externals above). The `mcp` type itself stays supported — a profile can
|
||||
# still list an MCP, it is then advisory rather than auto-toggled.
|
||||
MANAGED_MCPS=()
|
||||
|
||||
@@ -109,6 +110,26 @@ write_active() {
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
# Read a profile file. Output one line per entry: "<skill>\t<type>"
|
||||
@@ -259,6 +280,25 @@ skill_status() {
|
||||
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_skill() {
|
||||
@@ -330,10 +370,8 @@ enable_skill() {
|
||||
fi
|
||||
;;
|
||||
mcp)
|
||||
# Advisory only. The delegation branch that lived here served `magic`,
|
||||
# the single managed MCP; 21st.dev replaced it with a CLI (BDR-093), so
|
||||
# MANAGED_MCPS is empty and nothing is auto-registered. Re-add a branch
|
||||
# here the day a profile owns an MCP server again.
|
||||
# Advisory only: MANAGED_MCPS is empty, nothing is auto-registered.
|
||||
# Re-add a delegation branch here the day a profile owns an MCP server.
|
||||
if [ "$(skill_status "$skill" mcp)" = "enabled" ]; then
|
||||
: # already on
|
||||
else
|
||||
@@ -579,7 +617,7 @@ cmd_set() {
|
||||
|
||||
# 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`.
|
||||
# design leftovers (emil, the 21st pack…) no longer survive a `set backend`.
|
||||
disable_externals_not_in "$prof"
|
||||
disable_mcps_not_in "$prof"
|
||||
|
||||
@@ -588,39 +626,37 @@ cmd_set() {
|
||||
}
|
||||
|
||||
cmd_reset() {
|
||||
info "Re-enabling all gstack skills (move skills-disabled/gstack__* back)"
|
||||
enable_all_gstack
|
||||
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"
|
||||
info "Resetting to the default profile: $DEFAULT_PROFILE (exclusive — enables its list, parks any non-listed gstack or managed item currently on)"
|
||||
cmd_set "$DEFAULT_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
|
||||
# layer all gstack on top of their current profile, or trim it back down
|
||||
# to just what the active profile needs.
|
||||
# label intact (unlike reset, which switches to the default profile). Lets
|
||||
# the user layer all gstack on top of their current profile, or trim it
|
||||
# back down to just what the active profile needs.
|
||||
cmd_gstack() {
|
||||
local action="${1:-}"
|
||||
case "$action" in
|
||||
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.
|
||||
local parked
|
||||
parked="$(parked_gstack_count)"
|
||||
if [ "$parked" -eq 0 ]; then
|
||||
info "all gstack skills already enabled"
|
||||
info "nothing parked — gstack skills are linked per profile (set/apply/reset)"
|
||||
else
|
||||
enable_all_gstack
|
||||
ok "all gstack enabled ($parked skills restored)"
|
||||
ok "$parked parked gstack skills restored"
|
||||
fi
|
||||
;;
|
||||
off)
|
||||
# Disable gstack skills not needed by the active profile. Needs a real
|
||||
# active profile to know what to keep.
|
||||
# Disable gstack skills not needed by the active profile. A cache
|
||||
# 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
|
||||
active="$(head -n1 "$ACTIVE_CACHE" 2>/dev/null || echo none)"
|
||||
[ -z "$active" ] && active="none"
|
||||
if [ "$active" = "none" ] || [ ! -f "$PROFILES_DIR/$active.profile" ]; then
|
||||
active="$(active_profile)"
|
||||
if [ ! -f "$PROFILES_DIR/$active.profile" ]; then
|
||||
err "no active profile — 'gstack off' needs one to know what to keep"
|
||||
info "run: bash lib/profile.sh set <name> then: gstack off"
|
||||
return 1
|
||||
@@ -643,48 +679,27 @@ EOF
|
||||
}
|
||||
|
||||
cmd_current() {
|
||||
# A profile is "active" only if (a) most of its skills are enabled AND
|
||||
# (b) at least one non-listed gstack skill is currently disabled (i.e. a
|
||||
# `set` has actually been applied). Without (b), every profile reports
|
||||
# 100% trivially because the full gstack is on.
|
||||
local disabled_count=0
|
||||
if [ -d "$DISABLED_DIR" ]; then
|
||||
disabled_count=$(find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
if [ "$disabled_count" -eq 0 ]; then
|
||||
echo "none (all gstack skills enabled — no profile set)"
|
||||
# Label-driven (BDR-030): name active_profile() and score ONLY that
|
||||
# profile — no cross-profile best-guess scan, no fast path keyed on the
|
||||
# parked-gstack count (that count says nothing about which profile is on
|
||||
# when gstack starts off, as it does on a real tree).
|
||||
local label; label="$(active_profile)"
|
||||
if [ ! -f "$PROFILES_DIR/$label.profile" ]; then
|
||||
echo "$label (unknown profile — no lib/profiles/$label.profile; run: profile reset)"
|
||||
return 0
|
||||
fi
|
||||
# Pick the profile with the highest "available" ratio. An item counts as
|
||||
# available when its status is "enabled" (skills, plugins, MCPs) or
|
||||
# "installed" (CLIs). On ties, the profile with the larger total wins
|
||||
# — superset profiles describe state more completely than subsets.
|
||||
local f name total available score skill type status
|
||||
local best="" best_score=0 best_total=0
|
||||
for f in "$PROFILES_DIR"/*.profile; do
|
||||
[ -f "$f" ] || continue
|
||||
name="$(basename "$f" .profile)"
|
||||
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 "$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)"
|
||||
|
||||
local available total pct
|
||||
read -r available total <<<"$(profile_match "$label")"
|
||||
pct=0
|
||||
[ "$total" -gt 0 ] && pct=$((available * 100 / total))
|
||||
local parked; parked="$(parked_gstack_count)"
|
||||
|
||||
local cached; cached="$(read_cache)"
|
||||
if [ -z "$cached" ] || [ "$cached" = "none" ]; then
|
||||
echo "$label (default — not applied yet, ${pct}% of its items enabled; run: profile reset)"
|
||||
else
|
||||
echo "custom (best guess: ${best:-none} ${best_score}%, $disabled_count gstack skills disabled)"
|
||||
echo "$label (${pct}% match, $parked gstack skills disabled)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -711,10 +726,11 @@ USAGE:
|
||||
profile list list all available profiles
|
||||
profile show <name> show profile contents grouped by type + status
|
||||
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 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 diff <a> <b> compare two profiles
|
||||
|
||||
@@ -734,14 +750,15 @@ EXAMPLES:
|
||||
bash lib/profile.sh show design
|
||||
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 reset # restore everything
|
||||
bash lib/profile.sh reset # back to the default profile (full)
|
||||
|
||||
NOTE:
|
||||
"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.
|
||||
(emil-design-eng, frontend-design, design-motion-principles, impeccable,
|
||||
the five 21st design skills). Anything outside those allowlists stays
|
||||
advisory — run "claude plugin enable|disable" or
|
||||
"bash lib/toggle-external.sh enable|disable <tool>" yourself.
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ]
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/profile-set-managed.test.sh — `set` symmetry on managed
|
||||
# externals, gstack on-demand, external from-source (BDR-079). The MCP
|
||||
# assertions went with `magic` (2026-09-22): MANAGED_MCPS is empty now, the
|
||||
# 21st skills that replaced it are managed as externals, so the pack's
|
||||
# park/restore round-trip is what this covers on that side.
|
||||
# externals, gstack on-demand, external from-source (BDR-079). No MCP is
|
||||
# managed (MANAGED_MCPS is empty): the 21st skills are managed as externals,
|
||||
# so the pack's park/restore round-trip is what this covers on that side.
|
||||
# Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH.
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
# audit skills, etc.) instead of all-or-nothing gstack toggling, use:
|
||||
# bash lib/profile.sh list
|
||||
# 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
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"21st": {
|
||||
"source": "npm:@21st-dev/cli",
|
||||
"version": "latest",
|
||||
"note": "21st.dev CLI (bin `21st`) — supersedes the @21st-dev/magic MCP server (2026-09-22). Standalone CLI + a pack of 7 skills, no MCP, no API key: auth is `21st login` (browser token in ~/.config/21st). Install: npm install -g @21st-dev/cli. The skill pack is staged-installed into skills-external/21st-* by install-plugins.sh Step 8.7 — `21st skills install` refuses to write through the ~/.claude/skills symlink."
|
||||
"note": "21st.dev CLI (bin `21st`) — standalone CLI + a pack of 7 skills, no MCP, no API key: auth is `21st login` (browser token in ~/.config/21st). Install: npm install -g @21st-dev/cli. The skill pack is staged-installed into skills-external/21st-* by install-plugins.sh Step 8.7 — `21st skills install` refuses to write through the ~/.claude/skills symlink."
|
||||
},
|
||||
"graphifyy": {
|
||||
"source": "pypi:graphifyy",
|
||||
|
||||
+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
|
||||
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
|
||||
|
||||
```bash
|
||||
@@ -81,7 +87,7 @@ bash "$HOME/.claude/lib/profile.sh" list
|
||||
# Show profile contents + per-skill status
|
||||
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
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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 |
|
||||
| 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 |
|
||||
| `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 | — |
|
||||
| `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
|
||||
|
||||
- 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
|
||||
(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
|
||||
the user asks.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user