diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..b7c2a49 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Canonical Tap House SessionStart hook for Claude Code on the web. +# +# Fresh web-session containers clone the repo bare: no submodules, no +# pre-commit hook installed — so `git commit` runs unformatted and the +# clang-format CI gate fails on code a local clone would have fixed at commit +# time. This hook closes that gap at session start: +# +# 1. init submodules recursively (kernels, SDKs, vendored test harnesses) +# 2. install pre-commit and register the repo's canonical hook +# (.pre-commit-config.yaml — the Tap-wide pinned clang-format) +# 3. warm the pinned clang-format binary so the first commit doesn't +# pay the download (the container snapshot caches it) +# +# Canonical copy: taphouse (distributed by scripts/sync.sh alongside +# .clang-format / .clang-tidy / .pre-commit-config.yaml; drift-guarded where +# present). Web-only; local clones are untouched. +set -euo pipefail + +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +cd "$CLAUDE_PROJECT_DIR" + +echo "session-start: initializing submodules ..." +git submodule update --init --recursive + +echo "session-start: installing pre-commit ..." +if ! python3 -m pre_commit --version >/dev/null 2>&1; then + python3 -m pip install --quiet pre-commit +fi +python3 -m pre_commit install +python3 -m pre_commit install-hooks + +echo "session-start: done." diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e06b033 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml index 7681ffc..4f1454c 100644 --- a/.github/workflows/drift-check.yml +++ b/.github/workflows/drift-check.yml @@ -46,5 +46,16 @@ jobs: fail=1 fi fi + # The Claude Code web SessionStart hook is guarded only when BOTH + # sides carry it: older pinned TapHouse refs predate the hook, and + # .claude/settings.json is deliberately unguarded (repos may extend + # their settings with repo-specific hooks). + if [ -f ".claude/hooks/session-start.sh" ] && [ -f ".taphouse/.claude/hooks/session-start.sh" ]; then + if ! diff -q ".taphouse/.claude/hooks/session-start.sh" ".claude/hooks/session-start.sh" >/dev/null 2>&1; then + echo "::error file=.claude/hooks/session-start.sh::session-start.sh differs from TapHouse canonical (ref ${{ inputs.ref }}). Re-run taphouse/scripts/sync.sh and commit." + diff -u ".taphouse/.claude/hooks/session-start.sh" ".claude/hooks/session-start.sh" || true + fail=1 + fi + fi if [ "$fail" -eq 0 ]; then echo "All shared configs match TapHouse@${{ inputs.ref }}."; fi exit $fail diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..14c35ae --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,38 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +**TapHouse** — the single source of truth for the Tap family's shared style and setup files. +Nothing here is consumed at build time; the files are *distributed as copies* into each consumer +repo's root by `scripts/sync.sh`, and consumer CI runs the reusable +`.github/workflows/drift-check.yml` against a pinned tag to guarantee the copies never silently +diverge. `README.md` is the authoritative description of the mechanism; read it before changing +anything. + +## The rules that keep this working + +- **Never hand-edit a synced copy in a consumer repo.** Change the canonical file *here*, tag, + then re-run `scripts/sync.sh` into each consumer. A hand-edited consumer copy fails that repo's + drift job — that is the mechanism working, not a nuisance. +- **Adding a file to the canonical set touches three places:** the file itself, the copy step in + `scripts/sync.sh`, and (if it should be guarded) a comparison in `drift-check.yml`. Files that + not every consumer carries — `scripts/tidy.sh`, `.claude/hooks/session-start.sh` — are guarded + *conditionally* (only when present; the hook additionally only when the pinned TapHouse ref + carries it, so older pins don't break). `.claude/settings.json` is deliberately unguarded and + synced only-if-missing: repos may extend their settings with repo-specific hooks. +- **Two version knobs, both living here** (see README "Updating the rules"): the TapHouse tag + (consumers pin it in their `style.yml` `ref:`) and the clang-format version (the mirror `rev:` + inside `.pre-commit-config.yaml`). Tagging is a deliberate release act — bump consumer `ref:`s + in the same sweep as the re-sync, and don't move existing tags. +- **Consumer rollout order matters:** merge the TapHouse change first, then sync + commit in + consumers. The conditional guards make the in-between state safe, but a guarded unconditional + file (the core four) must exist at the pinned ref before consumers reference it. + +## Conventions recorded here but not yet drift-enforced + +The family C++ namespace convention lives in README.md (one `tap::` sub-namespace per +repo) rather than in the drift-checked `STYLE.md`, so recording it didn't force a family-wide +re-sync. Promote such conventions into `STYLE.md` (a tagged release) only once every consumer +complies. diff --git a/README.md b/README.md index 0ba8658..f4a0c0b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Canonical **Tap House Rules** — the shared C++ style for the Tap family of libraries (AmbiTap, SampleRateTap, OscTap, AmbiTap-Pd, AmbiTap-Max, …). This repo is the single source of truth for four root-level config files, plus -one distributed helper script: +two distributed helper scripts: | File | Enforces | |------|----------| @@ -13,6 +13,7 @@ one distributed helper script: | `STYLE.md` | The human-readable rules and rationale | | `.pre-commit-config.yaml` | Local git-hook wiring: formats staged C/C++ **before** each commit, so the clang-format CI gate can't fail on something a developer could have caught locally | | `scripts/tidy.sh` | Local mirror of the CI **clang-tidy** gate (naming + mandatory braces) over a repo's own TUs. Distributed to C++ repos that run the clang-tidy gate; kept a single copy here so it can't fork per-repo (it was, briefly). Repo-agnostic — no project name is baked in. | +| `.claude/hooks/session-start.sh` | Claude Code **web** sessions: fresh containers clone bare (no submodules, no pre-commit hook), so agent commits could bypass the local format layer entirely. The hook initializes submodules, installs the pinned pre-commit hook, and warms its clang-format at session start. Registered per-repo by `.claude/settings.json` (created by sync only if missing — repos may extend their settings, so only the hook *script* is drift-guarded). | `clang-format` and `clang-tidy` discover their config by walking **up** the directory tree from each source file, so those config files (and the @@ -75,7 +76,9 @@ consumer has migrated and it can be enforced. ``` Now `git commit` reformats staged C/C++ with TapHouse's pinned clang-format before the commit lands. First run fetches the pinned hook; thereafter it is - cached and offline. + cached and offline. (Claude Code **web** sessions do this automatically via + the synced SessionStart hook, once it's merged on the repo's default + branch — no per-clone step there.) 3. **Run the same hook in CI** instead of a hand-installed clang-format, so the version can never skew from local: diff --git a/scripts/sync.sh b/scripts/sync.sh index ba2e7bd..b0227e1 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -27,4 +27,19 @@ cp "$here/scripts/tidy.sh" "$target/scripts/tidy.sh" chmod +x "$target/scripts/tidy.sh" echo "synced scripts/tidy.sh -> $target/scripts/tidy.sh" +# Claude Code web SessionStart hook — initializes submodules and installs the +# pinned pre-commit hook in fresh web-session containers, so the clang-format +# gate can never fail on code a local clone would have formatted at commit +# time. The hook script is canonical (drift-guarded where present); the +# .claude/settings.json registration is created only if missing, because repos +# may legitimately extend their settings with repo-specific hooks. +mkdir -p "$target/.claude/hooks" +cp "$here/.claude/hooks/session-start.sh" "$target/.claude/hooks/session-start.sh" +chmod +x "$target/.claude/hooks/session-start.sh" +echo "synced .claude/hooks/session-start.sh -> $target/.claude/hooks/session-start.sh" +if [ ! -f "$target/.claude/settings.json" ]; then + cp "$here/.claude/settings.json" "$target/.claude/settings.json" + echo "created $target/.claude/settings.json (registers the hook; repo-specific settings merge there)" +fi + echo "Done. Review and commit the updated files in: $target"