diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 9f87573..95c8d50 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -423,3 +423,31 @@ rules: - Gate re-reads `.profile` directly — duplicates `read_profile` parsing in the gate; two parsers drift. - Gate parses full `show` output — pays claude calls per plugin/mcp, fragile in hook context. - **Reference**: `lib/profile.sh` `cmd_show` (+ `--plain` branch), `skills/profile/SKILL.md`, commit 5776195. Linked to [[BDR-018]] (prior `profile.sh` command addition). + +--- + +## BDR-025 — Design gate = profile-based; remedy always `/profile design`; magic required-but-manual; unknown → fail-visible; claude resolved via PATH-repair + +- **Date**: 2026-06-21 +- **Status**: accepted +- **Decision**: `design-tool-gate.sh` checks whether the `design` profile's design-core tools are active and, if not, points at ONE command — `/profile design`, never an atomic per-tool toggle. **tier = profil**: every non-trivial tier (Build / design-system / review) draws from the one `design` profile (a superset of all tiers) → the gate checks that profile, so ZERO hardcoded tier→tools list. Gate scope = the `# GATE-BLOCK:` allowlist in `design.profile` (only real design tools trip; bundled browser/plan/shotgun/graphify ignored). Structure + types from `profile.sh show design --plain` (BDR-024 contract); per-tool state per channel (skill symlink / `claude plugin list` / `claude mcp list` / `command -v`), mirroring `profile.sh:skill_status()`. Three outcomes: blocking/manual → INCOMPLETE exit 10; unknown-only → READY-BUT-UNVERIFIED exit 11 (fail-visible); else READY exit 0. **magic = required-but-manual** class: TRIPS the gate (NOT advisory), names the `MAGIC_API_KEY` step. **claude resolved via `ensure_claude_on_path()`** (probe known dirs + nvm glob `sort -V | tail -1` = newest, prepend the bin dir carrying claude AND node) — because `command -v claude` depends on PATH carrying the nvm bin, absent in a sanitized subshell/hook; integral to the final gate design, not a detail. +- **Why**: single source of truth = profile system → no CLAUDE.md tier→tools dup (P3/P5 just removed it), no tool→profile map to drift. Credible gate: trips only on real design tools, not bundled infra → not ignored by reflex. magic is the load-bearing design tool, so silence on it would defeat the gate's purpose. Gate runs from hooks/skills where PATH may be sanitized → robust claude resolution is required for it to verify at all. +- **Alternatives rejected**: + - Hardcoded tier→tools list — reintroduces the CLAUDE.md dup just removed; drifts when a tool is added to a profile. + - magic advisory (mention, don't trip) — fail-OPEN on the very tool the gate exists to catch. + - Strict fail-closed on unknown — false blocks when claude merely slow/unreachable → gate gets ignored. fail-VISIBLE (exit 11) chosen. + - Depend on `command -v claude` alone — fails in sanitized-PATH hook → unknown. Proven by `PATH=/usr/bin:/bin` test (magic-on → READY/0, magic-off → INCOMPLETE/10). +- **Reference**: `lib/design-tool-gate.sh`, `lib/design-gate.md`, `lib/profiles/design.profile` (`# GATE-BLOCK:`), commits 3eefb8a / 4d19135 / f963318. Linked to [[BDR-024]] (the `--plain` parse contract this consumes), [[LRN-036]] (`command -v` PATH dependence, the real cause), [[LRN-037]] (proven on the real subject in real context). + +--- + +## BDR-026 — Secret source-of-truth outside the repo (`~/.claude/.env`) reached via a `repo/.env` symlink + +- **Date**: 2026-06-21 +- **Status**: accepted +- **Decision**: real secret lives in `~/.claude/.env` (outside the git tree); `repo/.env` is a symlink → it. `source "$REPO/.env"` follows the symlink transparently → ZERO change to any read path (`toggle-external.sh` `load_env`, `install-plugins.sh` check, gate). `link.sh` `link_env()` creates the symlink defensively: links only when `repo/.env` is absent or already the right link; a residual REAL `repo/.env` is left untouched with a migrate hint — never clobbered, so the secret can't be destroyed. Idempotent. `.gitignore` hardened to `.env` + `.env.*` + `!.env.example`. Messages point at `~/.claude/.env` (the canonical edit location). +- **Why**: secret never enters the git tree — not as content (it's a link) nor by accident (gitignored). Even a stray `git add .` can't stage the real key. Repo stays usable: the symlink is visible/editable from the repo. Read paths follow the link → no script logic changed. +- **Alternatives rejected**: + - Secret in `repo/.env`, gitignored (status quo) — one `git add -f` or a `.gitignore` slip leaks it; the secret physically sits in the tree. + - Scripts read `~/.claude/.env` directly — makes the symlink redundant but rewrites every read path and loses repo-local visibility. +- **Reference**: `link.sh` `link_env()`, `.gitignore`, `lib/toggle-external.sh`, `install-plugins.sh`, `.env.example`, commits 131d0bc / f9cc866. Linked to [[BDR-025]] (magic's `MAGIC_API_KEY`, consumed by the gate's required-but-manual class). diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 1c7ccc1..8c4173f 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -509,3 +509,13 @@ rules: - **Fix**: don't trust inherited PATH. `ensure_claude_on_path()` probes known dirs (`~/.claude/local`, `~/.local/bin`, `/usr/local/bin`, nvm glob `sort -V | tail -1` = newest) + prepends bin dir (carries claude AND its node runtime, same dir; claude shebang needs node). Fail-visible exit 11 = the MITIGATION/net, NOT the cause. - **Future application**: any script shelling out a CLI that may run from hook/subshell → resolve the binary's bin dir explicitly, don't assume interactive PATH. Test under `PATH=/usr/bin:/bin` to simulate sanitized context. Distinguish alias/function (interactive-only, never in subshell) vs real binary on PATH (what `command -v` finds in scripts). - **Reference**: `ensure_claude_on_path()` in `lib/design-tool-gate.sh`, commit f963318. Linked to [[LRN-034]] (narrated/plausible state ≠ ground truth — here the plausible alias theory was wrong; test the real subshell, don't accept it). + +--- + +## LRN-037 — Verify the load-bearing scenario on the REAL subject in REAL context, not a stub or a logic argument + +- **Date**: 2026-06-21 +- **Context**: design-gate chantier. 4 successive plausible claims each REFUTED only by running the real thing: (1) .env read path was `$REPO/.env`, not `~/.claude/.env` (read the actual script); (2) fail-open — unknown folded into silent READY (saw it in live output); (3) "alias dies in subshell = cause" (refuted: real binary on inherited PATH → `command -v` succeeds); (4) real cause = PATH carrying nvm bin (proven by `PATH=/usr/bin:/bin` run). Logic/stub never caught any. The DISCRIMINATING magic-OFF-under-stripped-PATH → exit 10 is what proved the gate truly runs `claude mcp list` vs. defaulting to READY. +- **Pattern**: for the load-bearing scenario, run it on the REAL subject in the REAL invocation context (prod path `$HOME/.claude/lib/...`, prod-like PATH), not a stub or a "the code path is correct" argument. A stub proves branch coverage; only the real subject proves the integration. Always add a DISCRIMINATING case — force the failure state; the check must REPORT it, not pass by default (a check that only ever passes proves nothing). +- **Future application**: any "fixed/works" claim on a critical path → produce the real run output (command + lines + exit code) before capitalizing or shipping; don't summarize ("condition met") in place of the output. Stub/logic = necessary for branch coverage, never sufficient for the integration claim. Most rentable discipline of the whole segment: every refutation came from execution, none from reasoning. +- **Reference**: design-gate chantier, the `PATH=/usr/bin:/bin` matrix (magic-on → READY/0, magic-off → INCOMPLETE/10), commits 4d19135 / f963318. Linked to [[LRN-036]] (the concrete instance: the PATH cause surfaced only by the real run), [[LRN-034]] (its twin — 034 = don't trust a narrated *claim*; 037 = don't trust a *stub/logic argument* as proof; both demand execution against ground truth).