From 3b98a2890ef1da7b4cd2b3b308c34e9f378d7cf1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 22:56:33 +0000 Subject: [PATCH 1/2] Make web sessions provision the full gate quietly and match CI exactly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DX improvements for Claude Code on the web sessions, mined from session friction: - session-start.sh now emits one line per step and sends verbose output to /tmp/session-start.log — the raw apt-get output previously injected ~38KB into the agent's context every session start. - Provision actionlint + gitleaks (go install) and pin prettier to CI's version, so check.sh enforces in web sessions the same gates CI runs instead of self-skipping them and failing late in CI. - Unshallow the clone at session start: a shallow clone has origin/main but no merge base, which crashes diff-cover/mutation with a confusing 'fatal: no merge base' instead of self-skipping. - Single-source the non-Python tool pins in scripts/gate_tool_pins.sh, sourced by both ci.yml and the hook (they can't live in pyproject — gitleaks/actionlint ship no PyPI distribution). - shellcheck-gate the hook and pins file in check.sh; add an explicit 600s hook timeout and a few safe read-only permission allowlist entries. - Document the provisioning contract in AGENTS.md. https://claude.ai/code/session_01FUszDSwvuSLgvBXnXJQ3M7 --- .claude/hooks/session-start.sh | 104 ++++++++++++++++++++++++++------- .claude/settings.json | 9 ++- .github/workflows/ci.yml | 17 ++++-- AGENTS.md | 7 +++ scripts/check.sh | 5 +- scripts/gate_tool_pins.sh | 12 ++++ 6 files changed, 125 insertions(+), 29 deletions(-) create mode 100644 scripts/gate_tool_pins.sh diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index f188a40c..9465ce4c 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -2,13 +2,23 @@ # SessionStart hook: provision a Claude Code on the web container so the canonical # gate (./scripts/check.sh) runs green here exactly as it does in CI and locally. # -# Mirrors the deps CI installs (.github/workflows/ci.yml): +# Mirrors the deps CI installs (.github/workflows/ci.yml); shared tool pins live +# in scripts/gate_tool_pins.sh: # - system: libportaudio2 (sounddevice), ffmpeg (--sample stream sources), shellcheck -# - node: markdownlint-cli@0.45.0 (check.sh calls `markdownlint` directly) +# - node: markdownlint-cli + prettier, pinned to CI's versions +# - go: actionlint + gitleaks (Go binaries, no PyPI/npm wheel) — without them +# check.sh silently self-skips those gates here and the failure only +# surfaces in CI # - python: `uv sync` to materialize the locked dev environment up front # +# Hook stdout is injected into the agent's context at session start, so emit one +# short line per step and send everything verbose to $LOG — a past session burned +# ~38KB of context on raw apt-get output. +# # Only runs in the remote (web) environment — local dev machines already have -# these and shouldn't be reprovisioned. Idempotent and non-interactive. +# these and shouldn't be reprovisioned. Idempotent and non-interactive. Every step +# soft-fails with a warning: a partially provisioned container is still usable, +# and check.sh self-skips whatever is missing (CI still enforces it). set -euo pipefail # Web-only: skip entirely on local machines. @@ -18,8 +28,13 @@ fi cd "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}" -# Pin markdownlint-cli to the version CI uses so lint results match exactly. -MARKDOWNLINT_VERSION="0.45.0" +# Tool pins are shared with .github/workflows/ci.yml so lint results match CI +# exactly; bump them in scripts/gate_tool_pins.sh, not here. +# shellcheck source=scripts/gate_tool_pins.sh +. scripts/gate_tool_pins.sh + +LOG="/tmp/session-start.log" +: >"$LOG" log() { echo "[session-start] $*"; } @@ -31,32 +46,79 @@ command -v ffmpeg >/dev/null 2>&1 || need_apt=1 command -v shellcheck >/dev/null 2>&1 || need_apt=1 ldconfig -p 2>/dev/null | grep -q portaudio || need_apt=1 if [ "$need_apt" = "1" ]; then - log "installing system deps (libportaudio2, ffmpeg, shellcheck)" export DEBIAN_FRONTEND=noninteractive # `apt-get update` can exit non-zero when unrelated third-party PPAs are # unreachable; the main Ubuntu archive (which has these packages) still # refreshes, so tolerate that failure and let the install be the real signal. - apt-get update -qq || log "apt-get update reported errors (likely unrelated PPAs); continuing" - # Don't let a missing system package abort the whole session — a partially - # provisioned container is still usable; check.sh self-skips shellcheck. - apt-get install -y --no-install-recommends libportaudio2 ffmpeg shellcheck \ - || log "WARNING: apt-get install failed; some system deps may be missing" + apt-get update -qq >>"$LOG" 2>&1 || log "apt-get update reported errors (likely unrelated PPAs); continuing" + if apt-get install -y --no-install-recommends libportaudio2 ffmpeg shellcheck >>"$LOG" 2>&1; then + log "installed system deps (libportaudio2, ffmpeg, shellcheck)" + else + log "WARNING: apt-get install failed; some system deps may be missing (see $LOG)" + fi else log "system deps already present" fi -# 2. markdownlint CLI (Node) — check.sh invokes `markdownlint` as a global binary, -# not through uv, so it must be on PATH. -if ! command -v markdownlint >/dev/null 2>&1; then - log "installing markdownlint-cli@${MARKDOWNLINT_VERSION}" - npm install -g "markdownlint-cli@${MARKDOWNLINT_VERSION}" -else +# 2. Node lint CLIs — check.sh invokes `markdownlint` and `prettier` as global +# binaries, not through uv, so they must be on PATH at CI's pinned versions +# (the base image may ship a different prettier, and formatting output can +# differ across versions). +if command -v markdownlint >/dev/null 2>&1; then log "markdownlint already present" +elif npm install -g "markdownlint-cli@${MARKDOWNLINT_VERSION}" >>"$LOG" 2>&1; then + log "installed markdownlint-cli@${MARKDOWNLINT_VERSION}" +else + log "WARNING: markdownlint install failed; check.sh's markdownlint gate will error (see $LOG)" +fi +if [ "$(prettier --version 2>/dev/null || true)" = "$PRETTIER_VERSION" ]; then + log "prettier ${PRETTIER_VERSION} already present" +elif npm install -g "prettier@${PRETTIER_VERSION}" >>"$LOG" 2>&1; then + log "installed prettier@${PRETTIER_VERSION}" +else + log "WARNING: prettier install failed; check.sh self-skips its gate (CI still runs it; see $LOG)" +fi + +# 3. Go gate binaries (actionlint + gitleaks), same pinned versions CI builds. +# GOBIN=/usr/local/bin so they're on PATH for every later shell. `go install` +# can't bake a comparable version string, so idempotency is presence-only — +# fine, since containers are ephemeral and a pin bump rebuilds on cold start. +install_go_tool() { # $1 = binary name, $2 = module@version + if command -v "$1" >/dev/null 2>&1; then + log "$1 already present" + elif GOBIN=/usr/local/bin go install "$2" >>"$LOG" 2>&1; then + log "installed $1 ($2)" + else + log "WARNING: go install $2 failed; check.sh self-skips the $1 gate (CI still runs it; see $LOG)" + fi +} +if command -v go >/dev/null 2>&1; then + install_go_tool actionlint "$ACTIONLINT_MODULE" + install_go_tool gitleaks "$GITLEAKS_MODULE" +else + log "go not found; skipping actionlint/gitleaks (check.sh self-skips them; CI still runs them)" fi -# 3. Python environment — materialize the locked dev env so the first `uv run` +# 4. Git history — web containers start from a shallow clone, where origin/main +# can exist with NO merge base to the session branch; check.sh's diff-scoped +# tail gates (diff-cover/mutation) then crash with "fatal: ... no merge base" +# instead of self-skipping. Unshallow up front so they just work. +if [ "$(git rev-parse --is-shallow-repository 2>/dev/null)" = "true" ]; then + if git fetch --unshallow origin main >>"$LOG" 2>&1; then + log "unshallowed clone (merge base with origin/main available for diff gates)" + else + log "WARNING: git fetch --unshallow failed; diff-cover/mutation gates may error (see $LOG)" + fi +else + log "clone already has full history" +fi + +# 5. Python environment — materialize the locked dev env so the first `uv run` # doesn't pay the full sync cost mid-task. `uv` syncs the default dev group. -log "syncing uv environment (locked dev group)" -uv sync +if uv sync >>"$LOG" 2>&1; then + log "uv environment synced (locked dev group)" +else + log "WARNING: uv sync failed (see $LOG)" +fi -log "provisioning complete" +log "provisioning complete (full output: $LOG)" diff --git a/.claude/settings.json b/.claude/settings.json index 09db6b35..d1186d4c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -30,6 +30,9 @@ "Bash(git diff *)", "Bash(git log *)", "Bash(git show *)", + "Bash(git rev-parse *)", + "Bash(git ls-files *)", + "Bash(git grep *)", "Bash(git branch *)", "Bash(git add *)", "Bash(git commit *)", @@ -39,6 +42,9 @@ "Bash(git worktree *)", "Bash(gh pr view *)", "Bash(gh pr list *)", + "Bash(gh pr diff *)", + "Bash(gh pr checks *)", + "Bash(timeout 30 uv run assembly *)", "Bash(gh run view *)", "Bash(gh run list *)", "mcp__assemblyai-docs__search_docs", @@ -73,7 +79,8 @@ "hooks": [ { "type": "command", - "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh", + "timeout": 600 } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6e46e61..f19ae03b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,10 +49,13 @@ jobs: - name: System deps (PortAudio + ffmpeg) run: sudo apt-get update && sudo apt-get install -y libportaudio2 ffmpeg - # check.sh lints Markdown and template JS/CSS via Node CLIs; pin to the - # versions used locally. The runner ships Node, so a global npm install suffices. + # check.sh lints Markdown and template JS/CSS via Node CLIs; versions are + # pinned in scripts/gate_tool_pins.sh (shared with the web session-start + # hook). The runner ships Node, so a global npm install suffices. - name: Node lint CLIs - run: npm install -g markdownlint-cli@0.45.0 prettier@3.8.3 + run: | + source scripts/gate_tool_pins.sh + npm install -g "markdownlint-cli@${MARKDOWNLINT_VERSION}" "prettier@${PRETTIER_VERSION}" # check.sh runs every tool through `uv run` / `uv build` for a locked, # reproducible env, so only uv must be on PATH (installed from PyPI to match @@ -63,12 +66,14 @@ jobs: # actionlint and gitleaks are Go binaries (no PyPI wheel), so check.sh self-skips # them locally like shellcheck. Build them here with the runner's preinstalled Go, - # pinned to a release tag, and put GOPATH/bin on PATH so check.sh enforces them. + # pinned via scripts/gate_tool_pins.sh (shared with the web session-start hook), + # and put GOPATH/bin on PATH so check.sh enforces them. # (gitleaks v8's Go module path is still github.com/zricethezav/gitleaks/v8.) - name: Workflow + secret scanners (actionlint, gitleaks) run: | - go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 - go install github.com/zricethezav/gitleaks/v8@v8.21.2 + source scripts/gate_tool_pins.sh + go install "$ACTIONLINT_MODULE" + go install "$GITLEAKS_MODULE" echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - name: Lint, typecheck, test diff --git a/AGENTS.md b/AGENTS.md index bcaaffc9..4849051a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,13 @@ Lessons that cost iterations getting the patch-coverage and mutation tail gates Lessons that cost time in agent sessions — read before exercising `uv run assembly` by hand: +- **Web/remote containers are fully provisioned at session start** + (`.claude/hooks/session-start.sh`): system deps, `markdownlint`/`prettier`, and the Go + gate binaries (`actionlint`, `gitleaks`) are installed at CI's pinned versions, so + `./scripts/check.sh` enforces the same gates CI does — a gate that "self-skips locally" + should *not* be skipping in a web session. If one is, read `/tmp/session-start.log` to + see what failed to provision. Keep the hook's stdout terse (one line per step) — it is + injected into the agent's context every session. - **Probe network reachability first.** Remote/sandboxed environments often allowlist PyPI but block `api.assemblyai.com` / `streaming.assemblyai.com` / `llm-gateway.assemblyai.com` (`curl -s https://api.assemblyai.com/v2/transcript -H "authorization: $ASSEMBLYAI_API_KEY"` diff --git a/scripts/check.sh b/scripts/check.sh index 641139b8..92bda801 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -122,7 +122,10 @@ echo "==> shellcheck" # Static-lint this gate script. CI's ubuntu runner ships shellcheck; # locally it's skipped with a notice if not installed. if command -v shellcheck >/dev/null 2>&1; then - shellcheck scripts/check.sh scripts/docker_build_check.sh scripts/cut_release.sh + # -x + --source-path=. let it follow the hook's `. scripts/gate_tool_pins.sh` + # (paths resolve from the repo root, where this script always runs). + shellcheck -x --source-path=. scripts/check.sh scripts/docker_build_check.sh \ + scripts/cut_release.sh scripts/gate_tool_pins.sh .claude/hooks/session-start.sh else echo " shellcheck not found; skipping (CI runs it)" fi diff --git a/scripts/gate_tool_pins.sh b/scripts/gate_tool_pins.sh new file mode 100644 index 00000000..c90df9f2 --- /dev/null +++ b/scripts/gate_tool_pins.sh @@ -0,0 +1,12 @@ +# shellcheck shell=bash +# Single source of truth for the gate's non-Python tool pins. Python tools are +# pinned in pyproject.toml/uv.lock; these four have no PyPI distribution +# (markdownlint/prettier are npm packages, actionlint/gitleaks are Go binaries), +# so their versions live here instead. Sourced by both provisioning paths: +# - .github/workflows/ci.yml (the CI runner) +# - .claude/hooks/session-start.sh (Claude Code on the web containers) +# Bump a pin here and both environments pick it up together. +export MARKDOWNLINT_VERSION="0.45.0" +export PRETTIER_VERSION="3.8.3" +export ACTIONLINT_MODULE="github.com/rhysd/actionlint/cmd/actionlint@v1.7.7" +export GITLEAKS_MODULE="github.com/zricethezav/gitleaks/v8@v8.21.2" From f36a85915908777b614dd81f240550ec8cb9c499 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 23:01:01 +0000 Subject: [PATCH 2/2] gitignore pytest-xdist per-worker coverage intermediates Parallel pytest-cov runs write .coverage.vm.pid*.* files that are combined and removed on success but linger after an interrupted run, showing up as untracked files that trip commit hooks. https://claude.ai/code/session_01FUszDSwvuSLgvBXnXJQ3M7 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2510994d..2865af01 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ .ruff_cache/ .mypy_cache/ .coverage +.coverage.* coverage.xml htmlcov/