From 3d0ee262c10bafb1c4861dbb479f5028a2a1bc77 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Fri, 3 Jul 2026 15:52:37 +0200 Subject: [PATCH 1/2] fix(hooks): tighten design-toolchain trigger + add fire-log counter The 07-02 tightening left bare tokens common in non-UI talk (design, component, theme, transition, frontend, palette) -> ~6 false-fires/session during the ECC config audit. Dropped them; dashboard now word-boundary matched (kills the ecc_dashboard.py filename match, keeps 'admin dashboard'); kept animation; added 'front-end design' bigram. Each fire now logs time+token+excerpt to a light file so 're-firing?' is measured, not argued. Regression test 18/18, shellcheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS --- hooks/design-toolchain-reminder.sh | 28 +++++++++++---- lib/tests/design-toolchain-reminder.test.sh | 39 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 lib/tests/design-toolchain-reminder.test.sh diff --git a/hooks/design-toolchain-reminder.sh b/hooks/design-toolchain-reminder.sh index 6664483..998c77c 100755 --- a/hooks/design-toolchain-reminder.sh +++ b/hooks/design-toolchain-reminder.sh @@ -9,6 +9,10 @@ # This is a soft nudge: the tiered rule itself says trivial work uses NO # toolchain, so a false positive (e.g. "API design") costs only a reminder the # model can disregard. Always exits 0 so it never blocks prompt submission. +# +# Every fire appends one line (time, matched token, prompt excerpt) to +# ~/.claude/logs/design-toolchain-fires.log — a counter so the next "is it +# over-firing?" decision is measured, not anecdotal. set -euo pipefail @@ -24,15 +28,25 @@ prompt="$(printf '%s' "$input" \ lc="$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')" # UI/design build and review signals (FR + EN). Word boundaries (\b) avoid -# substring false matches like perform/platform/information. Tightened -# 2026-07-02: ultra-generic English tokens (page, form, menu, card, style, -# look, screen, interface, color) fired on a large share of NON-UI prompts — -# ~200 tokens of reminder each time (measured: 6 fires during a pure config -# audit). Kept: unambiguous design vocabulary + FR aesthetic words; specific -# compounds (stylesheet, styling, formulaire, écran) still match. -pattern='design|redesign|refonte|refont|ui/ux|ux/ui|\bui\b|\bux\b|ui kit|design system|design-system|frontend|front-end|front end|composant|component|\bnavbar\b|\bsidebar\b|\bmodal\b|\bbouton\b|\bbutton\b|formulaire|\bhero\b|\bheader\b|\bfooter\b|dropdown|tooltip|\bbadge\b|\bchart\b|graphique|accordion|carousel|\bslider\b|landing|dashboard|homepage|home page|\baccueil\b|\bécran\b|\becran\b|portfolio|maquette|mockup|wireframe|prototype|\bjoli\b|\bjolie\b|\bbeau\b|\bbelle\b|esth[eé]tique|aesthetic|\bvisuel\b|\bvisual\b|embellir|fignol|peaufin|polish|styliser|styling|stylesheet|\bskin\b|charte graphique|\bbrand\b|branding|\blogo\b|favicon|ic[oô]ne|\bicon\b|\bcss\b|tailwind|shadcn|couleur|palette|gradient|d[eé]grad[eé]|\bombre\b|spacing|espacement|\bmarge\b|\bpadding\b|\bmargin\b|\bradius\b|arrondi|\bhover\b|dark mode|light mode|\btheme\b|th[eè]me|typograph|\bfont\b|\bfonts\b|font pairing|\bpolice\b|animation|\bmotion\b|transition|micro-interaction|keyframe|glassmorph|neumorph|claymorph|skeuomorph|brutalis|bento|minimalis|responsive|figma' +# substring false matches like perform/platform/information. +# Tightened 2026-07-02: dropped ultra-generic tokens (page, form, menu, card, +# style, look, screen, interface, color) that fired on non-UI prompts. +# Tightened again 2026-07-03: dropped bare design|component|composant|theme| +# thème|transition|frontend|front-end|palette — all common in non-UI technical +# talk (a design decision, a system component, the theme of a discussion, a +# state transition, frontend architecture). Kept as UI-specific compounds: +# "design system", "redesign", "front-?end design". dashboard -> \bdashboard\b +# so a filename like ecc_dashboard.py no longer matches while "admin dashboard" +# still does. animation kept (rarely non-UI). +pattern='redesign|refonte|refont|ui/ux|ux/ui|\bui\b|\bux\b|ui kit|design system|design-system|front-?end design|\bnavbar\b|\bsidebar\b|\bmodal\b|\bbouton\b|\bbutton\b|formulaire|\bhero\b|\bheader\b|\bfooter\b|dropdown|tooltip|\bbadge\b|\bchart\b|graphique|accordion|carousel|\bslider\b|landing|\bdashboard\b|homepage|home page|\baccueil\b|\bécran\b|\becran\b|portfolio|maquette|mockup|wireframe|prototype|\bjoli\b|\bjolie\b|\bbeau\b|\bbelle\b|esth[eé]tique|aesthetic|\bvisuel\b|\bvisual\b|embellir|fignol|peaufin|polish|styliser|styling|stylesheet|\bskin\b|charte graphique|\bbrand\b|branding|\blogo\b|favicon|ic[oô]ne|\bicon\b|\bcss\b|tailwind|shadcn|couleur|gradient|d[eé]grad[eé]|\bombre\b|spacing|espacement|\bmarge\b|\bpadding\b|\bmargin\b|\bradius\b|arrondi|\bhover\b|dark mode|light mode|typograph|\bfont\b|\bfonts\b|font pairing|\bpolice\b|animation|\bmotion\b|micro-interaction|keyframe|glassmorph|neumorph|claymorph|skeuomorph|brutalis|bento|minimalis|responsive|figma' if printf '%s' "$lc" | grep -Eq "$pattern"; then + # Counter: log the fire (time, matched token, excerpt) — best-effort, never blocks. + logf="${HOME}/.claude/logs/design-toolchain-fires.log" + mkdir -p "$(dirname "$logf")" 2>/dev/null || true + printf '%s\t%s\t%s\n' "$(date -Iseconds)" \ + "$(printf '%s' "$lc" | grep -oiE "$pattern" | head -1 || true)" \ + "$(printf '%s' "$prompt" | tr '\n\t' ' ' | cut -c1-100)" >> "$logf" 2>/dev/null || true cat <<'EOF' [design-toolchain] UI/design signal detected. Apply CLAUDE.md "Design work — full toolchain (tiered by scope)": - Trivial (≤2 files, single cosmetic value, CSS tweak) → /hotfix, NO toolchain. diff --git a/lib/tests/design-toolchain-reminder.test.sh b/lib/tests/design-toolchain-reminder.test.sh new file mode 100644 index 0000000..a7d59a1 --- /dev/null +++ b/lib/tests/design-toolchain-reminder.test.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# lib/tests/design-toolchain-reminder.test.sh +set -u +H="$(cd "$(dirname "$0")/../.." && pwd)/hooks/design-toolchain-reminder.sh" +pass=0; fail=0 +check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1)); + printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; } +# fire() -> "fire" if the hook emits the reminder, else "quiet". +fire() { if printf '{"prompt":"%s"}' "$1" | bash "$H" | grep -q "design-toolchain"; then + echo fire; else echo quiet; fi; } + +# --- Dropped/neutralized tokens must be QUIET (non-UI senses) --- +check D1-design "$(fire 'a design decision for the API')" quiet +check D2-component "$(fire 'this system component')" quiet +check D3-composant "$(fire 'le composant backend')" quiet +check D4-theme "$(fire 'the theme of the audit')" quiet +check D5-transition "$(fire 'state transition to develop')" quiet +check D6-frontend "$(fire 'frontend architecture')" quiet +check D7-palette "$(fire 'a palette of options')" quiet +check D8-dash-file "$(fire 'ecc_dashboard.py')" quiet + +# --- Real UI signals must still FIRE --- +check F1-button "$(fire 'add a button')" fire +check F2-navbar "$(fire 'the navbar layout')" fire +check F3-landing "$(fire 'build a landing page')" fire +check F4-glass "$(fire 'a glassmorphism card')" fire +check F5-redesign "$(fire 'redesign the app')" fire +check F6-frontdesign "$(fire 'frontend design work')" fire +check F7-admin-dash "$(fire 'admin dashboard screen')" fire +check F8-animation "$(fire 'add an animation')" fire +check F9-designsys "$(fire 'our design system')" fire + +# --- Fire is logged (time + token + excerpt) --- +tmp="$(mktemp -d)" +printf '{"prompt":"a glassmorphism card"}' | HOME="$tmp" bash "$H" >/dev/null 2>&1 +check L1-logged "$(grep -c 'glassmorph' "$tmp/.claude/logs/design-toolchain-fires.log" 2>/dev/null)" 1 +rm -rf "$tmp" + +printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ] From dfb79e7a886cdce5dda2f51e6c4147c449348308 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Fri, 3 Jul 2026 15:52:37 +0200 Subject: [PATCH 2/2] chore(memory): LRN-091 banner-blindness (corrob LRN-047) + journal 2026-07-03 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS --- .claude/memory/journal.md | 2 ++ .claude/memory/learnings.md | 6 ++++++ .claude/tasks/TODO.md | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index fdfb1e9..d91825b 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -312,3 +312,5 @@ rules: - Shipped config-protection hook (feature/config-protection-hook): PreToolUse blocks Edit/Write to quality-gate files (settings/gitflow/.githooks/doctor/hooks-self/lib-tests/lint). One-shot sentinel .claude/.config-edit-ok (non-empty reason, logged+consumed) — NOT env-var (launch-time = set-and-forget = garde mort). Own idiom, not ECC import. shellcheck clean, test 20/20. - Live dogfood: hook went active mid-session via symlinked settings (link.sh); v1 (no self-guard) let its OWN edit through → v2 added hooks/*.sh + lib/tests/* self-guard, then blocked the test-file edit; recovered via sentinel. User's self-guard requirement vindicated. - Next: #2 design-toolchain trigger fix (residual false-fires post-ed2408e, 5× this session). +- #2 done (bugfix/design-toolchain-trigger): trigger tightened — dropped bare design|component|composant|theme|thème|transition|frontend|front-end|palette; dashboard→\bdashboard\b (kills ecc_dashboard.py filename match, keeps "admin dashboard"); kept animation; added "front-?end design" bigram + fire-log counter (time+token+excerpt, ~/.claude/logs/design-toolchain-fires.log) so future "re-firing?" is measured. Test 18/18, shellcheck clean, live dogfood green. [[LRN-091]] corrob [[LRN-047]]. +- Double dogfood of #1 guard: config-protection blocked + sentinel-bypassed my own edits to the now-guarded design hook + its test — first real use of the guard, friction validated in passing (one-shot sentinel .claude/.config-edit-ok, non-empty reason, logged+consumed). ECC second-regard closed: #1 config-protection + #2 trigger fix, both merged to develop, nothing pushed. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 7b8953f..377556d 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -971,3 +971,9 @@ rules: - **context**: ECC 2nd-look 2026-07-03 (Opus 4.8, 6 agents, repo unchanged since 01/07). [[BDR-047]] audit (01/07) inventoried the declarative surface + concluded zero import — right on facts, but hooks/ (ECC's only live subsystem) was OUT of scope and held the sole real adaptation → config-protection PreToolUse guard. - **future application**: next external-repo value audit → enumerate hooks/, scripts/, runners FIRST; treat rules/docs/SKILL.md as claims to verify ("is it wired?"), not value. Described capability ≠ wired capability. - **cousin**: [[LRN-087]] presence-flag ≠ capability; [[LRN-089]] forwarded-args silently dropped — same family: a visible signal (a file, a flag, a `paths:`) lying about real behavior. + +## LRN-091 — a soft-nudge hook that over-fires gets ignored (banner-blindness) +- **pattern**: keyword-triggered nudge (design-toolchain reminder) with bare common tokens fires on non-UI work → reader tunes it out. Same class as a diagnostic that cries false [[LRN-047]]: a signal wrong too often stops being read. +- **rule**: keep a token BARE only when its UI sense dominates largely in a dev context (glassmorphism, navbar). Token common in non-UI talk (design, component, theme, transition, frontend) → require a UI-specific bigram (design system, front-end design) or drop; in doubt → bigram-or-drop. Borderline standalone nouns (dashboard, animation) may stay bare as an assumed call — the fire-log arbitrates later on data, not gut. (NOT "never bare tokens" — animation stays bare here by design.) +- **context**: design-toolchain-reminder.sh — 07-02 tightening (dropped page/form/menu/…) insufficient; 6 bare tokens still false-fired ~6×/session during the ECC config audit (design, ecc_dashboard.py, component, frontend, theme, transition, palette). 07-03 fix: dropped them, dashboard→`\bdashboard\b` (filename match killed, "admin dashboard" kept), added a fire-log (time+token+excerpt). `lib/tests/design-toolchain-reminder.test.sh` locks it (18 checks). +- **cousin**: [[LRN-047]] a doctor that cries false is ignored. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 81c6fab..e17c1fe 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,17 @@ # TODO +## 2026-07-03 — design-toolchain trigger fix (bugfix/design-toolchain-trigger) +Root cause (NOT a kill-switch, per user): ed2408e (07-02) dropped ultra-generic +tokens but left bare tokens common in non-UI talk → ~6× false-fire THIS session +(design, dashboard via ecc_dashboard.py, component, frontend, theme, transition, +palette). Fix = tighten the trigger only + a fire-log counter for measured +re-fire decisions. + +- [ ] hooks/design-toolchain-reminder.sh — drop bare design|component|composant|theme|thème|transition|frontend|front-end|palette; dashboard→\bdashboard\b; keep animation; add "front-?end design" bigram; + fire-log (time+token+excerpt) +- [ ] lib/tests/design-toolchain-reminder.test.sh — 8 dropped tokens quiet; button/navbar/landing/glassmorphism/redesign/"frontend design"/"admin dashboard"/animation fire; ecc_dashboard.py quiet; fire logged +- [ ] Verify — shellcheck + bash -n + test PASS + live dogfood (hook now quiet on session tokens) +- [ ] GATE before finish (user); sentinel one-shot to edit the now-guarded hook + ## 2026-07-03 — config-protection hook (feature/config-protection-hook) Goal: PreToolUse hook blocks Edit/Write to this config's quality-gate files (guardrails an agent must not weaken to make an error pass). Adaptation from ECC