Fix bd-51: Add git hooks to eliminate auto-flush race condition

- Added --flush-only flag to bd sync command
- Created pre-commit hook to flush pending changes before commit
- Created post-merge hook to import changes after pull/merge
- Added install script for easy setup
- Updated AGENTS.md with git hooks workflow
- Resolves race condition where daemon auto-flush fires after commit

Amp-Thread-ID: https://ampcode.com/threads/T-00b80d3a-4194-4c75-a60e-25a318cf9f91
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-24 22:17:06 -07:00
parent c87f9007a5
commit 0344e1f08b
7 changed files with 199 additions and 265 deletions

View File

@@ -1,200 +1,102 @@
# Git Hooks for Beads
# bd Git Hooks
Optional git hooks for immediate export/import of beads issues.
This directory contains git hooks that integrate bd (beads) with your git workflow, solving the race condition between daemon auto-flush and git commits.
**NOTE**: As of bd v0.9+, **auto-sync is enabled by default!** These hooks are optional and provide:
- **Immediate export** (no 5-second debounce wait)
- **Guaranteed import** after every git operation
- **Extra safety** for critical workflows
## The Problem
## What These Hooks Do
When using bd in daemon mode, operations trigger a 5-second debounced auto-flush to JSONL. This creates a race condition:
- **pre-commit**: Exports SQLite → JSONL before every commit (immediate, no debounce)
- **post-merge**: Imports JSONL → SQLite after git pull/merge (guaranteed)
- **post-checkout**: Imports JSONL → SQLite after branch switching (guaranteed)
1. User closes issue via MCP → daemon schedules flush (5 sec delay)
2. User commits code changes → JSONL appears clean
3. Daemon flush fires → JSONL modified after commit
4. Result: dirty working tree showing JSONL changes
This keeps your `.beads/issues.jsonl` (committed to git) in sync with your local SQLite database (gitignored).
## The Solution
## Do You Need These Hooks?
These git hooks ensure bd changes are always synchronized with your commits:
**Most users don't need hooks anymore!** bd automatically:
- Exports after CRUD operations (5-second debounce)
- Imports when JSONL is newer than DB
**Install hooks if you:**
- Want immediate export (no waiting 5 seconds)
- Want guaranteed import after every git operation
- Need extra certainty for team workflows
- Prefer explicit automation over automatic behavior
- **pre-commit** - Flushes pending bd changes to JSONL before commit
- **post-merge** - Imports updated JSONL after git pull/merge
## Installation
### Quick Install
From your repository root:
```bash
cd /path/to/your/project
./examples/git-hooks/install.sh
```
The installer will prompt before overwriting existing hooks.
This will:
- Copy hooks to `.git/hooks/`
- Make them executable
- Back up any existing hooks
### Manual Install
```bash
# Copy hooks to .git/hooks/
cp examples/git-hooks/pre-commit .git/hooks/
cp examples/git-hooks/post-merge .git/hooks/
cp examples/git-hooks/post-checkout .git/hooks/
# Make them executable
chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/post-merge
chmod +x .git/hooks/post-checkout
```
## Usage
Once installed, the hooks run automatically:
```bash
# Creating/updating issues
bd create "New feature" -p 1
bd update bd-1 --status in_progress
# Committing changes - hook exports automatically
git add .
git commit -m "Update feature"
# 🔗 Exporting beads issues to JSONL...
# ✓ Beads issues exported and staged
# Pulling changes - hook imports automatically
git pull
# 🔗 Importing beads issues from JSONL...
# ✓ Beads issues imported successfully
# Switching branches - hook imports automatically
git checkout feature-branch
# 🔗 Importing beads issues from JSONL...
# ✓ Beads issues imported successfully
cp examples/git-hooks/pre-commit .git/hooks/pre-commit
cp examples/git-hooks/post-merge .git/hooks/post-merge
chmod +x .git/hooks/pre-commit .git/hooks/post-merge
```
## How It Works
### The Workflow
### pre-commit
1. You work with bd commands (`create`, `update`, `close`)
2. Changes are stored in SQLite (`.beads/*.db`) - fast local queries
3. Before commit, hook exports to JSONL (`.beads/issues.jsonl`) - git-friendly
4. JSONL is committed to git (source of truth)
5. After pull/merge/checkout, hook imports JSONL back to SQLite
6. Your local SQLite cache is now in sync with git
### Why This Design?
**SQLite for speed**:
- Fast queries (dependency trees, ready work)
- Rich SQL capabilities
- Sub-100ms response times
**JSONL for git**:
- Clean diffs (one issue per line)
- Mergeable (independent lines)
- Human-readable
- AI-resolvable conflicts
Best of both worlds!
## Troubleshooting
### Hook not running
Before each commit, the hook runs:
```bash
# Check if hook is executable
ls -l .git/hooks/pre-commit
# Should show -rwxr-xr-x
# Make it executable if needed
chmod +x .git/hooks/pre-commit
bd sync --flush-only
```
### Export/import fails
This:
1. Exports any pending database changes to `.beads/issues.jsonl`
2. Stages the JSONL file if modified
3. Allows the commit to proceed with clean state
The hook is silent on success, fast (no git operations), and safe (fails commit if flush fails).
### post-merge
After a git pull or merge, the hook runs:
```bash
# Check if bd is in PATH
which bd
# Check if you're in a beads-initialized directory
bd list
bd import -i .beads/issues.jsonl --resolve-collisions
```
### Merge conflicts in issues.jsonl
This ensures your local database reflects the merged state. The hook:
- Only runs if `.beads/issues.jsonl` exists
- Automatically resolves ID collisions from branch merges
- Warns on failure but doesn't block the merge
If you get merge conflicts in `.beads/issues.jsonl`:
## Compatibility
1. Most conflicts are safe to resolve by keeping both sides
2. Each line is an independent issue
3. Look for `<<<<<<< HEAD` markers
4. Keep all lines that don't conflict
5. For actual conflicts on the same issue, choose the newest
- **Auto-sync**: Works alongside bd's automatic 5-second debounce
- **Direct mode**: Hooks work in both daemon and `--no-daemon` mode
- **Worktrees**: Safe to use with git worktrees
Example conflict:
## Benefits
```
<<<<<<< HEAD
{"id":"bd-3","title":"Updated title","status":"closed","updated_at":"2025-10-12T10:00:00Z"}
=======
{"id":"bd-3","title":"Updated title","status":"in_progress","updated_at":"2025-10-12T09:00:00Z"}
>>>>>>> feature-branch
```
✅ No more dirty working tree after commits
✅ Database always in sync with git
✅ Automatic collision resolution on merge
✅ Fast and silent operation
✅ Optional - manual `bd sync` still works
Resolution: Keep the HEAD version (newer timestamp).
## Uninstall
After resolving:
```bash
git add .beads/issues.jsonl
git commit
bd import -i .beads/issues.jsonl # Sync to SQLite
```
## Uninstalling
Remove the hooks:
```bash
rm .git/hooks/pre-commit
rm .git/hooks/post-merge
rm .git/hooks/post-checkout
rm .git/hooks/pre-commit .git/hooks/post-merge
```
## Customization
Your backed-up hooks (if any) are in `.git/hooks/*.backup-*`.
### Skip hook for one commit
## Related
```bash
git commit --no-verify -m "Skip hooks"
```
### Add to existing hooks
If you already have git hooks, you can append to them:
```bash
# Append to existing pre-commit
cat examples/git-hooks/pre-commit >> .git/hooks/pre-commit
```
### Filter exports
Export only specific issues:
```bash
# Edit pre-commit hook, change:
bd export --format=jsonl -o .beads/issues.jsonl
# To:
bd export --format=jsonl --status=open -o .beads/issues.jsonl
```
## See Also
- [Git hooks documentation](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
- [../../TEXT_FORMATS.md](../../TEXT_FORMATS.md) - JSONL merge strategies
- [../../GIT_WORKFLOW.md](../../GIT_WORKFLOW.md) - Design rationale
- See [bd-51](../../.beads/bd-51) for the race condition bug report
- See [AGENTS.md](../../AGENTS.md) for the full git workflow
- See [examples/](../) for other integrations

View File

@@ -1,83 +1,64 @@
#!/usr/bin/env bash
#!/bin/bash
#
# Install Beads git hooks
# Install bd git hooks
#
# This script copies the hooks to .git/hooks/ and makes them executable
# This script copies the bd git hooks to your .git/hooks directory
# and makes them executable.
#
# Usage:
# ./examples/git-hooks/install.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
# Check if we're in a git repository
if ! git rev-parse --git-dir &> /dev/null; then
echo "Error: Not in a git repository"
if [ ! -d .git ]; then
echo "Error: Not in a git repository root" >&2
echo "Run this script from the root of your git repository" >&2
exit 1
fi
echo "Installing Beads git hooks to $HOOKS_DIR"
echo ""
# Install pre-commit hook
if [[ -f "$HOOKS_DIR/pre-commit" ]]; then
echo "$HOOKS_DIR/pre-commit already exists"
read -p "Overwrite? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Skipping pre-commit"
else
cp "$SCRIPT_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
chmod +x "$HOOKS_DIR/pre-commit"
echo "✓ Installed pre-commit hook"
fi
else
cp "$SCRIPT_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
chmod +x "$HOOKS_DIR/pre-commit"
echo "✓ Installed pre-commit hook"
# Check if we're in a bd workspace
if [ ! -d .beads ]; then
echo "Error: Not in a bd workspace" >&2
echo "Run 'bd init' first" >&2
exit 1
fi
# Install post-merge hook
if [[ -f "$HOOKS_DIR/post-merge" ]]; then
echo "$HOOKS_DIR/post-merge already exists"
read -p "Overwrite? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Skipping post-merge"
else
cp "$SCRIPT_DIR/post-merge" "$HOOKS_DIR/post-merge"
chmod +x "$HOOKS_DIR/post-merge"
echo "✓ Installed post-merge hook"
fi
else
cp "$SCRIPT_DIR/post-merge" "$HOOKS_DIR/post-merge"
chmod +x "$HOOKS_DIR/post-merge"
echo "✓ Installed post-merge hook"
fi
# Find the script directory (handles being called from anywhere)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Install post-checkout hook
if [[ -f "$HOOKS_DIR/post-checkout" ]]; then
echo "$HOOKS_DIR/post-checkout already exists"
read -p "Overwrite? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Skipping post-checkout"
else
cp "$SCRIPT_DIR/post-checkout" "$HOOKS_DIR/post-checkout"
chmod +x "$HOOKS_DIR/post-checkout"
echo "✓ Installed post-checkout hook"
# Hooks to install
HOOKS="pre-commit post-merge"
echo "Installing bd git hooks..."
for hook in $HOOKS; do
src="$SCRIPT_DIR/$hook"
dst=".git/hooks/$hook"
if [ ! -f "$src" ]; then
echo "Warning: Hook $hook not found at $src" >&2
continue
fi
else
cp "$SCRIPT_DIR/post-checkout" "$HOOKS_DIR/post-checkout"
chmod +x "$HOOKS_DIR/post-checkout"
echo "✓ Installed post-checkout hook"
fi
# Backup existing hook if present
if [ -f "$dst" ]; then
backup="$dst.backup-$(date +%Y%m%d-%H%M%S)"
echo " Backing up existing $hook to $backup"
mv "$dst" "$backup"
fi
# Copy and make executable
cp "$src" "$dst"
chmod +x "$dst"
echo " Installed $hook"
done
echo ""
echo "✓ Beads git hooks installed successfully!"
echo "✓ Git hooks installed successfully"
echo ""
echo "These hooks will:"
echo " • Export issues to JSONL before every commit"
echo " • Import issues from JSONL after merges"
echo " • Import issues from JSONL after branch checkouts"
echo "Hooks installed:"
echo " pre-commit - Flushes pending bd changes to JSONL before commit"
echo " post-merge - Imports updated JSONL after git pull/merge"
echo ""
echo "To uninstall, simply delete the hooks from $HOOKS_DIR"
echo "To uninstall, remove .git/hooks/pre-commit and .git/hooks/post-merge"

View File

@@ -1,33 +1,41 @@
#!/usr/bin/env bash
#!/bin/sh
#
# Beads post-merge hook
# Automatically imports JSONL to SQLite database after pulling/merging
# bd (beads) post-merge hook
#
# Install: cp examples/git-hooks/post-merge .git/hooks/post-merge && chmod +x .git/hooks/post-merge
# This hook imports updated issues from .beads/issues.jsonl after a
# git pull or merge, ensuring the database stays in sync with git.
#
# Installation:
# cp examples/git-hooks/post-merge .git/hooks/post-merge
# chmod +x .git/hooks/post-merge
#
# Or use the install script:
# examples/git-hooks/install.sh
set -e
# Check if bd is installed
if ! command -v bd &> /dev/null; then
echo "Warning: bd not found in PATH, skipping import"
# Check if bd is available
if ! command -v bd >/dev/null 2>&1; then
echo "Warning: bd command not found, skipping post-merge import" >&2
exit 0
fi
# Check if issues.jsonl exists
if [[ ! -f .beads/issues.jsonl ]]; then
# No JSONL file, nothing to import
# Check if we're in a bd workspace
if [ ! -d .beads ]; then
# Not a bd workspace, nothing to do
exit 0
fi
# Import issues from JSONL
echo "🔗 Importing beads issues from JSONL..."
# Check if issues.jsonl exists and was updated
if [ ! -f .beads/issues.jsonl ]; then
exit 0
fi
if bd import -i .beads/issues.jsonl 2>/dev/null; then
echo "✓ Beads issues imported successfully"
else
echo "Warning: bd import failed"
echo "You may need to resolve conflicts manually"
exit 1
# Import the updated JSONL
# The auto-import feature should handle this, but we force it here
# to ensure immediate sync after merge
if ! bd import -i .beads/issues.jsonl --resolve-collisions >/dev/null 2>&1; then
echo "Warning: Failed to import bd changes after merge" >&2
echo "Run 'bd import -i .beads/issues.jsonl --resolve-collisions' manually" >&2
# Don't fail the merge, just warn
fi
exit 0

View File

@@ -1,33 +1,42 @@
#!/usr/bin/env bash
#!/bin/sh
#
# Beads pre-commit hook
# Automatically exports SQLite database to JSONL before committing
# bd (beads) pre-commit hook
#
# Install: cp examples/git-hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
# This hook ensures that any pending bd issue changes are flushed to
# .beads/issues.jsonl before the commit is created, preventing the
# race condition where daemon auto-flush fires after the commit.
#
# Installation:
# cp examples/git-hooks/pre-commit .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
#
# Or use the install script:
# examples/git-hooks/install.sh
set -e
# Check if bd is installed
if ! command -v bd &> /dev/null; then
echo "Warning: bd not found in PATH, skipping export"
# Check if bd is available
if ! command -v bd >/dev/null 2>&1; then
echo "Warning: bd command not found, skipping pre-commit flush" >&2
exit 0
fi
# Check if .beads directory exists
if [[ ! -d .beads ]]; then
# No beads database, nothing to do
# Check if we're in a bd workspace
if [ ! -d .beads ]; then
# Not a bd workspace, nothing to do
exit 0
fi
# Export issues to JSONL
echo "🔗 Exporting beads issues to JSONL..."
# Flush pending changes to JSONL
# Use --flush-only to skip git operations (we're already in a git hook)
# Suppress output unless there's an error
if ! bd sync --flush-only >/dev/null 2>&1; then
echo "Error: Failed to flush bd changes to JSONL" >&2
echo "Run 'bd sync --flush-only' manually to diagnose" >&2
exit 1
fi
if bd export --format=jsonl -o .beads/issues.jsonl 2>/dev/null; then
# Add the JSONL file to the commit
git add .beads/issues.jsonl
echo "✓ Beads issues exported and staged"
else
echo "Warning: bd export failed, continuing anyway"
# If the JSONL file was modified, stage it
if [ -f .beads/issues.jsonl ]; then
git add .beads/issues.jsonl 2>/dev/null || true
fi
exit 0