From 132bcdf7c5c2a9e1f7493e6568c2e4688f83fe47 Mon Sep 17 00:00:00 2001 From: bastien Date: Mon, 28 Sep 2026 12:51:56 +0200 Subject: [PATCH] =?UTF-8?q?chore(memory):=2021st=20sign-in=20gate=20?= =?UTF-8?q?=E2=80=94=20contract,=20plan=20r3,=20TODO,=20journal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/journal.md | 1 + .claude/tasks/TODO.md | 19 ++ .../2026-09-28-21st-signin-gate-1215.md | 49 +++++ .../plans/2026-09-28-21st-signin-gate-1215.md | 190 ++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 .claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md create mode 100644 .claude/tasks/plans/2026-09-28-21st-signin-gate-1215.md diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 0d6b515..27d1327 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -537,3 +537,4 @@ rules: - User go: feature/doctor-vendored-skills merged into develop via `gitflow finish` → 2c94a0c, no conflict, pushed, local + origin copies removed by the lib. develop == origin/develop, no working branch anywhere. `make doctor` now covers the 11 vendored externals. - Skill-catalog audit (user: "tour des skills, doublons, économiser tokens"): 5 analyzers over 150 skills / 53.5k chars desc; 78 listed name-only this session (listing budget ≈1 % ctx, least-invoked lose desc → gain = routing quality + no broken 100 KB body invoked, not listing chars). Found: frontend-design plugin byte-dup of managed copy; brightdata 21 skills keyless + hostile WebFetch routing; gstack ship trunk-based (origin/HEAD=main), land-and-deploy auto-merge+deploy, autoplan/make-pdf/diagram/careful/guard/freeze dead paths (only bin + browse/dist linked); security-guidance = Opus call per code turn + agentic commit review, 0 findings/6 days; doctor.sh undercount ×6. User go: tier 1, superpowers vendor-7 (tier 2 later), 21st trio parked (CLI `Not logged in`), rule "full ⊇ every profile, max = everything". Live: brightdata disabled, frontend-design plugin uninstalled, `set full` → 75 skills (was 89). - /feat by hand on feature/skill-catalog-prune: contract 18 criteria; plan r1→r4 (3 challengers, confirmation FATAL(4): nested SKILL.md in browser-skills/openclaw/node_modules, ./setup global symlink, update-all 3rd copy); 4 feater parallel DONE; GATE 0 UNMET(4) = MY heredoc CHECKs (gates.sh single-line) → oracles to `.oracles/*.py` → MET; verifier ECARTS(1) = floor-guard `xit(` false-positive on `sys.exit(` → restructure → CONFORME; security PASS. 41 suites green minus 2 pre-existing T16a, shellcheck clean. UNMERGED — human gate. Registries pending user approval. +- User: "quand on détecte qu'on a besoin de 21st, on demande de log si c'est pas fait et on attend". /feat by hand on the same branch: design gate gains exit 12 `SIGN-IN REQUIRED` (three-state whoami probe, unknown → 11 with diagnostic, explicit "proceed without 21st" only skip); challenge round dropped my shared-helper idea (would break 4 fixture suites + change installer semantics) and my in-session `export TWENTYFIRST_TOKEN` remedy (env does not persist across tool calls). Executor DONE first pass, GATE 0 MET, verifier CONFORME 7/7, security PASS, 8/8 hermetic. Gate now exits 12 live here until `21st login`. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 5ce74bd..dccf6b9 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,24 @@ # TODO +## 2026-09-28 — design gate asks for `21st login` and waits (feature/skill-catalog-prune) +User: "si on veut l'utiliser, on demande à l'utilisateur de se log, plus simple que +dire c'est pas logged on utilise pas… on demande de log si c'est pas fait et on +attend". Contract `.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md`. +- [x] S1 three-state probe `twentyfirst_auth_state` INLINE in design-tool-gate.sh + (challenge r2 dropped the shared helper: install-plugins/toggle-external keep + their own semantics); `in` (TWENTYFIRST_TOKEN / API_KEY_21ST, or whoami + "Logged in as") / `out` (exact "Not logged in") / `unknown:whoami: rc=…` + → exit 11 with a CLI-specific remedy; exit 12 `SIGN-IN REQUIRED`; + `DESIGN_GATE_REPO_OVERRIDE`; hermetic suite 8/8 (stub control, in, out, + token, absent, INCOMPLETE wins, unknown ×2). +- [x] S2 design-gate.md §3 branch 12: STOP, ask `! 21st login` (or any terminal + on this machine), END THE TURN, re-run on reply; explicit "proceed without + 21st" = the only skip, stated visibly, not re-asked in the run; no in-session + token export; §4 resume path; feat/bugfix STEP 0.5 name SIGN-IN REQUIRED. +- [x] S3 plan r1→r3 (3 challengers + confirmation), executor DONE, GATE 0 MET, + verifier CONFORME 7/7, security PASS. Live on this machine: gate exits 12 + until `21st login`. Committed in place on feature/skill-catalog-prune. + ## 2026-09-28 — skill-catalog prune, tier 1 (feature/skill-catalog-prune) User go after the 5-agent duplicate audit (150 skills, 53.5k chars of descriptions, 78 listed name-only in session = listing budget exceeded). Contract diff --git a/.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md b/.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md new file mode 100644 index 0000000..e362ddc --- /dev/null +++ b/.claude/tasks/contracts/2026-09-28-21st-signin-gate-1215.md @@ -0,0 +1,49 @@ +# 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= ` with a CLI-specific remedy in the 11 block; stdout-only classification, `/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/** diff --git a/.claude/tasks/plans/2026-09-28-21st-signin-gate-1215.md b/.claude/tasks/plans/2026-09-28-21st-signin-gate-1215.md new file mode 100644 index 0000000..f153240 --- /dev/null +++ b/.claude/tasks/plans/2026-09-28-21st-signin-gate-1215.md @@ -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= ` + (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".