diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-24 11:00:39 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-24 11:00:39 -0500 |
| commit | 781fa0786e2096797e630dcb81ffbc62ed98460b (patch) | |
| tree | e47a1662ed9ff8ea1b1744c407873974a6bd0b2b /working/hook-fail-open/validate-el.sh | |
| parent | 267d1de7b8a8e7fd22b156433567c88216ec3d0f (diff) | |
| download | rulesets-781fa0786e2096797e630dcb81ffbc62ed98460b.tar.gz rulesets-781fa0786e2096797e630dcb81ffbc62ed98460b.zip | |
chore(inbox): park two hook fail-open fixes from .emacs.d
Diffstat (limited to 'working/hook-fail-open/validate-el.sh')
| -rw-r--r-- | working/hook-fail-open/validate-el.sh | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/working/hook-fail-open/validate-el.sh b/working/hook-fail-open/validate-el.sh new file mode 100644 index 0000000..870eefe --- /dev/null +++ b/working/hook-fail-open/validate-el.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Validate and test .el files after Edit/Write/MultiEdit. +# PostToolUse hook: receives tool-call JSON on stdin. +# +# On success: exit 0 silent. +# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude +# sees a structured error in its context, THEN exit 2 to block the tool +# pipeline. stderr still echoes the error for terminal visibility. +# +# Phase 1: check-parens + byte-compile +# Phase 2: for non-test .el files, run matching tests/test-<stem>*.el + +set -u + +# Emit a JSON failure payload and exit 2. Arguments: +# $1 — short failure type (e.g. "PAREN CHECK FAILED") +# $2 — file path +# $3 — emacs output (error body), always sent to Claude in additionalContext +# $4 — optional compact terminal echo; when set, the terminal shows this +# instead of the full $3 (Claude still gets the full $3). Used by the +# test runner so a failing suite prints a short summary to the pane +# rather than dumping every ERT backtrace. +fail_json() { + local ctx + ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \ + | jq -Rs .)" + cat <<EOF +{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}} +EOF + printf '%s: %s\n%s\n' "$1" "$2" "${4:-$3}" >&2 + exit 2 +} + +# Portable project root: prefer Claude Code's env var, fall back to deriving +# from this script's location ($project/.claude/hooks/validate-el.sh). +PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}" + +f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')" +[ -z "$f" ] && exit 0 +[ "${f##*.}" = "el" ] || exit 0 + +# --- Phase 1: syntax + byte-compile --- +case "$f" in + */init.el|*/early-init.el) + # Byte-compile here would load the full package graph. Parens only. + if ! output="$(emacs --batch --no-site-file --no-site-lisp "$f" \ + --eval '(check-parens)' 2>&1)"; then + fail_json "PAREN CHECK FAILED" "$f" "$output" + fi + ;; + *.el) + # -L the file's own directory (and a sibling project root for files + # under a tests/ subdir) so cross-project edits compile against their + # own modules, not just this project's. + if ! output="$(emacs --batch --no-site-file --no-site-lisp \ + --eval '(setq load-prefer-newer t)' \ + -L "$(dirname "$f")" \ + -L "$(dirname "$f")/.." \ + -L "$PROJECT_ROOT" \ + -L "$PROJECT_ROOT/modules" \ + -L "$PROJECT_ROOT/tests" \ + -L "$PROJECT_ROOT/themes" \ + --eval '(package-initialize)' \ + "$f" \ + --eval '(check-parens)' \ + --eval "(or (byte-compile-file \"$f\") (kill-emacs 1))" 2>&1)"; then + fail_json "VALIDATION FAILED" "$f" "$output" + fi + ;; +esac + +# --- Phase 2: test runner --- +# Determine which tests (if any) apply to this edit. Works for projects with +# source at root, in modules/, or elsewhere — stem-based test lookup is the +# common pattern. +tests=() +case "$f" in + */init.el|*/early-init.el) + : # Phase 1 handled it; skip test runner + ;; + "$PROJECT_ROOT/tests/testutil-"*.el) + stem="$(basename "${f%.el}")" + stem="${stem#testutil-}" + mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort) + ;; + "$PROJECT_ROOT/tests/test-"*.el) + tests=("$f") + ;; + *.el) + # Any other .el under the project — find matching tests by stem + stem="$(basename "${f%.el}")" + mapfile -t tests < <(find "$PROJECT_ROOT/tests" -maxdepth 1 -name "test-${stem}*.el" 2>/dev/null | sort) + ;; +esac + +count="${#tests[@]}" +if [ "$count" -ge 1 ]; then + load_args=() + for t in "${tests[@]}"; do load_args+=("-l" "$t"); done + if ! output="$(emacs --batch --no-site-file --no-site-lisp \ + --eval '(setq load-prefer-newer t)' \ + -L "$PROJECT_ROOT" \ + -L "$PROJECT_ROOT/modules" \ + -L "$PROJECT_ROOT/tests" \ + -L "$PROJECT_ROOT/themes" \ + --eval '(package-initialize)' \ + --eval "(cd \"$PROJECT_ROOT/tests\")" \ + -l ert "${load_args[@]}" \ + --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then + # Terminal gets a compact summary (the run tally + the failing test names); + # Claude still gets the full backtrace via additionalContext. Keeps the + # pane from drowning in ERT stack frames on every red test. + summary="$(printf '%s\n' "$output" \ + | grep -E '^Ran [0-9]+ tests|unexpected results:|^[[:space:]]+FAILED' || true)" + [ -n "$summary" ] && summary="${summary}"$'\n'"(full backtrace in Claude's context)" + fail_json "TESTS FAILED ($count test file(s))" "$f" "$output" "$summary" + fi +fi + +exit 0 |
