chore(memory): 21st sign-in gate — contract, plan r3, TODO, journal
This commit is contained in:
@@ -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".
|
||||
Reference in New Issue
Block a user