-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpre-commit
More file actions
105 lines (90 loc) · 3.12 KB
/
Copy pathpre-commit
File metadata and controls
105 lines (90 loc) · 3.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/bin/bash
# .git/hooks/pre-commit (install into EACH worktree — see install-hooks.sh)
#
# Enforces the lock protocol from AGENTS.md at the git level instead of
# relying on the agent to "remember" to check .agent-locks.json.
#
# Behavior:
# - Finds the lock file at the repo root (shared across all worktrees).
# - For every staged file, checks if another agent holds a lock on it.
# - Blocks the commit if: locked by someone else AND read_safe=false AND not stale (15min).
# - Allows the commit if: unlocked, locked by yourself, locked-but-read_safe,
# or locked-but-stale (mirrors the "you may steal it" rule in AGENTS.md).
#
# Identify yourself by exporting AGENT_ID before committing, e.g.:
# export AGENT_ID="deepseek-test-a"
# If unset, falls back to "unknown-agent" (will never match an existing lock,
# so it behaves conservatively — i.e. treats every lock as someone else's).
set -e
AGENT_ID="${AGENT_ID:-unknown-agent}"
STALE_SECONDS=$((15 * 60))
# Resolve the main repo root — works from worktrees and the main repo alike.
MAIN_REPO_ROOT=$(cd "$(dirname "$(git rev-parse --git-common-dir)")" && pwd)
LOCK_FILE="$MAIN_REPO_ROOT/.agent-locks.json"
if [ ! -f "$LOCK_FILE" ] || [ ! -s "$LOCK_FILE" ]; then
exit 0
fi
STAGED_FILES=$(git diff --cached --name-only)
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
NOW_EPOCH=$(date -u +%s)
BLOCKED=0
while IFS= read -r FILE; do
RESULT=$(python3 - "$LOCK_FILE" "$FILE" "$AGENT_ID" "$NOW_EPOCH" "$STALE_SECONDS" <<'PYEOF'
import json, sys, datetime
lock_file, target_file, agent_id, now_epoch, stale_seconds = sys.argv[1:6]
now_epoch = int(now_epoch)
stale_seconds = int(stale_seconds)
try:
with open(lock_file) as f:
locks = json.load(f)
except Exception:
print("OK no-lock-file-or-unparseable")
sys.exit(0)
if isinstance(locks, dict):
locks = [locks]
for entry in locks:
if entry.get("file") != target_file:
continue
owner = entry.get("agent", "")
if owner == agent_id:
print("OK own-lock")
sys.exit(0)
if entry.get("read_safe") is True:
print("OK read-safe-lock")
sys.exit(0)
heartbeat = entry.get("heartbeat") or entry.get("locked_since")
try:
hb_epoch = int(datetime.datetime.fromisoformat(
heartbeat.replace("Z", "+00:00")
).timestamp())
except Exception:
print(f"BLOCK unparseable-heartbeat owner={owner}")
sys.exit(0)
age = now_epoch - hb_epoch
if age > stale_seconds:
print(f"OK stale-lock age={age}s owner={owner}")
sys.exit(0)
print(f"BLOCK active-lock owner={owner} age={age}s")
sys.exit(0)
print("OK no-matching-lock")
PYEOF
)
case "$RESULT" in
BLOCK*)
echo "✗ BLOCKED: $FILE — $RESULT"
BLOCKED=1
;;
OK*)
;;
esac
done <<< "$STAGED_FILES"
if [ "$BLOCKED" = "1" ]; then
echo ""
echo "Commit blocked: you are trying to commit a file locked by another agent."
echo "Per AGENTS.md: do not edit a file that's locked (read_safe: false) unless the lock is stale (>15min)."
echo "Set AGENT_ID env var to identify yourself if this is your own lock being misattributed."
exit 1
fi
exit 0