feat(design-gate): ask for 21st login and wait instead of skipping
The design gate checked the 21st CLI with command -v only, so an installed-but-signed-out CLI read as READY and every 21st step failed downstream. tool_active now probes 21st whoami through a three-state function: signed in (TWENTYFIRST_TOKEN or API_KEY_21ST set, or 'Logged in as'), signed out (exact 'Not logged in'), unknown (rc != 0, timeout, unexpected output). Signed out is a new verdict, SIGN-IN REQUIRED, exit 12: design-gate.md tells the orchestrator to ask the user to run ! 21st login, end the turn, re-run the gate on their reply, and to skip 21st only on an explicit 'proceed without 21st', never silently. Unknown surfaces as exit 11 with the whoami diagnostic and a CLI-runtime remedy, so a node/PATH failure can never loop on a sign-in prompt. INCOMPLETE still wins. Hermetic suite lib/tests/design-tool-gate.test.sh (stub CLI, fixture repo through DESIGN_GATE_REPO_OVERRIDE) covers every state.
This commit is contained in:
+30
-6
@@ -61,7 +61,7 @@ skill symlink, `claude plugin list`, `claude mcp list`, `command -v`. It never
|
||||
reads `disabledMcpServers` (unreliable for bi-modal servers like context7).
|
||||
The core set lives in `design.profile`, not in the script or here — single source.
|
||||
|
||||
Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it) · `10` = incomplete (gate trips) · `2` = error.
|
||||
Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it) · `10` = incomplete (gate trips) · `12` = sign-in required (21st installed, signed out) · `2` = error.
|
||||
|
||||
### 3. Branch on the result
|
||||
|
||||
@@ -82,18 +82,39 @@ Exit codes: `0` = ready · `11` = ready-but-unverified (proceed, but surface it)
|
||||
but the CLI they shell out to is a global npm install: tell the user to run
|
||||
`npm i -g @21st-dev/cli` then `21st login` (no API key, no MCP).
|
||||
- Do NOT hand-activate individual tools. The profile is the unit of activation.
|
||||
- **12 / `SIGN-IN REQUIRED`** → STOP. The 21st CLI is installed but `21st
|
||||
whoami` reports signed out. Relay the script's block, then ask the user to
|
||||
run `! 21st login` (the `!` prefix runs it in this session, browser flow,
|
||||
saves a local token) — or run `21st login` in any terminal on this
|
||||
machine, then reply (the token is a local file; any terminal works, `!`
|
||||
in-session is just the convenient form). 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; after that,
|
||||
a later `12` in the same run is reported in one line, never re-asked.
|
||||
Never skip silently ("not logged in, so we don't use it" is the failure
|
||||
this branch closes). Never run `21st login` yourself — it opens a browser
|
||||
and needs the human. `TWENTYFIRST_TOKEN` is a shell-profile setting
|
||||
followed by a session restart, never an in-session `export` (tool calls
|
||||
don't share a shell, and a secret doesn't belong in the transcript).
|
||||
- **11 / `READY BUT UNVERIFIED`** → `claude` was unreachable, so the design
|
||||
plugin (ui-ux-pro-max) could NOT be checked. Do NOT report a plain "ready":
|
||||
proceed only after telling the user that N tool(s) went unverified and having
|
||||
them confirm with `claude plugin list`. Fail-visible, not fail-silent.
|
||||
them confirm with `claude plugin list`. Fail-visible, not fail-silent. A
|
||||
`21st (whoami: rc=… …)` entry in this block means the CLI itself could not
|
||||
answer (a runtime/PATH problem, e.g. node under nvm) — the remedy is the
|
||||
diagnostic the script prints, never a sign-in prompt; relay its own line.
|
||||
|
||||
### 4. Animation library — suggest-only (fires only on a real motion signal)
|
||||
|
||||
Orthogonal to the toolchain check above: §2-3 are about Claude's design TOOLS;
|
||||
this is about the PROJECT's runtime dep. Evaluate it only once the toolchain is
|
||||
resolved and you're actually proceeding with the build (READY, or after the user
|
||||
ran `/profile design`). Never on the INCOMPLETE stop path — that path has one
|
||||
action only (`/profile design`); don't stack an optional note on it.
|
||||
resolved and you're actually proceeding with the build (READY, after the user
|
||||
ran `/profile design`, or after the sign-in re-run returns READY). Never on
|
||||
the INCOMPLETE stop path — that path has one action only (`/profile
|
||||
design`); don't stack an optional note on it.
|
||||
|
||||
**Fires only when ALL THREE hold** — drop any one → no suggestion, stay silent:
|
||||
|
||||
@@ -182,11 +203,14 @@ remedy is always `/profile <that>` — a profile, never a lone tool.
|
||||
the profile system is the single source of truth for what's active.
|
||||
- the `21st` CLI is REQUIRED (it trips the gate) and `/profile design` cannot
|
||||
install it — the gate names the two commands; surface them to the user.
|
||||
Signed out → exit 12: ask `! 21st login`, wait, re-run; explicit opt-out
|
||||
only, never a silent skip.
|
||||
- The design-core set (what trips the gate) is declared in `design.profile` on
|
||||
the `# GATE-BLOCK:` line(s) — edit there to add/remove a blocking design tool,
|
||||
not in the script.
|
||||
- The state check shells out to `claude` (plugin/mcp list): a few seconds.
|
||||
Trivial / non-design tasks skip it entirely (no signal, or trivial tier).
|
||||
- `design-tool-gate.sh`'s per-type state checks MIRROR
|
||||
`profile.sh:skill_status()` — change one, sync the other.
|
||||
`profile.sh:skill_status()` — change one, sync the other, except the 21st
|
||||
auth state: gate-only, no skill_status counterpart.
|
||||
- Do NOT run this gate on pure backend/API/CLI tasks (no signals = no gate).
|
||||
|
||||
+101
-22
@@ -17,7 +17,8 @@
|
||||
# -> fall back to every skill/plugin/mcp entry (coarse).
|
||||
#
|
||||
# State (active or not) is checked per channel, by type. These per-type
|
||||
# checks MIRROR profile.sh:skill_status() — change one, sync the other.
|
||||
# checks MIRROR profile.sh:skill_status() — change one, sync the other,
|
||||
# except the 21st auth state: gate-only, no skill_status counterpart.
|
||||
#
|
||||
# type channel class
|
||||
# gstack|external|personal skill symlink in skills/ blocking
|
||||
@@ -31,18 +32,21 @@
|
||||
# it, and the remedy is `/profile design` + a manual step.
|
||||
# This is where the `21st` CLI lands: required, never
|
||||
# silent (npm i -g @21st-dev/cli, then 21st login).
|
||||
# 21st's sign-in state is three-valued: in (active),
|
||||
# out (exit 12, ask to sign in), unknown (exit 11).
|
||||
# Both classes trip the gate. Tools NOT on the GATE-BLOCK allowlist are
|
||||
# ignored entirely (browser/plan/shotgun tooling, graphify).
|
||||
#
|
||||
# disabledMcpServers is NEVER read — unreliable for bi-modal servers
|
||||
# (context7 can appear there yet be active via another channel).
|
||||
#
|
||||
# Exit: 0 = ready · 11 = ready-but-unverified (proceed, say so) · 10 = incomplete (trips) · 2 = error.
|
||||
# Exit: 0 = ready · 11 = ready-but-unverified (proceed, say so) ·
|
||||
# 10 = incomplete (trips) · 12 = sign-in required (21st) · 2 = error.
|
||||
# Usage: design-tool-gate.sh [profile] (default profile: design)
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REPO="${DESIGN_GATE_REPO_OVERRIDE:-$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
PROFILE_SH="${DESIGN_GATE_PROFILE_SH:-$REPO/lib/profile.sh}"
|
||||
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
|
||||
PROFILES_DIR="$REPO/lib/profiles"
|
||||
@@ -104,6 +108,35 @@ ensure_21st_on_path() {
|
||||
}
|
||||
ensure_21st_on_path
|
||||
|
||||
# 21st's sign-in state, three-valued. `whoami` is a local token read (no
|
||||
# network), rc 0 either way — so rc alone can't tell signed-in from signed-
|
||||
# out; the FIRST LINE of stdout does. A token env, when already exported by
|
||||
# the user's shell profile, wins without a CLI call (never requested here:
|
||||
# tool calls don't share a shell, and a secret doesn't belong in a comment
|
||||
# or the transcript). `timeout 15` bounds a hung CLI; stdin is closed so a
|
||||
# CLI that reads stdin can't eat the gate's own `read` loop; stderr never
|
||||
# enters the match (stdout only). Echoes: in | out | unknown:<diagnostic>.
|
||||
twentyfirst_auth_state() {
|
||||
if [ -n "${TWENTYFIRST_TOKEN:-}" ] || [ -n "${API_KEY_21ST:-}" ]; then
|
||||
echo in
|
||||
return
|
||||
fi
|
||||
local line rc
|
||||
if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)"; then
|
||||
rc=0
|
||||
else
|
||||
rc=$?
|
||||
fi
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
case "$line" in
|
||||
"Logged in as "*) echo in; return ;;
|
||||
"Not logged in"*) echo out; return ;;
|
||||
esac
|
||||
fi
|
||||
[ -n "$line" ] || line="no output"
|
||||
echo "unknown:whoami: rc=$rc ${line:0:60}"
|
||||
}
|
||||
|
||||
# Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated).
|
||||
# Empty => fall back to "every gate-relevant entry is in scope" (coarse).
|
||||
core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \
|
||||
@@ -115,8 +148,10 @@ in_scope() {
|
||||
case " $core_set " in *" $1 "*) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
|
||||
# State of one tool, by type. Mirrors profile.sh:skill_status() — keep in sync.
|
||||
# Echoes: active | inactive | unknown (unknown = can't verify, claude absent)
|
||||
# State of one tool, by type. Mirrors profile.sh:skill_status() — keep in
|
||||
# sync, except the 21st auth state (gate-only, no skill_status counterpart).
|
||||
# Echoes: active | inactive | unknown (can't verify, claude absent) |
|
||||
# signedout | unknown:<diagnostic> (last two: 21st CLI only)
|
||||
tool_active() {
|
||||
local name="$1" type="$2"
|
||||
case "$type" in
|
||||
@@ -135,7 +170,15 @@ tool_active() {
|
||||
if "$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${name}"; then echo active; else echo inactive; fi
|
||||
;;
|
||||
cli)
|
||||
if command -v "$name" >/dev/null 2>&1; then echo active; else echo inactive; fi
|
||||
command -v "$name" >/dev/null 2>&1 || { echo inactive; return; }
|
||||
[ "$name" = "21st" ] || { echo active; return; }
|
||||
local auth
|
||||
auth="$(twentyfirst_auth_state)"
|
||||
case "$auth" in
|
||||
in) echo active ;;
|
||||
out) echo signedout ;;
|
||||
unknown:*) echo "$auth" ;;
|
||||
esac
|
||||
;;
|
||||
*) echo inactive ;;
|
||||
esac
|
||||
@@ -150,12 +193,17 @@ plain="$("$PROFILE_SH" show "$PROFILE" --plain 2>/dev/null)" \
|
||||
blocking=() # inactive, /profile design activates it (skill/plugin)
|
||||
manual=() # inactive, required but needs a manual step (mcp key / cli install)
|
||||
unverified=() # can't check (claude CLI absent)
|
||||
signedout=() # 21st CLI installed, not signed in
|
||||
unverified_cli=() # 21st CLI: whoami answered something unexpected
|
||||
while IFS=$'\t' read -r type name; do
|
||||
[ -n "$type" ] || continue
|
||||
in_scope "$name" || continue # ignore non-core tooling (browser, plan-*, graphify)
|
||||
case "$(tool_active "$name" "$type")" in
|
||||
active) ;;
|
||||
unknown) unverified+=("$name") ;;
|
||||
state="$(tool_active "$name" "$type")"
|
||||
case "$state" in
|
||||
active) ;;
|
||||
signedout) signedout+=("$name") ;;
|
||||
unknown) unverified+=("$name") ;;
|
||||
unknown:*) unverified_cli+=("$name (${state#unknown:})") ;;
|
||||
*)
|
||||
case "$type" in
|
||||
gstack|external|personal|plugin) blocking+=("$name") ;;
|
||||
@@ -165,11 +213,31 @@ while IFS=$'\t' read -r type name; do
|
||||
esac
|
||||
done <<< "$plain"
|
||||
|
||||
# Verdict — three outcomes:
|
||||
# print_unverified — the "also unverified" lines shared by the 10, 11 and 12
|
||||
# blocks: a claude-unreachable tool keeps its existing remedy; a 21st CLI
|
||||
# that answered whoami with something unexpected gets its own — the two are
|
||||
# never merged, so no block blames 21st for a claude problem or vice versa.
|
||||
print_unverified() {
|
||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
|
||||
fi
|
||||
local entry name diag
|
||||
for entry in "${unverified_cli[@]}"; do
|
||||
name="${entry%% (*}"
|
||||
diag="${entry#*\(}"; diag="${diag%\)}"
|
||||
echo " $name could not answer: $diag —" \
|
||||
"a CLI runtime/PATH problem (node under nvm?)," \
|
||||
"not a sign-in problem; fix it, then re-run"
|
||||
done
|
||||
}
|
||||
|
||||
# Verdict — four outcomes, checked in order:
|
||||
# blocking/manual non-empty -> INCOMPLETE (exit 10): the gate trips.
|
||||
# only unverified non-empty -> READY BUT UNVERIFIED (exit 11): fail-VISIBLE.
|
||||
# claude was unreachable, so the plugin channel (ui-ux-pro-max) could not
|
||||
# be checked. Never pass this as a silent READY — proceed, but say so.
|
||||
# else signedout non-empty -> SIGN-IN REQUIRED (exit 12): ask the user to
|
||||
# run `21st login`, end the turn, wait, re-run — never a silent skip.
|
||||
# else unverified/unverified_cli non-empty -> READY BUT UNVERIFIED (exit
|
||||
# 11): fail-VISIBLE. claude unreachable and/or 21st couldn't answer
|
||||
# whoami — never pass either as a silent READY.
|
||||
# nothing pending -> READY (exit 0).
|
||||
if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
||||
echo "design toolchain: INCOMPLETE"
|
||||
@@ -182,19 +250,30 @@ if [ "${#blocking[@]}" -gt 0 ] || [ "${#manual[@]}" -gt 0 ]; then
|
||||
*" 21st "*) echo " 21st needs the CLI: npm i -g @21st-dev/cli then 21st login" ;;
|
||||
esac
|
||||
fi
|
||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
|
||||
fi
|
||||
print_unverified
|
||||
echo " → run: /profile $PROFILE"
|
||||
exit 10
|
||||
fi
|
||||
|
||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||
echo "design toolchain: READY BUT UNVERIFIED — ${#unverified[@]} tool(s) not checked"
|
||||
echo " unverified (claude CLI unreachable): ${unverified[*]}"
|
||||
echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
|
||||
echo " active. Proceed only after checking manually:"
|
||||
echo " claude plugin list"
|
||||
if [ "${#signedout[@]}" -gt 0 ]; then
|
||||
echo "design toolchain: SIGN-IN REQUIRED — 21st CLI installed, not signed in"
|
||||
echo " ask the user to run in this session:" \
|
||||
" ! 21st login" \
|
||||
" (browser flow, saves a local token)"
|
||||
echo " then re-run this gate before any 21st step — never skip 21st silently"
|
||||
print_unverified
|
||||
exit 12
|
||||
fi
|
||||
|
||||
if [ "${#unverified[@]}" -gt 0 ] || [ "${#unverified_cli[@]}" -gt 0 ]; then
|
||||
echo "design toolchain: READY BUT UNVERIFIED —" \
|
||||
"$(( ${#unverified[@]} + ${#unverified_cli[@]} )) tool(s) not checked"
|
||||
print_unverified
|
||||
if [ "${#unverified[@]}" -gt 0 ]; then
|
||||
echo " the gate could NOT confirm the design plugin (ui-ux-pro-max) is"
|
||||
echo " active. Proceed only after checking manually:"
|
||||
echo " claude plugin list"
|
||||
fi
|
||||
exit 11
|
||||
fi
|
||||
|
||||
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/design-tool-gate.test.sh — hermetic suite for the 21st sign-in
|
||||
# state added to lib/design-tool-gate.sh (contract 2026-09-28-21st-signin-
|
||||
# gate-1215): a fake `21st` CLI on a fixture PATH drives every whoami answer
|
||||
# (signed in / signed out / garbage / nonzero rc) through the real gate
|
||||
# script, with HOME and PATH redirected into the fixture so the machine's
|
||||
# real CLI is never reachable. A loud precondition proves that redirection
|
||||
# actually holds before any case runs.
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
GATE="$ROOT/lib/design-tool-gate.sh"
|
||||
PASS=0; FAIL=0
|
||||
|
||||
ok() { echo "PASS $1"; PASS=$((PASS + 1)); }
|
||||
bad() { echo "FAIL $1 — $2"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Precondition, loud: the sanitized PATH below and ensure_21st_on_path()'s
|
||||
# own probes must never resolve a REAL 21st — else CLI_ABSENT_10 (and every
|
||||
# other case) would silently exercise this machine's CLI instead of the stub.
|
||||
if PATH=/usr/bin:/bin command -v 21st >/dev/null 2>&1 \
|
||||
|| [ -e /usr/local/bin/21st ]; then
|
||||
echo "FAIL precondition: system-wide 21st present," \
|
||||
"CLI_ABSENT case not hermetic"
|
||||
FAIL=$((FAIL + 1))
|
||||
echo "PASS=$PASS FAIL=$FAIL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
mkdir -p "$WORK/repo/lib/profiles" "$WORK/repo/skills" "$WORK/bin" "$WORK/home"
|
||||
|
||||
# GATE-BLOCK allowlist: 21st (cli) always, ghost-skill (external) only used
|
||||
# by INCOMPLETE_WINS to prove a blocking miss still trips the gate.
|
||||
cat > "$WORK/repo/lib/profiles/design.profile" <<'EOF'
|
||||
# GATE-BLOCK: 21st ghost-skill
|
||||
21st cli
|
||||
ghost-skill external
|
||||
EOF
|
||||
|
||||
# Fake profile.sh: `show design --plain` cats whatever the case wrote to
|
||||
# plain.txt. $WORK is read from the environment at run time (exported
|
||||
# below), never baked in here — the heredoc is quoted on purpose.
|
||||
cat > "$WORK/repo/lib/profile.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
[ "$1" = show ] && [ "$3" = --plain ] && { cat "$WORK/plain.txt"; exit 0; }
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$WORK/repo/lib/profile.sh"
|
||||
export WORK
|
||||
|
||||
# Fake 21st CLI: `whoami` answers per $FAKE_21ST_MODE, the real CLI's exact
|
||||
# sentences for in/out (proven by STUB_CONTROL below), a garbage line at
|
||||
# rc 0, or the signed-out sentence at a nonzero rc (proves rc wins).
|
||||
cat > "$WORK/bin/21st" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
[ "${1:-}" = whoami ] || exit 1
|
||||
signedout='Not logged in. Run `21st login`, or set TWENTYFIRST_TOKEN.'
|
||||
case "${FAKE_21ST_MODE:-in}" in
|
||||
in) echo "Logged in as tester (saved locally)."; exit 0 ;;
|
||||
out) echo "$signedout"; exit 0 ;;
|
||||
garbage) echo "Something unexpected"; exit 0 ;;
|
||||
fail) echo "$signedout"; exit 3 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$WORK/bin/21st"
|
||||
|
||||
# gate_run <FAKE_21ST_MODE> <plain, \t and \n escapes> [PATH override]
|
||||
# -> sets $GATE_OUT / $GATE_RC. TWENTYFIRST_TOKEN and API_KEY_21ST are
|
||||
# always unset here; TOKEN_READY below sets them explicitly instead.
|
||||
gate_run() {
|
||||
local mode="$1" plain="$2" gate_path="${3:-$WORK/bin:/usr/bin:/bin}"
|
||||
printf '%b\n' "$plain" > "$WORK/plain.txt"
|
||||
GATE_OUT="$(env -u TWENTYFIRST_TOKEN -u API_KEY_21ST \
|
||||
HOME="$WORK/home" PATH="$gate_path" \
|
||||
DESIGN_GATE_REPO_OVERRIDE="$WORK/repo" \
|
||||
DESIGN_GATE_PROFILE_SH="$WORK/repo/lib/profile.sh" \
|
||||
FAKE_21ST_MODE="$mode" bash "$GATE" 2>&1)"
|
||||
GATE_RC=$?
|
||||
}
|
||||
|
||||
# ── stub positive control ────────────────────────────────────────────────
|
||||
if FAKE_21ST_MODE=in "$WORK/bin/21st" whoami | grep -q '^Logged in as ' \
|
||||
&& FAKE_21ST_MODE=out "$WORK/bin/21st" whoami | grep -q '^Not logged in'
|
||||
then ok STUB_CONTROL; else bad STUB_CONTROL "stub sentences wrong"; fi
|
||||
|
||||
# ── SIGNED_IN_READY: whoami says logged in -> exit 0, READY ────────────────
|
||||
gate_run in 'cli\t21st'
|
||||
if [ "$GATE_RC" -eq 0 ] && echo "$GATE_OUT" | grep -q 'toolchain: READY'; then
|
||||
ok SIGNED_IN_READY
|
||||
else
|
||||
bad SIGNED_IN_READY "rc=$GATE_RC out=$GATE_OUT"
|
||||
fi
|
||||
|
||||
# ── SIGNED_OUT_12: whoami says not logged in -> exit 12, ask to sign in ────
|
||||
gate_run out 'cli\t21st'
|
||||
if [ "$GATE_RC" -eq 12 ] && echo "$GATE_OUT" | grep -q 'SIGN-IN REQUIRED' \
|
||||
&& echo "$GATE_OUT" | grep -q '21st login' \
|
||||
&& ! echo "$GATE_OUT" | grep -q 'INCOMPLETE'; then
|
||||
ok SIGNED_OUT_12
|
||||
else
|
||||
bad SIGNED_OUT_12 "rc=$GATE_RC out=$GATE_OUT"
|
||||
fi
|
||||
|
||||
# ── TOKEN_READY: a token env wins even while whoami reports signed out ─────
|
||||
printf 'cli\t21st\n' > "$WORK/plain.txt"
|
||||
out_t="$(env 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=out TWENTYFIRST_TOKEN=x bash "$GATE" 2>&1)"; rc_t=$?
|
||||
out_k="$(env 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=out API_KEY_21ST=x bash "$GATE" 2>&1)"; rc_k=$?
|
||||
if [ "$rc_t" -eq 0 ] && [ "$rc_k" -eq 0 ]; then
|
||||
ok TOKEN_READY
|
||||
else
|
||||
bad TOKEN_READY "TOKEN rc=$rc_t ($out_t) | API_KEY rc=$rc_k ($out_k)"
|
||||
fi
|
||||
|
||||
# ── CLI_ABSENT_10: 21st off PATH -> exit 10, INCOMPLETE (not sign-in) ──────
|
||||
gate_run in 'cli\t21st' /usr/bin:/bin
|
||||
if [ "$GATE_RC" -eq 10 ] && echo "$GATE_OUT" | grep -q 'INCOMPLETE'; then
|
||||
ok CLI_ABSENT_10
|
||||
else
|
||||
bad CLI_ABSENT_10 "rc=$GATE_RC out=$GATE_OUT"
|
||||
fi
|
||||
|
||||
# ── INCOMPLETE_WINS: a blocking miss outranks a signed-out 21st ────────────
|
||||
gate_run out 'cli\t21st\nexternal\tghost-skill'
|
||||
if [ "$GATE_RC" -eq 10 ] && echo "$GATE_OUT" | grep -q 'INCOMPLETE' \
|
||||
&& ! echo "$GATE_OUT" | grep -q 'SIGN-IN REQUIRED'; then
|
||||
ok INCOMPLETE_WINS
|
||||
else
|
||||
bad INCOMPLETE_WINS "rc=$GATE_RC out=$GATE_OUT"
|
||||
fi
|
||||
|
||||
# ── UNKNOWN_11: whoami answers something else -> surfaced, never guessed ───
|
||||
gate_run garbage 'cli\t21st'
|
||||
if [ "$GATE_RC" -eq 11 ] && echo "$GATE_OUT" | grep -q 'whoami: rc=0' \
|
||||
&& echo "$GATE_OUT" | grep -q 'Something unexpected' \
|
||||
&& ! echo "$GATE_OUT" | grep -q '21st login' \
|
||||
&& ! echo "$GATE_OUT" | grep -q 'claude CLI unreachable'; then
|
||||
ok UNKNOWN_11
|
||||
else
|
||||
bad UNKNOWN_11 "garbage: rc=$GATE_RC out=$GATE_OUT"
|
||||
fi
|
||||
|
||||
gate_run fail 'cli\t21st'
|
||||
if [ "$GATE_RC" -eq 11 ] && echo "$GATE_OUT" | grep -q 'whoami: rc=3'; then
|
||||
ok UNKNOWN_11
|
||||
else
|
||||
bad UNKNOWN_11 "fail: rc=$GATE_RC out=$GATE_OUT"
|
||||
fi
|
||||
|
||||
echo "PASS=$PASS FAIL=$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in New Issue
Block a user