191 lines
12 KiB
Markdown
191 lines
12 KiB
Markdown
# 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".
|