Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .githooks/_lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Shared helpers for the versioned hooks in this directory. Sourced, not run.
#
# WHY THESE HOOKS EXIST AT ALL: setting `core.hooksPath` REPLACES `.git/hooks`
# wholesale — git stops looking there entirely. graphify installs post-commit,
# post-checkout, and post-merge into `$GIT_COMMON_DIR/hooks/`, so every one of
# them needs a counterpart here or it is silently disabled. (Shipping only
# post-checkout killed graphify's commit/merge rebuilds on the operator box for
# about an hour — caught only by noticing the repo's own graph had gone STALE
# right after a merge.)
#
# Each hook here is a thin shim: skip in linked worktrees, otherwise hand off
# to the machine-local graphify hook, which pins an absolute interpreter path
# and therefore must never be committed.

# Absolute path, whether git handed us a relative one or not.
gfy_abs() {
case "$1" in
/*) printf '%s\n' "$1" ;;
*) printf '%s\n' "$PWD/$1" ;;
esac
}

gfy_common_dir() {
gfy_abs "$(git rev-parse --git-common-dir 2>/dev/null || echo .git)"
}

# True in a linked worktree (its per-worktree git dir differs from the common one).
gfy_is_linked_worktree() {
_gd=$(gfy_abs "$(git rev-parse --git-dir 2>/dev/null || echo .git)")
[ "$_gd" != "$(gfy_common_dir)" ]
}

# Hand off to the machine-local hook of the same name, if present.
gfy_chain() {
_name=$1
shift
_local="$(gfy_common_dir)/hooks/$_name"
if [ -x "$_local" ]; then
exec "$_local" "$@"
fi
exit 0
}
91 changes: 91 additions & 0 deletions .githooks/post-checkout
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/bin/sh
# Versioned post-checkout hook — enabled per machine with:
#
# git config core.hooksPath .githooks
#
# Purpose: make the graphify knowledge graph usable from a *linked worktree*.
#
# `graphify-out/` is gitignored by design (only `graphify-out/.gitignore` is
# tracked — the graph is multi-MB, rewritten on every commit, and would
# conflict across parallel worker branches). So `git worktree add` gives a
# worktree an EMPTY `graphify-out/`, and `graphify query` — which resolves
# `graphify-out/graph.json` strictly relative to cwd, with no upward walk and
# no `--graph` override — fails there. Every agent working in a worktree is
# graph-blind.
#
# Git runs this hook on `git worktree add` (verified: cwd = the new worktree,
# $3 = 1), which is why the bootstrap lives here rather than in any one
# creator's dispatch code: one implementation covers every `worktree add`
# call site — remote agent dispatch, Claude Code's `.claude/worktrees/`,
# review worktrees, and anything created by hand — on every machine, with no
# creator-side changes.
#
# The fix is symlinking the base checkout's graph *contents* into the
# worktree's `graphify-out/` — never replacing the directory itself. A worker
# branch differs from the base by a handful of files, so the base graph is the
# right answer for "where is X handled / what calls this" navigation. It is
# NOT the right answer for "did my change land" — the linked graph reflects
# the base checkout's HEAD, not the worktree's edits.
#
# claude-coordinator#1617: an earlier version of this hook replaced the whole
# `graphify-out/` directory with a symlink to the base checkout.
# `git worktree add` materialises `graphify-out/` with only the tracked
# `.gitignore` checked out (everything else in the directory is
# untracked-and-ignored), and `rm -rf graphify-out` before `ln -sfn` deleted
# that tracked file out from under git — leaving `git status` showing a
# deleted tracked file *and* an untracked, machine-local, absolute-path
# symlink. Every worktree's own rescue-commit machinery then committed both.
# `graphify-out/.gitignore` is `*` / `!.gitignore`, so anything placed
# *inside* the directory is already invisible to git for free — the fix is to
# keep the directory (and its tracked `.gitignore`) and symlink each entry of
# the base graph into it instead of swapping the directory out from under git.
#
# See .githooks/_lib.sh for why every graphify hook needs a shim here.

set -u
. "$(dirname "$0")/_lib.sh"

# $3 == 1 means a branch checkout (not a file checkout). `git worktree add`
# sets it. Anything else is not our business.
if [ "${3:-0}" != "1" ]; then
exit 0
fi

if gfy_is_linked_worktree; then
# The base checkout is the parent of the common git dir.
_base=$(CDPATH= cd -- "$(dirname -- "$(gfy_common_dir)")" 2>/dev/null && pwd) || _base=""
if [ -n "$_base" ] && [ -f "$_base/graphify-out/graph.json" ]; then
# Only bootstrap when graph.json here is absent, or is itself a
# symlink from a previous run of this hook (idempotent re-link on a
# later checkout in the same worktree). If a REAL graph was built
# in this worktree, leave it alone.
if [ ! -e graphify-out/graph.json ] || [ -L graphify-out/graph.json ]; then
# `git worktree add` already checked out the tracked
# graphify-out/.gitignore, materialising the directory — never
# remove it, only add symlinks alongside it.
mkdir -p graphify-out 2>/dev/null
_linked=0
for _entry in "$_base"/graphify-out/* "$_base"/graphify-out/.[!.]*; do
[ -e "$_entry" ] || continue
_name=$(basename -- "$_entry")
# .gitignore is the tracked file that keeps this directory
# self-ignoring — never shadow it with a symlink to the
# base's copy.
[ "$_name" = ".gitignore" ] && continue
ln -sfn "$_entry" "graphify-out/$_name" 2>/dev/null && _linked=1
done
[ "$_linked" = "1" ] &&
echo "[graphify] linked graphify-out/* -> $_base/graphify-out/* (read-only base graph)"
fi
fi
# Never rebuild in a linked worktree. Two reasons, both load-bearing:
# 1. graphify-out/graph.json (and friends) are symlinks to the SHARED
# base graph — a rebuild here would overwrite it from a
# feature-branch tree.
# 2. A worktree can be reaped mid-rebuild, which is how the graphify
# hook's own "burns a full AST pass and then dies with ENOENT"
# comment came to be.
exit 0
fi

gfy_chain post-checkout "$@"
19 changes: 19 additions & 0 deletions .githooks/post-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh
# Versioned post-commit shim.
#
# core.hooksPath REPLACES .git/hooks wholesale, so without this file
# graphify's post-commit rebuild is silently DISABLED on any machine that opts
# into the versioned hooks. See .githooks/_lib.sh.
#
# Linked worktrees never rebuild: graphify-out there is a symlink to the base
# checkout's SHARED graph, so a rebuild would overwrite it from a
# feature-branch tree — and a worktree can be reaped mid-rebuild.

set -u
. "$(dirname "$0")/_lib.sh"

if gfy_is_linked_worktree; then
exit 0
fi

gfy_chain post-commit "$@"
19 changes: 19 additions & 0 deletions .githooks/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh
# Versioned post-merge shim.
#
# core.hooksPath REPLACES .git/hooks wholesale, so without this file
# graphify's post-merge rebuild is silently DISABLED on any machine that opts
# into the versioned hooks. See .githooks/_lib.sh.
#
# Linked worktrees never rebuild: graphify-out there is a symlink to the base
# checkout's SHARED graph, so a rebuild would overwrite it from a
# feature-branch tree — and a worktree can be reaped mid-rebuild.

set -u
. "$(dirname "$0")/_lib.sh"

if gfy_is_linked_worktree; then
exit 0
fi

gfy_chain post-merge "$@"
Loading
Loading