Files
claude/.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md
T

7.3 KiB

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/