Merge branch 'main' of github.com:steveyegge/beads

This commit is contained in:
Steve Yegge
2025-11-06 19:16:48 -08:00
10 changed files with 251 additions and 387 deletions

24
.beads/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# SQLite databases
*.db
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
# Legacy database files
db.sqlite
bd.db
# Keep JSONL exports and config (source of truth for git)
!*.jsonl
!metadata.json
!config.json
# Exclude merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.left.jsonl

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -95,10 +95,18 @@ Output to stdout by default, or use -o flag for file output.`,
os.Exit(1)
}
// Export command doesn't work with daemon - need direct access
// Export command requires direct database access for consistent snapshot
// If daemon is connected, close it and open direct connection
if daemonClient != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: export command forcing direct mode (closes daemon connection)\n")
}
_ = daemonClient.Close()
daemonClient = nil
}
// Ensure we have a direct store connection
if store == nil {
// Initialize store directly even if daemon is running
var err error
if dbPath == "" {
fmt.Fprintf(os.Stderr, "Error: no database path found\n")

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
@@ -25,8 +26,30 @@ Behavior:
- New issues are created
- Collisions (same ID, different content) are detected and reported
- Use --dedupe-after to find and merge content duplicates after import
- Use --dry-run to preview changes without applying them`,
- Use --dry-run to preview changes without applying them
NOTE: Import requires direct database access and does not work with daemon mode.
The command automatically uses --no-daemon when executed.`,
Run: func(cmd *cobra.Command, args []string) {
// Import requires direct database access due to complex transaction handling
// and collision detection. Force direct mode regardless of daemon state.
if daemonClient != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: import command forcing direct mode (closes daemon connection)\n")
}
_ = daemonClient.Close()
daemonClient = nil
// Now initialize direct store
var err error
store, err = sqlite.New(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
os.Exit(1)
}
defer func() { _ = store.Close() }()
}
input, _ := cmd.Flags().GetString("input")
skipUpdate, _ := cmd.Flags().GetBool("skip-existing")
strict, _ := cmd.Flags().GetBool("strict")

View File

@@ -326,21 +326,29 @@ func replaceIDReferences(text string, mapping map[string]string) string {
// isHashID checks if an ID is hash-based (not sequential)
func isHashID(id string) bool {
// Hash IDs contain hex characters after the prefix
// Sequential IDs are only digits
// Hash IDs contain hex letters (a-f), sequential IDs are only digits
// May have hierarchical suffix like .1 or .1.2
parts := strings.SplitN(id, "-", 2)
if len(parts) != 2 {
return false
}
// Check if the suffix starts with a hex digit (a-f)
suffix := parts[1]
if len(suffix) == 0 {
// Strip hierarchical suffix like .1 or .1.2
baseSuffix := strings.Split(suffix, ".")[0]
if len(baseSuffix) == 0 {
return false
}
// If it contains any letter a-f, it's a hash ID
return regexp.MustCompile(`[a-f]`).MatchString(suffix)
// Must be valid hex
if !regexp.MustCompile(`^[0-9a-f]+$`).MatchString(baseSuffix) {
return false
}
// If it contains letters a-f, it's a hash ID
// Sequential IDs are only digits 0-9
return regexp.MustCompile(`[a-f]`).MatchString(baseSuffix)
}
// saveMappingFile saves the ID mapping to a JSON file

View File

@@ -1,21 +1,29 @@
# bd Git Hooks
This directory contains git hooks that integrate bd (beads) with your git workflow, solving the race condition between daemon auto-flush and git commits.
This directory contains git hooks that integrate bd (beads) with your git workflow, preventing stale JSONL from being pushed to remote.
## The Problem
When using bd in daemon mode, operations trigger a 5-second debounced auto-flush to JSONL. This creates a race condition:
Two race conditions can occur:
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
1. **Between operations and commits**: Daemon auto-flush (5s debounce) may fire after commit
- User closes issue via MCP → daemon schedules flush (5 sec delay)
- User commits code changes → JSONL appears clean
- Daemon flush fires → JSONL modified after commit
- Result: dirty working tree showing JSONL changes
2. **Between commits and pushes**: Changes made after commit but before push (bd-my64)
- User commits → pre-commit hook flushes JSONL
- User adds comments or updates issues
- User pushes → outdated JSONL is pushed
- Result: remote has stale JSONL
## The Solution
These git hooks ensure bd changes are always synchronized with your commits:
These git hooks ensure bd changes are always synchronized with your commits and pushes:
- **pre-commit** - Flushes pending bd changes to JSONL before commit
- **pre-commit** - Flushes pending bd changes to JSONL before commit and stages it
- **pre-push** - Blocks push if JSONL has uncommitted changes (bd-my64)
- **post-merge** - Imports updated JSONL after git pull/merge
## Installation
@@ -37,8 +45,9 @@ This will:
```bash
cp examples/git-hooks/pre-commit .git/hooks/pre-commit
cp examples/git-hooks/pre-push .git/hooks/pre-push
cp examples/git-hooks/post-merge .git/hooks/post-merge
chmod +x .git/hooks/pre-commit .git/hooks/post-merge
chmod +x .git/hooks/pre-commit .git/hooks/pre-push .git/hooks/post-merge
```
## How It Works
@@ -58,16 +67,33 @@ This:
The hook is silent on success, fast (no git operations), and safe (fails commit if flush fails).
### pre-push
Before each push, the hook:
```bash
bd sync --flush-only # Flush pending changes (if bd available)
git status --porcelain .beads/*.jsonl # Check for uncommitted changes
```
This prevents pushing stale JSONL by:
1. Flushing pending in-memory changes from daemon's 5s debounce
2. Checking for uncommitted changes (staged, unstaged, untracked, deleted)
3. Failing the push with clear error message if changes exist
4. Instructing user to commit JSONL before pushing again
This solves bd-my64: changes made between commit and push (or pending debounced flushes) are caught before reaching remote.
### post-merge
After a git pull or merge, the hook runs:
```bash
bd import -i .beads/issues.jsonl
bd import -i .beads/beads.jsonl
```
This ensures your local database reflects the merged state. The hook:
- Only runs if `.beads/issues.jsonl` exists
- Only runs if `.beads/beads.jsonl` exists (also checks `issues.jsonl` for backward compat)
- Imports any new issues or updates from the merge
- Warns on failure but doesn't block the merge
@@ -92,7 +118,7 @@ This ensures your local database reflects the merged state. The hook:
Remove the hooks:
```bash
rm .git/hooks/pre-commit .git/hooks/post-merge
rm .git/hooks/pre-commit .git/hooks/pre-push .git/hooks/post-merge
```
Your backed-up hooks (if any) are in `.git/hooks/*.backup-*`.

View File

@@ -59,7 +59,7 @@ echo "✓ Git hooks installed successfully"
echo ""
echo "Hooks installed:"
echo " pre-commit - Flushes pending bd changes to JSONL before commit"
echo " pre-push - Blocks push if JSONL has uncommitted changes (bd-my64)"
echo " post-merge - Imports updated JSONL after git pull/merge"
echo " pre-push - Exports database to JSONL before push (prevents stale JSONL)"
echo ""
echo "To uninstall, remove the hooks from .git/hooks/"

View File

@@ -4,7 +4,7 @@
# bd (beads) pre-commit hook
#
# This hook ensures that any pending bd issue changes are flushed to
# .beads/issues.jsonl before the commit is created, preventing the
# .beads/beads.jsonl before the commit is created, preventing the
# race condition where daemon auto-flush fires after the commit.
#
# Installation:
@@ -35,9 +35,10 @@ if ! bd sync --flush-only >/dev/null 2>&1; then
exit 1
fi
# If the JSONL file was modified, stage it
if [ -f .beads/issues.jsonl ]; then
git add .beads/issues.jsonl 2>/dev/null || true
fi
# Stage both possible JSONL files (backward compatibility)
# git add is harmless if file doesn't exist
for f in .beads/beads.jsonl .beads/issues.jsonl; do
[ -f "$f" ] && git add "$f" 2>/dev/null || true
done
exit 0

View File

@@ -3,9 +3,14 @@
#
# bd (beads) pre-push hook
#
# This hook ensures that the database is exported to JSONL before pushing,
# preventing the problem where database changes are committed without
# corresponding JSONL updates.
# This hook prevents pushing stale JSONL by:
# 1. Flushing any pending in-memory changes to JSONL (if bd available)
# 2. Checking for uncommitted changes (staged, unstaged, untracked, deleted)
# 3. Failing the push with clear instructions if changes found
#
# The pre-commit hook already exports changes, but this catches:
# - Changes made between commit and push
# - Pending debounced flushes (5s daemon delay)
#
# Installation:
# cp examples/git-hooks/pre-push .git/hooks/pre-push
@@ -14,38 +19,43 @@
# Or use the install script:
# examples/git-hooks/install.sh
# Check if bd is available
if ! command -v bd >/dev/null 2>&1; then
echo "Warning: bd command not found, skipping pre-push export check" >&2
exit 0
fi
# Check if we're in a bd workspace
if [ ! -d .beads ]; then
# Not a bd workspace, nothing to do
exit 0
fi
# Check if database is newer than JSONL
DB_FILE=".beads/beads.db"
JSONL_FILE=".beads/beads.jsonl"
# Optionally flush pending bd changes so they surface in JSONL
# This prevents the race where a debounced flush lands after the check
if command -v bd >/dev/null 2>&1; then
bd sync --flush-only >/dev/null 2>&1 || true
fi
if [ -f "$DB_FILE" ] && [ -f "$JSONL_FILE" ]; then
# Get modification times
if [ "$DB_FILE" -nt "$JSONL_FILE" ]; then
echo "⚠️ Database is newer than JSONL - exporting before push..." >&2
# Force export to ensure JSONL is up to date
if ! BEADS_NO_DAEMON=1 bd export --output "$JSONL_FILE" >/dev/null 2>&1; then
echo "Error: Failed to export database to JSONL" >&2
echo "Run 'bd export' manually to diagnose" >&2
exit 1
fi
# Stage the updated JSONL
git add "$JSONL_FILE" 2>/dev/null || true
echo "✓ Exported database to JSONL" >&2
# Collect all tracked or existing JSONL files (supports both old and new names)
FILES=""
for f in .beads/beads.jsonl .beads/issues.jsonl; do
# Include file if it exists in working tree OR is tracked by git (even if deleted)
if git ls-files --error-unmatch "$f" >/dev/null 2>&1 || [ -f "$f" ]; then
FILES="$FILES $f"
fi
done
# Check for any uncommitted changes using porcelain status
# This catches: staged, unstaged, untracked, deleted, renamed, and conflicts
if [ -n "$FILES" ]; then
# shellcheck disable=SC2086
if [ -n "$(git status --porcelain -- $FILES 2>/dev/null)" ]; then
echo "❌ Error: Beads JSONL has uncommitted changes" >&2
echo "" >&2
echo "You made changes to bd issues between your last commit and this push." >&2
echo "Please commit the updated JSONL before pushing:" >&2
echo "" >&2
# shellcheck disable=SC2086
echo " git add $FILES" >&2
echo ' git commit -m "Update bd JSONL"' >&2
echo " git push" >&2
echo "" >&2
exit 1
fi
fi