# 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= ` (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 — ` (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 `. 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:`; 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: — 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= 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 `: `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".