From 2f83815ddddb96bfbf5ed9c7aa27cb924a73b85c Mon Sep 17 00:00:00 2001 From: Straffern <14233825+Straffern@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:38:56 +0100 Subject: [PATCH 01/13] Update vendorHash for Go module dependencies (#313) --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 5e161558..5da069c0 100644 --- a/default.nix +++ b/default.nix @@ -9,7 +9,7 @@ pkgs.buildGoModule { subPackages = [ "cmd/bd" ]; doCheck = false; # Go module dependencies hash (computed via nix build) - vendorHash = "sha256-eUwVXAe9d/e3OWEav61W8lI0bf/IIQYUol8QUiQiBbo="; + vendorHash = "sha256-jpaeKw5dbZuhV9Z18aQ9tDMS/Eo7HaXiZefm26UlPyI="; # Git is required for tests nativeBuildInputs = [ pkgs.git ]; From 92f3af56c5b40c0070f2eb23c8c266b4a1f79096 Mon Sep 17 00:00:00 2001 From: Peter Loron Date: Sat, 15 Nov 2025 11:53:38 -0800 Subject: [PATCH 02/13] fix: Improve missing git hook message (#306) Install hooks with 'bd hooks install' message added to bd doctor output. Co-authored-by: Peter Loron --- cmd/bd/doctor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index 51793984..f08d7307 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -1202,7 +1202,7 @@ func checkGitHooks(path string) doctorCheck { } } - hookInstallMsg := "See https://github.com/steveyegge/beads/tree/main/examples/git-hooks for installation instructions" + hookInstallMsg := "Install hooks with 'bd hooks install'. See https://github.com/steveyegge/beads/tree/main/examples/git-hooks for installation instructions" if len(installedHooks) > 0 { return doctorCheck{ From bd6dca507d4c67f3e93a7d453af15e0dc8cb587c Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 11:55:15 -0800 Subject: [PATCH 03/13] Remove orphaned TestFormatDependencyType test The formatDependencyType function was removed in commit 57b6ea6 when the dependency display UI was simplified. The test was left behind and is now failing CI. This completes the cleanup that was partially done in PR #309. --- cmd/bd/show_test.go | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 cmd/bd/show_test.go diff --git a/cmd/bd/show_test.go b/cmd/bd/show_test.go deleted file mode 100644 index 6c4cdaa0..00000000 --- a/cmd/bd/show_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "testing" - - "github.com/steveyegge/beads/internal/types" -) - -func TestFormatDependencyType(t *testing.T) { - tests := []struct { - name string - depType types.DependencyType - expected string - }{ - {"blocks", types.DepBlocks, "blocks"}, - {"related", types.DepRelated, "related"}, - {"parent-child", types.DepParentChild, "parent-child"}, - {"discovered-from", types.DepDiscoveredFrom, "discovered-from"}, - {"unknown", types.DependencyType("unknown"), "unknown"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := formatDependencyType(tt.depType) - if result != tt.expected { - t.Errorf("formatDependencyType(%v) = %v, want %v", tt.depType, result, tt.expected) - } - }) - } -} From 944ed1033d3f534e36f8f298fe204d759624151e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 12:42:02 -0800 Subject: [PATCH 04/13] Fix bd-yvlc: in-memory database deadlock in migrations - Force single connection for all in-memory databases (including file::memory:) - Close rows before executing statements in external_ref migration - Prevents connection pool deadlock with MaxOpenConns(1) - Fixes test failures in syncbranch_test.go --- .beads/beads.jsonl | 2 ++ .../sqlite/migrations/002_external_ref_column.go | 6 +++++- internal/storage/sqlite/sqlite.go | 11 +++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 455d7dac..1dd39a5f 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -334,6 +334,7 @@ {"id":"bd-d6aq","content_hash":"45a169a72749bb3bc93190bb3e5891950409f264baeac4394cd1a3ad5a75c0f2","title":"Test reservation expiration and renewal","description":"Verify TTL-based reservation expiration works correctly.\n\nAcceptance Criteria:\n- Reserve with short TTL (30s)\n- Verify other agents can't claim\n- Wait for expiration\n- Verify reservation auto-released\n- Other agent can now claim\n- Test renewal/heartbeat mechanism\n\nFile: tests/integration/test_reservation_ttl.py","notes":"Implemented comprehensive TTL/expiration test suite in tests/integration/test_reservation_ttl.py\n\nTest Coverage:\n✅ Short TTL reservations (30s) - verifies TTL is properly set\n✅ Reservation blocking - confirms agent2 cannot claim while agent1 holds reservation\n✅ Auto-release after expiration - validates expired reservations are auto-cleaned and become available\n✅ Renewal/heartbeat - tests that re-reserving extends expiration time\n\nAll 4 tests passing in 56.9s total (including 30s+ wait time for expiration tests).\n\nMock server implements full TTL management:\n- Reservation class with expiration tracking\n- Auto-cleanup of expired reservations on each request\n- Renewal support (same agent re-reserving)\n- 409 conflict for cross-agent reservation attempts","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-07T22:43:21.547821-08:00","updated_at":"2025-11-08T03:54:04.855132-08:00","closed_at":"2025-11-08T02:24:30.296982-08:00","source_repo":".","dependencies":[{"issue_id":"bd-d6aq","depends_on_id":"bd-m9th","type":"blocks","created_at":"2025-11-07T22:43:21.548731-08:00","created_by":"daemon"}]} {"id":"bd-d76d","content_hash":"b65da5fe9f89a98f1e6fad6ee32d463126ef72785fec4d6dfa5a4774c6a8a393","title":"Modify EnsureIDs to support parent resurrection","description":"Update internal/storage/sqlite/ids.go:189-202 to call TryResurrectParent before failing on missing parent. Add resurrection mode flag, log resurrected parents for transparency. Maintain backwards compatibility with strict validation mode.","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-04T12:31:59.659507-08:00","updated_at":"2025-11-05T00:08:42.814463-08:00","closed_at":"2025-11-05T00:08:42.814466-08:00","source_repo":"."} {"id":"bd-d7e88238","content_hash":"ff14f04a04bf89f52bda3d584933df6b09b554cce8665f47f429f1ac52dafb94","title":"Rapid 3","description":"","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-29T19:11:57.459655-07:00","updated_at":"2025-11-08T01:49:23.462353-08:00","closed_at":"2025-11-07T23:18:52.333825-08:00","source_repo":"."} +{"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"open","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:24:34.50322-08:00","source_repo":"."} {"id":"bd-d9e0","content_hash":"de4e01414f8863b63cb693a709048b85c3f4417f03e7d7b2528560076be0e1f7","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-02T12:32:00.159298-08:00","closed_at":"2025-11-02T12:32:00.1593-08:00","source_repo":"."} {"id":"bd-dcd6f14b","content_hash":"c07a4b8a39e6e81513278ee335fe14aa767cbcba72e3b511cfd95705053483b1","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00","source_repo":"."} {"id":"bd-de0h","content_hash":"8b8b43683607e73012cf8bd7cf8631c6ae34498d0c93ca5b77d3f68944c8088d","title":"bd message: Add HTTP client timeout to prevent hangs","description":"HTTP client in `sendAgentMailRequest` uses default http.Post with no timeout.\n\n**Location:** cmd/bd/message.go:181\n\n**Problem:**\n- Can hang indefinitely if server is unresponsive\n- No way to cancel stuck requests\n- Poor UX in flaky networks\n\n**Fix:**\n```go\nclient := \u0026http.Client{Timeout: 30 * time.Second}\nresp, err := client.Post(url, \"application/json\", bytes.NewReader(reqBody))\n```\n\n**Impact:** Production reliability and security issue","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T12:54:24.942645-08:00","updated_at":"2025-11-08T12:56:59.948929-08:00","closed_at":"2025-11-08T12:56:59.948929-08:00","source_repo":".","dependencies":[{"issue_id":"bd-de0h","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:54.860847-08:00","created_by":"daemon"}]} @@ -490,6 +491,7 @@ {"id":"bd-xzrv","content_hash":"45b45aaa47b9fc254ce74750b92f5527862672d9826c7ad59e006bdb1bc9939f","title":"Write Agent Mail integration guide","description":"Comprehensive guide for setting up and using Agent Mail with Beads.\n\nAcceptance Criteria:\n- Installation instructions\n- Configuration (environment variables)\n- Architecture diagram\n- Benefits and tradeoffs\n- When to use vs not use\n- Troubleshooting section\n- Migration from git-only mode\n\nFile: docs/AGENT_MAIL.md\n\nSections:\n- Quick start\n- How it works\n- Integration points\n- Graceful degradation\n- Multi-machine deployment\n- FAQ","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-07T22:42:51.231066-08:00","updated_at":"2025-11-08T01:51:40.352442-08:00","closed_at":"2025-11-08T00:40:38.798162-08:00","source_repo":".","dependencies":[{"issue_id":"bd-xzrv","depends_on_id":"bd-fzbg","type":"blocks","created_at":"2025-11-07T22:42:51.232246-08:00","created_by":"daemon"}]} {"id":"bd-yek6","content_hash":"f155913af8c58c0a7ea3da6a7d9e232e8cb29c3825f2d6f272a5417a449692a9","title":"CLI tests (cli_fast_test.go) are slow and should be integration tests","description":"The TestCLI_* tests in cmd/bd/cli_fast_test.go are taking 4-5 seconds each (40+ seconds total), making them the slowest part of the fast test suite.\n\nCurrent timings:\n- TestCLI_Import: 4.73s\n- TestCLI_Blocked: 4.33s \n- TestCLI_DepTree: 4.15s\n- TestCLI_Close: 3.59s\n- TestCLI_DepAdd: 3.50s\n- etc.\n\nThese tests compile the bd binary once in init(), but then execute it multiple times per test with filesystem operations. Despite being named \"fast\", they're actually end-to-end CLI integration tests.\n\nOptions:\n1. Tag with //go:build integration (move to integration suite)\n2. Optimize: Use in-memory databases, reduce exec calls, better parallelization\n3. Keep as-is but understand they're the baseline for \"fast\" tests\n\nTotal test suite currently: 13.8s (cmd/bd alone is 12.8s, and most of that is these CLI tests)","notes":"Fixed by reusing existing bd binary from repo root instead of rebuilding.\n\nBefore: 15+ minutes (rebuilding binary for every test package)\nAfter: ~12 seconds (reuses pre-built binary)\n\nThe init() function now checks for ../../bd first before falling back to building. This means `go build \u0026\u0026 go test` is now fast.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T20:19:12.822543-08:00","updated_at":"2025-11-05T20:31:19.321787-08:00","closed_at":"2025-11-05T20:31:19.321787-08:00","source_repo":"."} {"id":"bd-yuf7","content_hash":"97e18d89914d698df5ec673d40ff980a87a29e1435a887ec2b5dd77d7d412a79","title":"bd config set succeeds but doesn't persist to config.toml","description":"Commands like `bd config set daemon.auto_push true` return \"Set daemon.auto_push = true\" but the config file is never created and `bd info --json | jq '.config'` returns null.\n\n**Steps to reproduce:**\n1. Run `bd config set daemon.auto_push true`\n2. See success message: \"Set daemon.auto_push = true\"\n3. Check `cat .beads/config.toml` → file doesn't exist\n4. Check `bd info --json | jq '.config'` → returns null\n\n**Expected:**\n- .beads/config.toml should be created with the setting\n- bd info should show the config value\n\n**Impact:**\nUsers can't enable auto-push/auto-commit via CLI as documented in AGENTS.md","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-08T01:14:58.726198-08:00","updated_at":"2025-11-08T01:17:41.377912-08:00","closed_at":"2025-11-08T01:17:41.377912-08:00","source_repo":"."} +{"id":"bd-yvlc","content_hash":"f121c6b2674a550c12f99331eb6a45a4817bd4161ea377104c212f56c0589af5","title":"URGENT: main branch has failing tests (syncbranch migration error)","description":"The main branch has failing tests that are blocking CI for all PRs.\n\n## Problem\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\n## Evidence\n- Last 5 CI runs on main: ALL FAILED\n- Tests fail locally on current main (bd6dca5)\n- Affects: TestGet, TestSet, TestUnset in internal/syncbranch\n\n## Impact\n- Blocking all PR merges\n- CI shows red for all branches\n- Can't trust test results\n\n## Root Cause\nMigration order issue - trying to create index on external_ref column before the issues table exists, or before the external_ref column is added to the issues table.\n\n## Quick Fix Needed\nNeed to investigate migration order in internal/storage/sqlite/migrations.go and ensure:\n1. issues table is created first\n2. external_ref column is added to issues table\n3. THEN index on external_ref is created\n\nThis is CRITICAL - main should never have breaking tests.","design":"Investigation steps:\n1. Check internal/storage/sqlite/migrations.go\n2. Verify migration order and dependencies\n3. Look at external_ref_column migration specifically\n4. Ensure proper table/column creation before index\n5. Test fix locally with fresh database\n6. Push fix to main\n7. Verify CI passes","acceptance_criteria":"- All tests pass on main branch\n- CI is green\n- syncbranch_test.go tests all passing","status":"open","priority":0,"issue_type":"bug","created_at":"2025-11-15T12:25:31.51688-08:00","updated_at":"2025-11-15T12:25:31.51688-08:00","source_repo":"."} {"id":"bd-z0yn","content_hash":"1bb2f4940363e921d71f45e202cbadc1d90c4985ce5a048bb97d352f0a3ad9d0","title":"Channel isolation test - beads","description":"","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-11-08T04:21:17.327983-08:00","updated_at":"2025-11-08T04:21:17.365854-08:00","source_repo":"."} {"id":"bd-z3s3","content_hash":"24d99dc1a9a5f35af962137f5709d4b0f1b6a9ec91511c30a2517d790640cce8","title":"Create deployment scripts for GCP","description":"Automated provisioning scripts for GCP Compute Engine deployment.\n\nAcceptance Criteria:\n- Terraform/gcloud scripts\n- Static IP allocation\n- Firewall rules\n- NGINX reverse proxy config\n- TLS setup (Let's Encrypt)\n- Systemd service file\n\nFile: deployment/agent-mail/gcp/","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-07T22:43:43.294839-08:00","updated_at":"2025-11-07T22:43:43.294839-08:00","source_repo":".","dependencies":[{"issue_id":"bd-z3s3","depends_on_id":"bd-9li4","type":"blocks","created_at":"2025-11-07T23:04:27.982336-08:00","created_by":"daemon"}]} {"id":"bd-z528","content_hash":"3f332e9997d2b7eb0af23885820df5f607fe08671a2615cadec941bbe7d36f68","title":"Prevent test pollution in production database","description":"The bd-vxdr cleanup revealed test issues were created during manual testing in the production workspace (Nov 2-4, template feature development).\n\n**Root cause:** Manual testing with `./bd create \"Test issue\"` pollutes the production .beads database.\n\n**Prevention strategies:**\n1. Use TEST_DB environment variable for manual testing\n2. Add warning when creating issues with \"Test\" prefix\n3. Improve developer docs about testing workflow\n4. Consider adding `bd test-mode` command for isolated testing","notes":"**Implementation completed:**\n\n1. ✅ Added warning when creating issues with \"Test\" prefix in production database\n - Shows yellow warning with ⚠ symbol\n - Suggests using BEADS_DB for isolated testing\n - Warning appears in create.go after title validation\n\n2. ✅ Documented BEADS_DB testing workflow in AGENTS.md\n - Added \"Testing Workflow\" section in Development Guidelines\n - Includes manual testing examples with BEADS_DB\n - Includes automated testing examples with t.TempDir()\n - Clear warning about not polluting production database\n\n3. ⚠️ Decided against bd test-mode command\n - BEADS_DB already provides simple, flexible isolation\n - Additional command would add complexity without much benefit\n - Current approach follows Unix philosophy (env vars for config)\n\n**Files modified:**\n- cmd/bd/create.go - Added Test prefix warning\n- AGENTS.md - Added Testing Workflow section\n\n**Testing:**\n- Verified warning appears when creating \"Test\" prefix issues\n- Verified BEADS_DB isolation works correctly\n- Built successfully with `go build`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-07T16:07:28.255289-08:00","updated_at":"2025-11-08T01:49:23.463399-08:00","closed_at":"2025-11-07T22:43:28.669908-08:00","source_repo":"."} diff --git a/internal/storage/sqlite/migrations/002_external_ref_column.go b/internal/storage/sqlite/migrations/002_external_ref_column.go index fea879e6..d28833ff 100644 --- a/internal/storage/sqlite/migrations/002_external_ref_column.go +++ b/internal/storage/sqlite/migrations/002_external_ref_column.go @@ -11,7 +11,6 @@ func MigrateExternalRefColumn(db *sql.DB) error { if err != nil { return fmt.Errorf("failed to check schema: %w", err) } - defer func() { _ = rows.Close() }() for rows.Next() { var cid int @@ -20,6 +19,7 @@ func MigrateExternalRefColumn(db *sql.DB) error { var dflt *string err := rows.Scan(&cid, &name, &typ, ¬null, &dflt, &pk) if err != nil { + rows.Close() return fmt.Errorf("failed to scan column info: %w", err) } if name == "external_ref" { @@ -29,9 +29,13 @@ func MigrateExternalRefColumn(db *sql.DB) error { } if err := rows.Err(); err != nil { + rows.Close() return fmt.Errorf("error reading column info: %w", err) } + // Close rows before executing any statements to avoid deadlock with MaxOpenConns(1) + rows.Close() + if !columnExists { _, err := db.Exec(`ALTER TABLE issues ADD COLUMN external_ref TEXT`) if err != nil { diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index a56132af..790cdcdf 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -56,11 +56,14 @@ func New(path string) (*SQLiteStorage, error) { return nil, fmt.Errorf("failed to open database: %w", err) } - // For :memory: databases, force single connection to ensure cache sharing works properly. - // SQLite's shared cache mode for in-memory databases only works reliably with one connection. - // Without this, different connections in the pool can't see each other's writes (bd-b121). - if path == ":memory:" { + // For all in-memory databases (including file::memory:), force single connection. + // SQLite's in-memory databases are isolated per connection by default. + // Without this, different connections in the pool can't see each other's writes (bd-b121, bd-yvlc). + isInMemory := path == ":memory:" || + (strings.HasPrefix(path, "file:") && strings.Contains(path, "mode=memory")) + if isInMemory { db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) } // Test connection From 690c73fc317af8237f56a617eea4804550d7b0b6 Mon Sep 17 00:00:00 2001 From: Ryan <2199132+rsnodgrass@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:46:13 -0800 Subject: [PATCH 05/13] Performance Improvements (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add performance testing framework foundation Implements foundation for comprehensive performance testing and user diagnostics for beads databases at 10K-20K scale. Components added: - Fixture generator (internal/testutil/fixtures/) for realistic test data * LargeSQLite/XLargeSQLite: 10K/20K issues with epic hierarchies * LargeFromJSONL/XLargeFromJSONL: test JSONL import path * Realistic cross-linked dependencies, labels, assignees * Reproducible with seeded RNG - User diagnostics (bd doctor --perf) for field performance data * Collects platform info (OS, arch, Go/SQLite versions) * Measures key operation timings (ready, list, show, search) * Generates CPU profiles for bug reports * Clean separation in cmd/bd/doctor/perf.go Test data characteristics: - 10% epics, 30% features, 60% tasks - 4-level hierarchies (Epic → Feature → Task → Subtask) - 20% cross-epic blocking dependencies - Realistic status/priority/label distributions Supports bd-l954 (Performance Testing Framework epic) Closes bd-6ed8, bd-q59i * perf: optimize GetReadyWork with compound index (20x speedup) Add compound index on dependencies(depends_on_id, type, issue_id) to eliminate performance bottleneck in GetReadyWork recursive CTE query. Performance improvements (10K issue database): - GetReadyWork: 752ms → 36.6ms (20.5x faster) - Target: <50ms ✓ ACHIEVED - 20K database: ~1500ms → 79.4ms (19x faster) Benchmark infrastructure enhancements: - Add dataset caching in /tmp/beads-bench-cache/ to avoid regenerating 10K-20K issues on every benchmark run (first run: ~2min, subsequent: <5s) - Add progress logging during fixture generation (shows 10%, 20%... completion) - Add database size logging (17.5 MB for 10K, 35.1 MB for 20K) - Document rationale for only benchmarking large datasets (>10K issues) - Add CPU/trace profiling with --profile flag for performance debugging Schema changes: - internal/storage/sqlite/schema.go: Add idx_dependencies_depends_on_type_issue New files: - internal/storage/sqlite/bench_helpers_test.go: Reusable benchmark setup with caching - internal/storage/sqlite/sqlite_bench_test.go: Comprehensive benchmarks for critical operations - Makefile: Convenient benchmark execution (make bench-quick, make bench) Related: - Resolves bd-5qim (optimize GetReadyWork performance) - Builds on bd-6ed8 (fixture generator), bd-q59i (bd doctor --perf) * perf: add WASM compilation cache to eliminate cold-start overhead Configure wazero compilation cache for ncruces/go-sqlite3 to avoid ~220ms JIT compilation on every process start. Cache configuration: - Location: ~/.cache/beads/wasm/ (platform-specific via os.UserCacheDir) - Automatic version management: wazero keys entries by its version - Fallback: in-memory cache if directory creation fails - No cleanup needed: old versions are harmless (~5-10MB each) Performance impact: - First run: ~220ms (populate cache) - Subsequent runs: ~20ms (load from cache) - Savings: ~200ms per cold start Cache invalidation: - Automatic when wazero version changes (upgrades use new cache dir) - Manual cleanup: rm -rf ~/.cache/beads/wasm/ (safe to delete anytime) This complements daemon mode: - Daemon mode: eliminates startup cost by keeping process alive - WASM cache: reduces startup cost for one-off commands or daemon restarts Changes: - internal/storage/sqlite/sqlite.go: Add init() with cache setup * refactor: improve maintainability of performance testing code Extract common patterns and eliminate duplication across benchmarks, fixture generation, and performance diagnostics. Replace magic numbers with explicit configuration to improve readability and make it easier to tune test parameters. * docs: clarify profiling behavior and add missing documentation Add explanatory comments for profiling setup to clarify why --profile forces direct mode (captures actual database operations instead of RPC overhead) and document the stopCPUProfile function's role in flushing profile data to disk. Also fix gosec G104 linter warning by explicitly ignoring Close() error during cleanup. * fix: prevent bench-quick from running indefinitely Added //go:build bench tags and skipped timeout-prone benchmarks to prevent make bench-quick from running for hours. Changes: - Add //go:build bench tag to cycle_bench_test.go and compact_bench_test.go - Skip Dense graph benchmarks (documented to timeout >120s) - Fix compact benchmark prefix: bd- → bd (validation expects prefix without trailing dash) Before: make bench-quick ran for 3.5+ hours (12,699s) before manual interrupt After: make bench-quick completes in ~25 seconds The Dense graph benchmarks are known to timeout and represent rare edge cases that don't need optimization for typical workflows. --- .gitignore | 8 + Makefile | 57 ++ cmd/bd/doctor.go | 18 +- cmd/bd/doctor/perf.go | 276 +++++++++ cmd/bd/main.go | 25 + internal/storage/sqlite/bench_helpers_test.go | 245 ++++++++ internal/storage/sqlite/compact_bench_test.go | 5 + internal/storage/sqlite/cycle_bench_test.go | 4 + internal/storage/sqlite/schema.go | 1 + internal/storage/sqlite/sqlite.go | 49 ++ internal/storage/sqlite/sqlite_bench_test.go | 145 +++++ internal/testutil/fixtures/fixtures.go | 541 ++++++++++++++++++ internal/testutil/fixtures/fixtures_test.go | 128 +++++ 13 files changed, 1501 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 cmd/bd/doctor/perf.go create mode 100644 internal/storage/sqlite/bench_helpers_test.go create mode 100644 internal/storage/sqlite/sqlite_bench_test.go create mode 100644 internal/testutil/fixtures/fixtures.go create mode 100644 internal/testutil/fixtures/fixtures_test.go diff --git a/.gitignore b/.gitignore index 5cc872c4..24b80498 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,11 @@ __pycache__/ *.so .Python .envrc + +# Performance profiling files (benchmarks, bd doctor --perf, and bd --profile) +*.prof +*.out +beads-perf-*.prof +bench-cpu-*.prof +bd-profile-*.prof +bd-trace-*.out diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..489d6cf3 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +# Makefile for beads project + +.PHONY: all build test bench bench-quick clean install help + +# Default target +all: build + +# Build the bd binary +build: + @echo "Building bd..." + go build -o bd ./cmd/bd + +# Run all tests +test: + @echo "Running tests..." + go test ./... + +# Run performance benchmarks (10K and 20K issue databases with automatic CPU profiling) +# Generates CPU profile: internal/storage/sqlite/bench-cpu-.prof +# View flamegraph: go tool pprof -http=:8080 +bench: + @echo "Running performance benchmarks..." + @echo "This will generate 10K and 20K issue databases and profile all operations." + @echo "CPU profiles will be saved to internal/storage/sqlite/" + @echo "" + go test -bench=. -benchtime=1s -tags=bench -run=^$$ ./internal/storage/sqlite/ -timeout=30m + @echo "" + @echo "Benchmark complete. Profile files saved in internal/storage/sqlite/" + @echo "View flamegraph: cd internal/storage/sqlite && go tool pprof -http=:8080 bench-cpu-*.prof" + +# Run quick benchmarks (shorter benchtime for faster feedback) +bench-quick: + @echo "Running quick performance benchmarks..." + go test -bench=. -benchtime=100ms -tags=bench -run=^$$ ./internal/storage/sqlite/ -timeout=15m + +# Install bd to GOPATH/bin +install: build + @echo "Installing bd to $$(go env GOPATH)/bin..." + go install ./cmd/bd + +# Clean build artifacts and benchmark profiles +clean: + @echo "Cleaning..." + rm -f bd + rm -f internal/storage/sqlite/bench-cpu-*.prof + rm -f beads-perf-*.prof + +# Show help +help: + @echo "Beads Makefile targets:" + @echo " make build - Build the bd binary" + @echo " make test - Run all tests" + @echo " make bench - Run performance benchmarks (generates CPU profiles)" + @echo " make bench-quick - Run quick benchmarks (shorter benchtime)" + @echo " make install - Install bd to GOPATH/bin" + @echo " make clean - Remove build artifacts and profile files" + @echo " make help - Show this help message" diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index f08d7307..2b1194d3 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -46,6 +46,7 @@ type doctorResult struct { var ( doctorFix bool + perfMode bool ) var doctorCmd = &cobra.Command{ @@ -68,11 +69,19 @@ This command checks: - Git hooks (pre-commit, post-merge, pre-push) - .beads/.gitignore up to date +Performance Mode (--perf): + Run performance diagnostics on your database: + - Times key operations (bd ready, bd list, bd show, etc.) + - Collects system info (OS, arch, SQLite version, database stats) + - Generates CPU profile for analysis + - Outputs shareable report for bug reports + Examples: bd doctor # Check current directory bd doctor /path/to/repo # Check specific repository bd doctor --json # Machine-readable output - bd doctor --fix # Automatically fix issues`, + bd doctor --fix # Automatically fix issues + bd doctor --perf # Performance diagnostics`, Run: func(cmd *cobra.Command, args []string) { // Use global jsonOutput set by PersistentPreRun @@ -89,6 +98,12 @@ Examples: os.Exit(1) } + // Run performance diagnostics if --perf flag is set + if perfMode { + doctor.RunPerformanceDiagnostics(absPath) + return + } + // Run diagnostics result := runDiagnostics(absPath) @@ -1309,4 +1324,5 @@ func checkSchemaCompatibility(path string) doctorCheck { func init() { rootCmd.AddCommand(doctorCmd) + doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile") } diff --git a/cmd/bd/doctor/perf.go b/cmd/bd/doctor/perf.go new file mode 100644 index 00000000..b023029d --- /dev/null +++ b/cmd/bd/doctor/perf.go @@ -0,0 +1,276 @@ +package doctor + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "time" + + "github.com/steveyegge/beads/internal/beads" +) + +var cpuProfileFile *os.File + +// RunPerformanceDiagnostics runs performance diagnostics and generates a CPU profile +func RunPerformanceDiagnostics(path string) { + fmt.Println("\nBeads Performance Diagnostics") + fmt.Println(strings.Repeat("=", 50)) + + // Check if .beads directory exists + beadsDir := filepath.Join(path, ".beads") + if _, err := os.Stat(beadsDir); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Error: No .beads/ directory found at %s\n", path) + fmt.Fprintf(os.Stderr, "Run 'bd init' to initialize beads\n") + os.Exit(1) + } + + // Get database path + dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Error: No database found at %s\n", dbPath) + os.Exit(1) + } + + // Collect platform info + platformInfo := collectPlatformInfo(dbPath) + fmt.Printf("\nPlatform: %s\n", platformInfo["os_arch"]) + fmt.Printf("Go: %s\n", platformInfo["go_version"]) + fmt.Printf("SQLite: %s\n", platformInfo["sqlite_version"]) + + // Collect database stats + dbStats := collectDatabaseStats(dbPath) + fmt.Printf("\nDatabase Statistics:\n") + fmt.Printf(" Total issues: %s\n", dbStats["total_issues"]) + fmt.Printf(" Open issues: %s\n", dbStats["open_issues"]) + fmt.Printf(" Closed issues: %s\n", dbStats["closed_issues"]) + fmt.Printf(" Dependencies: %s\n", dbStats["dependencies"]) + fmt.Printf(" Labels: %s\n", dbStats["labels"]) + fmt.Printf(" Database size: %s\n", dbStats["db_size"]) + + // Start CPU profiling + profilePath := fmt.Sprintf("beads-perf-%s.prof", time.Now().Format("2006-01-02-150405")) + if err := startCPUProfile(profilePath); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to start CPU profiling: %v\n", err) + } else { + defer stopCPUProfile() + fmt.Printf("\nCPU profiling enabled: %s\n", profilePath) + } + + // Time key operations + fmt.Printf("\nOperation Performance:\n") + + // Measure GetReadyWork + readyDuration := measureOperation("bd ready", func() error { + return runReadyWork(dbPath) + }) + fmt.Printf(" bd ready %dms\n", readyDuration.Milliseconds()) + + // Measure SearchIssues (list open) + listDuration := measureOperation("bd list --status=open", func() error { + return runListOpen(dbPath) + }) + fmt.Printf(" bd list --status=open %dms\n", listDuration.Milliseconds()) + + // Measure GetIssue (show random issue) + showDuration := measureOperation("bd show ", func() error { + return runShowRandom(dbPath) + }) + if showDuration > 0 { + fmt.Printf(" bd show %dms\n", showDuration.Milliseconds()) + } + + // Measure SearchIssues with filters + searchDuration := measureOperation("bd list (complex filters)", func() error { + return runComplexSearch(dbPath) + }) + fmt.Printf(" bd list (complex filters) %dms\n", searchDuration.Milliseconds()) + + fmt.Printf("\nProfile saved: %s\n", profilePath) + fmt.Printf("Share this file with bug reports for performance issues.\n\n") + fmt.Printf("View flamegraph:\n") + fmt.Printf(" go tool pprof -http=:8080 %s\n\n", profilePath) +} + +func collectPlatformInfo(dbPath string) map[string]string { + info := make(map[string]string) + + // OS and architecture + info["os_arch"] = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) + + // Go version + info["go_version"] = runtime.Version() + + // SQLite version + db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro") + if err == nil { + defer db.Close() + var version string + if err := db.QueryRow("SELECT sqlite_version()").Scan(&version); err == nil { + info["sqlite_version"] = version + } else { + info["sqlite_version"] = "unknown" + } + } else { + info["sqlite_version"] = "unknown" + } + + return info +} + +func collectDatabaseStats(dbPath string) map[string]string { + stats := make(map[string]string) + + db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro") + if err != nil { + stats["total_issues"] = "error" + stats["open_issues"] = "error" + stats["closed_issues"] = "error" + stats["dependencies"] = "error" + stats["labels"] = "error" + stats["db_size"] = "error" + return stats + } + defer db.Close() + + // Total issues + var total int + if err := db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&total); err == nil { + stats["total_issues"] = fmt.Sprintf("%d", total) + } else { + stats["total_issues"] = "error" + } + + // Open issues + var open int + if err := db.QueryRow("SELECT COUNT(*) FROM issues WHERE status != 'closed'").Scan(&open); err == nil { + stats["open_issues"] = fmt.Sprintf("%d", open) + } else { + stats["open_issues"] = "error" + } + + // Closed issues + var closed int + if err := db.QueryRow("SELECT COUNT(*) FROM issues WHERE status = 'closed'").Scan(&closed); err == nil { + stats["closed_issues"] = fmt.Sprintf("%d", closed) + } else { + stats["closed_issues"] = "error" + } + + // Dependencies + var deps int + if err := db.QueryRow("SELECT COUNT(*) FROM dependencies").Scan(&deps); err == nil { + stats["dependencies"] = fmt.Sprintf("%d", deps) + } else { + stats["dependencies"] = "error" + } + + // Labels + var labels int + if err := db.QueryRow("SELECT COUNT(DISTINCT label) FROM labels").Scan(&labels); err == nil { + stats["labels"] = fmt.Sprintf("%d", labels) + } else { + stats["labels"] = "error" + } + + // Database file size + if info, err := os.Stat(dbPath); err == nil { + sizeMB := float64(info.Size()) / (1024 * 1024) + stats["db_size"] = fmt.Sprintf("%.2f MB", sizeMB) + } else { + stats["db_size"] = "error" + } + + return stats +} + +func startCPUProfile(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + cpuProfileFile = f + return pprof.StartCPUProfile(f) +} + +// stopCPUProfile stops CPU profiling and closes the profile file. +// Must be called after pprof.StartCPUProfile() to flush profile data to disk. +func stopCPUProfile() { + pprof.StopCPUProfile() + if cpuProfileFile != nil { + _ = cpuProfileFile.Close() // best effort cleanup + } +} + +func measureOperation(name string, op func() error) time.Duration { + start := time.Now() + if err := op(); err != nil { + return 0 + } + return time.Since(start) +} + +// runQuery executes a read-only database query and returns any error +func runQuery(dbPath string, queryFn func(*sql.DB) error) error { + db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro") + if err != nil { + return err + } + defer db.Close() + return queryFn(db) +} + +func runReadyWork(dbPath string) error { + return runQuery(dbPath, func(db *sql.DB) error { + // simplified ready work query (the real one is more complex) + _, err := db.Query(` + SELECT id FROM issues + WHERE status IN ('open', 'in_progress') + AND id NOT IN ( + SELECT issue_id FROM dependencies WHERE type = 'blocks' + ) + LIMIT 100 + `) + return err + }) +} + +func runListOpen(dbPath string) error { + return runQuery(dbPath, func(db *sql.DB) error { + _, err := db.Query("SELECT id, title, status FROM issues WHERE status != 'closed' LIMIT 100") + return err + }) +} + +func runShowRandom(dbPath string) error { + return runQuery(dbPath, func(db *sql.DB) error { + // get a random issue + var issueID string + if err := db.QueryRow("SELECT id FROM issues ORDER BY RANDOM() LIMIT 1").Scan(&issueID); err != nil { + return err + } + + // get issue details + _, err := db.Query("SELECT * FROM issues WHERE id = ?", issueID) + return err + }) +} + +func runComplexSearch(dbPath string) error { + return runQuery(dbPath, func(db *sql.DB) error { + // complex query with filters + _, err := db.Query(` + SELECT i.id, i.title, i.status, i.priority + FROM issues i + LEFT JOIN labels l ON i.id = l.issue_id + WHERE i.status IN ('open', 'in_progress') + AND i.priority <= 2 + GROUP BY i.id + LIMIT 100 + `) + return err + }) +} diff --git a/cmd/bd/main.go b/cmd/bd/main.go index 9d791b67..5d02f394 100644 --- a/cmd/bd/main.go +++ b/cmd/bd/main.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + "runtime/pprof" + "runtime/trace" "slices" "sync" "time" @@ -78,6 +80,9 @@ var ( noAutoImport bool sandboxMode bool noDb bool // Use --no-db mode: load from JSONL, write back after each command + profileEnabled bool + profileFile *os.File + traceFile *os.File ) func init() { @@ -95,6 +100,7 @@ func init() { rootCmd.PersistentFlags().BoolVar(&noAutoImport, "no-auto-import", false, "Disable automatic JSONL import when newer than DB") rootCmd.PersistentFlags().BoolVar(&sandboxMode, "sandbox", false, "Sandbox mode: disables daemon and auto-sync") rootCmd.PersistentFlags().BoolVar(&noDb, "no-db", false, "Use no-db mode: load from JSONL, no SQLite") + rootCmd.PersistentFlags().BoolVar(&profileEnabled, "profile", false, "Generate CPU profile for performance analysis") // Add --version flag to root command (same behavior as version subcommand) rootCmd.Flags().BoolP("version", "v", false, "Print version information") @@ -141,6 +147,23 @@ var rootCmd = &cobra.Command{ actor = config.GetString("actor") } + // Performance profiling setup + // When --profile is enabled, force direct mode to capture actual database operations + // rather than just RPC serialization/network overhead. This gives accurate profiles + // of the storage layer, query performance, and business logic. + if profileEnabled { + noDaemon = true + timestamp := time.Now().Format("20060102-150405") + if f, _ := os.Create(fmt.Sprintf("bd-profile-%s-%s.prof", cmd.Name(), timestamp)); f != nil { + profileFile = f + _ = pprof.StartCPUProfile(f) + } + if f, _ := os.Create(fmt.Sprintf("bd-trace-%s-%s.out", cmd.Name(), timestamp)); f != nil { + traceFile = f + _ = trace.Start(f) + } + } + // Skip database initialization for commands that don't need a database noDbCommands := []string{ cmdDaemon, @@ -505,6 +528,8 @@ var rootCmd = &cobra.Command{ if store != nil { _ = store.Close() } + if profileFile != nil { pprof.StopCPUProfile(); _ = profileFile.Close() } + if traceFile != nil { trace.Stop(); _ = traceFile.Close() } }, } diff --git a/internal/storage/sqlite/bench_helpers_test.go b/internal/storage/sqlite/bench_helpers_test.go new file mode 100644 index 00000000..1680b077 --- /dev/null +++ b/internal/storage/sqlite/bench_helpers_test.go @@ -0,0 +1,245 @@ +//go:build bench + +package sqlite + +import ( + "context" + "fmt" + "io" + "os" + "runtime/pprof" + "sync" + "testing" + "time" + + "github.com/steveyegge/beads/internal/storage" + "github.com/steveyegge/beads/internal/testutil/fixtures" +) + +var ( + profileOnce sync.Once + profileFile *os.File + benchCacheDir = "/tmp/beads-bench-cache" +) + +// startBenchmarkProfiling starts CPU profiling for the entire benchmark run. +// Uses sync.Once to ensure it only runs once per test process. +// The profile is saved to bench-cpu-.prof in the current directory. +func startBenchmarkProfiling(b *testing.B) { + b.Helper() + profileOnce.Do(func() { + profilePath := fmt.Sprintf("bench-cpu-%s.prof", time.Now().Format("2006-01-02-150405")) + f, err := os.Create(profilePath) + if err != nil { + b.Logf("Warning: failed to create CPU profile: %v", err) + return + } + profileFile = f + + if err := pprof.StartCPUProfile(f); err != nil { + b.Logf("Warning: failed to start CPU profiling: %v", err) + f.Close() + return + } + + b.Logf("CPU profiling enabled: %s", profilePath) + + // Register cleanup to stop profiling when all benchmarks complete + b.Cleanup(func() { + pprof.StopCPUProfile() + if profileFile != nil { + profileFile.Close() + b.Logf("CPU profile saved: %s", profilePath) + b.Logf("View flamegraph: go tool pprof -http=:8080 %s", profilePath) + } + }) + }) +} + +// Benchmark setup rationale: +// We only provide Large (10K) and XLarge (20K) setup functions because +// small databases don't exhibit the performance characteristics we need to optimize. +// See sqlite_bench_test.go for full rationale. +// +// Dataset caching: +// Datasets are cached in /tmp/beads-bench-cache/ to avoid regenerating 10K-20K +// issues on every benchmark run. Cached databases are ~10-30MB and reused across runs. + +// getCachedOrGenerateDB returns a cached database or generates it if missing. +// cacheKey should be unique per dataset type (e.g., "large", "xlarge"). +// generateFn is called only if the cached database doesn't exist. +func getCachedOrGenerateDB(b *testing.B, cacheKey string, generateFn func(context.Context, storage.Storage) error) string { + b.Helper() + + // Ensure cache directory exists + if err := os.MkdirAll(benchCacheDir, 0755); err != nil { + b.Fatalf("Failed to create benchmark cache directory: %v", err) + } + + dbPath := fmt.Sprintf("%s/%s.db", benchCacheDir, cacheKey) + + // Check if cached database exists + if stat, err := os.Stat(dbPath); err == nil { + sizeMB := float64(stat.Size()) / (1024 * 1024) + b.Logf("Using cached benchmark database: %s (%.1f MB)", dbPath, sizeMB) + return dbPath + } + + // Generate new database + b.Logf("===== Generating benchmark database: %s =====", dbPath) + b.Logf("This is a one-time operation that will be cached for future runs...") + b.Logf("Expected time: ~1-3 minutes for 10K issues, ~2-6 minutes for 20K issues") + + store, err := New(dbPath) + if err != nil { + b.Fatalf("Failed to create storage: %v", err) + } + + ctx := context.Background() + + // Initialize database with prefix + if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { + store.Close() + b.Fatalf("Failed to set issue_prefix: %v", err) + } + + // Generate dataset using provided function + if err := generateFn(ctx, store); err != nil { + store.Close() + os.Remove(dbPath) // cleanup partial database + b.Fatalf("Failed to generate dataset: %v", err) + } + + store.Close() + + // Log completion with final size + if stat, err := os.Stat(dbPath); err == nil { + sizeMB := float64(stat.Size()) / (1024 * 1024) + b.Logf("===== Database generation complete: %s (%.1f MB) =====", dbPath, sizeMB) + } + + return dbPath +} + +// copyFile copies a file from src to dst. +func copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + + return dstFile.Sync() +} + +// setupLargeBenchDB creates or reuses a cached 10K issue database. +// Returns configured storage instance and cleanup function. +// Uses //go:build bench tag to avoid running in normal tests. +// Automatically enables CPU profiling on first call. +// +// Note: Copies the cached database to a temp location for each benchmark +// to prevent mutations from affecting subsequent runs. +func setupLargeBenchDB(b *testing.B) (*SQLiteStorage, func()) { + b.Helper() + + // Start CPU profiling (only happens once per test run) + startBenchmarkProfiling(b) + + // Get or generate cached database + cachedPath := getCachedOrGenerateDB(b, "large", fixtures.LargeSQLite) + + // Copy to temp location to prevent mutations + tmpPath := b.TempDir() + "/large.db" + if err := copyFile(cachedPath, tmpPath); err != nil { + b.Fatalf("Failed to copy cached database: %v", err) + } + + // Open the temporary copy + store, err := New(tmpPath) + if err != nil { + b.Fatalf("Failed to open database: %v", err) + } + + return store, func() { + store.Close() + } +} + +// setupXLargeBenchDB creates or reuses a cached 20K issue database. +// Returns configured storage instance and cleanup function. +// Uses //go:build bench tag to avoid running in normal tests. +// Automatically enables CPU profiling on first call. +// +// Note: Copies the cached database to a temp location for each benchmark +// to prevent mutations from affecting subsequent runs. +func setupXLargeBenchDB(b *testing.B) (*SQLiteStorage, func()) { + b.Helper() + + // Start CPU profiling (only happens once per test run) + startBenchmarkProfiling(b) + + // Get or generate cached database + cachedPath := getCachedOrGenerateDB(b, "xlarge", fixtures.XLargeSQLite) + + // Copy to temp location to prevent mutations + tmpPath := b.TempDir() + "/xlarge.db" + if err := copyFile(cachedPath, tmpPath); err != nil { + b.Fatalf("Failed to copy cached database: %v", err) + } + + // Open the temporary copy + store, err := New(tmpPath) + if err != nil { + b.Fatalf("Failed to open database: %v", err) + } + + return store, func() { + store.Close() + } +} + +// setupLargeFromJSONL creates or reuses a cached 10K issue database via JSONL import path. +// Returns configured storage instance and cleanup function. +// Uses //go:build bench tag to avoid running in normal tests. +// Automatically enables CPU profiling on first call. +// +// Note: Copies the cached database to a temp location for each benchmark +// to prevent mutations from affecting subsequent runs. +func setupLargeFromJSONL(b *testing.B) (*SQLiteStorage, func()) { + b.Helper() + + // Start CPU profiling (only happens once per test run) + startBenchmarkProfiling(b) + + // Get or generate cached database with JSONL import path + cachedPath := getCachedOrGenerateDB(b, "large-jsonl", func(ctx context.Context, store storage.Storage) error { + tempDir := b.TempDir() + return fixtures.LargeFromJSONL(ctx, store, tempDir) + }) + + // Copy to temp location to prevent mutations + tmpPath := b.TempDir() + "/large-jsonl.db" + if err := copyFile(cachedPath, tmpPath); err != nil { + b.Fatalf("Failed to copy cached database: %v", err) + } + + // Open the temporary copy + store, err := New(tmpPath) + if err != nil { + b.Fatalf("Failed to open database: %v", err) + } + + return store, func() { + store.Close() + } +} diff --git a/internal/storage/sqlite/compact_bench_test.go b/internal/storage/sqlite/compact_bench_test.go index 2afbca29..f3ad9b63 100644 --- a/internal/storage/sqlite/compact_bench_test.go +++ b/internal/storage/sqlite/compact_bench_test.go @@ -1,3 +1,5 @@ +//go:build bench + package sqlite import ( @@ -124,6 +126,9 @@ func setupBenchDB(tb testing.TB) (*SQLiteStorage, func()) { } ctx := context.Background() + if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil { + tb.Fatalf("Failed to set issue_prefix: %v", err) + } if err := store.SetConfig(ctx, "compact_tier1_days", "30"); err != nil { tb.Fatalf("Failed to set config: %v", err) } diff --git a/internal/storage/sqlite/cycle_bench_test.go b/internal/storage/sqlite/cycle_bench_test.go index 21158ccf..cc1ae72d 100644 --- a/internal/storage/sqlite/cycle_bench_test.go +++ b/internal/storage/sqlite/cycle_bench_test.go @@ -1,3 +1,5 @@ +//go:build bench + package sqlite import ( @@ -48,11 +50,13 @@ func BenchmarkCycleDetection_Linear_5000(b *testing.B) { // BenchmarkCycleDetection_Dense_100 tests dense graph: each issue depends on 3-5 previous issues func BenchmarkCycleDetection_Dense_100(b *testing.B) { + b.Skip("Dense graph benchmarks timeout (>120s). Known issue, no optimization needed for rare use case.") benchmarkCycleDetectionDense(b, 100) } // BenchmarkCycleDetection_Dense_1000 tests dense graph with 1000 issues func BenchmarkCycleDetection_Dense_1000(b *testing.B) { + b.Skip("Dense graph benchmarks timeout (>120s). Known issue, no optimization needed for rare use case.") benchmarkCycleDetectionDense(b, 1000) } diff --git a/internal/storage/sqlite/schema.go b/internal/storage/sqlite/schema.go index 65f68cfe..0f325f1d 100644 --- a/internal/storage/sqlite/schema.go +++ b/internal/storage/sqlite/schema.go @@ -47,6 +47,7 @@ CREATE TABLE IF NOT EXISTS dependencies ( CREATE INDEX IF NOT EXISTS idx_dependencies_issue ON dependencies(issue_id); CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on ON dependencies(depends_on_id); CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on_type ON dependencies(depends_on_id, type); +CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on_type_issue ON dependencies(depends_on_id, type, issue_id); -- Labels table CREATE TABLE IF NOT EXISTS labels ( diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index 790cdcdf..eda83257 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -14,8 +14,10 @@ import ( // Import SQLite driver "github.com/steveyegge/beads/internal/types" + sqlite3 "github.com/ncruces/go-sqlite3" _ "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/embed" + "github.com/tetratelabs/wazero" ) // SQLiteStorage implements the Storage interface using SQLite @@ -25,6 +27,53 @@ type SQLiteStorage struct { closed atomic.Bool // Tracks whether Close() has been called } +// setupWASMCache configures WASM compilation caching to reduce SQLite startup time. +// Returns the cache directory path (empty string if using in-memory cache). +// +// Cache behavior: +// - Location: ~/.cache/beads/wasm/ (platform-specific via os.UserCacheDir) +// - Version management: wazero automatically keys cache by its version +// - Cleanup: Old versions remain harmless (~5-10MB each); manual cleanup if needed +// - Fallback: Uses in-memory cache if filesystem cache creation fails +// +// Performance impact: +// - First run: ~220ms (compile + cache) +// - Subsequent runs: ~20ms (load from cache) +func setupWASMCache() string { + cacheDir := "" + if userCache, err := os.UserCacheDir(); err == nil { + cacheDir = filepath.Join(userCache, "beads", "wasm") + } + + var cache wazero.CompilationCache + if cacheDir != "" { + // Try file-system cache first (persistent across runs) + if c, err := wazero.NewCompilationCacheWithDir(cacheDir); err == nil { + cache = c + // Optional: log cache location for debugging + // fmt.Fprintf(os.Stderr, "WASM cache: %s\n", cacheDir) + } + } + + // Fallback to in-memory cache if dir creation failed + if cache == nil { + cache = wazero.NewCompilationCache() + cacheDir = "" // Indicate in-memory fallback + // Optional: log fallback for debugging + // fmt.Fprintln(os.Stderr, "WASM cache: in-memory only") + } + + // Configure go-sqlite3's wazero runtime to use the cache + sqlite3.RuntimeConfig = wazero.NewRuntimeConfig().WithCompilationCache(cache) + + return cacheDir +} + +func init() { + // Setup WASM compilation cache to avoid 220ms JIT compilation overhead on every process start + _ = setupWASMCache() +} + // New creates a new SQLite storage backend func New(path string) (*SQLiteStorage, error) { // Build connection string with proper URI syntax diff --git a/internal/storage/sqlite/sqlite_bench_test.go b/internal/storage/sqlite/sqlite_bench_test.go new file mode 100644 index 00000000..48b27196 --- /dev/null +++ b/internal/storage/sqlite/sqlite_bench_test.go @@ -0,0 +1,145 @@ +//go:build bench + +package sqlite + +import ( + "context" + "testing" + + "github.com/steveyegge/beads/internal/types" +) + +// Benchmark size rationale: +// We only benchmark Large (10K) and XLarge (20K) databases because: +// - Small databases (<1K issues) perform acceptably without optimization +// - Performance issues only manifest at scale (10K+ issues) +// - Smaller benchmarks add code weight without providing optimization insights +// - Target users manage repos with thousands of issues, not hundreds + +// runBenchmark sets up a benchmark with consistent configuration and runs the provided test function. +// It handles store setup/cleanup, timer management, and allocation reporting uniformly across all benchmarks. +func runBenchmark(b *testing.B, setupFunc func(*testing.B) (*SQLiteStorage, func()), testFunc func(*SQLiteStorage, context.Context) error) { + b.Helper() + + store, cleanup := setupFunc(b) + defer cleanup() + + ctx := context.Background() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + if err := testFunc(store, ctx); err != nil { + b.Fatalf("benchmark failed: %v", err) + } + } +} + +// BenchmarkGetReadyWork_Large benchmarks GetReadyWork on 10K issue database +func BenchmarkGetReadyWork_Large(b *testing.B) { + runBenchmark(b, setupLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error { + _, err := store.GetReadyWork(ctx, types.WorkFilter{}) + return err + }) +} + +// BenchmarkGetReadyWork_XLarge benchmarks GetReadyWork on 20K issue database +func BenchmarkGetReadyWork_XLarge(b *testing.B) { + runBenchmark(b, setupXLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error { + _, err := store.GetReadyWork(ctx, types.WorkFilter{}) + return err + }) +} + +// BenchmarkSearchIssues_Large_NoFilter benchmarks searching all open issues +func BenchmarkSearchIssues_Large_NoFilter(b *testing.B) { + openStatus := types.StatusOpen + filter := types.IssueFilter{ + Status: &openStatus, + } + + runBenchmark(b, setupLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error { + _, err := store.SearchIssues(ctx, "", filter) + return err + }) +} + +// BenchmarkSearchIssues_Large_ComplexFilter benchmarks complex filtered search +func BenchmarkSearchIssues_Large_ComplexFilter(b *testing.B) { + openStatus := types.StatusOpen + filter := types.IssueFilter{ + Status: &openStatus, + PriorityMin: intPtr(0), + PriorityMax: intPtr(2), + } + + runBenchmark(b, setupLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error { + _, err := store.SearchIssues(ctx, "", filter) + return err + }) +} + +// BenchmarkCreateIssue_Large benchmarks issue creation in large database +func BenchmarkCreateIssue_Large(b *testing.B) { + runBenchmark(b, setupLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error { + issue := &types.Issue{ + Title: "Benchmark issue", + Description: "Test description", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + } + return store.CreateIssue(ctx, issue, "bench") + }) +} + +// BenchmarkUpdateIssue_Large benchmarks issue updates in large database +func BenchmarkUpdateIssue_Large(b *testing.B) { + // Setup phase: get an issue to update (not timed) + store, cleanup := setupLargeBenchDB(b) + defer cleanup() + ctx := context.Background() + + openStatus := types.StatusOpen + issues, err := store.SearchIssues(ctx, "", types.IssueFilter{ + Status: &openStatus, + }) + if err != nil || len(issues) == 0 { + b.Fatalf("Failed to get issues for update test: %v", err) + } + targetID := issues[0].ID + + // Benchmark phase: measure update operations + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + updates := map[string]interface{}{ + "status": types.StatusInProgress, + } + + if err := store.UpdateIssue(ctx, targetID, updates, "bench"); err != nil { + b.Fatalf("UpdateIssue failed: %v", err) + } + + // reset back to open for next iteration + updates["status"] = types.StatusOpen + if err := store.UpdateIssue(ctx, targetID, updates, "bench"); err != nil { + b.Fatalf("UpdateIssue failed: %v", err) + } + } +} + +// BenchmarkGetReadyWork_FromJSONL benchmarks ready work on JSONL-imported database +func BenchmarkGetReadyWork_FromJSONL(b *testing.B) { + runBenchmark(b, setupLargeFromJSONL, func(store *SQLiteStorage, ctx context.Context) error { + _, err := store.GetReadyWork(ctx, types.WorkFilter{}) + return err + }) +} + +// Helper function +func intPtr(i int) *int { + return &i +} diff --git a/internal/testutil/fixtures/fixtures.go b/internal/testutil/fixtures/fixtures.go new file mode 100644 index 00000000..ae632f8b --- /dev/null +++ b/internal/testutil/fixtures/fixtures.go @@ -0,0 +1,541 @@ +// Package fixtures provides realistic test data generation for benchmarks and tests. +package fixtures + +import ( + "context" + "encoding/json" + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "time" + + "github.com/steveyegge/beads/internal/storage" + "github.com/steveyegge/beads/internal/types" +) + +// labels used across all fixtures +var commonLabels = []string{ + "backend", + "frontend", + "urgent", + "tech-debt", + "documentation", + "performance", + "security", + "ux", + "api", + "database", +} + +// assignees used across all fixtures +var commonAssignees = []string{ + "alice", + "bob", + "charlie", + "diana", + "eve", + "frank", +} + +// epic titles for realistic data +var epicTitles = []string{ + "User Authentication System", + "Payment Processing Integration", + "Mobile App Redesign", + "Performance Optimization", + "API v2 Migration", + "Search Functionality Enhancement", + "Analytics Dashboard", + "Multi-tenant Support", + "Notification System", + "Data Export Feature", +} + +// feature titles (under epics) +var featureTitles = []string{ + "OAuth2 Integration", + "Password Reset Flow", + "Two-Factor Authentication", + "Session Management", + "API Endpoints", + "Database Schema", + "UI Components", + "Background Jobs", + "Error Handling", + "Testing Infrastructure", +} + +// task titles (under features) +var taskTitles = []string{ + "Implement login endpoint", + "Add validation logic", + "Write unit tests", + "Update documentation", + "Fix memory leak", + "Optimize query performance", + "Add error logging", + "Refactor helper functions", + "Update database migrations", + "Configure deployment", +} + +// Fixture size rationale: +// We only provide Large (10K) and XLarge (20K) fixtures because: +// - Performance characteristics only emerge at scale (10K+ issues) +// - Smaller fixtures don't provide meaningful optimization insights +// - Code weight matters; we avoid unused complexity +// - Target use case: repositories with thousands of issues + +// DataConfig controls the distribution and characteristics of generated test data +type DataConfig struct { + TotalIssues int // total number of issues to generate + EpicRatio float64 // percentage of issues that are epics (e.g., 0.1 for 10%) + FeatureRatio float64 // percentage of issues that are features (e.g., 0.3 for 30%) + OpenRatio float64 // percentage of issues that are open (e.g., 0.5 for 50%) + CrossLinkRatio float64 // percentage of tasks with cross-epic blocking dependencies (e.g., 0.2 for 20%) + MaxEpicAgeDays int // maximum age in days for epics (e.g., 180) + MaxFeatureAgeDays int // maximum age in days for features (e.g., 150) + MaxTaskAgeDays int // maximum age in days for tasks (e.g., 120) + MaxClosedAgeDays int // maximum days since closure (e.g., 30) + RandSeed int64 // random seed for reproducibility +} + +// DefaultLargeConfig returns configuration for 10K issue dataset +func DefaultLargeConfig() DataConfig { + return DataConfig{ + TotalIssues: 10000, + EpicRatio: 0.1, + FeatureRatio: 0.3, + OpenRatio: 0.5, + CrossLinkRatio: 0.2, + MaxEpicAgeDays: 180, + MaxFeatureAgeDays: 150, + MaxTaskAgeDays: 120, + MaxClosedAgeDays: 30, + RandSeed: 42, + } +} + +// DefaultXLargeConfig returns configuration for 20K issue dataset +func DefaultXLargeConfig() DataConfig { + return DataConfig{ + TotalIssues: 20000, + EpicRatio: 0.1, + FeatureRatio: 0.3, + OpenRatio: 0.5, + CrossLinkRatio: 0.2, + MaxEpicAgeDays: 180, + MaxFeatureAgeDays: 150, + MaxTaskAgeDays: 120, + MaxClosedAgeDays: 30, + RandSeed: 43, + } +} + +// LargeSQLite creates a 10K issue database with realistic patterns +func LargeSQLite(ctx context.Context, store storage.Storage) error { + cfg := DefaultLargeConfig() + return generateIssuesWithConfig(ctx, store, cfg) +} + +// XLargeSQLite creates a 20K issue database with realistic patterns +func XLargeSQLite(ctx context.Context, store storage.Storage) error { + cfg := DefaultXLargeConfig() + return generateIssuesWithConfig(ctx, store, cfg) +} + +// LargeFromJSONL creates a 10K issue database by exporting to JSONL and reimporting +func LargeFromJSONL(ctx context.Context, store storage.Storage, tempDir string) error { + cfg := DefaultLargeConfig() + cfg.RandSeed = 44 // different seed for JSONL path + return generateFromJSONL(ctx, store, tempDir, cfg) +} + +// XLargeFromJSONL creates a 20K issue database by exporting to JSONL and reimporting +func XLargeFromJSONL(ctx context.Context, store storage.Storage, tempDir string) error { + cfg := DefaultXLargeConfig() + cfg.RandSeed = 45 // different seed for JSONL path + return generateFromJSONL(ctx, store, tempDir, cfg) +} + +// generateIssuesWithConfig creates issues with realistic epic hierarchies and cross-links using provided configuration +func generateIssuesWithConfig(ctx context.Context, store storage.Storage, cfg DataConfig) error { + rng := rand.New(rand.NewSource(cfg.RandSeed)) + + // Calculate breakdown using configuration ratios + numEpics := int(float64(cfg.TotalIssues) * cfg.EpicRatio) + numFeatures := int(float64(cfg.TotalIssues) * cfg.FeatureRatio) + numTasks := cfg.TotalIssues - numEpics - numFeatures + + // Track created issues for cross-linking + var allIssues []*types.Issue + epicIssues := make([]*types.Issue, 0, numEpics) + featureIssues := make([]*types.Issue, 0, numFeatures) + taskIssues := make([]*types.Issue, 0, numTasks) + + // Progress tracking + createdIssues := 0 + lastPctLogged := -1 + + logProgress := func() { + pct := (createdIssues * 100) / cfg.TotalIssues + if pct >= lastPctLogged+10 { + fmt.Printf(" Progress: %d%% (%d/%d issues created)\n", pct, createdIssues, cfg.TotalIssues) + lastPctLogged = pct + } + } + + // Create epics + for i := 0; i < numEpics; i++ { + issue := &types.Issue{ + Title: fmt.Sprintf("%s (Epic %d)", epicTitles[i%len(epicTitles)], i), + Description: fmt.Sprintf("Epic for %s", epicTitles[i%len(epicTitles)]), + Status: randomStatus(rng, cfg.OpenRatio), + Priority: randomPriority(rng), + IssueType: types.TypeEpic, + Assignee: commonAssignees[rng.Intn(len(commonAssignees))], + CreatedAt: randomTime(rng, cfg.MaxEpicAgeDays), + UpdatedAt: time.Now(), + } + + if issue.Status == types.StatusClosed { + closedAt := randomTime(rng, cfg.MaxClosedAgeDays) + issue.ClosedAt = &closedAt + } + + if err := store.CreateIssue(ctx, issue, "fixture"); err != nil { + return fmt.Errorf("failed to create epic: %w", err) + } + + // Add labels to epics + for j := 0; j < rng.Intn(3)+1; j++ { + label := commonLabels[rng.Intn(len(commonLabels))] + _ = store.AddLabel(ctx, issue.ID, label, "fixture") + } + + epicIssues = append(epicIssues, issue) + allIssues = append(allIssues, issue) + createdIssues++ + logProgress() + } + + // Create features under epics + for i := 0; i < numFeatures; i++ { + parentEpic := epicIssues[i%len(epicIssues)] + + issue := &types.Issue{ + Title: fmt.Sprintf("%s (Feature %d)", featureTitles[i%len(featureTitles)], i), + Description: fmt.Sprintf("Feature under %s", parentEpic.Title), + Status: randomStatus(rng, cfg.OpenRatio), + Priority: randomPriority(rng), + IssueType: types.TypeFeature, + Assignee: commonAssignees[rng.Intn(len(commonAssignees))], + CreatedAt: randomTime(rng, cfg.MaxFeatureAgeDays), + UpdatedAt: time.Now(), + } + + if issue.Status == types.StatusClosed { + closedAt := randomTime(rng, cfg.MaxClosedAgeDays) + issue.ClosedAt = &closedAt + } + + if err := store.CreateIssue(ctx, issue, "fixture"); err != nil { + return fmt.Errorf("failed to create feature: %w", err) + } + + // Add parent-child dependency to epic + dep := &types.Dependency{ + IssueID: issue.ID, + DependsOnID: parentEpic.ID, + Type: types.DepParentChild, + CreatedAt: time.Now(), + CreatedBy: "fixture", + } + if err := store.AddDependency(ctx, dep, "fixture"); err != nil { + return fmt.Errorf("failed to add feature-epic dependency: %w", err) + } + + // Add labels + for j := 0; j < rng.Intn(3)+1; j++ { + label := commonLabels[rng.Intn(len(commonLabels))] + _ = store.AddLabel(ctx, issue.ID, label, "fixture") + } + + featureIssues = append(featureIssues, issue) + allIssues = append(allIssues, issue) + createdIssues++ + logProgress() + } + + // Create tasks under features + for i := 0; i < numTasks; i++ { + parentFeature := featureIssues[i%len(featureIssues)] + + issue := &types.Issue{ + Title: fmt.Sprintf("%s (Task %d)", taskTitles[i%len(taskTitles)], i), + Description: fmt.Sprintf("Task under %s", parentFeature.Title), + Status: randomStatus(rng, cfg.OpenRatio), + Priority: randomPriority(rng), + IssueType: types.TypeTask, + Assignee: commonAssignees[rng.Intn(len(commonAssignees))], + CreatedAt: randomTime(rng, cfg.MaxTaskAgeDays), + UpdatedAt: time.Now(), + } + + if issue.Status == types.StatusClosed { + closedAt := randomTime(rng, cfg.MaxClosedAgeDays) + issue.ClosedAt = &closedAt + } + + if err := store.CreateIssue(ctx, issue, "fixture"); err != nil { + return fmt.Errorf("failed to create task: %w", err) + } + + // Add parent-child dependency to feature + dep := &types.Dependency{ + IssueID: issue.ID, + DependsOnID: parentFeature.ID, + Type: types.DepParentChild, + CreatedAt: time.Now(), + CreatedBy: "fixture", + } + if err := store.AddDependency(ctx, dep, "fixture"); err != nil { + return fmt.Errorf("failed to add task-feature dependency: %w", err) + } + + // Add labels + for j := 0; j < rng.Intn(2)+1; j++ { + label := commonLabels[rng.Intn(len(commonLabels))] + _ = store.AddLabel(ctx, issue.ID, label, "fixture") + } + + taskIssues = append(taskIssues, issue) + allIssues = append(allIssues, issue) + createdIssues++ + logProgress() + } + + fmt.Printf(" Progress: 100%% (%d/%d issues created) - Complete!\n", cfg.TotalIssues, cfg.TotalIssues) + + // Add cross-links between tasks across epics using configured ratio + numCrossLinks := int(float64(numTasks) * cfg.CrossLinkRatio) + for i := 0; i < numCrossLinks; i++ { + fromTask := taskIssues[rng.Intn(len(taskIssues))] + toTask := taskIssues[rng.Intn(len(taskIssues))] + + // Avoid self-dependencies + if fromTask.ID == toTask.ID { + continue + } + + dep := &types.Dependency{ + IssueID: fromTask.ID, + DependsOnID: toTask.ID, + Type: types.DepBlocks, + CreatedAt: time.Now(), + CreatedBy: "fixture", + } + + // Ignore cycle errors for cross-links (they're expected) + _ = store.AddDependency(ctx, dep, "fixture") + } + + return nil +} + +// generateFromJSONL creates issues, exports to JSONL, clears DB, and reimports +func generateFromJSONL(ctx context.Context, store storage.Storage, tempDir string, cfg DataConfig) error { + // First generate issues normally + if err := generateIssuesWithConfig(ctx, store, cfg); err != nil { + return fmt.Errorf("failed to generate issues: %w", err) + } + + // Export to JSONL + jsonlPath := filepath.Join(tempDir, "issues.jsonl") + if err := exportToJSONL(ctx, store, jsonlPath); err != nil { + return fmt.Errorf("failed to export to JSONL: %w", err) + } + + // Clear all issues (we'll reimport them) + allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + return fmt.Errorf("failed to get all issues: %w", err) + } + + for _, issue := range allIssues { + if err := store.DeleteIssue(ctx, issue.ID); err != nil { + return fmt.Errorf("failed to delete issue %s: %w", issue.ID, err) + } + } + + // Import from JSONL + if err := importFromJSONL(ctx, store, jsonlPath); err != nil { + return fmt.Errorf("failed to import from JSONL: %w", err) + } + + return nil +} + +// exportToJSONL exports all issues to a JSONL file +func exportToJSONL(ctx context.Context, store storage.Storage, path string) error { + // Get all issues + allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + return fmt.Errorf("failed to query issues: %w", err) + } + + // Populate dependencies and labels for each issue + allDeps, err := store.GetAllDependencyRecords(ctx) + if err != nil { + return fmt.Errorf("failed to get dependencies: %w", err) + } + + for _, issue := range allIssues { + issue.Dependencies = allDeps[issue.ID] + + labels, err := store.GetLabels(ctx, issue.ID) + if err != nil { + return fmt.Errorf("failed to get labels for %s: %w", issue.ID, err) + } + issue.Labels = labels + } + + // Write to JSONL file + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create JSONL file: %w", err) + } + defer f.Close() + + encoder := json.NewEncoder(f) + for _, issue := range allIssues { + if err := encoder.Encode(issue); err != nil { + return fmt.Errorf("failed to encode issue: %w", err) + } + } + + return nil +} + +// importFromJSONL imports issues from a JSONL file +func importFromJSONL(ctx context.Context, store storage.Storage, path string) error { + // Read JSONL file + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read JSONL file: %w", err) + } + + // Parse issues + var issues []*types.Issue + lines := string(data) + for i, line := range splitLines(lines) { + if len(line) == 0 { + continue + } + + var issue types.Issue + if err := json.Unmarshal([]byte(line), &issue); err != nil { + return fmt.Errorf("failed to parse issue at line %d: %w", i+1, err) + } + + issues = append(issues, &issue) + } + + // Import issues directly using storage interface + // Step 1: Create all issues first (without dependencies/labels) + type savedMetadata struct { + deps []*types.Dependency + labels []string + } + metadata := make(map[string]savedMetadata) + + for _, issue := range issues { + // Save dependencies and labels for later + metadata[issue.ID] = savedMetadata{ + deps: issue.Dependencies, + labels: issue.Labels, + } + issue.Dependencies = nil + issue.Labels = nil + + if err := store.CreateIssue(ctx, issue, "fixture"); err != nil { + // Ignore duplicate errors + if !strings.Contains(err.Error(), "UNIQUE constraint failed") { + return fmt.Errorf("failed to create issue %s: %w", issue.ID, err) + } + } + } + + // Step 2: Add all dependencies (now that all issues exist) + for issueID, meta := range metadata { + for _, dep := range meta.deps { + if err := store.AddDependency(ctx, dep, "fixture"); err != nil { + // Ignore duplicate and cycle errors + if !strings.Contains(err.Error(), "already exists") && + !strings.Contains(err.Error(), "cycle") { + return fmt.Errorf("failed to add dependency for %s: %w", issueID, err) + } + } + } + + // Add labels + for _, label := range meta.labels { + _ = store.AddLabel(ctx, issueID, label, "fixture") + } + } + + return nil +} + +// splitLines splits a string by newlines +func splitLines(s string) []string { + var lines []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == '\n' { + lines = append(lines, s[start:i]) + start = i + 1 + } + } + if start < len(s) { + lines = append(lines, s[start:]) + } + return lines +} + +// randomStatus returns a random status with given open ratio +func randomStatus(rng *rand.Rand, openRatio float64) types.Status { + r := rng.Float64() + if r < openRatio { + // Open statuses: open, in_progress, blocked + statuses := []types.Status{types.StatusOpen, types.StatusInProgress, types.StatusBlocked} + return statuses[rng.Intn(len(statuses))] + } + return types.StatusClosed +} + +// randomPriority returns a random priority with realistic distribution +// P0: 5%, P1: 15%, P2: 50%, P3: 25%, P4: 5% +func randomPriority(rng *rand.Rand) int { + r := rng.Intn(100) + switch { + case r < 5: + return 0 + case r < 20: + return 1 + case r < 70: + return 2 + case r < 95: + return 3 + default: + return 4 + } +} + +// randomTime returns a random time up to maxDaysAgo days in the past +func randomTime(rng *rand.Rand, maxDaysAgo int) time.Time { + daysAgo := rng.Intn(maxDaysAgo) + return time.Now().Add(-time.Duration(daysAgo) * 24 * time.Hour) +} diff --git a/internal/testutil/fixtures/fixtures_test.go b/internal/testutil/fixtures/fixtures_test.go new file mode 100644 index 00000000..75cf2ec7 --- /dev/null +++ b/internal/testutil/fixtures/fixtures_test.go @@ -0,0 +1,128 @@ +package fixtures + +import ( + "context" + "testing" + + "github.com/steveyegge/beads/internal/storage/sqlite" + "github.com/steveyegge/beads/internal/types" +) + +func TestLargeSQLite(t *testing.T) { + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create storage: %v", err) + } + defer store.Close() + + ctx := context.Background() + + // Initialize database with prefix + if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + if err := LargeSQLite(ctx, store); err != nil { + t.Fatalf("LargeSQLite failed: %v", err) + } + + // Verify issue count + allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + t.Fatalf("Failed to search issues: %v", err) + } + + if len(allIssues) != 10000 { + t.Errorf("Expected 10000 issues, got %d", len(allIssues)) + } + + // Verify we have epics, features, and tasks + var epics, features, tasks int + for _, issue := range allIssues { + switch issue.IssueType { + case types.TypeEpic: + epics++ + case types.TypeFeature: + features++ + case types.TypeTask: + tasks++ + } + } + + if epics == 0 || features == 0 || tasks == 0 { + t.Errorf("Missing issue types: epics=%d, features=%d, tasks=%d", epics, features, tasks) + } + + t.Logf("Created %d epics, %d features, %d tasks", epics, features, tasks) +} + +func TestXLargeSQLite(t *testing.T) { + if testing.Short() { + t.Skip("Skipping XLarge test in short mode") + } + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create storage: %v", err) + } + defer store.Close() + + ctx := context.Background() + + // Initialize database with prefix + if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + if err := XLargeSQLite(ctx, store); err != nil { + t.Fatalf("XLargeSQLite failed: %v", err) + } + + // Verify issue count + allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + t.Fatalf("Failed to search issues: %v", err) + } + + if len(allIssues) != 20000 { + t.Errorf("Expected 20000 issues, got %d", len(allIssues)) + } +} + +func TestLargeFromJSONL(t *testing.T) { + if testing.Short() { + t.Skip("Skipping JSONL test in short mode") + } + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create storage: %v", err) + } + defer store.Close() + + ctx := context.Background() + + // Initialize database with prefix + if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + tempDir := t.TempDir() + + if err := LargeFromJSONL(ctx, store, tempDir); err != nil { + t.Fatalf("LargeFromJSONL failed: %v", err) + } + + // Verify issue count + allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + t.Fatalf("Failed to search issues: %v", err) + } + + if len(allIssues) != 10000 { + t.Errorf("Expected 10000 issues, got %d", len(allIssues)) + } +} From 6f7e7fa93012a35e4a8106c585a1ec2d1abc89f9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 12:52:34 -0800 Subject: [PATCH 06/13] Fix lint errors: handle error return values --- .beads/beads.jsonl | 4 ++-- cmd/bd/deletion_tracking.go | 2 +- cmd/bd/import.go | 2 +- cmd/bd/message.go | 2 +- cmd/bd/migrate_issues.go | 6 +++--- cmd/bd/show.go | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 1dd39a5f..646943e5 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -334,7 +334,7 @@ {"id":"bd-d6aq","content_hash":"45a169a72749bb3bc93190bb3e5891950409f264baeac4394cd1a3ad5a75c0f2","title":"Test reservation expiration and renewal","description":"Verify TTL-based reservation expiration works correctly.\n\nAcceptance Criteria:\n- Reserve with short TTL (30s)\n- Verify other agents can't claim\n- Wait for expiration\n- Verify reservation auto-released\n- Other agent can now claim\n- Test renewal/heartbeat mechanism\n\nFile: tests/integration/test_reservation_ttl.py","notes":"Implemented comprehensive TTL/expiration test suite in tests/integration/test_reservation_ttl.py\n\nTest Coverage:\n✅ Short TTL reservations (30s) - verifies TTL is properly set\n✅ Reservation blocking - confirms agent2 cannot claim while agent1 holds reservation\n✅ Auto-release after expiration - validates expired reservations are auto-cleaned and become available\n✅ Renewal/heartbeat - tests that re-reserving extends expiration time\n\nAll 4 tests passing in 56.9s total (including 30s+ wait time for expiration tests).\n\nMock server implements full TTL management:\n- Reservation class with expiration tracking\n- Auto-cleanup of expired reservations on each request\n- Renewal support (same agent re-reserving)\n- 409 conflict for cross-agent reservation attempts","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-07T22:43:21.547821-08:00","updated_at":"2025-11-08T03:54:04.855132-08:00","closed_at":"2025-11-08T02:24:30.296982-08:00","source_repo":".","dependencies":[{"issue_id":"bd-d6aq","depends_on_id":"bd-m9th","type":"blocks","created_at":"2025-11-07T22:43:21.548731-08:00","created_by":"daemon"}]} {"id":"bd-d76d","content_hash":"b65da5fe9f89a98f1e6fad6ee32d463126ef72785fec4d6dfa5a4774c6a8a393","title":"Modify EnsureIDs to support parent resurrection","description":"Update internal/storage/sqlite/ids.go:189-202 to call TryResurrectParent before failing on missing parent. Add resurrection mode flag, log resurrected parents for transparency. Maintain backwards compatibility with strict validation mode.","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-04T12:31:59.659507-08:00","updated_at":"2025-11-05T00:08:42.814463-08:00","closed_at":"2025-11-05T00:08:42.814466-08:00","source_repo":"."} {"id":"bd-d7e88238","content_hash":"ff14f04a04bf89f52bda3d584933df6b09b554cce8665f47f429f1ac52dafb94","title":"Rapid 3","description":"","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-29T19:11:57.459655-07:00","updated_at":"2025-11-08T01:49:23.462353-08:00","closed_at":"2025-11-07T23:18:52.333825-08:00","source_repo":"."} -{"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"open","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:24:34.50322-08:00","source_repo":"."} +{"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:43:11.49933-08:00","closed_at":"2025-11-15T12:43:11.49933-08:00","source_repo":"."} {"id":"bd-d9e0","content_hash":"de4e01414f8863b63cb693a709048b85c3f4417f03e7d7b2528560076be0e1f7","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-02T12:32:00.159298-08:00","closed_at":"2025-11-02T12:32:00.1593-08:00","source_repo":"."} {"id":"bd-dcd6f14b","content_hash":"c07a4b8a39e6e81513278ee335fe14aa767cbcba72e3b511cfd95705053483b1","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00","source_repo":"."} {"id":"bd-de0h","content_hash":"8b8b43683607e73012cf8bd7cf8631c6ae34498d0c93ca5b77d3f68944c8088d","title":"bd message: Add HTTP client timeout to prevent hangs","description":"HTTP client in `sendAgentMailRequest` uses default http.Post with no timeout.\n\n**Location:** cmd/bd/message.go:181\n\n**Problem:**\n- Can hang indefinitely if server is unresponsive\n- No way to cancel stuck requests\n- Poor UX in flaky networks\n\n**Fix:**\n```go\nclient := \u0026http.Client{Timeout: 30 * time.Second}\nresp, err := client.Post(url, \"application/json\", bytes.NewReader(reqBody))\n```\n\n**Impact:** Production reliability and security issue","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T12:54:24.942645-08:00","updated_at":"2025-11-08T12:56:59.948929-08:00","closed_at":"2025-11-08T12:56:59.948929-08:00","source_repo":".","dependencies":[{"issue_id":"bd-de0h","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:54.860847-08:00","created_by":"daemon"}]} @@ -491,7 +491,7 @@ {"id":"bd-xzrv","content_hash":"45b45aaa47b9fc254ce74750b92f5527862672d9826c7ad59e006bdb1bc9939f","title":"Write Agent Mail integration guide","description":"Comprehensive guide for setting up and using Agent Mail with Beads.\n\nAcceptance Criteria:\n- Installation instructions\n- Configuration (environment variables)\n- Architecture diagram\n- Benefits and tradeoffs\n- When to use vs not use\n- Troubleshooting section\n- Migration from git-only mode\n\nFile: docs/AGENT_MAIL.md\n\nSections:\n- Quick start\n- How it works\n- Integration points\n- Graceful degradation\n- Multi-machine deployment\n- FAQ","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-07T22:42:51.231066-08:00","updated_at":"2025-11-08T01:51:40.352442-08:00","closed_at":"2025-11-08T00:40:38.798162-08:00","source_repo":".","dependencies":[{"issue_id":"bd-xzrv","depends_on_id":"bd-fzbg","type":"blocks","created_at":"2025-11-07T22:42:51.232246-08:00","created_by":"daemon"}]} {"id":"bd-yek6","content_hash":"f155913af8c58c0a7ea3da6a7d9e232e8cb29c3825f2d6f272a5417a449692a9","title":"CLI tests (cli_fast_test.go) are slow and should be integration tests","description":"The TestCLI_* tests in cmd/bd/cli_fast_test.go are taking 4-5 seconds each (40+ seconds total), making them the slowest part of the fast test suite.\n\nCurrent timings:\n- TestCLI_Import: 4.73s\n- TestCLI_Blocked: 4.33s \n- TestCLI_DepTree: 4.15s\n- TestCLI_Close: 3.59s\n- TestCLI_DepAdd: 3.50s\n- etc.\n\nThese tests compile the bd binary once in init(), but then execute it multiple times per test with filesystem operations. Despite being named \"fast\", they're actually end-to-end CLI integration tests.\n\nOptions:\n1. Tag with //go:build integration (move to integration suite)\n2. Optimize: Use in-memory databases, reduce exec calls, better parallelization\n3. Keep as-is but understand they're the baseline for \"fast\" tests\n\nTotal test suite currently: 13.8s (cmd/bd alone is 12.8s, and most of that is these CLI tests)","notes":"Fixed by reusing existing bd binary from repo root instead of rebuilding.\n\nBefore: 15+ minutes (rebuilding binary for every test package)\nAfter: ~12 seconds (reuses pre-built binary)\n\nThe init() function now checks for ../../bd first before falling back to building. This means `go build \u0026\u0026 go test` is now fast.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T20:19:12.822543-08:00","updated_at":"2025-11-05T20:31:19.321787-08:00","closed_at":"2025-11-05T20:31:19.321787-08:00","source_repo":"."} {"id":"bd-yuf7","content_hash":"97e18d89914d698df5ec673d40ff980a87a29e1435a887ec2b5dd77d7d412a79","title":"bd config set succeeds but doesn't persist to config.toml","description":"Commands like `bd config set daemon.auto_push true` return \"Set daemon.auto_push = true\" but the config file is never created and `bd info --json | jq '.config'` returns null.\n\n**Steps to reproduce:**\n1. Run `bd config set daemon.auto_push true`\n2. See success message: \"Set daemon.auto_push = true\"\n3. Check `cat .beads/config.toml` → file doesn't exist\n4. Check `bd info --json | jq '.config'` → returns null\n\n**Expected:**\n- .beads/config.toml should be created with the setting\n- bd info should show the config value\n\n**Impact:**\nUsers can't enable auto-push/auto-commit via CLI as documented in AGENTS.md","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-08T01:14:58.726198-08:00","updated_at":"2025-11-08T01:17:41.377912-08:00","closed_at":"2025-11-08T01:17:41.377912-08:00","source_repo":"."} -{"id":"bd-yvlc","content_hash":"f121c6b2674a550c12f99331eb6a45a4817bd4161ea377104c212f56c0589af5","title":"URGENT: main branch has failing tests (syncbranch migration error)","description":"The main branch has failing tests that are blocking CI for all PRs.\n\n## Problem\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\n## Evidence\n- Last 5 CI runs on main: ALL FAILED\n- Tests fail locally on current main (bd6dca5)\n- Affects: TestGet, TestSet, TestUnset in internal/syncbranch\n\n## Impact\n- Blocking all PR merges\n- CI shows red for all branches\n- Can't trust test results\n\n## Root Cause\nMigration order issue - trying to create index on external_ref column before the issues table exists, or before the external_ref column is added to the issues table.\n\n## Quick Fix Needed\nNeed to investigate migration order in internal/storage/sqlite/migrations.go and ensure:\n1. issues table is created first\n2. external_ref column is added to issues table\n3. THEN index on external_ref is created\n\nThis is CRITICAL - main should never have breaking tests.","design":"Investigation steps:\n1. Check internal/storage/sqlite/migrations.go\n2. Verify migration order and dependencies\n3. Look at external_ref_column migration specifically\n4. Ensure proper table/column creation before index\n5. Test fix locally with fresh database\n6. Push fix to main\n7. Verify CI passes","acceptance_criteria":"- All tests pass on main branch\n- CI is green\n- syncbranch_test.go tests all passing","status":"open","priority":0,"issue_type":"bug","created_at":"2025-11-15T12:25:31.51688-08:00","updated_at":"2025-11-15T12:25:31.51688-08:00","source_repo":"."} +{"id":"bd-yvlc","content_hash":"f121c6b2674a550c12f99331eb6a45a4817bd4161ea377104c212f56c0589af5","title":"URGENT: main branch has failing tests (syncbranch migration error)","description":"The main branch has failing tests that are blocking CI for all PRs.\n\n## Problem\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\n## Evidence\n- Last 5 CI runs on main: ALL FAILED\n- Tests fail locally on current main (bd6dca5)\n- Affects: TestGet, TestSet, TestUnset in internal/syncbranch\n\n## Impact\n- Blocking all PR merges\n- CI shows red for all branches\n- Can't trust test results\n\n## Root Cause\nMigration order issue - trying to create index on external_ref column before the issues table exists, or before the external_ref column is added to the issues table.\n\n## Quick Fix Needed\nNeed to investigate migration order in internal/storage/sqlite/migrations.go and ensure:\n1. issues table is created first\n2. external_ref column is added to issues table\n3. THEN index on external_ref is created\n\nThis is CRITICAL - main should never have breaking tests.","design":"Investigation steps:\n1. Check internal/storage/sqlite/migrations.go\n2. Verify migration order and dependencies\n3. Look at external_ref_column migration specifically\n4. Ensure proper table/column creation before index\n5. Test fix locally with fresh database\n6. Push fix to main\n7. Verify CI passes","acceptance_criteria":"- All tests pass on main branch\n- CI is green\n- syncbranch_test.go tests all passing","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-15T12:25:31.51688-08:00","updated_at":"2025-11-15T12:43:11.489612-08:00","closed_at":"2025-11-15T12:43:11.489612-08:00","source_repo":"."} {"id":"bd-z0yn","content_hash":"1bb2f4940363e921d71f45e202cbadc1d90c4985ce5a048bb97d352f0a3ad9d0","title":"Channel isolation test - beads","description":"","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-11-08T04:21:17.327983-08:00","updated_at":"2025-11-08T04:21:17.365854-08:00","source_repo":"."} {"id":"bd-z3s3","content_hash":"24d99dc1a9a5f35af962137f5709d4b0f1b6a9ec91511c30a2517d790640cce8","title":"Create deployment scripts for GCP","description":"Automated provisioning scripts for GCP Compute Engine deployment.\n\nAcceptance Criteria:\n- Terraform/gcloud scripts\n- Static IP allocation\n- Firewall rules\n- NGINX reverse proxy config\n- TLS setup (Let's Encrypt)\n- Systemd service file\n\nFile: deployment/agent-mail/gcp/","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-07T22:43:43.294839-08:00","updated_at":"2025-11-07T22:43:43.294839-08:00","source_repo":".","dependencies":[{"issue_id":"bd-z3s3","depends_on_id":"bd-9li4","type":"blocks","created_at":"2025-11-07T23:04:27.982336-08:00","created_by":"daemon"}]} {"id":"bd-z528","content_hash":"3f332e9997d2b7eb0af23885820df5f607fe08671a2615cadec941bbe7d36f68","title":"Prevent test pollution in production database","description":"The bd-vxdr cleanup revealed test issues were created during manual testing in the production workspace (Nov 2-4, template feature development).\n\n**Root cause:** Manual testing with `./bd create \"Test issue\"` pollutes the production .beads database.\n\n**Prevention strategies:**\n1. Use TEST_DB environment variable for manual testing\n2. Add warning when creating issues with \"Test\" prefix\n3. Improve developer docs about testing workflow\n4. Consider adding `bd test-mode` command for isolated testing","notes":"**Implementation completed:**\n\n1. ✅ Added warning when creating issues with \"Test\" prefix in production database\n - Shows yellow warning with ⚠ symbol\n - Suggests using BEADS_DB for isolated testing\n - Warning appears in create.go after title validation\n\n2. ✅ Documented BEADS_DB testing workflow in AGENTS.md\n - Added \"Testing Workflow\" section in Development Guidelines\n - Includes manual testing examples with BEADS_DB\n - Includes automated testing examples with t.TempDir()\n - Clear warning about not polluting production database\n\n3. ⚠️ Decided against bd test-mode command\n - BEADS_DB already provides simple, flexible isolation\n - Additional command would add complexity without much benefit\n - Current approach follows Unix philosophy (env vars for config)\n\n**Files modified:**\n- cmd/bd/create.go - Added Test prefix warning\n- AGENTS.md - Added Testing Workflow section\n\n**Testing:**\n- Verified warning appears when creating \"Test\" prefix issues\n- Verified BEADS_DB isolation works correctly\n- Built successfully with `go build`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-07T16:07:28.255289-08:00","updated_at":"2025-11-08T01:49:23.463399-08:00","closed_at":"2025-11-07T22:43:28.669908-08:00","source_repo":"."} diff --git a/cmd/bd/deletion_tracking.go b/cmd/bd/deletion_tracking.go index b4d1f728..44f9b7d9 100644 --- a/cmd/bd/deletion_tracking.go +++ b/cmd/bd/deletion_tracking.go @@ -54,7 +54,7 @@ func merge3WayAndPruneDeletions(ctx context.Context, store storage.Storage, json // Ensure temp file cleanup on failure defer func() { if fileExists(tmpMerged) { - os.Remove(tmpMerged) + _ = os.Remove(tmpMerged) } }() diff --git a/cmd/bd/import.go b/cmd/bd/import.go index ab3b452a..1d832da9 100644 --- a/cmd/bd/import.go +++ b/cmd/bd/import.go @@ -545,7 +545,7 @@ func attemptAutoMerge(conflictedPath string) error { if err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() basePath := filepath.Join(tmpDir, "base.jsonl") leftPath := filepath.Join(tmpDir, "left.jsonl") diff --git a/cmd/bd/message.go b/cmd/bd/message.go index 3ae46879..f3d3d4dc 100644 --- a/cmd/bd/message.go +++ b/cmd/bd/message.go @@ -202,7 +202,7 @@ func sendAgentMailRequest(config *AgentMailConfig, method string, params interfa if err != nil { return nil, fmt.Errorf("failed to connect to Agent Mail server: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/cmd/bd/migrate_issues.go b/cmd/bd/migrate_issues.go index b9630c7e..acbafe43 100644 --- a/cmd/bd/migrate_issues.go +++ b/cmd/bd/migrate_issues.go @@ -630,7 +630,7 @@ func displayMigrationPlan(plan migrationPlan, dryRun bool) error { func confirmMigration(plan migrationPlan) bool { fmt.Printf("\nMigrate %d issues from %s to %s? [y/N] ", len(plan.IssueIDs), plan.From, plan.To) var response string - fmt.Scanln(&response) + _, _ = fmt.Scanln(&response) return strings.ToLower(strings.TrimSpace(response)) == "y" } @@ -698,6 +698,6 @@ func init() { migrateIssuesCmd.Flags().Bool("strict", false, "Fail on orphaned dependencies or missing repos") migrateIssuesCmd.Flags().Bool("yes", false, "Skip confirmation prompt") - migrateIssuesCmd.MarkFlagRequired("from") - migrateIssuesCmd.MarkFlagRequired("to") + _ = migrateIssuesCmd.MarkFlagRequired("from") + _ = migrateIssuesCmd.MarkFlagRequired("to") } diff --git a/cmd/bd/show.go b/cmd/bd/show.go index 6e437edf..785cd8fc 100644 --- a/cmd/bd/show.go +++ b/cmd/bd/show.go @@ -607,11 +607,11 @@ Examples: // Write current value to temp file if _, err := tmpFile.WriteString(currentValue); err != nil { - tmpFile.Close() + _ = tmpFile.Close() fmt.Fprintf(os.Stderr, "Error writing to temp file: %v\n", err) os.Exit(1) } - tmpFile.Close() + _ = tmpFile.Close() // Open the editor editorCmd := exec.Command(editor, tmpPath) From 1c2e88c6215ee3478fc7e3d0cc492bf97f96a302 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 13:19:53 -0800 Subject: [PATCH 07/13] bd sync: 2025-11-15 13:19:53 --- .beads/beads.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 646943e5..d8b43ffe 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -415,6 +415,7 @@ {"id":"bd-it3x","content_hash":"f31a3aae4297794bd42d7a8a8688ab5cdb4fa6c70f0ed88ffa93be93d76a2128","title":"Issue with labels","description":"","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-11-07T19:07:18.388873-08:00","updated_at":"2025-11-07T22:07:17.346541-08:00","closed_at":"2025-11-07T21:55:09.429989-08:00","source_repo":".","labels":["backend","urgent"]} {"id":"bd-iye7","content_hash":"1554b026ccacde081eb05d3889943d95ae9c75a21d3f06c346c57cbe2391dc46","title":"Add path normalization to getMultiRepoJSONLPaths()","description":"From bd-xo6b code review: getMultiRepoJSONLPaths() does not handle non-standard paths correctly.\n\nProblems:\n- No tilde expansion: ~/repos/foo treated as literal path\n- No absolute path conversion: ../other-repo breaks if working directory changes\n- No duplicate detection: If Primary=. and Additional=[.], same JSONL processed twice\n- No empty string handling: Empty paths create invalid /.beads/issues.jsonl\n\nImpact:\nConfig with tilde or relative paths will fail\n\nFix needed:\n1. Use filepath.Abs() for all paths\n2. Add tilde expansion via os.UserHomeDir()\n3. Deduplicate paths (use map to track seen paths)\n4. Filter out empty strings\n5. Validate paths exist and are readable\n\nFiles:\n- cmd/bd/deletion_tracking.go:333-358 (getMultiRepoJSONLPaths function)","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-06T19:31:51.882743-08:00","updated_at":"2025-11-06T19:35:41.246311-08:00","closed_at":"2025-11-06T19:35:41.246311-08:00","source_repo":".","dependencies":[{"issue_id":"bd-iye7","depends_on_id":"bd-xo6b","type":"discovered-from","created_at":"2025-11-06T19:32:12.267906-08:00","created_by":"daemon"}]} {"id":"bd-j7e2","content_hash":"aeb3aec5ebb3b7554949f7161f58408c445983c993aaa5b31e4df93b083cf19c","title":"RPC diagnostics: BD_RPC_DEBUG timing logs","description":"Add lightweight diagnostic logging for RPC connection attempts:\n- BD_RPC_DEBUG=1 prints to stderr:\n - Socket path being dialed\n - Socket exists check result \n - Dial start/stop time\n - Connection outcome\n- Improve bd daemon --status messaging when lock not held\n\nThis helps field triage of connection issues without verbose daemon logs.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-07T16:42:12.772364-08:00","updated_at":"2025-11-07T22:07:17.346817-08:00","closed_at":"2025-11-07T21:29:32.243458-08:00","source_repo":".","dependencies":[{"issue_id":"bd-j7e2","depends_on_id":"bd-ndyz","type":"discovered-from","created_at":"2025-11-07T16:42:12.773714-08:00","created_by":"daemon"}]} +{"id":"bd-jijf","content_hash":"e339d9cafb122fc2725cd7dc0a768a1fdb6f1bc38d532ca5af5f2048152c8cd0","title":"Fix: --parent flag doesn't create parent-child dependency","description":"When using `bd create --parent \u003cid\u003e`, the code generates a hierarchical child ID (e.g., bd-123.1) but never creates a parent-child dependency. This causes `bd epic status` to show zero children even though child issues exist.\n\nRoot cause: create.go generates child ID using store.GetNextChildID() but never calls store.AddDependency() with type parent-child.\n\nFix: After creating the issue when parentID is set, automatically add a parent-child dependency linking child -\u003e parent.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-15T13:15:22.138854-08:00","updated_at":"2025-11-15T13:18:29.301788-08:00","closed_at":"2025-11-15T13:18:29.301788-08:00","source_repo":"."} {"id":"bd-jjua","content_hash":"1f03e105aa89285214bad40695c14bf4b5f63e85caae26c8dd326cf592002d57","title":"Auto-invoke 3-way merge for JSONL conflicts","description":"Currently when git pull encounters merge conflicts in .beads/issues.jsonl, the post-merge hook fails with an error message pointing users to manual resolution or the beads-merge tool.\n\nThis is a poor user experience - the conflict detection is working, but we should automatically invoke the advanced 3-way merging instead of just telling users about it.\n\n**Current behavior:**\n- Detect conflict markers in JSONL\n- Display error with manual resolution options\n- Exit with failure\n\n**Desired behavior:**\n- Detect conflict markers in JSONL\n- Automatically invoke beads-merge 3-way merge\n- Only fail if automatic merge cannot resolve the conflicts\n\n**Reference:**\n- beads-merge tool: https://github.com/neongreen/mono/tree/main/beads-merge\n- Error occurs in post-merge hook during bd sync after git pull","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T03:09:18.258708-08:00","updated_at":"2025-11-08T03:15:55.529652-08:00","closed_at":"2025-11-08T03:15:55.529652-08:00","source_repo":".","comments":[{"id":6,"issue_id":"bd-jjua","author":"stevey","text":"Implemented automatic 3-way merge resolution for JSONL conflicts.\n\n**Changes Made:**\n\n1. **Modified conflict detection in cmd/bd/import.go (lines 105-152)**\n - When git conflict markers are detected, instead of immediately failing, the system now attempts automatic resolution\n - Calls new `attemptAutoMerge()` function to invoke bd merge tool\n - If auto-merge succeeds, restarts import with the merged JSONL\n - If auto-merge fails, falls back to displaying manual resolution instructions\n\n2. **Added attemptAutoMerge() function (lines 469-585)**\n - Extracts the three git conflict stages: base (:1), ours/left (:2), theirs/right (:3)\n - Creates temporary files for each version\n - Invokes `bd merge` command to perform intelligent 3-way merge\n - Writes merged result back to original file\n - Auto-stages the resolved file with git add\n\n**How it works:**\n- When git pull creates conflicts in .beads/issues.jsonl\n- The post-merge hook runs `bd sync --import-only`\n- Import detects conflict markers on line scan\n- Automatically extracts conflict versions from git\n- Runs bd merge tool with field-level merge intelligence\n- If successful, continues import seamlessly\n- Only fails if conflicts cannot be auto-resolved\n\n**Benefits:**\n- Zero user intervention for most JSONL conflicts\n- Leverages existing bd merge 3-way merge logic\n- Maintains data integrity with field-level merging\n- Graceful fallback to manual resolution when needed\n\n**Testing:**\n- Code builds successfully\n- Ready for real-world testing on next git pull conflict\n\nThe solution transforms the error into an automatic resolution step, significantly improving user experience.","created_at":"2025-11-08T11:11:06Z"},{"id":7,"issue_id":"bd-jjua","author":"stevey","text":"**Discovery: Git merge driver was already configured but not being triggered**\n\nThe 3-way merge tool was properly vendored and `bd init` does configure the git merge driver:\n- `git config merge.beads.driver \"bd merge %A %O %L %R\"`\n- `.gitattributes` entry for `.beads/beads.jsonl merge=beads`\n\nThis should have prevented conflicts entirely by auto-invoking `bd merge` during git merge operations.\n\n**Root Cause:**\nHowever, the automatic merge driver doesn't help when conflicts reach the import stage, which happens in the post-merge hook flow:\n1. Git pull encounters conflicts\n2. Post-merge hook runs `bd sync --import-only`\n3. Import reads the JSONL file and detects conflict markers\n4. Previous behavior: fail with error message\n\nThe merge driver prevents conflicts during git operations, but if conflicts somehow make it through (or if the merge driver itself produces conflicts that it can't resolve), the import process needed fallback handling.\n\n**Our Solution:**\nAdded automatic 3-way merge invocation at the import stage as a safety net. This provides defense-in-depth:\n- Primary: git merge driver prevents most conflicts\n- Fallback: import auto-merge handles any that slip through\n\n**Bonus Discovery:**\nFound that `.beads/issues.jsonl` is a zombie file that keeps reappearing despite multiple removal attempts in git history. Renamed it to `.beads/issues.jsonl.zombie-do-not-use` with a warning message. The canonical file is `.beads/beads.jsonl`.","created_at":"2025-11-08T11:14:35Z"}]} {"id":"bd-jx90","content_hash":"3dfa306c43d7febfbd072d4bb5c1b6018f8a7301380bb128f53abb0eca5deb65","title":"Add simple cleanup command to delete closed issues","description":"Users want a simple command to delete all closed issues without requiring Anthropic API key (unlike compact). Requested in GH #243.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-07T00:26:30.372137-08:00","updated_at":"2025-11-07T22:07:17.347122-08:00","closed_at":"2025-11-07T22:05:16.325863-08:00","source_repo":"."} {"id":"bd-k0j9","content_hash":"52d1e6f87bd7655018bd89dbbbaf8da66bdcba45de6138fd237810365a04606a","title":"Test dependency parent","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T11:23:02.505901-08:00","updated_at":"2025-11-05T11:23:20.91305-08:00","closed_at":"2025-11-05T11:23:20.91305-08:00","source_repo":"."} From b9919fe031a1a858e0eb86cae8a94e2d13bd5e4a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 13:20:02 -0800 Subject: [PATCH 08/13] Fix: --parent flag now creates parent-child dependency When using 'bd create --parent ', the system now automatically creates a parent-child dependency linking the child to the parent. This fixes epic status reporting which was showing zero children. Fixes #318 (bd-jijf) Changes: - cmd/bd/create.go: Add parent-child dependency after issue creation - internal/rpc/server_issues_epics.go: Add same fix for daemon mode Tested: - Created epic with children, verified epic status shows correct count - Verified closing all children makes epic eligible for closure - All tests pass Amp-Thread-ID: https://ampcode.com/threads/T-f1a1aee1-03bd-4e62-a63c-c1d339f8300b Co-authored-by: Amp --- cmd/bd/create.go | 12 ++++++++++++ internal/rpc/server_issues_epics.go | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cmd/bd/create.go b/cmd/bd/create.go index 714928f3..09121cae 100644 --- a/cmd/bd/create.go +++ b/cmd/bd/create.go @@ -313,6 +313,18 @@ var createCmd = &cobra.Command{ os.Exit(1) } + // If parent was specified, add parent-child dependency + if parentID != "" { + dep := &types.Dependency{ + IssueID: issue.ID, + DependsOnID: parentID, + Type: types.DepParentChild, + } + if err := store.AddDependency(ctx, dep, actor); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to add parent-child dependency %s -> %s: %v\n", issue.ID, parentID, err) + } + } + // Add labels if specified for _, label := range labels { if err := store.AddLabel(ctx, issue.ID, label, actor); err != nil { diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 92be7932..4e6b5d49 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -180,6 +180,21 @@ func (s *Server) handleCreate(req *Request) Response { } } + // If parent was specified, add parent-child dependency + if createArgs.Parent != "" { + dep := &types.Dependency{ + IssueID: issue.ID, + DependsOnID: createArgs.Parent, + Type: types.DepParentChild, + } + if err := store.AddDependency(ctx, dep, s.reqActor(req)); err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to add parent-child dependency %s -> %s: %v", issue.ID, createArgs.Parent, err), + } + } + } + // Add labels if specified for _, label := range createArgs.Labels { if err := store.AddLabel(ctx, issue.ID, label, s.reqActor(req)); err != nil { From 934ae04fa01c69a652fd794983ee5bda5e0c88d4 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 14:07:38 -0800 Subject: [PATCH 09/13] Fix gh-316: Prefer exact ID matches over prefix matches The ID disambiguation logic treated 'offlinebrew-3d0' as ambiguous when child IDs like 'offlinebrew-3d0.1' existed. Now the system: 1. Checks for exact full ID matches first (issue.ID == input) 2. Checks for exact hash matches (handling cross-prefix scenarios) 3. Only falls back to substring matching if no exact match is found Added test cases verifying: - 'offlinebrew-3d0' matches exactly, not ambiguously with children - '3d0' without prefix still resolves to exact match Amp-Thread-ID: https://ampcode.com/threads/T-5358ea57-e9ea-49e9-aedf-7044ebf8b52a Co-authored-by: Amp --- internal/utils/id_parser.go | 29 ++++++++++++++++++++++++++++- internal/utils/id_parser_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/utils/id_parser.go b/internal/utils/id_parser.go index 7c5b7cc8..a1a27bf9 100644 --- a/internal/utils/id_parser.go +++ b/internal/utils/id_parser.go @@ -81,14 +81,41 @@ func ResolvePartialID(ctx context.Context, store storage.Storage, input string) hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen) var matches []string + var exactMatch string + for _, issue := range issues { - issueHash := strings.TrimPrefix(issue.ID, prefixWithHyphen) + // Check for exact full ID match first (case: user typed full ID with different prefix) + if issue.ID == input { + exactMatch = issue.ID + break + } + + // Extract hash from each issue, regardless of its prefix + // This handles cross-prefix matching (e.g., "3d0" matching "offlinebrew-3d0") + var issueHash string + if idx := strings.Index(issue.ID, "-"); idx >= 0 { + issueHash = issue.ID[idx+1:] + } else { + issueHash = issue.ID + } + + // Check for exact hash match (excluding hierarchical children) + if issueHash == hashPart { + exactMatch = issue.ID + // Don't break - keep searching in case there's a full ID match + } + // Check if the issue hash contains the input hash as substring if strings.Contains(issueHash, hashPart) { matches = append(matches, issue.ID) } } + // Prefer exact match over substring matches + if exactMatch != "" { + return exactMatch, nil + } + if len(matches) == 0 { return "", fmt.Errorf("no issue found matching %q", input) } diff --git a/internal/utils/id_parser_test.go b/internal/utils/id_parser_test.go index 5c265983..bb1b4606 100644 --- a/internal/utils/id_parser_test.go +++ b/internal/utils/id_parser_test.go @@ -90,6 +90,21 @@ func TestResolvePartialID(t *testing.T) { Priority: 1, IssueType: types.TypeTask, } + // Test hierarchical IDs - parent and child + parentIssue := &types.Issue{ + ID: "offlinebrew-3d0", + Title: "Parent Epic", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeEpic, + } + childIssue := &types.Issue{ + ID: "offlinebrew-3d0.1", + Title: "Child Task", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + } if err := store.CreateIssue(ctx, issue1, "test"); err != nil { t.Fatal(err) @@ -100,6 +115,12 @@ func TestResolvePartialID(t *testing.T) { if err := store.CreateIssue(ctx, issue3, "test"); err != nil { t.Fatal(err) } + if err := store.CreateIssue(ctx, parentIssue, "test"); err != nil { + t.Fatal(err) + } + if err := store.CreateIssue(ctx, childIssue, "test"); err != nil { + t.Fatal(err) + } // Set config for prefix if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { @@ -149,6 +170,16 @@ func TestResolvePartialID(t *testing.T) { input: "bd-1", expected: "bd-1", // Will match exactly, not ambiguously }, + { + name: "exact match parent ID with hierarchical child - gh-316", + input: "offlinebrew-3d0", + expected: "offlinebrew-3d0", // Should match exactly, not be ambiguous with offlinebrew-3d0.1 + }, + { + name: "exact match parent without prefix - gh-316", + input: "3d0", + expected: "offlinebrew-3d0", // Should still prefer exact hash match + }, } for _, tt := range tests { From 59e635dbc798236d88638a400ef50fca5af6e0d4 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 14:07:55 -0800 Subject: [PATCH 10/13] bd sync: 2025-11-15 14:07:55 --- .beads/beads.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index d8b43ffe..30dc6671 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -337,6 +337,7 @@ {"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:43:11.49933-08:00","closed_at":"2025-11-15T12:43:11.49933-08:00","source_repo":"."} {"id":"bd-d9e0","content_hash":"de4e01414f8863b63cb693a709048b85c3f4417f03e7d7b2528560076be0e1f7","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-02T12:32:00.159298-08:00","closed_at":"2025-11-02T12:32:00.1593-08:00","source_repo":"."} {"id":"bd-dcd6f14b","content_hash":"c07a4b8a39e6e81513278ee335fe14aa767cbcba72e3b511cfd95705053483b1","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00","source_repo":"."} +{"id":"bd-dd6f6d26","content_hash":"dbcecb8b95f9f2939d97c61bd8cbe331bea866f47600bded213d3122e311c356","title":"Fix autoimport tests for content-hash collision scoring","description":"## Overview\nThree autoimport tests are failing after bd-cbed9619.4 because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.\n\n## Failing Tests\n1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved\n2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved \n3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior\n\n## Root Cause\nThese tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.\n\n## Example\nOld behavior: Issue with more references would be kept\nNew behavior: Issue with lexicographically lower content hash is kept\n\n## Solution\nUpdate each test to:\n1. Verify the new content-hash based behavior is correct\n2. Check that the remapped issue (not necessarily local/remote) has the expected content\n3. Ensure dependencies are preserved on the correct remapped issue\n\n## Acceptance Criteria\n- All three autoimport tests pass\n- Tests verify content-hash determinism (same collision always resolves the same way)\n- Tests check dependency preservation on remapped issues\n- Test documentation explains content-hash scoring expectations\n\n## Files to Modify\n- `cmd/bd/autoimport_collision_test.go`\n\n## Testing\nRun: `go test ./cmd/bd -run \"TestAutoImport.*Collision\" -v`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-08T03:09:48.253086-08:00","updated_at":"2025-11-08T03:09:48.253086-08:00","closed_at":"2025-11-08T02:28:35.317704-08:00","source_repo":".","dependencies":[{"issue_id":"bd-dd6f6d26","depends_on_id":"bd-cbed9619.4","type":"discovered-from","created_at":"2025-10-28T19:12:56.345276-07:00","created_by":"daemon"}]} {"id":"bd-de0h","content_hash":"8b8b43683607e73012cf8bd7cf8631c6ae34498d0c93ca5b77d3f68944c8088d","title":"bd message: Add HTTP client timeout to prevent hangs","description":"HTTP client in `sendAgentMailRequest` uses default http.Post with no timeout.\n\n**Location:** cmd/bd/message.go:181\n\n**Problem:**\n- Can hang indefinitely if server is unresponsive\n- No way to cancel stuck requests\n- Poor UX in flaky networks\n\n**Fix:**\n```go\nclient := \u0026http.Client{Timeout: 30 * time.Second}\nresp, err := client.Post(url, \"application/json\", bytes.NewReader(reqBody))\n```\n\n**Impact:** Production reliability and security issue","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T12:54:24.942645-08:00","updated_at":"2025-11-08T12:56:59.948929-08:00","closed_at":"2025-11-08T12:56:59.948929-08:00","source_repo":".","dependencies":[{"issue_id":"bd-de0h","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:54.860847-08:00","created_by":"daemon"}]} {"id":"bd-df11","content_hash":"9d688c3fe5f4994ab29ed22c8c4ae467f2069c4cbb676a2168303b2ffcba48c4","title":"Add import metrics for external_ref matching statistics","description":"Add observability for external_ref matching behavior during imports to help debug and optimize import operations.\n\nMetrics to track:\n- Number of issues matched by external_ref\n- Number of issues matched by ID\n- Number of issues matched by content hash\n- Number of external_ref updates vs creates\n- Average import time with vs without external_ref\n\nOutput format:\n- Add to ImportResult struct\n- Include in import command output\n- Consider structured logging\n\nUse cases:\n- Debugging slow imports\n- Understanding match distribution\n- Optimizing import performance\n\nRelated: bd-1022","status":"closed","priority":4,"issue_type":"chore","created_at":"2025-11-02T15:32:46.157899-08:00","updated_at":"2025-11-08T03:54:04.856564-08:00","closed_at":"2025-11-08T02:20:01.01371-08:00","source_repo":"."} {"id":"bd-df190564","content_hash":"4966d22faf43b7de1b27315f85365d7ed896741e4e589ed01ee16f4c2f600a24","title":"bd repair-deps - Orphaned dependency cleaner","description":"Find and fix orphaned dependency references.\n\nImplementation:\n- Scan all issues for dependencies pointing to non-existent issues\n- Report orphaned refs\n- Auto-fix with --fix flag\n- Interactive mode with --interactive\n\nFiles: cmd/bd/repair_deps.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T19:42:29.852745-07:00","updated_at":"2025-10-31T18:24:19.418221-07:00","closed_at":"2025-10-31T18:24:19.418221-07:00","source_repo":"."} From 3c9cf957ade941803210c213334f3a85e21acaea Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 14:08:03 -0800 Subject: [PATCH 11/13] Update bd JSONL Amp-Thread-ID: https://ampcode.com/threads/T-5358ea57-e9ea-49e9-aedf-7044ebf8b52a Co-authored-by: Amp --- .beads/beads.jsonl | 1 - 1 file changed, 1 deletion(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 30dc6671..d8b43ffe 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -337,7 +337,6 @@ {"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:43:11.49933-08:00","closed_at":"2025-11-15T12:43:11.49933-08:00","source_repo":"."} {"id":"bd-d9e0","content_hash":"de4e01414f8863b63cb693a709048b85c3f4417f03e7d7b2528560076be0e1f7","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-02T12:32:00.159298-08:00","closed_at":"2025-11-02T12:32:00.1593-08:00","source_repo":"."} {"id":"bd-dcd6f14b","content_hash":"c07a4b8a39e6e81513278ee335fe14aa767cbcba72e3b511cfd95705053483b1","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00","source_repo":"."} -{"id":"bd-dd6f6d26","content_hash":"dbcecb8b95f9f2939d97c61bd8cbe331bea866f47600bded213d3122e311c356","title":"Fix autoimport tests for content-hash collision scoring","description":"## Overview\nThree autoimport tests are failing after bd-cbed9619.4 because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.\n\n## Failing Tests\n1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved\n2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved \n3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior\n\n## Root Cause\nThese tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.\n\n## Example\nOld behavior: Issue with more references would be kept\nNew behavior: Issue with lexicographically lower content hash is kept\n\n## Solution\nUpdate each test to:\n1. Verify the new content-hash based behavior is correct\n2. Check that the remapped issue (not necessarily local/remote) has the expected content\n3. Ensure dependencies are preserved on the correct remapped issue\n\n## Acceptance Criteria\n- All three autoimport tests pass\n- Tests verify content-hash determinism (same collision always resolves the same way)\n- Tests check dependency preservation on remapped issues\n- Test documentation explains content-hash scoring expectations\n\n## Files to Modify\n- `cmd/bd/autoimport_collision_test.go`\n\n## Testing\nRun: `go test ./cmd/bd -run \"TestAutoImport.*Collision\" -v`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-08T03:09:48.253086-08:00","updated_at":"2025-11-08T03:09:48.253086-08:00","closed_at":"2025-11-08T02:28:35.317704-08:00","source_repo":".","dependencies":[{"issue_id":"bd-dd6f6d26","depends_on_id":"bd-cbed9619.4","type":"discovered-from","created_at":"2025-10-28T19:12:56.345276-07:00","created_by":"daemon"}]} {"id":"bd-de0h","content_hash":"8b8b43683607e73012cf8bd7cf8631c6ae34498d0c93ca5b77d3f68944c8088d","title":"bd message: Add HTTP client timeout to prevent hangs","description":"HTTP client in `sendAgentMailRequest` uses default http.Post with no timeout.\n\n**Location:** cmd/bd/message.go:181\n\n**Problem:**\n- Can hang indefinitely if server is unresponsive\n- No way to cancel stuck requests\n- Poor UX in flaky networks\n\n**Fix:**\n```go\nclient := \u0026http.Client{Timeout: 30 * time.Second}\nresp, err := client.Post(url, \"application/json\", bytes.NewReader(reqBody))\n```\n\n**Impact:** Production reliability and security issue","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T12:54:24.942645-08:00","updated_at":"2025-11-08T12:56:59.948929-08:00","closed_at":"2025-11-08T12:56:59.948929-08:00","source_repo":".","dependencies":[{"issue_id":"bd-de0h","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:54.860847-08:00","created_by":"daemon"}]} {"id":"bd-df11","content_hash":"9d688c3fe5f4994ab29ed22c8c4ae467f2069c4cbb676a2168303b2ffcba48c4","title":"Add import metrics for external_ref matching statistics","description":"Add observability for external_ref matching behavior during imports to help debug and optimize import operations.\n\nMetrics to track:\n- Number of issues matched by external_ref\n- Number of issues matched by ID\n- Number of issues matched by content hash\n- Number of external_ref updates vs creates\n- Average import time with vs without external_ref\n\nOutput format:\n- Add to ImportResult struct\n- Include in import command output\n- Consider structured logging\n\nUse cases:\n- Debugging slow imports\n- Understanding match distribution\n- Optimizing import performance\n\nRelated: bd-1022","status":"closed","priority":4,"issue_type":"chore","created_at":"2025-11-02T15:32:46.157899-08:00","updated_at":"2025-11-08T03:54:04.856564-08:00","closed_at":"2025-11-08T02:20:01.01371-08:00","source_repo":"."} {"id":"bd-df190564","content_hash":"4966d22faf43b7de1b27315f85365d7ed896741e4e589ed01ee16f4c2f600a24","title":"bd repair-deps - Orphaned dependency cleaner","description":"Find and fix orphaned dependency references.\n\nImplementation:\n- Scan all issues for dependencies pointing to non-existent issues\n- Report orphaned refs\n- Auto-fix with --fix flag\n- Interactive mode with --interactive\n\nFiles: cmd/bd/repair_deps.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T19:42:29.852745-07:00","updated_at":"2025-10-31T18:24:19.418221-07:00","closed_at":"2025-10-31T18:24:19.418221-07:00","source_repo":"."} From 33b913f3dd0e2634e4d72f5fbaf983cf6425493a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 14:13:53 -0800 Subject: [PATCH 12/13] bd sync: 2025-11-15 14:13:53 --- .beads/beads.jsonl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index d8b43ffe..b5518097 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -102,7 +102,7 @@ {"id":"bd-3e9ddc31","content_hash":"4e03660281dbe2c069617fc8d723d546d6e5eb386142c0359b862747867a1b90","title":"Replace getStorageForRequest with Direct Access","description":"Replace all getStorageForRequest(req) calls with s.storage","acceptance_criteria":"- No references to getStorageForRequest() in codebase (except in deleted file)\n- All handlers use s.storage directly\n- Code compiles without errors\n\nFiles to update:\n- internal/rpc/server_issues_epics.go (~8 calls)\n- internal/rpc/server_labels_deps_comments.go (~4 calls)\n- internal/rpc/server_compact.go (~2 calls)\n- internal/rpc/server_export_import_auto.go (~2 calls)\n- internal/rpc/server_routing_validation_diagnostics.go (~1 call)\n\nPattern: store, err := s.getStorageForRequest(req) → store := s.storage","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T23:20:10.393759-07:00","updated_at":"2025-10-30T17:12:58.21613-07:00","closed_at":"2025-10-28T14:08:38.06721-07:00","source_repo":"."} {"id":"bd-3ee2c7e9","content_hash":"80a0101dd9082c194cd4f138dc116c0fc14d178d8afacb6b5b61ee863ee2eea7","title":"Add \"bd daemons\" command for multi-daemon management","description":"Add a new \"bd daemons\" command with subcommands to manage daemon processes across all beads repositories/worktrees. Should show all running daemons with metadata (version, workspace, uptime, last sync), allow stopping/restarting individual daemons, auto-clean stale processes, view logs, and show exclusive lock status.","design":"Subcommands:\n- list: Show all running daemons with metadata (workspace, PID, version, socket path, uptime, last activity, exclusive lock status)\n- stop \u003cpath|pid\u003e: Gracefully stop a specific daemon\n- restart \u003cpath|pid\u003e: Stop and restart daemon\n- killall: Emergency stop all daemons\n- health: Verify each daemon responds to ping\n- logs \u003cpath\u003e: View daemon logs\n\nFeatures:\n- Auto-clean stale sockets/dead processes\n- Discovery: Scan for .beads/bd.sock files + running processes\n- Communication: Use existing socket protocol, add GET /status endpoint for metadata","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-10-26T16:53:40.970042-07:00","updated_at":"2025-11-02T17:12:34.621017-08:00","closed_at":"2025-11-02T17:12:34.62102-08:00","source_repo":"."} {"id":"bd-3f6a","content_hash":"7fef5b08bbb32c4f4ab7d906539a765b01f1a74d0bb71102c954a5bdec4b442e","title":"Add concurrent import race condition tests","description":"Currently no tests verify behavior when multiple clones import simultaneously with external_ref matching.\n\nScenarios to test:\n1. Two clones import same external_ref update at same time\n2. Clone A imports while Clone B updates same issue\n3. Verify transaction isolation prevents corruption\n4. Document expected behavior (last-write-wins vs timestamp-based)\n\nRelated: bd-1022\nFiles: internal/importer/external_ref_test.go","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-02T15:32:11.286956-08:00","updated_at":"2025-11-02T16:11:16.127009-08:00","closed_at":"2025-11-02T16:11:16.127009-08:00","source_repo":"."} -{"id":"bd-3f80d9e0","content_hash":"faa0e91ddc5cafa96a2e2b708a57eb84ecf0f74e236784ed1f64545ed8d6200b","title":"Improve internal/daemon test coverage (currently 22.5%)","description":"Daemon functionality needs better coverage:\n- Auto-start behavior\n- Lock file management\n- Discovery mechanisms\n- Connection handling\n- Error recovery\n\nCurrent coverage: 58.3% (improved from 22.5% as of Nov 2025)","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-29T14:06:30.832728-07:00","updated_at":"2025-11-08T17:56:47.734573-08:00","source_repo":"."} +{"id":"bd-3f80d9e0","content_hash":"faa0e91ddc5cafa96a2e2b708a57eb84ecf0f74e236784ed1f64545ed8d6200b","title":"Improve internal/daemon test coverage (currently 22.5%)","description":"Daemon functionality needs better coverage:\n- Auto-start behavior\n- Lock file management\n- Discovery mechanisms\n- Connection handling\n- Error recovery\n\nCurrent coverage: 58.3% (improved from 22.5% as of Nov 2025)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T14:06:30.832728-07:00","updated_at":"2025-11-15T14:13:47.303529-08:00","closed_at":"2025-11-15T14:13:47.303529-08:00","source_repo":"."} {"id":"bd-40a0","content_hash":"75611f4fb108e11cb4b98ded732fe94dd41ed700d8058b419e6fc796cf152391","title":"bd doctor should check for multiple DBs, multiple JSONLs, daemon health","description":"","design":"\nCurrently bd doctor only checks:\n- .beads/ directory exists\n- Database version vs CLI version \n- ID format (hash vs sequential)\n- CLI version vs latest GitHub release\n\nIt should ALSO check for operational issues that cause silent failures:\n\n1. **Multiple database files** (*.db excluding backups and vc.db)\n - Warn if multiple *.db files found (ambiguous which to use)\n - Suggest running 'bd migrate' or manually removing old DBs\n\n2. **Multiple JSONL files** \n - Check for both issues.jsonl and beads.jsonl\n - Warn about ambiguity, suggest standardizing on one\n\n3. **Daemon health** (integrate bd daemons health)\n - Check if daemon running for this workspace\n - Detect version mismatches between daemon and CLI\n - Detect zombie daemons (running but unresponsive)\n - Detect stale daemon.pid files\n\n4. **Database-JSONL sync issues**\n - Check if JSONL is newer than last import\n - Warn if they're out of sync\n\n5. **Permissions issues**\n - Check if .beads/ directory is writable\n - Check if database file is readable/writable\n\nImplementation approach:\n- Add new check functions to doctor.go\n- Reuse logic from bd daemons health\n- Keep checks fast (\u003c 1 second total)\n- Output actionable fixes for each issue\n","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-10-31T21:16:47.042913-07:00","updated_at":"2025-10-31T21:21:27.093525-07:00","closed_at":"2025-10-31T21:21:27.093525-07:00","source_repo":"."} {"id":"bd-4462","content_hash":"a3f7ca75994ca4efb8b5b6ae47ecf5b8544ad33510e4c6f72663efd8c2737f74","title":"Test basic bd commands in WASM (init, create, list)","description":"Compile and verify basic bd functionality works in WASM:\n- Test bd init --quiet\n- Test bd create with simple issue\n- Test bd list --json output\n- Verify SQLite database creation and queries work\n- Document any runtime issues or workarounds needed","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-02T21:58:07.291771-08:00","updated_at":"2025-11-02T23:07:10.273212-08:00","closed_at":"2025-11-02T23:07:10.273212-08:00","source_repo":".","dependencies":[{"issue_id":"bd-4462","depends_on_id":"bd-44d0","type":"parent-child","created_at":"2025-11-02T22:23:49.448668-08:00","created_by":"stevey"},{"issue_id":"bd-4462","depends_on_id":"bd-b4b0","type":"blocks","created_at":"2025-11-02T22:23:55.596771-08:00","created_by":"stevey"}]} {"id":"bd-44d0","content_hash":"a20f23c823907e546f852c1bbb0c09166100b2569d4a1192f0a7288ee5d918e8","title":"WASM port of bd for Claude Code Web sandboxes","description":"Enable beads to work in Claude Code Web sandboxes by compiling bd to WebAssembly.\n\n## Problem\nClaude Code Web sandboxes cannot install bd CLI due to network restrictions:\n- GitHub releases return 403\n- go install fails with DNS errors\n- Binary cannot be downloaded\n\n## Solution\nCompile bd Go codebase to WASM, publish to npm as drop-in replacement.\n\n## Technical Approach\n- Use GOOS=js GOARCH=wasm to compile bd\n- modernc.org/sqlite already supports js/wasm target\n- Publish to npm as bd-wasm package\n- Full feature parity with bd CLI\n\n## Success Criteria\n- bd-wasm installs via npm in web sandbox\n- All core bd commands work identically\n- JSONL output matches native bd\n- Performance within 2x of native","notes":"WASM port abandoned - Claude Code Web has full VMs not browser restrictions. Better: npm + native binary","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-11-02T18:32:27.660794-08:00","updated_at":"2025-11-02T23:36:38.679515-08:00","closed_at":"2025-11-02T23:36:38.679515-08:00","source_repo":"."} @@ -154,7 +154,7 @@ {"id":"bd-63e9","content_hash":"7c709804b6d15ce63897344b0674dfae6a4fe97e3ae2768585e2a3407484bad0","title":"Fix Nix flake build test failures","description":"Nix build is failing during test phase with same test errors as Windows.\n\n**Error:**\n```\nerror: Cannot build '/nix/store/rgyi1j44dm6ylrzlg2h3z97axmfq9hzr-beads-0.9.9.drv'.\nReason: builder failed with exit code 1.\nFAIL github.com/steveyegge/beads/cmd/bd 16.141s\n```\n\nThis may be related to test environment setup or the same issues affecting Windows tests.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-02T09:29:37.2851-08:00","updated_at":"2025-11-04T11:10:23.531386-08:00","closed_at":"2025-11-04T11:10:23.531389-08:00","source_repo":".","dependencies":[{"issue_id":"bd-63e9","depends_on_id":"bd-1231","type":"blocks","created_at":"2025-11-02T09:29:37.28618-08:00","created_by":"stevey"}]} {"id":"bd-64c05d00","content_hash":"ab391b33353bfe693ef571e9fcb4a222eb5289a07e60258bd88c29565e85c4d0","title":"Multi-clone collision resolution testing and documentation","description":"Epic to track improvements to multi-clone collision resolution based on ultrathinking analysis of-3d844c58 and [deleted:bd-71107098].\n\nCurrent state:\n- 2-clone collision resolution is SOUND and working correctly\n- Hash-based deterministic collision resolution works\n- Test fails due to timestamp comparison, not actual logic issues\n\nWork needed:\n1. Fix TestTwoCloneCollision to compare content not timestamps\n2. Add TestThreeCloneCollision for regression protection\n3. Document 3-clone ID non-determinism as known behavior","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-10-28T17:58:38.316626-07:00","updated_at":"2025-11-05T00:32:09.153134-08:00","closed_at":"2025-11-04T11:10:23.531681-08:00","source_repo":"."} {"id":"bd-64c05d00.1","content_hash":"4ed407ab9518dbf45a4097460354d7857dd53881b913ad770def31d46dc6dc15","title":"Fix TestTwoCloneCollision to compare content not timestamps","description":"The test at beads_twoclone_test.go:204-207 currently compares full JSON output including timestamps, causing false negative failures.\n\nCurrent behavior:\n- Both clones converge to identical semantic content\n- Clone A: test-2=\"Issue from clone A\", test-1=\"Issue from clone B\"\n- Clone B: test-1=\"Issue from clone B\", test-2=\"Issue from clone A\"\n- Titles match IDs correctly, no data corruption\n- Only timestamps differ (expected and acceptable)\n\nFix needed:\n- Replace exact JSON comparison with content-aware comparison\n- Normalize or ignore timestamp fields when asserting convergence\n- Test should PASS after this fix\n\nThis blocks completion of bd-71107098.","acceptance_criteria":"- Test compares issue content (title, description, status, priority) not timestamps\n- TestTwoCloneCollision passes\n- Both clones shown to have identical semantic content\n- Timestamps explicitly documented as acceptable difference","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T17:58:52.057194-07:00","updated_at":"2025-10-30T17:12:58.226744-07:00","closed_at":"2025-10-28T18:01:38.751895-07:00","source_repo":".","dependencies":[{"issue_id":"bd-64c05d00.1","depends_on_id":"bd-64c05d00","type":"parent-child","created_at":"2025-10-28T17:58:52.058202-07:00","created_by":"stevey"},{"issue_id":"bd-64c05d00.1","depends_on_id":"bd-71107098","type":"blocks","created_at":"2025-10-28T17:58:52.05873-07:00","created_by":"stevey"}]} -{"id":"bd-64c05d00.2","content_hash":"19918bb968b59b1e13e87504b2f02a826cd1dc4700f2cf3997500a463c01a2d6","title":"Document 3-clone ID non-determinism in collision resolution","description":"Document the known behavior of 3+ way collision resolution where ID assignments may vary based on sync order, even though content always converges correctly.\n\nUpdates needed:\n- Update bd-71107098 notes to mark 2-clone case as solved\n- Document 3-clone ID non-determinism as known limitation\n- Add explanation to ADVANCED.md or collision resolution docs\n- Explain why this happens (pairwise hash comparison is deterministic, but multi-way ID allocation uses sync-order dependent counters)\n- Clarify trade-offs: content convergence ✅ vs ID stability ❌\n\nKey points to document:\n- Hash-based resolution is pairwise deterministic\n- Content always converges correctly (all issues present with correct data)\n- Numeric ID assignments in 3+ way collisions depend on sync order\n- This is acceptable for most use cases (content convergence is primary goal)\n- Full determinism would require complex multi-way comparison","acceptance_criteria":"- bd-71107098 updated with notes about 2-clone solution being complete\n- 3-clone ID non-determinism documented in ADVANCED.md or similar\n- Explanation includes why it happens and trade-offs\n- Links to TestThreeCloneCollision as demonstration\n- Users understand this is expected behavior, not a bug","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-28T17:59:21.93014-07:00","updated_at":"2025-10-30T17:12:58.227375-07:00","source_repo":".","dependencies":[{"issue_id":"bd-64c05d00.2","depends_on_id":"bd-64c05d00","type":"parent-child","created_at":"2025-10-28T17:59:21.938709-07:00","created_by":"stevey"}]} +{"id":"bd-64c05d00.2","content_hash":"19918bb968b59b1e13e87504b2f02a826cd1dc4700f2cf3997500a463c01a2d6","title":"Document 3-clone ID non-determinism in collision resolution","description":"Document the known behavior of 3+ way collision resolution where ID assignments may vary based on sync order, even though content always converges correctly.\n\nUpdates needed:\n- Update bd-71107098 notes to mark 2-clone case as solved\n- Document 3-clone ID non-determinism as known limitation\n- Add explanation to ADVANCED.md or collision resolution docs\n- Explain why this happens (pairwise hash comparison is deterministic, but multi-way ID allocation uses sync-order dependent counters)\n- Clarify trade-offs: content convergence ✅ vs ID stability ❌\n\nKey points to document:\n- Hash-based resolution is pairwise deterministic\n- Content always converges correctly (all issues present with correct data)\n- Numeric ID assignments in 3+ way collisions depend on sync order\n- This is acceptable for most use cases (content convergence is primary goal)\n- Full determinism would require complex multi-way comparison","acceptance_criteria":"- bd-71107098 updated with notes about 2-clone solution being complete\n- 3-clone ID non-determinism documented in ADVANCED.md or similar\n- Explanation includes why it happens and trade-offs\n- Links to TestThreeCloneCollision as demonstration\n- Users understand this is expected behavior, not a bug","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T17:59:21.93014-07:00","updated_at":"2025-11-15T14:13:47.304584-08:00","closed_at":"2025-11-15T14:13:47.304584-08:00","source_repo":".","dependencies":[{"issue_id":"bd-64c05d00.2","depends_on_id":"bd-64c05d00","type":"parent-child","created_at":"2025-10-28T17:59:21.938709-07:00","created_by":"stevey"}]} {"id":"bd-64c05d00.3","content_hash":"e006b991353a26f949bc3ae4476849ef785f399f6aca866586eb6fa03d243b35","title":"Add TestThreeCloneCollision for regression protection","description":"Add a 3-clone collision test to document behavior and provide regression protection.\n\nPurpose:\n- Verify content convergence regardless of sync order\n- Document the ID non-determinism behavior (IDs may be assigned differently based on sync order)\n- Provide regression protection for multi-way collisions\n\nTest design:\n- 3 clones create same ID with different content\n- Test two different sync orders (A→B→C vs C→A→B)\n- Assert content sets match (ignore specific ID assignments)\n- Add comment explaining ID non-determinism is expected behavior\n\nKnown limitation:\n- Content always converges correctly (all issues present with correct titles)\n- Numeric ID assignments (test-2 vs test-3) depend on sync order\n- This is acceptable if content convergence is the primary goal","acceptance_criteria":"- TestThreeCloneCollision added to beads_twoclone_test.go (or new file)\n- Tests 3 clones with same ID collision\n- Tests two different sync orders\n- Asserts content convergence (all issues present, correct titles)\n- Documents ID non-determinism in test comments\n- Test passes consistently","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T17:59:05.941735-07:00","updated_at":"2025-10-30T17:12:58.227089-07:00","closed_at":"2025-10-28T18:09:12.717604-07:00","source_repo":".","dependencies":[{"issue_id":"bd-64c05d00.3","depends_on_id":"bd-64c05d00","type":"parent-child","created_at":"2025-10-28T17:59:05.942783-07:00","created_by":"stevey"}]} {"id":"bd-64z4","content_hash":"d707d871411b33bd6268d2a83ec6cc7696d9b38c86510dc7cb7a073fb2a8cfa3","title":"Assigned issue","description":"","status":"closed","priority":1,"issue_type":"task","assignee":"testuser","created_at":"2025-11-07T19:04:24.201309-08:00","updated_at":"2025-11-07T22:07:17.344151-08:00","closed_at":"2025-11-07T21:55:09.427387-08:00","source_repo":"."} {"id":"bd-6545","content_hash":"1d49e101cae39bc8115422fdef1e2cde999e88e176e5bc5614a5aefdbcd174da","title":"Update daemon commit logic for separate branch","description":"Modify daemon to use worktree for commits when sync.branch configured.\n\nTasks:\n- Update internal/daemon/server_export_import_auto.go\n- Detect sync.branch configuration\n- Ensure worktree exists before commit\n- Sync JSONL to worktree\n- Commit in worktree context\n- Push to configured branch\n- Fallback to current behavior if sync.branch not set\n- Handle git errors (network, permissions, conflicts)\n\nEstimated effort: 3-4 days","acceptance_criteria":"- When sync.branch configured, commits go to separate branch\n- When sync.branch not configured, commits to current branch (backward compatible)\n- No disruption to primary worktree\n- Git errors handled gracefully with retry\n- Daemon logs show which branch was used","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-02T15:22:35.598861-08:00","updated_at":"2025-11-04T11:10:23.531964-08:00","closed_at":"2025-11-04T11:10:23.531966-08:00","source_repo":".","dependencies":[{"issue_id":"bd-6545","depends_on_id":"bd-a101","type":"parent-child","created_at":"2025-11-02T15:22:48.375661-08:00","created_by":"stevey"}]} @@ -240,7 +240,7 @@ {"id":"bd-968f","content_hash":"41376d2927c9107898e7cb72aaa17a76d8b44692a78aa201123e8b2a0404ce34","title":"Add unit tests for config modes","description":"Test all four orphan_handling modes: strict (fails), resurrect (creates tombstone), skip (logs warning), allow (imports orphan). Verify error messages and logging output for each mode.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-04T12:32:21.367129-08:00","updated_at":"2025-11-05T00:44:27.948775-08:00","closed_at":"2025-11-05T00:44:27.948777-08:00","source_repo":"."} {"id":"bd-9826b69a","content_hash":"66b54987232cdf53d3b69004af2488330023ed8adb576257750a52550aa5ee59","title":"CRDT-based architecture for guaranteed convergence (v2.0)","description":"## Vision\nRedesign beads around Conflict-Free Replicated Data Types (CRDTs) to provide mathematical guarantees for N-way collision resolution at arbitrary scale.\n\n## Current Limitations\n- Content-hash based collision resolution fails at 5+ clones\n- Non-deterministic convergence in multi-round scenarios\n- UNIQUE constraint violations during rename operations\n- No formal proof of convergence properties\n\n## CRDT Benefits\n- Provably convergent (Strong Eventual Consistency)\n- Commutative/Associative/Idempotent operations\n- No coordination required between clones\n- Scales to 100+ concurrent workers\n- Well-understood mathematical foundations\n\n## Proposed Architecture\n\n### 1. UUID-Based IDs\nReplace sequential IDs with UUIDs:\n- Current: bd-1c63eb84, bd-9063acda, bd-4d80b7b1\n- CRDT: bd-a1b2c3d4-e5f6-7890-abcd-ef1234567890\n- Human aliases maintained separately: #42 maps to UUID\n\n### 2. Last-Write-Wins (LWW) Elements\nEach field becomes an LWW register:\n- title: (timestamp, clone_id, value)\n- status: (timestamp, clone_id, value)\n- Deterministic conflict resolution via Lamport timestamp + clone_id tiebreaker\n\n### 3. Operation Log\nTrack all operations as CRDT ops:\n- CREATE(uuid, timestamp, clone_id, fields)\n- UPDATE(uuid, field, timestamp, clone_id, value)\n- DELETE(uuid, timestamp, clone_id) - tombstone, not hard delete\n\n### 4. Sync as Merge\nSyncing becomes merging two CRDT states:\n- No merge conflicts possible\n- Deterministic merge function\n- Guaranteed convergence\n\n## Implementation Phases\n\n### Phase 1: Research \u0026 Design (4 weeks)\n- Study existing CRDT implementations (Automerge, Yjs, Loro)\n- Design schema for CRDT-based issue tracking\n- Prototype LWW-based Issue CRDT\n- Benchmark performance vs current system\n\n### Phase 2: Parallel Implementation (6 weeks)\n- Implement CRDT storage layer alongside SQLite\n- Build conversion tools: SQLite ↔ CRDT\n- Maintain backward compatibility with v1.x format\n- Migration path for existing databases\n\n### Phase 3: Testing \u0026 Validation (4 weeks)\n- Formal verification of convergence properties\n- Stress testing with 100+ clone scenario\n- Performance profiling and optimization\n- Documentation and examples\n\n### Phase 4: Migration \u0026 Rollout (4 weeks)\n- Release v2.0-beta with CRDT backend\n- Gradual migration from v1.x\n- Monitoring and bug fixes\n- Final v2.0 release\n\n## Risks \u0026 Mitigations\n\n**Risk 1: Performance overhead**\n- Mitigation: Benchmark early, optimize hot paths\n- CRDTs can be slower than append-only logs\n- May need compaction strategy\n\n**Risk 2: Storage bloat**\n- Mitigation: Implement operation log compaction\n- Tombstone garbage collection for deleted issues\n- Periodic snapshots to reduce log size\n\n**Risk 3: Breaking changes**\n- Mitigation: Maintain v1.x compatibility layer\n- Gradual migration tools\n- Dual-mode operation during transition\n\n**Risk 4: Complexity**\n- Mitigation: Use battle-tested CRDT libraries\n- Comprehensive documentation\n- Clear migration guide\n\n## Success Criteria\n- 100-clone collision test passes without failures\n- Formal proof of convergence properties\n- Performance within 2x of current system\n- Zero manual conflict resolution required\n- Backward compatible with v1.x databases\n\n## Timeline\n18-20 weeks total (4-5 months)\n\n## References\n- Automerge: https://automerge.org\n- Yjs: https://docs.yjs.dev\n- Loro: https://loro.dev\n- CRDT theory: Shapiro et al, A comprehensive study of CRDTs\n- Related issues: bd-0dcea000, bd-4d7fca8a, bd-6221bdcd","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-29T20:48:00.267736-07:00","updated_at":"2025-10-31T20:06:44.60536-07:00","closed_at":"2025-10-31T20:06:44.60536-07:00","source_repo":"."} {"id":"bd-98c4e1fa","content_hash":"24b80fab2399079003fd39235e3c7992d404577f8794cc367552340244308636","title":"Event-driven daemon architecture","description":"Replace 5-second polling sync loop with event-driven architecture that reacts instantly to changes. Eliminates stale data issues while reducing CPU ~60%. Key components: FileWatcher (fsnotify), Debouncer (500ms), RPC mutation events, optional git hooks. Target latency: \u003c500ms (vs 5000ms). See event_driven_daemon.md for full design.","notes":"## Implementation Progress\n\n**Completed:**\n1. ✅ Mutation events infrastructure (bd-143 equivalent)\n - MutationEvent channel in RPC server\n - Events emitted for all write operations: create, update, close, label add/remove, dep add/remove, comment add\n - Non-blocking emission with dropped event counter\n\n2. ✅ FileWatcher with fsnotify (bd-b0c7f7ef related)\n - Watches .beads/issues.jsonl and .git/refs/heads\n - 500ms debounce\n - Polling fallback if fsnotify unavailable\n\n3. ✅ Debouncer (bd-144 equivalent)\n - 500ms debounce for both export and import triggers\n - Thread-safe trigger/cancel\n\n4. ✅ Separate export-only and import-only functions\n - createExportFunc(): exports + optional commit/push (no pull/import)\n - createAutoImportFunc(): pull + import (no export)\n - Target latency \u003c500ms achieved by avoiding full sync\n\n5. ✅ Dropped events safety net (bd-eef03e0a related)\n - Atomic counter tracks dropped mutation events\n - 60-second health check triggers export if events were dropped\n - Prevents silent data loss from event storms\n\n**Still Needed:**\n- Platform-specific tests (bd-69bce74a)\n- Integration test for mutation→export latency (bd-140)\n- Unit tests for FileWatcher (bd-b0c7f7ef)\n- Unit tests for Debouncer (bd-144)\n- Event storm stress test (bd-eef03e0a)\n- Documentation update (bd-142)\n\n**Next Steps:**\nAdd comprehensive test coverage before enabling events mode by default.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-10-29T21:19:36.203436-07:00","updated_at":"2025-10-30T17:12:58.197875-07:00","closed_at":"2025-10-29T15:53:34.022335-07:00","source_repo":"."} -{"id":"bd-98c4e1fa.1","content_hash":"6440d1ece0a91c8f49adc09aafa7a998b049bcd51f257125ad8bc0b7b03e317b","title":"Update AGENTS.md with event-driven mode","description":"Document BEADS_DAEMON_MODE env var. Explain opt-in during Phase 1. Add troubleshooting for watcher failures.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-29T23:05:13.986452-07:00","updated_at":"2025-10-31T20:36:49.381832-07:00","source_repo":".","dependencies":[{"issue_id":"bd-98c4e1fa.1","depends_on_id":"bd-98c4e1fa","type":"parent-child","created_at":"2025-10-29T21:19:36.206187-07:00","created_by":"import-remap"},{"issue_id":"bd-98c4e1fa.1","depends_on_id":"bd-0e1f2b1b","type":"parent-child","created_at":"2025-10-31T19:38:09.131439-07:00","created_by":"stevey"}]} +{"id":"bd-98c4e1fa.1","content_hash":"6440d1ece0a91c8f49adc09aafa7a998b049bcd51f257125ad8bc0b7b03e317b","title":"Update AGENTS.md with event-driven mode","description":"Document BEADS_DAEMON_MODE env var. Explain opt-in during Phase 1. Add troubleshooting for watcher failures.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T23:05:13.986452-07:00","updated_at":"2025-11-15T14:13:47.304404-08:00","closed_at":"2025-11-15T14:13:47.304404-08:00","source_repo":".","dependencies":[{"issue_id":"bd-98c4e1fa.1","depends_on_id":"bd-98c4e1fa","type":"parent-child","created_at":"2025-10-29T21:19:36.206187-07:00","created_by":"import-remap"},{"issue_id":"bd-98c4e1fa.1","depends_on_id":"bd-0e1f2b1b","type":"parent-child","created_at":"2025-10-31T19:38:09.131439-07:00","created_by":"stevey"}]} {"id":"bd-9ae788be","content_hash":"22ad341d54105f9b2e9b7fecbafbca94100ea270b9ff8588e1fea6cf72603968","title":"Implement clone-scoped ID allocation to prevent N-way collisions","description":"## Problem\nCurrent ID allocation uses per-clone atomic counters (issue_counters table) that sync based on local database state. In N-way collision scenarios:\n- Clone B sees {test-1} locally, allocates test-2\n- Clone D sees {test-1, test-2, test-3} locally, allocates test-4\n- When same content gets assigned test-2 and test-4, convergence fails\n\nRoot cause: Each clone independently allocates IDs without global coordination, leading to overlapping assignments for the same content.\n\n## Solution\nAdd clone UUID to ID allocation to make every ID globally unique:\n\n**Current format:** `test-1`, `test-2`, `test-3`\n**New format:** `test-1-a7b3`, `test-2-a7b3`, `test-3-c4d9`\n\nWhere suffix is first 4 chars of clone UUID.\n\n## Implementation\n\n### 1. Add clone_identity table\n```sql\nCREATE TABLE clone_identity (\n clone_uuid TEXT PRIMARY KEY,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n```\n\n### 2. Modify getNextIDForPrefix()\n```go\nfunc (s *SQLiteStorage) getNextIDForPrefix(ctx context.Context, prefix string) (string, error) {\n cloneUUID := s.getOrCreateCloneUUID(ctx)\n shortUUID := cloneUUID[:4]\n \n nextNum := s.getNextCounterForPrefix(ctx, prefix)\n return fmt.Sprintf(\"%s-%d-%s\", prefix, nextNum, shortUUID), nil\n}\n```\n\n### 3. Update ID parsing logic\nAll places that parse IDs (utils.ExtractIssueNumber, etc.) need to handle new format.\n\n### 4. Migration strategy\n- Existing IDs remain unchanged (no suffix)\n- New IDs get clone suffix automatically\n- Display layer can hide suffix in UI: `bd-cb64c226.3-a7b3` → `#42`\n\n## Benefits\n- **Zero collision risk**: Same content in different clones gets different IDs\n- **Maintains readability**: Still sequential numbering within clone\n- **No coordination needed**: Works offline, no central authority\n- **Scales to 100+ clones**: 4-char hex = 65,536 unique clones\n\n## Concerns\n- ID format change may break existing integrations\n- Need migration path for existing databases\n- Display logic needs update to hide/show suffixes appropriately\n\n## Success Criteria\n- 10+ clone collision test passes without failures\n- Existing issues continue to work (backward compatibility)\n- Documentation updated with new ID format\n- Migration guide for v1.x → v2.x\n\n## Timeline\nMedium-term (v1.1-v1.2), 2-3 weeks implementation\n\n## References\n- Related to bd-e6d71828 (immediate fix)\n- See beads_nway_test.go for failing N-way tests","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-29T10:22:52.260524-07:00","updated_at":"2025-11-08T01:49:23.460028-08:00","closed_at":"2025-11-08T00:36:58.134558-08:00","source_repo":"."} {"id":"bd-9b13","content_hash":"9a17da93fb23cdcfcc294d2e7e00239973530ab8c5cc08f110112c9924ca94e1","title":"Backend task","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-03T19:11:59.359262-08:00","updated_at":"2025-11-05T00:25:06.484312-08:00","closed_at":"2025-11-05T00:25:06.484312-08:00","source_repo":".","labels":["backend","week1"]} {"id":"bd-9bsx","content_hash":"f84ca8560b9f09a14af959b4f567647aec050faaa9348775aa08955d913fe9e1","title":"Recurring dirty state after merge conflicts - bd sync keeps failing","description":"## Problem\n\n`bd sync` consistently fails with merge conflicts in `.beads/beads.jsonl`, creating a loop:\n1. User runs `bd sync`\n2. Git merge conflict occurs\n3. User resolves with `git checkout --theirs` (takes remote)\n4. Daemon auto-exports database state (which has local changes)\n5. JSONL becomes dirty again immediately\n6. Repeat\n\nThis has been happening for **weeks** and is extremely frustrating.\n\n## Root Cause\n\nThe recommended conflict resolution (`git checkout --theirs`) throws away local database state (comments, dependencies, closed issues). The daemon then immediately re-exports, creating a dirty state.\n\n## Current Workaround\n\nManual `bd export -o .beads/beads.jsonl \u0026\u0026 git add \u0026\u0026 git commit \u0026\u0026 git push` after every failed sync.\n\n## Example Session\n\n```bash\n$ bd sync\nCONFLICT (content): Merge conflict in .beads/beads.jsonl\n\n$ git checkout --theirs .beads/beads.jsonl \u0026\u0026 bd import \u0026\u0026 git add \u0026\u0026 git commit \u0026\u0026 git push\n# Pushed successfully\n\n$ git status\nmodified: .beads/beads.jsonl # DIRTY AGAIN!\n```\n\n## Lost Data in Recent Session\n\n- bd-ry1u closure (lost in merge)\n- Comments on bd-08fd, bd-23a8, bd-6049, bd-87a0 (lost)\n- Dependencies that existed only in local DB\n\n## Potential Solutions\n\n1. **Use beads-merge tool** - Implement proper 3-way JSONL merge (bd-bzfy)\n2. **Smarter conflict resolution** - Detect when `--theirs` will lose data, warn user\n3. **Sync validation** - Check if JSONL == DB after merge, re-export if needed\n4. **Daemon awareness** - Pause auto-export during merge resolution\n5. **Transaction log** - Replay local changes after merge instead of losing them\n\n## Related Issues\n\n- bd-bzfy (beads-merge integration)\n- Possibly related to daemon auto-export behavior","notes":"## Solution Implemented\n\nFixed the recurring dirty state after merge conflicts by adding **sync validation** before re-exporting.\n\n### Root Cause\nLines 217-237 in `sync.go` unconditionally re-exported DB to JSONL after every import, even when they were already in sync. This created an infinite loop:\n1. User runs `bd sync` which pulls and imports remote JSONL\n2. Sync unconditionally re-exports DB (which has local changes)\n3. JSONL becomes dirty immediately\n4. Repeat\n\n### Fix\nAdded `dbNeedsExport()` function in `integrity.go` that checks:\n- If JSONL exists\n- If DB modification time is newer than JSONL\n- If DB and JSONL issue counts match\n\nNow `bd sync` only re-exports if DB actually has changes that differ from JSONL.\n\n### Changes\n- Added `dbNeedsExport()` in `cmd/bd/integrity.go` (lines 228-271)\n- Updated `sync.go` lines 217-251 to check before re-exporting\n- Added comprehensive tests in `cmd/bd/sync_merge_test.go`\n\n### Testing\nAll tests pass including 4 new tests:\n- `TestDBNeedsExport_InSync` - Verifies no export when synced\n- `TestDBNeedsExport_DBNewer` - Detects DB modifications\n- `TestDBNeedsExport_CountMismatch` - Catches divergence\n- `TestDBNeedsExport_NoJSONL` - Handles missing JSONL\n\nThis prevents the weeks-long frustration of merge conflicts causing infinite dirty loops.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-05T17:52:14.776063-08:00","updated_at":"2025-11-05T17:58:35.611942-08:00","closed_at":"2025-11-05T17:58:35.611942-08:00","source_repo":"."} @@ -337,6 +337,7 @@ {"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:43:11.49933-08:00","closed_at":"2025-11-15T12:43:11.49933-08:00","source_repo":"."} {"id":"bd-d9e0","content_hash":"de4e01414f8863b63cb693a709048b85c3f4417f03e7d7b2528560076be0e1f7","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-02T12:32:00.159298-08:00","closed_at":"2025-11-02T12:32:00.1593-08:00","source_repo":"."} {"id":"bd-dcd6f14b","content_hash":"c07a4b8a39e6e81513278ee335fe14aa767cbcba72e3b511cfd95705053483b1","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00","source_repo":"."} +{"id":"bd-dd6f6d26","content_hash":"dbcecb8b95f9f2939d97c61bd8cbe331bea866f47600bded213d3122e311c356","title":"Fix autoimport tests for content-hash collision scoring","description":"## Overview\nThree autoimport tests are failing after bd-cbed9619.4 because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.\n\n## Failing Tests\n1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved\n2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved \n3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior\n\n## Root Cause\nThese tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.\n\n## Example\nOld behavior: Issue with more references would be kept\nNew behavior: Issue with lexicographically lower content hash is kept\n\n## Solution\nUpdate each test to:\n1. Verify the new content-hash based behavior is correct\n2. Check that the remapped issue (not necessarily local/remote) has the expected content\n3. Ensure dependencies are preserved on the correct remapped issue\n\n## Acceptance Criteria\n- All three autoimport tests pass\n- Tests verify content-hash determinism (same collision always resolves the same way)\n- Tests check dependency preservation on remapped issues\n- Test documentation explains content-hash scoring expectations\n\n## Files to Modify\n- `cmd/bd/autoimport_collision_test.go`\n\n## Testing\nRun: `go test ./cmd/bd -run \"TestAutoImport.*Collision\" -v`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-08T03:09:48.253086-08:00","updated_at":"2025-11-08T03:09:48.253086-08:00","closed_at":"2025-11-08T02:28:35.317704-08:00","source_repo":".","dependencies":[{"issue_id":"bd-dd6f6d26","depends_on_id":"bd-cbed9619.4","type":"discovered-from","created_at":"2025-10-28T19:12:56.345276-07:00","created_by":"daemon"}]} {"id":"bd-de0h","content_hash":"8b8b43683607e73012cf8bd7cf8631c6ae34498d0c93ca5b77d3f68944c8088d","title":"bd message: Add HTTP client timeout to prevent hangs","description":"HTTP client in `sendAgentMailRequest` uses default http.Post with no timeout.\n\n**Location:** cmd/bd/message.go:181\n\n**Problem:**\n- Can hang indefinitely if server is unresponsive\n- No way to cancel stuck requests\n- Poor UX in flaky networks\n\n**Fix:**\n```go\nclient := \u0026http.Client{Timeout: 30 * time.Second}\nresp, err := client.Post(url, \"application/json\", bytes.NewReader(reqBody))\n```\n\n**Impact:** Production reliability and security issue","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T12:54:24.942645-08:00","updated_at":"2025-11-08T12:56:59.948929-08:00","closed_at":"2025-11-08T12:56:59.948929-08:00","source_repo":".","dependencies":[{"issue_id":"bd-de0h","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:54.860847-08:00","created_by":"daemon"}]} {"id":"bd-df11","content_hash":"9d688c3fe5f4994ab29ed22c8c4ae467f2069c4cbb676a2168303b2ffcba48c4","title":"Add import metrics for external_ref matching statistics","description":"Add observability for external_ref matching behavior during imports to help debug and optimize import operations.\n\nMetrics to track:\n- Number of issues matched by external_ref\n- Number of issues matched by ID\n- Number of issues matched by content hash\n- Number of external_ref updates vs creates\n- Average import time with vs without external_ref\n\nOutput format:\n- Add to ImportResult struct\n- Include in import command output\n- Consider structured logging\n\nUse cases:\n- Debugging slow imports\n- Understanding match distribution\n- Optimizing import performance\n\nRelated: bd-1022","status":"closed","priority":4,"issue_type":"chore","created_at":"2025-11-02T15:32:46.157899-08:00","updated_at":"2025-11-08T03:54:04.856564-08:00","closed_at":"2025-11-08T02:20:01.01371-08:00","source_repo":"."} {"id":"bd-df190564","content_hash":"4966d22faf43b7de1b27315f85365d7ed896741e4e589ed01ee16f4c2f600a24","title":"bd repair-deps - Orphaned dependency cleaner","description":"Find and fix orphaned dependency references.\n\nImplementation:\n- Scan all issues for dependencies pointing to non-existent issues\n- Report orphaned refs\n- Auto-fix with --fix flag\n- Interactive mode with --interactive\n\nFiles: cmd/bd/repair_deps.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T19:42:29.852745-07:00","updated_at":"2025-10-31T18:24:19.418221-07:00","closed_at":"2025-10-31T18:24:19.418221-07:00","source_repo":"."} From 42233073bcc3a766131d6083f0533bde19353958 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 14:14:06 -0800 Subject: [PATCH 13/13] Update bd JSONL after closing 8-char hash issues Amp-Thread-ID: https://ampcode.com/threads/T-5358ea57-e9ea-49e9-aedf-7044ebf8b52a Co-authored-by: Amp --- .beads/beads.jsonl | 1 - 1 file changed, 1 deletion(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index b5518097..9c90a1c7 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -337,7 +337,6 @@ {"id":"bd-d84j","content_hash":"d007e5a786a932117a1a4f7a875eb1449424eaaf44f7595d2d4ac01330068d57","title":"Fix PR #319: Performance Improvements - CI failures and lint errors","description":"PR #319 (Performance Improvements) has excellent performance optimizations but is blocked by CI failures.\n\n## The PR\n- URL: https://github.com/steveyegge/beads/pull/319\n- Author: @rsnodgrass (Ryan)\n- Claimed improvements: bd ready 20.5x faster (752ms → 36.6ms), startup 10.5x faster\n\n## CI Failures\n\n### Lint Errors (8 total)\n1. cmd/bd/deletion_tracking.go:57 - unchecked os.Remove\n2. cmd/bd/import.go:548 - unchecked os.RemoveAll\n3. cmd/bd/message.go:205 - unchecked resp.Body.Close\n4. cmd/bd/migrate_issues.go:633 - unchecked fmt.Scanln\n5. cmd/bd/migrate_issues.go:701 - unchecked MarkFlagRequired\n6. cmd/bd/migrate_issues.go:702 - unchecked MarkFlagRequired\n7. cmd/bd/show.go:610 - gosec G104 unhandled error\n8. cmd/bd/show.go:614 - gosec G104 unhandled error\n\n### Test Failures\nAll syncbranch_test.go tests failing with:\n\"migration external_ref_column failed: failed to create index on external_ref: sqlite3: SQL logic error: no such table: main.issues\"\n\nThis suggests the PR branch needs rebasing on current main.\n\n## Required Work\n\n### 1. Fix Lint Errors\nAdd proper error handling for all 8 flagged locations. Most can use _ = or log warnings.\n\n### 2. Rebase on Current Main\nThe migration test failures indicate the branch is out of sync. Need to:\n- git fetch upstream\n- git rebase upstream/main\n- Resolve any conflicts\n- Verify tests pass locally\n\n### 3. Verify CI Passes\n- All lint checks green\n- All tests pass (Linux, Windows, Nix)\n\n## Optional Improvements\n- Consider splitting into smaller PRs (core index, WASM cache, testing infra)\n- Add documentation for benchmark usage\n- Extract helper functions in doctor/perf.go for better testability\n\n## Value\nThis PR delivers real performance improvements. The index optimization alone is worth merging quickly once CI is fixed.","design":"Workflow:\n1. Checkout PR branch locally\n2. Rebase on current main\n3. Fix all 8 lint errors\n4. Run full test suite locally\n5. Push updated branch\n6. Verify CI passes\n7. Request re-review from maintainers","acceptance_criteria":"- All lint errors fixed\n- All tests passing on all platforms\n- PR rebased on current main\n- CI checks all green","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-15T12:24:34.50322-08:00","updated_at":"2025-11-15T12:43:11.49933-08:00","closed_at":"2025-11-15T12:43:11.49933-08:00","source_repo":"."} {"id":"bd-d9e0","content_hash":"de4e01414f8863b63cb693a709048b85c3f4417f03e7d7b2528560076be0e1f7","title":"Extract validation functions to validators.go","description":"Move validatePriority, validateStatus, validateIssueType, validateTitle, validateEstimatedMinutes, validateFieldUpdate to validators.go","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T19:28:54.915909-07:00","updated_at":"2025-11-02T12:32:00.159298-08:00","closed_at":"2025-11-02T12:32:00.1593-08:00","source_repo":"."} {"id":"bd-dcd6f14b","content_hash":"c07a4b8a39e6e81513278ee335fe14aa767cbcba72e3b511cfd95705053483b1","title":"Batch test 4","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-29T15:29:02.053523-07:00","updated_at":"2025-10-31T12:00:43.182861-07:00","closed_at":"2025-10-31T12:00:43.182861-07:00","source_repo":"."} -{"id":"bd-dd6f6d26","content_hash":"dbcecb8b95f9f2939d97c61bd8cbe331bea866f47600bded213d3122e311c356","title":"Fix autoimport tests for content-hash collision scoring","description":"## Overview\nThree autoimport tests are failing after bd-cbed9619.4 because they expect behavior based on the old reference-counting collision resolution, but the system now uses deterministic content-hash scoring.\n\n## Failing Tests\n1. `TestAutoImportMultipleCollisionsRemapped` - expects local versions preserved\n2. `TestAutoImportAllCollisionsRemapped` - expects local versions preserved \n3. `TestAutoImportCollisionRemapMultipleFields` - expects specific collision resolution behavior\n\n## Root Cause\nThese tests were written when ScoreCollisions used reference counting to determine which version to keep. Now it uses content-hash comparison (introduced in commit 2e87329), which produces different but deterministic results.\n\n## Example\nOld behavior: Issue with more references would be kept\nNew behavior: Issue with lexicographically lower content hash is kept\n\n## Solution\nUpdate each test to:\n1. Verify the new content-hash based behavior is correct\n2. Check that the remapped issue (not necessarily local/remote) has the expected content\n3. Ensure dependencies are preserved on the correct remapped issue\n\n## Acceptance Criteria\n- All three autoimport tests pass\n- Tests verify content-hash determinism (same collision always resolves the same way)\n- Tests check dependency preservation on remapped issues\n- Test documentation explains content-hash scoring expectations\n\n## Files to Modify\n- `cmd/bd/autoimport_collision_test.go`\n\n## Testing\nRun: `go test ./cmd/bd -run \"TestAutoImport.*Collision\" -v`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-08T03:09:48.253086-08:00","updated_at":"2025-11-08T03:09:48.253086-08:00","closed_at":"2025-11-08T02:28:35.317704-08:00","source_repo":".","dependencies":[{"issue_id":"bd-dd6f6d26","depends_on_id":"bd-cbed9619.4","type":"discovered-from","created_at":"2025-10-28T19:12:56.345276-07:00","created_by":"daemon"}]} {"id":"bd-de0h","content_hash":"8b8b43683607e73012cf8bd7cf8631c6ae34498d0c93ca5b77d3f68944c8088d","title":"bd message: Add HTTP client timeout to prevent hangs","description":"HTTP client in `sendAgentMailRequest` uses default http.Post with no timeout.\n\n**Location:** cmd/bd/message.go:181\n\n**Problem:**\n- Can hang indefinitely if server is unresponsive\n- No way to cancel stuck requests\n- Poor UX in flaky networks\n\n**Fix:**\n```go\nclient := \u0026http.Client{Timeout: 30 * time.Second}\nresp, err := client.Post(url, \"application/json\", bytes.NewReader(reqBody))\n```\n\n**Impact:** Production reliability and security issue","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T12:54:24.942645-08:00","updated_at":"2025-11-08T12:56:59.948929-08:00","closed_at":"2025-11-08T12:56:59.948929-08:00","source_repo":".","dependencies":[{"issue_id":"bd-de0h","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:54.860847-08:00","created_by":"daemon"}]} {"id":"bd-df11","content_hash":"9d688c3fe5f4994ab29ed22c8c4ae467f2069c4cbb676a2168303b2ffcba48c4","title":"Add import metrics for external_ref matching statistics","description":"Add observability for external_ref matching behavior during imports to help debug and optimize import operations.\n\nMetrics to track:\n- Number of issues matched by external_ref\n- Number of issues matched by ID\n- Number of issues matched by content hash\n- Number of external_ref updates vs creates\n- Average import time with vs without external_ref\n\nOutput format:\n- Add to ImportResult struct\n- Include in import command output\n- Consider structured logging\n\nUse cases:\n- Debugging slow imports\n- Understanding match distribution\n- Optimizing import performance\n\nRelated: bd-1022","status":"closed","priority":4,"issue_type":"chore","created_at":"2025-11-02T15:32:46.157899-08:00","updated_at":"2025-11-08T03:54:04.856564-08:00","closed_at":"2025-11-08T02:20:01.01371-08:00","source_repo":"."} {"id":"bd-df190564","content_hash":"4966d22faf43b7de1b27315f85365d7ed896741e4e589ed01ee16f4c2f600a24","title":"bd repair-deps - Orphaned dependency cleaner","description":"Find and fix orphaned dependency references.\n\nImplementation:\n- Scan all issues for dependencies pointing to non-existent issues\n- Report orphaned refs\n- Auto-fix with --fix flag\n- Interactive mode with --interactive\n\nFiles: cmd/bd/repair_deps.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T19:42:29.852745-07:00","updated_at":"2025-10-31T18:24:19.418221-07:00","closed_at":"2025-10-31T18:24:19.418221-07:00","source_repo":"."}