chore(memory): 21st sign-in gate — contract, plan r3, TODO, journal

This commit is contained in:
bastien
2026-09-28 12:51:56 +02:00
parent bd3e525bb3
commit 132bcdf7c5
4 changed files with 259 additions and 0 deletions
+1
View File
@@ -537,3 +537,4 @@ rules:
- User go: feature/doctor-vendored-skills merged into develop via `gitflow finish` → 2c94a0c, no conflict, pushed, local + origin copies removed by the lib. develop == origin/develop, no working branch anywhere. `make doctor` now covers the 11 vendored externals.
- Skill-catalog audit (user: "tour des skills, doublons, économiser tokens"): 5 analyzers over 150 skills / 53.5k chars desc; 78 listed name-only this session (listing budget ≈1 % ctx, least-invoked lose desc → gain = routing quality + no broken 100 KB body invoked, not listing chars). Found: frontend-design plugin byte-dup of managed copy; brightdata 21 skills keyless + hostile WebFetch routing; gstack ship trunk-based (origin/HEAD=main), land-and-deploy auto-merge+deploy, autoplan/make-pdf/diagram/careful/guard/freeze dead paths (only bin + browse/dist linked); security-guidance = Opus call per code turn + agentic commit review, 0 findings/6 days; doctor.sh undercount ×6. User go: tier 1, superpowers vendor-7 (tier 2 later), 21st trio parked (CLI `Not logged in`), rule "full ⊇ every profile, max = everything". Live: brightdata disabled, frontend-design plugin uninstalled, `set full` → 75 skills (was 89).
- /feat by hand on feature/skill-catalog-prune: contract 18 criteria; plan r1→r4 (3 challengers, confirmation FATAL(4): nested SKILL.md in browser-skills/openclaw/node_modules, ./setup global symlink, update-all 3rd copy); 4 feater parallel DONE; GATE 0 UNMET(4) = MY heredoc CHECKs (gates.sh single-line) → oracles to `<contract>.oracles/*.py` → MET; verifier ECARTS(1) = floor-guard `xit(` false-positive on `sys.exit(` → restructure → CONFORME; security PASS. 41 suites green minus 2 pre-existing T16a, shellcheck clean. UNMERGED — human gate. Registries pending user approval.
- User: "quand on détecte qu'on a besoin de 21st, on demande de log si c'est pas fait et on attend". /feat by hand on the same branch: design gate gains exit 12 `SIGN-IN REQUIRED` (three-state whoami probe, unknown → 11 with diagnostic, explicit "proceed without 21st" only skip); challenge round dropped my shared-helper idea (would break 4 fixture suites + change installer semantics) and my in-session `export TWENTYFIRST_TOKEN` remedy (env does not persist across tool calls). Executor DONE first pass, GATE 0 MET, verifier CONFORME 7/7, security PASS, 8/8 hermetic. Gate now exits 12 live here until `21st login`.
+19
View File
@@ -1,5 +1,24 @@
# TODO
## 2026-09-28 — design gate asks for `21st login` and waits (feature/skill-catalog-prune)
User: "si on veut l'utiliser, on demande à l'utilisateur de se log, plus simple que
dire c'est pas logged on utilise pas… on demande de log si c'est pas fait et on
attend". Contract `.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md`.
- [x] S1 three-state probe `twentyfirst_auth_state` INLINE in design-tool-gate.sh
(challenge r2 dropped the shared helper: install-plugins/toggle-external keep
their own semantics); `in` (TWENTYFIRST_TOKEN / API_KEY_21ST, or whoami
"Logged in as") / `out` (exact "Not logged in") / `unknown:whoami: rc=…`
→ exit 11 with a CLI-specific remedy; exit 12 `SIGN-IN REQUIRED`;
`DESIGN_GATE_REPO_OVERRIDE`; hermetic suite 8/8 (stub control, in, out,
token, absent, INCOMPLETE wins, unknown ×2).
- [x] S2 design-gate.md §3 branch 12: STOP, ask `! 21st login` (or any terminal
on this machine), END THE TURN, re-run on reply; explicit "proceed without
21st" = the only skip, stated visibly, not re-asked in the run; no in-session
token export; §4 resume path; feat/bugfix STEP 0.5 name SIGN-IN REQUIRED.
- [x] S3 plan r1→r3 (3 challengers + confirmation), executor DONE, GATE 0 MET,
verifier CONFORME 7/7, security PASS. Live on this machine: gate exits 12
until `21st login`. Committed in place on feature/skill-catalog-prune.
## 2026-09-28 — skill-catalog prune, tier 1 (feature/skill-catalog-prune)
User go after the 5-agent duplicate audit (150 skills, 53.5k chars of descriptions,
78 listed name-only in session = listing budget exceeded). Contract
@@ -0,0 +1,49 @@
# CONTRACT — 21st-signin-gate
- date: 2026-09-28 | flow: feat (ad-hoc dispatch, /feat gates replayed by the orchestrator) | branch: feature/skill-catalog-prune (working branch, commit in place)
- status: active
## REQUEST (verbatim — IMMUTABLE)
> Il faudrait pour 21st. Que, si on veut l'utiliser. Alors on demande à l'utilisateur de se log. Plus simple que de dire ah bah c'est pas logged on utilise pas. Donc ajoute ça quelque part, quand on detect qu'on a besoin de 21st, on demande de log si c'est pas fait et on attend
## CLARIFICATIONS
- Pass A: none — request complete. "Detect we need 21st" = the design gate (lib/design-gate.md → lib/design-tool-gate.sh), the single place the 21st CLI is required (GATE-BLOCK of design.profile); the 21st skills themselves are machine-owned (`21st skills install`) and are not edited.
- Pass B: no visible / public-name / scope choice left open — the gate message wording follows the gate's existing style, the exit code and the helper file are internal. Proceeds silently.
- [challenge 2026-09-28, 3 lenses: simplicity CONCERNS(1), correctness FATAL(2), robustness FATAL(4); every BLOCKER/MAJOR closed by a named plan change, r2] (a) NO shared helper: the predicate lives inline in lib/design-tool-gate.sh, toggle-external.sh and install-plugins.sh are untouched (their inline checks keep their own semantics); (b) three-state predicate `in` / `out` (exact "Not logged in" sentence) / `unknown` (rc≠0, timeout, unexpected line) → `unknown` surfaces as exit 11 with the raw diagnostic, never as the sign-in remedy; (c) no in-session `export TWENTYFIRST_TOKEN` remedy (env does not persist across tool calls, secrets stay out of the transcript) — the env var is honored when already present; (d) explicit user opt-out "proceed without 21st", stated visibly, scoped to the run; silent skip forbidden.
- [confirmation pass 2026-09-28, robustness CONCERNS(1), all closed by named changes, r3] unknown diagnostic pinned to `whoami: rc=<rc> <line>` with a CLI-specific remedy in the 11 block; stdout-only classification, `</dev/null`, rc captured under pipefail (test proves rc≠0 beats the sentence); hermeticity precondition on the sanitized PATH; MIRROR note at both sites; doc offers any terminal on this machine and does not re-ask after an explicit opt-out; `API_KEY_21ST` honored next to `TWENTYFIRST_TOKEN` (the CLI's second token env).
- Sign-in predicate = the CLI's own auth paths: `TWENTYFIRST_TOKEN` or `API_KEY_21ST` non-empty, or `21st whoami` first line starting with `Logged in as ` (local token read, no network; same sentence lib/toggle-external.sh:245 and install-plugins.sh:1059 test today). `whoami` wrapped in `timeout 15`; any other answer = `unknown`.
- Waiting = the orchestrator asks the user to run `! 21st login` in the session (browser flow) and ENDS THE TURN; on the user's reply it re-runs the gate before continuing. The agent never runs `21st login` itself (opens a browser, needs the human). A signed-out 21st is never treated as absent and its steps are never skipped.
- Functions ≤ 25 logic lines, 80-char lines; shellcheck clean; hermetic tests neutralize the real machine (`HOME` and `PATH` point into the fixture so `ensure_21st_on_path` cannot find the real CLI).
- Executors never run `21st login`, `profile.sh set|apply|reset`, `claude plugin …`, never commit.
## ACCEPTANCE CRITERIA
1. The three-state predicate lives in the gate script only; toggle-external.sh and install-plugins.sh are byte-identical to HEAD. [challenge r2]
CHECK: grep -q '^twentyfirst_auth_state()' lib/design-tool-gate.sh && grep -q 'DESIGN_GATE_REPO_OVERRIDE' lib/design-tool-gate.sh && git diff --quiet HEAD -- lib/toggle-external.sh install-plugins.sh && [ ! -e lib/twentyfirst-auth.sh ] && echo GATE_ONLY
EXPECT: GATE_ONLY
EVIDENCE: MET exit=0 marker-found :: GATE_ONLY
2. Live gate on this machine (21st installed, not signed in, no TWENTYFIRST_TOKEN): exit 12, output names `21st login`, and does NOT claim INCOMPLETE nor READY.
CHECK: env -u TWENTYFIRST_TOKEN bash lib/design-tool-gate.sh >/tmp/dtg.out 2>&1; rc=$?; cat /tmp/dtg.out; [ "$rc" = 12 ] && grep -q '21st login' /tmp/dtg.out && grep -q 'SIGN-IN REQUIRED' /tmp/dtg.out && ! grep -q 'INCOMPLETE' /tmp/dtg.out && ! grep -qE 'toolchain: READY' /tmp/dtg.out && echo LIVE_SIGNIN_12
EXPECT: LIVE_SIGNIN_12
EVIDENCE: MET exit=0 marker-found :: design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in ask the user to run in this session: ! 21st login (browser flow, save…
3. Hermetic suite green: stub control; signed-in → 0 READY; signed-out → 12 with `21st login`; TWENTYFIRST_TOKEN or API_KEY_21ST set → 0; CLI absent → 10 INCOMPLETE; INCOMPLETE wins over signed-out; unknown whoami answer (garbage rc 0, or the signed-out sentence with rc 3) → 11 with `whoami: rc=` diagnostic, without the sign-in remedy and without the claude-unreachable remedy. [challenge r2, r3]
CHECK: out=$(make test suite=lib/tests/design-tool-gate.test.sh 2>&1); echo "$out" | grep -qE 'FAIL=[1-9]' && { echo "$out" | tail -15; exit 1; }; for k in STUB_CONTROL SIGNED_IN_READY SIGNED_OUT_12 TOKEN_READY CLI_ABSENT_10 INCOMPLETE_WINS UNKNOWN_11; do echo "$out" | grep -q "PASS $k" || { echo "missing PASS $k"; exit 1; }; done; echo "$out" | grep -qE 'PASS=[1-9]' && echo SUITE_GREEN
EXPECT: SUITE_GREEN
EVIDENCE: MET exit=0 marker-found :: SUITE_GREEN
4. Gate doc and its two citers carry the new branch: design-gate.md documents exit 12 / SIGN-IN REQUIRED with `! 21st login`, "end the turn", re-run, the explicit opt-out "proceed without 21st", and never an in-session `export TWENTYFIRST_TOKEN`; feat and bugfix STEP 0.5 name SIGN-IN REQUIRED. [challenge r2]
CHECK: grep -q 'SIGN-IN REQUIRED' lib/design-gate.md && grep -q '! 21st login' lib/design-gate.md && grep -qi 'end the turn' lib/design-gate.md && grep -qi 'proceed without 21st' lib/design-gate.md && ! grep -qiE 'export TWENTYFIRST_TOKEN' lib/design-gate.md lib/design-tool-gate.sh && grep -q 'SIGN-IN REQUIRED' skills/feat/SKILL.md && grep -q 'SIGN-IN REQUIRED' skills/bugfix/SKILL.md && echo DOC_WIRED
EXPECT: DOC_WIRED
EVIDENCE: MET exit=0 marker-found :: DOC_WIRED
5. shellcheck clean on the two touched shell files; doctrine-citers and design-toolchain-reminder suites still green.
CHECK: shellcheck lib/design-tool-gate.sh lib/tests/design-tool-gate.test.sh && for s in doctrine-citers design-toolchain-reminder; do out=$(make test suite=lib/tests/$s.test.sh 2>&1) || { echo "$s rc"; exit 1; }; echo "$out" | grep -qE 'FAIL=[1-9]' && { echo "$s FAIL"; exit 1; }; done; echo SHELL_SUITES_OK
EXPECT: SHELL_SUITES_OK
EVIDENCE: MET exit=0 marker-found :: SHELL_SUITES_OK
6. When the gate is also INCOMPLETE (a blocking tool missing), the INCOMPLETE verdict (exit 10) wins; the sign-in state surfaces on the re-run after `/profile design` (hermetic case `INCOMPLETE_WINS`). [challenge r2: no extra line]
CHECK: out=$(make test suite=lib/tests/design-tool-gate.test.sh 2>&1); echo "$out" | grep -q 'PASS INCOMPLETE_WINS' && echo PRECEDENCE_OK
EXPECT: PRECEDENCE_OK
EVIDENCE: MET exit=0 marker-found :: PRECEDENCE_OK
7. CHANGELOG `[Unreleased]` names the new gate state and the remedy.
## FILE SCOPE
- lib/design-tool-gate.sh
- lib/design-gate.md, skills/feat/SKILL.md, skills/bugfix/SKILL.md (STEP 0.5 bullet only), CHANGELOG.md
- lib/tests/design-tool-gate.test.sh (new)
- Orchestrator-only: .claude/tasks/**, .claude/memory/**
@@ -0,0 +1,190 @@
# PLAN — 21st-signin-gate (feat, ad-hoc dispatch) — r3 (after confirmation pass)
- r3 closes the confirmation pass (robustness CONCERNS(1)): MAJOR 1 — the
unknown diagnostic format is pinned to `unknown:whoami: rc=<rc> <line>`
(rendered `21st (whoami: rc=3 …)`) and the 11 block prints a CLI-specific
remedy for 21st instead of `claude plugin list`; MINOR 2 — classify on
stdout only (`2>/dev/null`), stderr never enters the match; MINOR 3 — rc
captured through `if line="$(…)"` under pipefail, and the `fail` stub prints
the signed-out sentence AND exits 3 so the test proves rc≠0 wins; MINOR 4 —
`</dev/null` on the whoami call (the gate loop reads the profile on stdin);
MINOR 5 — hermeticity precondition = `! PATH=/usr/bin:/bin command -v 21st`
and no `/usr/local/bin/21st`; MINOR 6 — MIRROR note at both sites: the auth
state is gate-only, `profile.sh:skill_status()` has no counterpart; MINOR 7
— doc offers "or run `21st login` in any terminal on this machine, then
reply"; MINOR 8 — a 12 after an explicit opt-out in the same run is
reported once, not re-asked. Also honors `API_KEY_21ST` next to
`TWENTYFIRST_TOKEN` (the CLI's second token env, per its getToken).
- date: 2026-09-28 | contract: contracts/2026-09-28-21st-signin-gate-1215.md
- branch: feature/skill-catalog-prune (working branch → commit in place)
- executor: 1 feater (sonnet-pinned)
- r2 closes: simplicity MAJOR 1 (no shared helper — predicate inline in the
gate, toggle-external.sh and install-plugins.sh untouched, which also
voids correctness BLOCKER 1 / MAJOR 2 / MINOR 3 and robustness BLOCKER 1 /
MINOR 5-6), robustness MAJOR 2 (three-state predicate: `in` / `out` on the
exact "Not logged in" sentence / `unknown` → exit 11 with the raw
diagnostic, never the sign-in remedy), MAJOR 3 (no in-session
`export TWENTYFIRST_TOKEN` remedy; token path documented as shell profile +
restart), MAJOR 4 (explicit user opt-out "proceed without 21st", scoped to
the run, stated visibly; silent skip stays forbidden), correctness MINOR 4
(fake profile.sh executable, per-case output), MINOR 5 / robustness MINOR 7
(`also unverified` line in the 12 block), MINOR 6 (design-gate.md §4 names
the resume-after-sign-in path), robustness MINOR 8 (test asserts no
system-wide 21st first), simplicity MINOR 2 (no extra INCOMPLETE line, the
precedence test case stays), MINOR 4 (helper cases dropped), MINOR 5
(feat/bugfix bullets point at design-gate.md §3, no restated remedy).
## Ground truth (verified 2026-09-28)
- `lib/design-tool-gate.sh` (`set -euo pipefail`) checks the `21st` cli entry
with `command -v` only (`tool_active`, case `cli`). `ensure_21st_on_path`
probes `~/.local/bin`, `/usr/local/bin` and `~/.nvm/versions/node/*/bin`.
Exit codes: 0 ready · 11 ready-but-unverified · 10 incomplete · 2 error.
`REPO` is derived from the script path (no override); `PROFILE_SH` has
`DESIGN_GATE_PROFILE_SH`; `[ -x "$PROFILE_SH" ]` is required.
- `21st whoami` is a local token read, rc 0 both ways: `Logged in as <user>
(saved …).` or `Not logged in. Run \`21st login\`, or set TWENTYFIRST_TOKEN.`
The CLI is `#!/usr/bin/env node` under nvm: with a sanitized PATH it can
fail (rc≠0, "env: node: No such file") — that is NOT "signed out".
On this machine: installed, not signed in; the live gate today returns 0.
- Consumers: lib/design-gate.md §3 (verdict branches) and §4 (resume list);
skills/feat and skills/bugfix STEP 0.5 bullets; hotfix skips the gate.
- No hermetic test covers design-tool-gate.sh today. Existing suites copy
toggle-external.sh / profile.sh into fixtures — NOT touched by this plan.
## Approach
1. `lib/design-tool-gate.sh`:
- `REPO="${DESIGN_GATE_REPO_OVERRIDE:-$(cd -P … && pwd)}"` (fixture seam,
same idiom as PROFILE_REPO_OVERRIDE). Nothing new is sourced.
- New function `twentyfirst_auth_state` (≤ 25 logic lines): echoes `in`
when `${TWENTYFIRST_TOKEN:-}` or `${API_KEY_21ST:-}` is non-empty; else
`if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)";
then rc=0; else rc=$?; fi` (pipefail is set: rc is 21st's rc, 124 on
timeout; stdout only, stderr never enters the match; stdin closed so a
CLI reading stdin cannot eat the gate's profile loop). `in` when rc=0
and the line starts with `Logged in as `; `out` when rc=0 and the line
starts with `Not logged in`; otherwise exactly
`unknown:whoami: rc=<rc> <first 60 chars of line, or "no output">`.
Comment: the token envs are honored when already present (a shell-
profile export), never requested in-session.
- `tool_active` case `cli`: `command -v` fails → `inactive`; name `21st` →
`case "$(twentyfirst_auth_state)"` in `in` → `active`, `out` →
`signedout`, `unknown:*` → `unknown:<diag>`; other cli names → `active`.
- Main loop: state `signedout` → `signedout+=("$name")`; state `unknown:*`
→ `unverified_cli+=("$name (${state#unknown:})")`, rendered
`21st (whoami: rc=3 Something unexpected)`; the existing bare `unknown`
(claude unreachable) keeps filling `unverified`.
- Verdict order: blocking/manual → INCOMPLETE exit 10 (block unchanged).
Else signedout non-empty → print
`design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in`
` ask the user to run in this session: ! 21st login (browser flow, saves a local token)`
` then re-run this gate before any 21st step — never skip 21st silently`
plus the `also unverified` line(s) when either unverified array is
non-empty; exit 12.
Else unverified or unverified_cli non-empty → 11: one helper
`print_unverified` (≤ 25 logic lines) prints, for `unverified`, the
existing claude-unreachable block (`claude plugin list` remedy) and, for
`unverified_cli`, ` 21st could not answer: <diag> — a CLI runtime/PATH
problem (node under nvm?), not a sign-in problem; fix it, then re-run`.
The 10 and 12 blocks reuse the same helper for their `also unverified`
lines so no block ever says "claude CLI unreachable" about 21st. Else 0.
- Header comment: exit codes line gains `12 = sign-in required (21st)`;
the `required-manual` paragraph gets two lines on the three auth states;
the MIRROR sentence ("tool_active MIRRORS profile.sh:skill_status()")
gains "except the 21st auth state, gate-only, no skill_status
counterpart".
2. `lib/design-gate.md`:
- Exit-codes line: add `12 = sign-in required (21st installed, signed out)`.
- §3 new branch **12 / `SIGN-IN REQUIRED`** → STOP. Relay the script's
block. Ask the user to run `! 21st login` (the `!` prefix runs it in this
session, browser flow, saves a local token). END THE TURN and wait. On
the user's reply, re-run `design-tool-gate.sh` before any 21st step:
READY → continue; still 12 → ask again once, then offer the opt-out.
Explicit refusal — the user answers "proceed without 21st" (or words to
that effect) → say visibly `21st skipped for this run at your request`
and continue with the rest of the toolchain, 21st steps left out. Never
skip silently ("not logged in, so we don't use it" is the failure this
branch closes). Never run `21st login` yourself. `TWENTYFIRST_TOKEN` is
a shell-profile setting followed by a session restart, never an
in-session `export` (tool calls do not share a shell, and a secret does
not belong in the transcript).
- §3 **11** bullet: a `21st (whoami: rc=… …)` entry means the CLI could
not answer (runtime/PATH problem, node under nvm), so the remedy is the
diagnostic, not a sign-in; relay the script's own line.
- §3 **12** bullet also says: "or run `21st login` in any terminal on this
machine, then reply" (the token is a local file, any terminal works;
`! …` in-session is the convenient form, not the only one); and: after
an explicit opt-out, a later 12 in the same run is reported in one line,
never re-asked.
- §4 first paragraph: "(READY, after the user ran `/profile design`, or
after the sign-in re-run returns READY)".
- §IMPORTANT 21st bullet: add "signed out → exit 12: ask `! 21st login`,
wait, re-run; explicit opt-out only". §IMPORTANT MIRROR bullet: add
"except the 21st auth state: gate-only, no skill_status counterpart".
3. `skills/feat/SKILL.md` and `skills/bugfix/SKILL.md` STEP 0.5 bullet →
"If signals found → run `design-tool-gate.sh`; INCOMPLETE → tell the user
to run `/profile design`; SIGN-IN REQUIRED → design-gate.md §3 (ask
`! 21st login`, wait) before proceeding." No restated remedy beyond that.
4. `lib/tests/design-tool-gate.test.sh` (hermetic, `set -u`, `check` helper,
`trap 'rm -rf "$WORK"' EXIT`, style of lib/tests/skill-routing-census.test.sh):
- Precondition, loud: `! PATH=/usr/bin:/bin command -v 21st` and
`[ ! -e /usr/local/bin/21st ]` (the two places the sanitized test PATH
and `ensure_21st_on_path` could still find a real CLI) else print
`FAIL precondition: system-wide 21st present, CLI_ABSENT case not
hermetic` and count a FAIL.
- Fixture `$WORK/repo`: `lib/profiles/design.profile` holding
`# GATE-BLOCK: 21st ghost-skill` and entries `21st cli`,
`ghost-skill external`; `lib/profile.sh` = an executable stub that
`cat`s `$WORK/plain.txt` for `show design --plain` (per case the test
writes `cli\t21st` alone, or `cli\t21st` + `external\tghost-skill`);
`skills/` empty. `$WORK/bin/21st` = executable stub: `whoami` prints per
`$FAKE_21ST_MODE`: `in` → `Logged in as tester (saved locally).`,
`out` → `Not logged in. Run \`21st login\`, or set TWENTYFIRST_TOKEN.`,
`garbage` → `Something unexpected` (rc 0), `fail` → prints the exact
signed-out sentence AND exits 3 (proves rc≠0 overrides the sentence).
- Every gate run: `env -u TWENTYFIRST_TOKEN HOME=$WORK/home
PATH=$WORK/bin:/usr/bin:/bin DESIGN_GATE_REPO_OVERRIDE=$WORK/repo
DESIGN_GATE_PROFILE_SH=$WORK/repo/lib/profile.sh FAKE_21ST_MODE=<mode>
bash "$ROOT/lib/design-tool-gate.sh"` (CLAUDE_BIN irrelevant: no
plugin/mcp entry in the fixture).
- Stub positive control first: the stub prints the expected sentence for
`in` and `out` (`PASS STUB_CONTROL`).
- Cases, each `PASS <NAME>`: `SIGNED_IN_READY` (rc 0, `READY`);
`SIGNED_OUT_12` (rc 12, `SIGN-IN REQUIRED`, `21st login`, no
`INCOMPLETE`); `TOKEN_READY` (mode out + `TWENTYFIRST_TOKEN=x` → rc 0);
`CLI_ABSENT_10` (PATH without `$WORK/bin` → rc 10, `INCOMPLETE`);
`INCOMPLETE_WINS` (plain adds ghost-skill, mode out → rc 10,
`INCOMPLETE`, no `SIGN-IN REQUIRED` line); `UNKNOWN_11` (mode garbage →
rc 11, output has `whoami: rc=0` and `Something unexpected`, lacks
`21st login` and lacks `claude CLI unreachable`; mode fail → rc 11 with
`whoami: rc=3`). `TOKEN_READY` also checks `API_KEY_21ST=x` alone → rc 0.
Summary `PASS=n FAIL=m`, rc 1 on any FAIL.
5. CHANGELOG `[Unreleased]` → Added: design gate `SIGN-IN REQUIRED` (exit 12)
when the 21st CLI is installed but signed out — the agent asks for
`! 21st login` and waits, explicit opt-out only; unknown whoami answers
surface as unverified with the diagnostic.
## Edge cases
- `21st whoami` hang: `timeout 15` → rc 124 → `unknown`, exit 11 with the
diagnostic (not a sign-in loop).
- `TWENTYFIRST_TOKEN` set but invalid: the CLI decides at call time; the gate
honors the env var as `in` (documented).
- INCOMPLETE and signed out at once: 10 wins by construction (the re-run
after `/profile design` returns 12); no extra line.
- The fixture never sees the real `~/.nvm` (HOME redirected) and the test
fails loudly if a system-wide 21st exists.
- `set -euo pipefail`: the `whoami` capture must not abort the script on a
nonzero rc (run inside `if`, or `|| true`).
## Tests
- lib/tests/design-tool-gate.test.sh (new); `make test suite=` for
doctrine-citers, design-toolchain-reminder (unchanged suites, stay green).
- shellcheck lib/design-tool-gate.sh lib/tests/design-tool-gate.test.sh.
## Disposition (RELATED MEMORY)
- honors BDR-025 — GATE-BLOCK single source untouched; a state is added, not
a scope.
- honors BDR-093 — 21st auth is `21st login` / TWENTYFIRST_TOKEN, no key, no MCP.
- honors LRN-102 — the STOP asks in the turn's final text and ends the turn.
- honors "ask, don't guess" — a signed-out tool becomes a question to the
human, and an explicit refusal is an answer, never a silent skip.
- honors LRN-096 (vacuous guard class) — an unknown answer is surfaced, not
swallowed as "signed out".