#!/bin/bash # Pre-commit hook: verify all defuns in staged .org files compile in the daemon. # For each changed .org file, it tangles to .lisp then sends the entire file # to the daemon for compilation. This catches undefined symbol references, # syntax errors, and broken function bodies. # # Install: # ln -sf ../../scripts/pre-commit-repl-check .git/hooks/pre-commit # # Requires: running daemon on port 9105, repl script, emacs with ob-tangle. # # Returns 0 (pass) or 1 (fail). set -euo pipefail IFS=$'\n\t' REPL=$(command -v repl 2>/dev/null || echo "/home/user/.opencode/bin/repl") PORT=9105 PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo "/home/user/memex/projects/passepartout") # Check daemon connectivity if ! timeout 2 bash -c "echo >/dev/tcp/127.0.0.1/$PORT" 2>/dev/null; then echo "ERROR: Daemon not reachable on 127.0.0.1:$PORT. Start it first." >&2 exit 1 fi # Collect changed .org files from the index CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep '\.org$' || true) if [ -z "$CHANGED" ]; then exit 0 fi FAILED=0 for orgfile in $CHANGED; do [ -f "$orgfile" ] || continue # Determine the tangle target from the org file's PROPERTY line TANGLE=$(grep 'header-args.*:tangle' "$orgfile" | sed "s/.*:tangle //" | head -1 || true) if [ -z "$TANGLE" ]; then echo "SKIP: $orgfile — no :tangle header" >&2 continue fi # Skip files that depend on external libraries not loaded in the daemon BASENAME=$(basename "$orgfile") case "$BASENAME" in gateway-tui.org) echo "SKIP: $orgfile — external dependency (croatoan)" >&2 continue ;; esac # Resolve relative tangle path ORG_DIR=$(dirname "$orgfile") LISP_FILE=$(cd "$ORG_DIR" && realpath -m "$TANGLE" 2>/dev/null || echo "$ORG_DIR/$TANGLE") # Tangle the org file to lisp if ! emacs --batch -L "$PROJECT_DIR" --eval "(require 'ob-tangle)" \ --eval "(org-babel-tangle-file \"$ORG_DIR/$(basename "$orgfile")\")" \ /dev/null; then echo "FAIL: $orgfile — tangling failed" >&2 FAILED=1 continue fi if [ ! -f "$LISP_FILE" ]; then echo "SKIP: $orgfile — tangle target $LISP_FILE not found" >&2 continue fi # Compile the lisp file in the daemon. # We send a Lisp form that compiles the file and returns T or an error string. # Using format to avoid backquote/comma issues. LISP_ABS=$(realpath "$LISP_FILE" 2>/dev/null || echo "$LISP_FILE") CODE=$(cat <<-LISPEOF (let ((*standard-output* (make-broadcast-stream)) (*error-output* (make-broadcast-stream))) (handler-case (progn (compile-file "$LISP_ABS") (load (compile-file-pathname "$LISP_ABS")) (format nil "OK")) (error (c) (format nil "COMPILE-ERROR: ~a" c)))) LISPEOF ) RESULT=$(printf '%s' "$CODE" | timeout 10 "$REPL" 2>/dev/null || echo "DAEMON-UNREACHABLE") if echo "$RESULT" | grep -q '^COMPILE-ERROR:\|^DAEMON-UNREACHABLE\|^$'; then echo "REJECT: $(basename "$orgfile") — compilation failed: $RESULT" >&2 FAILED=1 else echo "OK: $(basename "$orgfile")" >&2 fi done if [ "$FAILED" -eq 1 ]; then echo "" >&2 echo "COMMIT REJECTED: REPL compilation check failed." >&2 echo "Fix errors, or bypass with: git commit --no-verify" >&2 exit 1 fi