From f8eda5bf16915b0c7d46aae68d5c4e803a4d30ef Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 16 May 2026 11:39:08 -0400 Subject: [PATCH] fix(3597)(3347): write graphify rebuild lock in parent hook to close ENOTEMPTY race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook double-forked the rebuild subprocess and returned before the subprocess wrote .planning/graphs/.rebuild.lock. Callers (notably the feat-3347 test cleanup) waited for the lock to disappear before rm -rf'ing the tmpdir, but an absent lock was ambiguous: it could mean "subprocess finished and trapped lock removal" OR "subprocess hasn't started yet." Under ubuntu CI load the second case won, cleanup raced ahead, and rmSync walked into .planning/graphs while the subprocess was still creating files — surfacing as ENOTEMPTY: directory not empty, rmdir '/tmp/gsd-3347-*/.planning/graphs' on the "dispatches on: git commit -m fix" test. Spawn the rebuild as a regular backgrounded job, capture $!, and write the lock file synchronously in the parent before exit. Lock-presence is now a reliable in-flight signal; the rebuild script's existing trap-on-EXIT rm still owns cleanup. Validated: holodeck (ubuntu docker) 11224 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- hooks/gsd-graphify-update.sh | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/hooks/gsd-graphify-update.sh b/hooks/gsd-graphify-update.sh index 5c31f36d1..122a864f0 100755 --- a/hooks/gsd-graphify-update.sh +++ b/hooks/gsd-graphify-update.sh @@ -134,19 +134,21 @@ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)" REBUILD_SCRIPT="$HOOK_DIR/lib/gsd-graphify-rebuild.sh" [ -f "$REBUILD_SCRIPT" ] || exit 0 -# Detach the rebuild. Portable double-fork via subshell + disown — works on -# macOS (no setsid) and Linux. Redirect all I/O to /dev/null so the hook -# returns instantly even if the child keeps stdout/stderr handles open. -( - bash "$REBUILD_SCRIPT" \ - "$STATUS_FILE" \ - "$LOCK_FILE" \ - "$HEAD_SHA" \ - "$MS_START" \ - "$GRAPHIFY_BIN" \ - /dev/null 2>&1 & - disown -) & -disown +# Detach the rebuild. Spawn as a regular background job so we can capture +# its PID via $! and write it to the lock file synchronously here in the +# parent. This eliminates a startup race where a caller (e.g. test cleanup) +# observing an absent lock could not distinguish "subprocess finished" from +# "subprocess hasn't started yet." With the lock written before this hook +# returns, lock-presence is a reliable in-flight signal. +bash "$REBUILD_SCRIPT" \ + "$STATUS_FILE" \ + "$LOCK_FILE" \ + "$HEAD_SHA" \ + "$MS_START" \ + "$GRAPHIFY_BIN" \ + /dev/null 2>&1 & +REBUILD_PID=$! +echo "$REBUILD_PID" > "$LOCK_FILE" +disown "$REBUILD_PID" 2>/dev/null || true exit 0