chore(memory): BDR-101 + LRN-170 + LRN-171 + EVAL-031 — feat default-profile-full

This commit is contained in:
bastien
2026-09-25 16:38:53 +02:00
parent 0926cc74b5
commit 1b418cae84
7 changed files with 496 additions and 0 deletions
+10
View File
@@ -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-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-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-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. - **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. - **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]]. - **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]].
+8
View File
@@ -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-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-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-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. - **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. - **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. - **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]].
+1
View File
@@ -510,3 +510,4 @@ rules:
## 2026-09-25 ## 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. - 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.
+18
View File
@@ -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-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-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-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]]). - **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. - **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). - **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]].
+26
View File
@@ -1,5 +1,31 @@
# TODO # 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) ## 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 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 `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.