diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index b6eb38a..97ce2fb 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -33,6 +33,7 @@ rules: | BLK-011 | 2026-06-27 | init-project STEP 13 GSD post-FINISH creates ROADMAP.md → stranded doc (3rd post-FINISH artifact) | resolved (STEP 12 removed) | | BLK-012 | 2026-06-29 | gitflow_init half-applied: socle-commit failure swallowed → hook activated on partial run → re-run self-blocks | resolved | | BLK-013 | 2026-06-30 | `make plugin` Error 127 — npm absent on apt-`nodejs` host (Step 4 gsd-pi aborts, Steps 5-10 + residual cleanup never run) | resolved (env) | +| BLK-014 | 2026-07-01 | `make install` aborts npm EEXIST on `~/.local/bin/claude` when claude already installed via native installer — no presence guard | resolved | --- @@ -165,3 +166,14 @@ rules: - **Fix-forward**: install-plugins.sh Step 1 should GUARANTEE npm on apt-`nodejs` hosts — detect missing npm + `corepack enable npm` (not just check node) → stops Error 127 recurring on any fresh apt machine. - **Status**: resolved (env-level: corepack shim + npm prefix; zero repo change). Fix-forward (script hardening) NOT built. - **Reference**: discovered fixing `make plugin` 2026-06-30. Distinct from [[BLK-003]] (macOS playwright hardcoded path) + the Playwright-chromium `make plugin` failure. Blocked residual = [[BDR-030]]/[[LRN-042]]. + +--- + +## BLK-014 — `make install` aborts npm EEXIST when claude already present + +- **Date**: 2026-07-01 +- **Friction**: `make install` → install.sh Step 2 `npm install -g @anthropic-ai/claude-code@latest` fails EEXIST on `~/.local/bin/claude` when claude already installed → `else err` → `exit 1`. Bootstrap not idempotent on Claude Code step; rest (auth, symlinks, plugins) never runs. +- **Real cause**: claude installed via NATIVE installer, not npm — `~/.local/bin/claude` = symlink → `~/.local/share/claude/versions/` (`npm ls -g @anthropic-ai/claude-code` = empty; `claude --version` = 2.1.197). npm prefix `~/.local` (set by [[BLK-013]]) targets same `~/.local/bin/claude` → npm won't clobber a bin it doesn't own → EEXIST. Channel conflict, not double-install. Step had NO presence guard, unlike RTK (install-plugins.sh:388) / GSD (:419) / claude check (:252). +- **Solution**: install.sh — skip-if-present guard `command -v claude` (mirror RTK/GSD), npm only fresh machine (`elif`). update-all.sh — channel-aware updater: `npm ls -g` → npm-managed uses npm, else native uses `claude update` (self-update). Never `npm --force` (would clobber native, break self-update). +- **Status**: resolved. Fix `8dc4027`, branch `bugfix/install-claude-idempotent`, pending merge validation. +- **Reference**: [[BLK-013]] npm prefix `~/.local` = contributing factor (npm bin over native bin). install-plugins.sh already pointed to code.claude.com (native) — install.sh was the npm outlier. Fresh-machine `elif npm` branch channel-consistency = open design question (potential BDR). Pattern → [[LRN-085]]. diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 085a025..48f8639 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -67,6 +67,7 @@ rules: | BDR-043 | 2026-06-30 | BDR-015 trigger cleared — 5 ex-broken gstack symlinks repaired → darwin re-baseline back in scope (unblocked, NOT run) | accepted | | BDR-044 | 2026-06-30 | auto-skill-dispatch won't-build — under-routing fear inverted to over-routing by cartography, then measured: model discriminates (clear→route, ambiguous→ask, trivial→abstain) | accepted · won't-build | | BDR-045 | 2026-07-01 | Standalone memory/doc skills branch to chore/* via aiguillage (hook exemption kept) | accepted | +| BDR-046 | 2026-07-01 | Claude Code installs via official native installer (curl claude.ai/install.sh), drop npm from install.sh | accepted | --- @@ -693,3 +694,17 @@ rules: - (C) codify exemption + human habit — enforces NOTHING mechanically; goal was automatic. - (D) narrow the exemption by size/scope in the hook — fuzzy, false positives. - **Honest residual**: a MANUAL `git commit` of `.claude/**` on `main` still passes — B covers the skill path only. Non-blocking hook WARN on manual `.claude/**`-on-main = DEFERRED. See [[BDR-034]], [[BDR-039]], [[LRN-084]]. + +--- + +## BDR-046 — Claude Code installs via the official native installer, not npm + +- **Date**: 2026-07-01 +- **Decision**: install.sh fresh-machine branch installs Claude Code via `curl -fsSL https://claude.ai/install.sh | bash` (official native installer), not `npm install -g @anthropic-ai/claude-code`. Skip-if-present guard unchanged. update-all.sh stays channel-aware (native → `claude update`, legacy npm → npm). +- **Why**: official quickstart (code.claude.com/docs) lists Native (recommended) / Homebrew / WinGet / apt only — npm is NO longer a documented channel. npm collided with the native symlink `~/.local/bin/claude` → EEXIST ([[BLK-014]]), and npm bypasses native background auto-update. install-plugins.sh already pointed to code.claude.com (native) — install.sh was the npm outlier; this aligns them. +- **Alternatives rejected**: + - (A) keep npm on fresh install — deprecated channel, re-introduces the EEXIST class on any machine with a prior native install, no auto-update. + - (B) `claude install` subcommand — needs claude already present (chicken-and-egg on fresh machine); curl bootstrap is the documented first-time path. + - (C) Homebrew/apt — platform-specific; curl covers macOS/Linux/WSL uniformly and matches the doc's "recommended". +- **Honest residual**: `curl | bash` = pipe-to-remote-bash (accepted: official Anthropic domain, same pattern already used for nvm at install.sh:29). node/npm still installed as prereqs — needed by the plugins step (gsd-pi), not by claude. PATH export added so the auth step finds the freshly-installed binary. See [[BLK-014]], [[LRN-085]]. +- **Status**: accepted. Commits 8dc4027 + 6be627e, branch bugfix/install-claude-idempotent, pending merge. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index ade16ab..466d6e8 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -290,3 +290,5 @@ rules: ## 2026-07-01 - gitflow aiguillage-standalone (BDR-045): chore type + 4 standalone memory/doc skills branch off develop before writing; hook exemption kept. 64/64 green (e8807a7). Then repaired 5 direct-on-main `chore(memory)` → chore/reconcile-memory branches (LRN-084, LRN-034 corrob). +- BLK-014 fixed: install.sh npm EEXIST on `~/.local/bin/claude` (native symlink, npm prefix `~/.local` from BLK-013) → skip-if-present guard + channel-aware update-all.sh (`claude update` for native). LRN-085. Commit 8dc4027, branch bugfix/install-claude-idempotent pending merge. +- BDR-046: install.sh switched fresh-install from npm → official native installer (`curl claude.ai/install.sh | bash`); npm no longer a documented channel (verified quickstart). Aligns with install-plugins.sh. Commit 6be627e, same branch. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 650577f..444c88b 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -104,6 +104,7 @@ rules: | LRN-082 | 2026-06-30 | Trigger-cleared on a multi-motif exclusion lifts only the named motif — re-check the others before acting | any "exclusion lifted / precondition cleared" — verify ALL grounds, not just the named one | | LRN-083 | 2026-06-30 | subagents are an INVALID instrument for measuring main-loop spontaneous routing — SUBAGENT-STOP + delegated framing pin them to the no-route floor | any RED of whether the MAIN loop self-invokes; use fresh main-loop sessions, observe via the human | | LRN-084 | 2026-07-01 | protection hook enforces PROD not the full branch-flow; exemption masked the rule-vs-guard divergence | a guard exempts a class / checks one predicate — verify it encodes full intent | +| LRN-085 | 2026-07-01 | Idempotent CLI install/update: `command -v` skip-if-present guard + detect channel (`npm ls -g` vs native symlink) before choosing updater; never `npm --force` over a bin npm doesn't own | any installer/updater for a CLI with >1 install channel | --- @@ -915,3 +916,13 @@ rules: - **pattern**: the gitflow pre-commit hook is a PROTECTION guard (block code on main/develop), NOT a flow enforcer. It exempts `.claude/**` and can only test "on a protected base" — it can NEVER verify "branched FROM develop" (no base knowledge). So "every change via a branch from develop" is only HALF-encoded by the hook; the base half lives solely upstream in `gitflow_start`. The exemption is scoped to the SIDE-CAR ([[BDR-034]]); it has no branch to follow when memory IS the work → standalone memory fell back to `main`. - **why it matters**: a multi-repo raccord committed 5 `chore(memory)` direct on `main` and NOTHING flagged it — nothing was violated, the exemption worked as designed. The divergence was guard (declares PROD protection) vs intended rule (all via branch); the exemption MASKED it, the raccord revealed it by violating the unencoded half. A guard encoding only PART of the intent reads as full enforcement — a false-green. - **future application**: when a guard exempts a class or checks one predicate, ask what it does NOT encode and whether a human leans on it for MORE than it enforces. Enforce the unencoded half where it actually lives (the aiguillage at skill start, [[BDR-045]]), do not push it into a guard that structurally can't hold it. Verify the guard's real scope against the rule's full scope before trusting "it would have caught it." See [[BDR-034]], [[BDR-045]], [[LRN-034]]. + +--- + +## LRN-085 — Idempotent CLI install/update: presence guard + channel detection, never `--force` + +- **Date**: 2026-07-01 +- **Context**: install.sh npm-installed claude blindly → EEXIST abort when claude present via native installer (symlink npm doesn't own). Sibling steps (RTK/GSD) already had `command -v` skip guards; install.sh didn't. See [[BLK-014]]. +- **Pattern**: (a) idempotent install step = `command -v ` guard → skip-if-present with version echo, install only in `else`/`elif`. For a BINARY this IS a deterministic oracle (contrast [[LRN-054]]: conversation-state presence has none → don't skip-branch). (b) a CLI can ship via >1 channel (npm vs native). npm can't clobber a bin symlink it doesn't own → EEXIST; `npm --force` = wrong (npm itself says "recklessly", breaks native self-update). Detect channel first: `npm ls -g ` succeeds → npm-managed → npm; else native → `claude update` self-updater. (c) install ≠ update: first-time installer skips-if-present; the update script does the channel-aware upgrade. +- **Future application**: any installer/updater for a CLI reachable via multiple channels — guard with `command -v`, branch the updater on detected channel, never blind `--force` over a foreign-owned bin. Caveat [[LRN-036]]: `command -v` needs the bin dir on PATH in shelled-out/hook contexts. +- **Reference**: [[BLK-014]], mirrors RTK/GSD guard in install-plugins.sh. Related [[LRN-005]] (plugin enable idempotency), [[LRN-039]] (installer config drift). diff --git a/install.sh b/install.sh index 7187feb..82b3f05 100755 --- a/install.sh +++ b/install.sh @@ -22,8 +22,9 @@ echo "" # ── 1. Check prerequisites ── echo "── Checking prerequisites..." -# node + npm drive the Claude Code CLI install below. On a fresh machine -# they may be absent — install the current LTS via nvm instead of aborting. +# node + npm are needed by the plugins step (install-plugins.sh: gsd-pi et al.); +# Claude Code itself now installs via its own native installer below. On a fresh +# machine node/npm may be absent — install the current LTS via nvm, not abort. install_node_via_nvm() { info "Node.js/npm missing — installing LTS via nvm..." curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash @@ -54,9 +55,20 @@ ok "npm $(npm -v)" # ── 2. Install Claude Code CLI ── echo "" -echo "── Installing Claude Code (latest)..." +echo "── Installing Claude Code..." -if npm install -g @anthropic-ai/claude-code@latest; then +# Idempotent + official channel. Skip if already present (mirrors the RTK/GSD +# guard) — the binary is a native-installer symlink at ~/.local/bin/claude that +# self-updates. On a fresh machine install via the official native installer +# (code.claude.com/docs quickstart), NOT npm: npm is no longer a documented +# channel, would collide with the native symlink (EEXIST), and bypasses the +# built-in auto-update. Upgrades are `make update`'s job, not first-time install. +if command -v claude &>/dev/null; then + ok "Claude Code already installed ($(claude --version 2>/dev/null | head -1))" +elif curl -fsSL https://claude.ai/install.sh | bash; then + # Native installer targets ~/.local/bin — put it on PATH for the auth + + # verification steps that follow in this same (non-login) shell. + export PATH="$HOME/.local/bin:$PATH" ok "Claude Code installed: $(claude --version 2>/dev/null || echo 'unknown')" else err "Claude Code installation failed" diff --git a/update-all.sh b/update-all.sh index 28fbc84..621e8ed 100644 --- a/update-all.sh +++ b/update-all.sh @@ -27,7 +27,15 @@ echo "── Updating Claude Code CLI..." if command -v claude &>/dev/null; then CURRENT_VER=$(claude --version 2>/dev/null | head -1 || echo "unknown") info "Current: $CURRENT_VER" - if npm install -g @anthropic-ai/claude-code@latest 2>/dev/null; then + # Use the updater that matches the install channel: npm-managed installs + # update via npm; native-installer installs self-update via `claude update` + # (npm would EEXIST on the ~/.local/bin/claude symlink it does not own). + if npm ls -g @anthropic-ai/claude-code &>/dev/null; then + UPDATE_CMD=(npm install -g @anthropic-ai/claude-code@latest) + else + UPDATE_CMD=(claude update) + fi + if "${UPDATE_CMD[@]}" &>/dev/null; then NEW_VER=$(claude --version 2>/dev/null | head -1 || echo "unknown") if [ "$CURRENT_VER" = "$NEW_VER" ]; then ok "Claude Code already up to date ($NEW_VER)" @@ -35,7 +43,7 @@ if command -v claude &>/dev/null; then ok "Claude Code updated: $CURRENT_VER → $NEW_VER" fi else - warn "Claude Code update failed — try manually: npm install -g @anthropic-ai/claude-code@latest" + warn "Claude Code update failed — try manually: ${UPDATE_CMD[*]}" fi else warn "Claude Code not found — install first with: make install"