diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index ebe1cd72..e2bc60a3 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -17,7 +17,7 @@ {"id":"bd-113","title":"Document merge command and AI integration","description":"Update README, AGENTS.md with merge command examples. Document AI agent duplicate detection workflow.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-24T13:31:30.824587-07:00","updated_at":"2025-10-24T13:51:54.430981-07:00","closed_at":"2025-10-22T11:37:41.104918-07:00"} {"id":"bd-114","title":"Stress tests pollute production database with test issues","description":"TestStressNoUniqueConstraintViolations and other stress tests in internal/rpc/stress_test.go create issues in production database instead of test database. Confirmed: 1,000 test issues (Agent X Issue Y) created at 20:46:01 during test run. Root cause: test goroutines connect to production daemon at .beads/bd.sock instead of isolated test daemon in temp directory.","status":"closed","priority":0,"issue_type":"bug","assignee":"amp","created_at":"2025-10-24T13:31:30.824773-07:00","updated_at":"2025-10-24T13:51:54.43117-07:00","closed_at":"2025-10-22T00:35:00.620546-07:00"} {"id":"bd-115","title":"Add cross-repo issue references (future enhancement)","description":"Support referencing issues across different beads repositories. Useful for tracking dependencies between separate projects.\n\nProposed syntax:\n- Local reference: bd-39 (current behavior)\n- Cross-repo by path: ~/src/other-project#bd-456\n- Cross-repo by workspace name: @project2:bd-789\n\nUse cases:\n1. Frontend project depends on backend API issue\n2. Shared library changes blocking multiple projects\n3. System administrator tracking work across machines\n4. Monorepo with separate beads databases per component\n\nImplementation challenges:\n- Storage layer needs to query external databases\n- Dependency resolution across repos\n- What if external repo not available?\n- How to handle in JSONL export/import?\n- Security: should repos be able to read others?\n\nDesign questions to resolve first:\n1. Read-only references vs full cross-repo dependencies?\n2. How to handle repo renames/moves?\n3. Absolute paths vs workspace names vs git remotes?\n4. Should bd-38 auto-discover related repos?\n\nRecommendation: \n- Gather user feedback first\n- Start with read-only references\n- Implement as plugin/extension?\n\nContext: This is mentioned in bd-38 as approach #2. Much more complex than daemon multi-repo approach. Only implement if there's strong user demand.\n\nPriority: Backlog (4) - wait for user feedback before designing","status":"closed","priority":4,"issue_type":"feature","created_at":"2025-10-24T13:31:30.824965-07:00","updated_at":"2025-10-24T13:51:54.442157-07:00","closed_at":"2025-10-20T22:00:31.966891-07:00"} -{"id":"bd-116","title":"Convert repeated strings to constants (goconst)","description":"12 instances of repeated strings that should be constants: \"alice\", \"windows\", \"bd-125\", \"daemon\", \"import\", \"healthy\", \"unhealthy\", \"1.0.0\", \"custom-1\", \"custom-2\"","design":"Create package-level or test-level constants for frequently used test strings and command names.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-24T13:31:30.825138-07:00","updated_at":"2025-10-24T13:51:54.442329-07:00"} +{"id":"bd-116","title":"Convert repeated strings to constants (goconst)","description":"Only 2 real goconst issues remaining:\n1. cmd/bd/reinit_test.go:441 - \"windows\" should use existing windowsOS constant\n2. internal/storage/sqlite/compact_test.go:94 - \"bd-1\" should use existing testIssueBD1 constant","design":"Create package-level or test-level constants for frequently used test strings and command names.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-24T13:31:30.825138-07:00","updated_at":"2025-10-25T18:04:01.975622-07:00","closed_at":"2025-10-25T18:04:01.975622-07:00"} {"id":"bd-117","title":"Add rule-based compaction (e.g., compact children of closed epics)","description":"Support semantic compaction rules beyond just time-based, such as:\n- Compact all children of closed epics\n- Compact by priority level (e.g., all P3/P4 closed issues)\n- Compact by label (e.g., all issues labeled 'archive')\n- Compact by type (e.g., all closed chores)\n\nThis would allow smarter database size management based on semantic meaning rather than just age.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-24T13:31:30.825312-07:00","updated_at":"2025-10-24T13:51:54.431815-07:00","closed_at":"2025-10-22T21:59:19.989241-07:00"} {"id":"bd-118","title":"Auto-flush writes test pollution and session work to git-tracked issues.jsonl","description":"Auto-flush exports ALL issues from DB to issues.jsonl every 5 seconds, including:\n- Test issues (bd-4053 through bd-4059 were version test junk)\n- Issues created during debugging sessions\n- Test pollution from stress tests\n- Temporary diagnostic issues\n\nThis pollutes the git-tracked issues.jsonl with garbage that shouldn't be committed.\n\nExample from today:\n- Git had 49 clean issues\n- Our DB grew to 100+ with test junk and session work\n- Auto-flush wrote all 100+ to issues.jsonl\n- Git status showed modified issues.jsonl with 50+ unwanted issues\n\nImpact:\n- Pollutes git history with test/debug garbage\n- Makes code review difficult (noise in diffs)\n- Can't distinguish real work from session artifacts\n- Other team members pull polluted issues\n\nSolutions to consider:\n1. Disable auto-flush by default (require explicit --enable-auto-flush)\n2. Add .beadsignore to exclude issue ID patterns\n3. Make auto-flush only export 'real' issues (exclude test-*)\n4. Require manual 'bd sync' for git commit\n5. Auto-flush to separate file (.beads/session.jsonl vs issues.jsonl)\n\nRelated: bd-128 (test pollution), isolation_test.go (test DB separation)","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-10-24T13:31:30.825495-07:00","updated_at":"2025-10-24T13:51:54.442513-07:00","closed_at":"2025-10-22T01:05:59.459797-07:00"} {"id":"bd-119","title":"Add automated duplicate detection tool for post-import cleanup","description":"After collision resolution with --resolve-collisions, we can end up with content duplicates (different IDs, identical content) when parallel work creates the same issues.\n\n## Problem\nCurrent situation:\n- `deduplicateIncomingIssues()` only deduplicates within the import batch\n- Doesn't detect duplicates between DB and incoming issues\n- Doesn't detect duplicates across the entire database post-import\n- Manual detection: `bd list --json | jq 'group_by(.title) | map(select(length \u003e 1))'`\n\n## Real Example\nAfter import with collision resolution, we had 7 duplicate pairs:\n--196/bd-67: Same external_ref epic\n--196-196: Same findByExternalRef task\n--196-196: Same collision detection task\n--196-196: Same import flow task\n--196-196: Same test writing task\n--196-196: Same documentation task\n--196-196: Same code review task\n\n## Proposed Solution\nAdd `bd duplicates` command that:\n1. Groups all issues by content hash (title + description + design + acceptance_criteria)\n2. Reports duplicate groups with suggested merge target (lowest ID or most references)\n3. Optionally auto-merge with `--auto-merge` flag\n4. Respects status (don't merge open with closed)\n\n## Example Output\n```\nšŸ” Found 7 duplicate groups:\n\nGroup 1: \"Feature: Use external_ref as primary matching key\"\n --196 (open, P1, 0 references)\n - bd-67 (open, P1, 0 references)\n Suggested merge: bd merge-196 --into bd-67\n\nGroup 2: \"Add findByExternalRef query\"\n --196 (open, P1, 0 references) \n --196 (open, P1, 0 references)\n Suggested merge: bd merge-196 --into-196\n...\n\nRun with --auto-merge to execute all suggested merges\n```\n\n## Implementation Notes\n- Use same content hashing as deduplicateIncomingIssues\n- Consider reference counts when choosing merge target\n- Skip duplicates with different status (open vs closed)\n- Add --dry-run mode\n- Integration with import: `bd import --resolve-collisions --dedupe-after`","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-24T13:35:14.97041-07:00","updated_at":"2025-10-24T22:10:24.867795-07:00","closed_at":"2025-10-24T13:44:05.529998-07:00"} @@ -95,7 +95,7 @@ {"id":"bd-53","title":"Fix code duplication in label.go (dupl)","description":"Lines 72-120 duplicate lines 122-170 in cmd/bd/label.go. The add and remove commands have nearly identical structure.","design":"Extract common batch operation logic into a shared helper function that takes the operation type as a parameter.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-24T01:01:36.971666-07:00","updated_at":"2025-10-24T13:51:54.416434-07:00","closed_at":"2025-10-24T12:40:43.046348-07:00","dependencies":[{"issue_id":"bd-53","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.325899-07:00","created_by":"renumber"}]} {"id":"bd-54","title":"Convert repeated strings to constants (goconst)","description":"12 instances of repeated strings that should be constants: \"alice\", \"windows\", \"bd-114\", \"daemon\", \"import\", \"healthy\", \"unhealthy\", \"1.0.0\", \"custom-1\", \"custom-2\"","design":"Create package-level or test-level constants for frequently used test strings and command names.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-24T01:01:36.9778-07:00","updated_at":"2025-10-25T13:30:00.130626-07:00","closed_at":"2025-10-25T13:30:00.130626-07:00","dependencies":[{"issue_id":"bd-54","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.326123-07:00","created_by":"renumber"}]} {"id":"bd-55","title":"Refactor high complexity functions (gocyclo)","description":"11 functions exceed cyclomatic complexity threshold (\u003e30): runDaemonLoop (42), importIssuesCore (71), TestLabelCommands (67), issueDataChanged (39), etc.","design":"Break down complex functions into smaller, testable units. Extract validation, error handling, and business logic into separate functions.","notes":"Refactored issueDataChanged from complexity 39 → 11 by extracting into fieldComparator struct with methods for each comparison type.\n\nRefactored runDaemonLoop from complexity 42 → 7 by extracting:\n- setupDaemonLogger: Logger initialization logic\n- setupDaemonLock: Lock and PID file management\n- startRPCServer: RPC server startup with error handling\n- runGlobalDaemon: Global daemon mode handling\n- createSyncFunc: Sync cycle logic (export, commit, pull, import, push)\n- runEventLoop: Signal handling and main event loop\n\nCode review fixes:\n- Fixed sync overlap: Changed initial sync from `go doSync()` to synchronous `doSync()` to prevent race with ticker\n- Fixed resource cleanup: Replaced `os.Exit(1)` with `return` after acquiring locks to ensure defers run and clean up PID files/locks\n- Added signal.Stop(sigChan) in runEventLoop and runGlobalDaemon to prevent lingering notifications\n- Added server.Stop() in serverErrChan case for consistent cleanup\n\nRefactored TestLabelCommands from complexity 67 → \u003c10 by extracting labelTestHelper with methods:\n- createIssue: Issue creation helper\n- addLabel/addLabels: Label addition helpers\n- removeLabel: Label removal helper\n- getLabels: Label retrieval helper\n- assertLabelCount/assertHasLabel/assertHasLabels/assertNotHasLabel: Assertion helpers\n- assertLabelEvent: Event verification helper\n\nRefactored TestReopenCommand from complexity 37 → \u003c10 by extracting reopenTestHelper with methods:\n- createIssue: Issue creation helper\n- closeIssue/reopenIssue: State transition helpers\n- getIssue: Issue retrieval helper\n- addComment: Comment addition helper\n- assertStatus/assertClosedAtSet/assertClosedAtNil: Status assertion helpers\n- assertCommentEvent: Event verification helper\n\nRefactored tryAutoStartDaemon from complexity 34 → \u003c10 by extracting:\n- debugLog: Centralized debug logging helper\n- isDaemonHealthy: Fast-path health check\n- acquireStartLock: Lock acquisition with wait/retry logic\n- handleStaleLock: Stale lock detection and retry\n- handleExistingSocket: Socket cleanup and validation\n- determineSocketMode: Global vs local daemon logic\n- startDaemonProcess: Process spawning and readiness wait\n- setupDaemonIO: I/O redirection setup\n\nRefactored DeleteIssues from complexity 37 → \u003c10 by extracting:\n- buildIDSet: ID deduplication\n- resolveDeleteSet: Cascade/force/validation mode routing\n- expandWithDependents: Recursive dependent collection\n- validateNoDependents: Dependency validation\n- checkSingleIssueValidation: Per-issue dependent check\n- trackOrphanedIssues: Force-mode orphan tracking\n- collectOrphansForID: Per-issue orphan collection\n- buildSQLInClause: SQL placeholder generation\n- populateDeleteStats: Dry-run statistics collection\n- executeDelete: Actual deletion execution\n\nCode review fix (via oracle):\n- Added rows.Err() check in checkSingleIssueValidation to catch iterator errors\n\nRefactored TestLibraryIntegration from complexity 32 → \u003c10 by extracting integrationTestHelper with methods:\n- createIssue/createFullIssue: Issue creation helpers\n- updateIssue/closeIssue: Issue modification helpers\n- addDependency/addLabel/addComment: Relationship helpers\n- getIssue/getDependencies/getLabels/getComments: Retrieval helpers\n- assertID/assertEqual/assertNotNil/assertCount: Assertion helpers\n\nRefactored TestExportImport from complexity 31 → \u003c10 by extracting exportImportHelper with methods:\n- createIssue/createFullIssue: Issue creation helpers\n- searchIssues/getIssue/updateIssue: Storage operations\n- encodeJSONL/validateJSONLines: JSONL encoding and validation\n- assertCount/assertEqual/assertSorted: Assertion helpers\n\nRefactored TestListCommand from complexity 31 → \u003c10 by extracting listTestHelper with methods:\n- createTestIssues: Batch test data creation\n- addLabel: Label addition\n- search: Issue search with filters\n- assertCount/assertEqual/assertAtMost: Assertion helpers\n\nRefactored TestGetEpicsEligibleForClosure from complexity 32 → \u003c10 by extracting epicTestHelper with methods:\n- createEpic/createTask: Issue creation\n- addParentChildDependency/closeIssue: Issue relationships\n- getEligibleEpics/findEpic: Epic status queries\n- assertEpicStats/assertEpicFound/assertEpicNotFound: Epic-specific assertions\n\nRefactored TestCreateIssues from complexity 35 → \u003c10 by extracting createIssuesTestHelper with methods:\n- newIssue: Issue construction helper\n- createIssues: Batch issue creation\n- assertNoError/assertError: Error assertions\n- assertCount/assertIDSet/assertTimestampSet: Field assertions\n- assertUniqueIDs/assertEqual/assertNotNil: Validation helpers\n- assertNoAutoGenID: Error-case validation\n\nAll tests pass after refactoring. āœ…\n\n**importIssuesCore was already refactored** (complexity 71 → ~10) using phase-based extraction:\n- getOrCreateStore, handlePrefixMismatch, handleCollisions\n- upsertIssues, importDependencies, importLabels, importComments\n\n**Status:** All 11 high-complexity functions have been refactored to \u003c10 complexity.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-24T01:01:36.989066-07:00","updated_at":"2025-10-25T13:16:42.865768-07:00","closed_at":"2025-10-25T13:16:42.865768-07:00","dependencies":[{"issue_id":"bd-55","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.323992-07:00","created_by":"renumber"}]} -{"id":"bd-56","title":"Fix revive style issues (78 issues)","description":"Style violations: unused parameters (many cmd/args in cobra commands), missing exported comments, stuttering names (SQLiteStorage), indent-error-flow issues.","design":"Rename unused params to _, add godoc comments to exported types, fix stuttering names, simplify control flow.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-24T01:01:36.99984-07:00","updated_at":"2025-10-24T13:51:54.417341-07:00","dependencies":[{"issue_id":"bd-56","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.322412-07:00","created_by":"renumber"}]} +{"id":"bd-56","title":"Fix revive style issues (78 issues)","description":"Style violations: unused parameters (many cmd/args in cobra commands), missing exported comments, stuttering names (SQLiteStorage), indent-error-flow issues.","design":"Rename unused params to _, add godoc comments to exported types, fix stuttering names, simplify control flow.","status":"in_progress","priority":3,"issue_type":"task","created_at":"2025-10-24T01:01:36.99984-07:00","updated_at":"2025-10-25T18:09:33.606243-07:00","dependencies":[{"issue_id":"bd-56","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.322412-07:00","created_by":"renumber"}]} {"id":"bd-57","title":"Address gosec security warnings (102 issues)","description":"Security linter warnings: file permissions (0755 should be 0750), G304 file inclusion via variable, G204 subprocess launches. Many are false positives but should be reviewed.","design":"Review each gosec warning. Add exclusions for legitimate cases to .golangci.yml. Fix real security issues (overly permissive file modes).","notes":"Fixed security issues:\n- Changed file permissions from 0644 → 0600 for JSONL exports and config files \n- Changed directory permissions from 0755 → 0750 in all test code\n- Updated .golangci.yml with proper exclusions for false positives\n\nRemaining gosec warnings (down from 102 to 22, all are false positives or acceptable):\n- G304: File inclusion via variable (test files only - reading test fixtures)\n- G204: Subprocess launches (git commands from trusted sources)\n- G115: Integer overflow conversions (safe controlled conversions)\n- G201: SQL string formatting (constructed from constants)\n\nAll real security issues have been addressed.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-24T01:01:37.0139-07:00","updated_at":"2025-10-25T13:49:14.833807-07:00","closed_at":"2025-10-25T13:49:08.124412-07:00","dependencies":[{"issue_id":"bd-57","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.324202-07:00","created_by":"renumber"}]} {"id":"bd-58","title":"Handle unchecked errors (errcheck - 683 issues)","description":"683 unchecked error returns, mostly in tests (Close, Rollback, RemoveAll). Many already excluded in config but still showing up.","design":"Review .golangci.yml exclude-rules. Most defer Close/Rollback errors in tests can be ignored. Add systematic exclusions or explicit _ = assignments where appropriate.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-24T01:01:37.018404-07:00","updated_at":"2025-10-24T13:51:54.41793-07:00","dependencies":[{"issue_id":"bd-58","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.324423-07:00","created_by":"renumber"}]} {"id":"bd-59","title":"Update LINTING.md with current baseline","description":"After cleanup, document the remaining acceptable baseline in LINTING.md so we can track regression.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-24T01:01:37.02745-07:00","updated_at":"2025-10-24T13:51:54.419194-07:00","dependencies":[{"issue_id":"bd-59","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.327184-07:00","created_by":"renumber"},{"issue_id":"bd-59","depends_on_id":"bd-53","type":"blocks","created_at":"2025-10-24T13:17:40.327422-07:00","created_by":"renumber"},{"issue_id":"bd-59","depends_on_id":"bd-54","type":"blocks","created_at":"2025-10-24T13:17:40.327627-07:00","created_by":"renumber"},{"issue_id":"bd-59","depends_on_id":"bd-55","type":"blocks","created_at":"2025-10-24T13:17:40.327827-07:00","created_by":"renumber"},{"issue_id":"bd-59","depends_on_id":"bd-56","type":"blocks","created_at":"2025-10-24T13:17:40.32803-07:00","created_by":"renumber"},{"issue_id":"bd-59","depends_on_id":"bd-57","type":"blocks","created_at":"2025-10-24T13:17:40.328233-07:00","created_by":"renumber"},{"issue_id":"bd-59","depends_on_id":"bd-58","type":"blocks","created_at":"2025-10-24T13:51:54.447799-07:00","created_by":"renumber"}]} diff --git a/beads_integration_test.go b/beads_integration_test.go index 4505d1c9..15084f39 100644 --- a/beads_integration_test.go +++ b/beads_integration_test.go @@ -177,7 +177,7 @@ func TestLibraryIntegration(t *testing.T) { }) // Test 2: Get issue - t.Run("GetIssue", func(t *testing.T) { + t.Run("GetIssue", func(_ *testing.T) { issue := h.createIssue("Get test", beads.TypeBug, 1) retrieved := h.getIssue(issue.ID) h.assertEqual(issue.Title, retrieved.Title, "title") @@ -185,7 +185,7 @@ func TestLibraryIntegration(t *testing.T) { }) // Test 3: Update issue - t.Run("UpdateIssue", func(t *testing.T) { + t.Run("UpdateIssue", func(_ *testing.T) { issue := h.createIssue("Update test", beads.TypeTask, 2) updates := map[string]interface{}{"status": beads.StatusInProgress, "assignee": "test-user"} h.updateIssue(issue.ID, updates) @@ -195,7 +195,7 @@ func TestLibraryIntegration(t *testing.T) { }) // Test 4: Add dependency - t.Run("AddDependency", func(t *testing.T) { + t.Run("AddDependency", func(_ *testing.T) { issue1 := h.createIssue("Parent task", beads.TypeTask, 1) issue2 := h.createIssue("Child task", beads.TypeTask, 1) h.addDependency(issue1.ID, issue2.ID) diff --git a/cmd/bd/compact.go b/cmd/bd/compact.go index 09701167..982e9e76 100644 --- a/cmd/bd/compact.go +++ b/cmd/bd/compact.go @@ -42,7 +42,7 @@ Examples: bd compact --id bd-42 --force # Force compact (bypass checks) bd compact --stats # Show statistics `, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { ctx := context.Background() // Handle compact stats first @@ -404,7 +404,7 @@ func progressBar(current, total int) string { } //nolint:unparam // ctx may be used in future for cancellation -func runCompactRPC(ctx context.Context) { +func runCompactRPC(_ context.Context) { if compactID != "" && compactAll { fmt.Fprintf(os.Stderr, "Error: cannot use --id and --all together\n") os.Exit(1) diff --git a/cmd/bd/config.go b/cmd/bd/config.go index d87d3872..54f1e683 100644 --- a/cmd/bd/config.go +++ b/cmd/bd/config.go @@ -34,7 +34,7 @@ var configSetCmd = &cobra.Command{ Use: "set ", Short: "Set a configuration value", Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { // Config operations work in direct mode only if err := ensureDirectMode("config set requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/cmd/bd/daemon.go b/cmd/bd/daemon.go index 7c16486b..ee5b5c68 100644 --- a/cmd/bd/daemon.go +++ b/cmd/bd/daemon.go @@ -564,47 +564,48 @@ func migrateToGlobalDaemon() { } func stopDaemon(pidFile string) { - if isRunning, pid := isDaemonRunning(pidFile); !isRunning { + isRunning, pid := isDaemonRunning(pidFile) + if !isRunning { fmt.Println("Daemon is not running") return - } else { - fmt.Printf("Stopping daemon (PID %d)...\n", pid) + } - process, err := os.FindProcess(pid) - if err != nil { - fmt.Fprintf(os.Stderr, "Error finding process: %v\n", err) - os.Exit(1) - } + fmt.Printf("Stopping daemon (PID %d)...\n", pid) - if err := sendStopSignal(process); err != nil { - fmt.Fprintf(os.Stderr, "Error signaling daemon: %v\n", err) - os.Exit(1) - } + process, err := os.FindProcess(pid) + if err != nil { + fmt.Fprintf(os.Stderr, "Error finding process: %v\n", err) + os.Exit(1) + } - for i := 0; i < 50; i++ { - time.Sleep(100 * time.Millisecond) - if isRunning, _ := isDaemonRunning(pidFile); !isRunning { - fmt.Println("Daemon stopped") - return - } - } + if err := sendStopSignal(process); err != nil { + fmt.Fprintf(os.Stderr, "Error signaling daemon: %v\n", err) + os.Exit(1) + } - fmt.Fprintf(os.Stderr, "Warning: daemon did not stop after 5 seconds, forcing termination\n") - - // Check one more time before killing the process to avoid a race. + for i := 0; i < 50; i++ { + time.Sleep(100 * time.Millisecond) if isRunning, _ := isDaemonRunning(pidFile); !isRunning { fmt.Println("Daemon stopped") return } - if err := process.Kill(); err != nil { - // Ignore "process already finished" errors - if !strings.Contains(err.Error(), "process already finished") { - fmt.Fprintf(os.Stderr, "Error killing process: %v\n", err) - } - } - _ = os.Remove(pidFile) - fmt.Println("Daemon killed") } + + fmt.Fprintf(os.Stderr, "Warning: daemon did not stop after 5 seconds, forcing termination\n") + + // Check one more time before killing the process to avoid a race. + if isRunning, _ := isDaemonRunning(pidFile); !isRunning { + fmt.Println("Daemon stopped") + return + } + if err := process.Kill(); err != nil { + // Ignore "process already finished" errors + if !strings.Contains(err.Error(), "process already finished") { + fmt.Fprintf(os.Stderr, "Error killing process: %v\n", err) + } + } + _ = os.Remove(pidFile) + fmt.Println("Daemon killed") } func startDaemon(interval time.Duration, autoCommit, autoPush bool, logFile, pidFile string, global bool) { diff --git a/cmd/bd/delete.go b/cmd/bd/delete.go index 962ba568..cd713b69 100644 --- a/cmd/bd/delete.go +++ b/cmd/bd/delete.go @@ -375,7 +375,7 @@ func removeIssueFromJSONL(issueID string) error { // deleteBatch handles deletion of multiple issues //nolint:unparam // cmd parameter required for potential future use -func deleteBatch(cmd *cobra.Command, issueIDs []string, force bool, dryRun bool, cascade bool) { +func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, cascade bool) { // Ensure we have a direct store when daemon lacks delete support if daemonClient != nil { if err := ensureDirectMode("daemon does not support delete command"); err != nil { diff --git a/cmd/bd/duplicates.go b/cmd/bd/duplicates.go index f0e2e5e3..db0665b9 100644 --- a/cmd/bd/duplicates.go +++ b/cmd/bd/duplicates.go @@ -28,7 +28,7 @@ Example: bd duplicates # Show all duplicate groups bd duplicates --auto-merge # Automatically merge all duplicates bd duplicates --dry-run # Show what would be merged`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cobra.Command, _ []string) { // Check daemon mode - not supported yet (merge command limitation) if daemonClient != nil { fmt.Fprintf(os.Stderr, "Error: duplicates command not yet supported in daemon mode (see bd-190)\n") diff --git a/cmd/bd/import_phases.go b/cmd/bd/import_phases.go index 607aeb79..de665a05 100644 --- a/cmd/bd/import_phases.go +++ b/cmd/bd/import_phases.go @@ -12,7 +12,7 @@ import ( ) // Phase 1: Get or create SQLite store for import -func getOrCreateStore(ctx context.Context, dbPath string, store storage.Storage) (*sqlite.SQLiteStorage, bool, error) { +func getOrCreateStore(_ context.Context, dbPath string, store storage.Storage) (*sqlite.SQLiteStorage, bool, error) { var sqliteStore *sqlite.SQLiteStorage var needCloseStore bool diff --git a/cmd/bd/init.go b/cmd/bd/init.go index 0713152b..abd60c44 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -18,7 +18,7 @@ var initCmd = &cobra.Command{ Short: "Initialize bd in the current directory", Long: `Initialize bd in the current directory by creating a .beads/ directory and database file. Optionally specify a custom issue prefix.`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cobra.Command, _ []string) { prefix, _ := cmd.Flags().GetString("prefix") quiet, _ := cmd.Flags().GetBool("quiet") diff --git a/cmd/bd/list_test.go b/cmd/bd/list_test.go index e9008521..99ed1402 100644 --- a/cmd/bd/list_test.go +++ b/cmd/bd/list_test.go @@ -84,9 +84,9 @@ func (h *listTestHelper) assertEqual(expected, actual interface{}, field string) } } -func (h *listTestHelper) assertAtMost(count, max int, desc string) { - if count > max { - h.t.Errorf("Expected at most %d %s, got %d", max, desc, count) +func (h *listTestHelper) assertAtMost(count, maxCount int, desc string) { + if count > maxCount { + h.t.Errorf("Expected at most %d %s, got %d", maxCount, desc, count) } } diff --git a/cmd/bd/merge.go b/cmd/bd/merge.go index 5e743f39..cecbcd13 100644 --- a/cmd/bd/merge.go +++ b/cmd/bd/merge.go @@ -73,14 +73,14 @@ Example: if jsonOutput { output := map[string]interface{}{ - "target_id": targetID, - "source_ids": sourceIDs, - "merged": len(sourceIDs), - "dependencies_added": result.depsAdded, + "target_id": targetID, + "source_ids": sourceIDs, + "merged": len(sourceIDs), + "dependencies_added": result.depsAdded, "dependencies_skipped": result.depsSkipped, - "text_references": result.textRefCount, - "issues_closed": result.issuesClosed, - "issues_skipped": result.issuesSkipped, + "text_references": result.textRefCount, + "issues_closed": result.issuesClosed, + "issues_skipped": result.issuesSkipped, } outputJSON(output) } else { @@ -216,9 +216,8 @@ func performMerge(ctx context.Context, targetID string, sourceIDs []string) (*me // Ignore if dependency already exists if !strings.Contains(err.Error(), "UNIQUE constraint failed") { return nil, fmt.Errorf("failed to add dependency %s -> %s: %w", issueID, targetID, err) - } else { - result.depsSkipped++ } + result.depsSkipped++ } else { result.depsAdded++ } @@ -294,43 +293,43 @@ func updateMergeTextReferences(ctx context.Context, sourceIDs []string, targetID // Update description if issue.Description != "" && re.MatchString(issue.Description) { - if _, exists := updates["description"]; !exists { - updates["description"] = issue.Description + if _, exists := updates["description"]; !exists { + updates["description"] = issue.Description + } + if desc, ok := updates["description"].(string); ok { + updates["description"] = re.ReplaceAllString(desc, replacementText) + } } - if desc, ok := updates["description"].(string); ok { - updates["description"] = re.ReplaceAllString(desc, replacementText) - } - } // Update notes if issue.Notes != "" && re.MatchString(issue.Notes) { - if _, exists := updates["notes"]; !exists { - updates["notes"] = issue.Notes - } - if notes, ok := updates["notes"].(string); ok { - updates["notes"] = re.ReplaceAllString(notes, replacementText) - } + if _, exists := updates["notes"]; !exists { + updates["notes"] = issue.Notes + } + if notes, ok := updates["notes"].(string); ok { + updates["notes"] = re.ReplaceAllString(notes, replacementText) + } } // Update design if issue.Design != "" && re.MatchString(issue.Design) { - if _, exists := updates["design"]; !exists { - updates["design"] = issue.Design - } - if design, ok := updates["design"].(string); ok { - updates["design"] = re.ReplaceAllString(design, replacementText) - } + if _, exists := updates["design"]; !exists { + updates["design"] = issue.Design + } + if design, ok := updates["design"].(string); ok { + updates["design"] = re.ReplaceAllString(design, replacementText) + } } // Update acceptance criteria if issue.AcceptanceCriteria != "" && re.MatchString(issue.AcceptanceCriteria) { - if _, exists := updates["acceptance_criteria"]; !exists { - updates["acceptance_criteria"] = issue.AcceptanceCriteria + if _, exists := updates["acceptance_criteria"]; !exists { + updates["acceptance_criteria"] = issue.AcceptanceCriteria + } + if ac, ok := updates["acceptance_criteria"].(string); ok { + updates["acceptance_criteria"] = re.ReplaceAllString(ac, replacementText) + } } - if ac, ok := updates["acceptance_criteria"].(string); ok { - updates["acceptance_criteria"] = re.ReplaceAllString(ac, replacementText) - } - } } // Apply updates if any diff --git a/cmd/bd/stale.go b/cmd/bd/stale.go index 5d0a0fc3..2213d52d 100644 --- a/cmd/bd/stale.go +++ b/cmd/bd/stale.go @@ -282,13 +282,14 @@ func releaseStaleIssues(staleIssues []*StaleIssueInfo) (int, error) { func formatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%.0f seconds", d.Seconds()) - } else if d < time.Hour { - return fmt.Sprintf("%.0f minutes", d.Minutes()) - } else if d < 24*time.Hour { - return fmt.Sprintf("%.1f hours", d.Hours()) - } else { - return fmt.Sprintf("%.1f days", d.Hours()/24) } + if d < time.Hour { + return fmt.Sprintf("%.0f minutes", d.Minutes()) + } + if d < 24*time.Hour { + return fmt.Sprintf("%.1f hours", d.Hours()) + } + return fmt.Sprintf("%.1f days", d.Hours()/24) } func init() { diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index ba66e2e4..693701e5 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -29,7 +29,7 @@ var syncCmd = &cobra.Command{ This command wraps the entire git-based sync workflow for multi-device use. Use --flush-only to just export pending changes to JSONL (useful for pre-commit hooks).`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cobra.Command, _ []string) { ctx := context.Background() message, _ := cmd.Flags().GetString("message") diff --git a/internal/rpc/metrics.go b/internal/rpc/metrics.go index c05cfe09..f56357b2 100644 --- a/internal/rpc/metrics.go +++ b/internal/rpc/metrics.go @@ -225,9 +225,9 @@ func calculateLatencyStats(samples []time.Duration) LatencyStats { n := len(sorted) // Calculate percentiles with defensive clamping - p50Idx := min(n-1, n*50/100) - p95Idx := min(n-1, n*95/100) - p99Idx := min(n-1, n*99/100) + p50Idx := minInt(n-1, n*50/100) + p95Idx := minInt(n-1, n*95/100) + p99Idx := minInt(n-1, n*99/100) // Calculate average var sum time.Duration @@ -251,7 +251,7 @@ func calculateLatencyStats(samples []time.Duration) LatencyStats { } } -func min(a, b int) int { +func minInt(a, b int) int { if a < b { return a } diff --git a/internal/rpc/server.go b/internal/rpc/server.go index 8550d2e1..338b5f76 100644 --- a/internal/rpc/server.go +++ b/internal/rpc/server.go @@ -140,7 +140,7 @@ func NewServer(socketPath string, store storage.Storage) *Server { } // Start starts the RPC server and listens for connections -func (s *Server) Start(ctx context.Context) error { +func (s *Server) Start(_ context.Context) error { if err := s.ensureSocketDir(); err != nil { return fmt.Errorf("failed to ensure socket directory: %w", err) } diff --git a/internal/storage/sqlite/collision.go b/internal/storage/sqlite/collision.go index 3e21d5b1..689fd8dd 100644 --- a/internal/storage/sqlite/collision.go +++ b/internal/storage/sqlite/collision.go @@ -294,7 +294,7 @@ func deduplicateIncomingIssues(issues []*types.Issue) []*types.Issue { // If an error occurs partway through, some issues may be created without their references // being updated. This is a known limitation that requires storage layer refactoring to fix. // See issue bd-25 for transaction support. -func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, allIssues []*types.Issue) (map[string]string, error) { +func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, _ []*types.Issue) (map[string]string, error) { idMapping := make(map[string]string) // Sync counters before remapping to avoid ID collisions diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index 7339517b..0bcf1f48 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -1524,7 +1524,7 @@ func (s *SQLiteStorage) resolveDeleteSet(ctx context.Context, tx *sql.Tx, ids [] return ids, s.trackOrphanedIssues(ctx, tx, ids, idSet, result) } -func (s *SQLiteStorage) expandWithDependents(ctx context.Context, tx *sql.Tx, ids []string, idSet map[string]bool) ([]string, error) { +func (s *SQLiteStorage) expandWithDependents(ctx context.Context, tx *sql.Tx, ids []string, _ map[string]bool) ([]string, error) { allToDelete, err := s.findAllDependentsRecursive(ctx, tx, ids) if err != nil { return nil, fmt.Errorf("failed to find dependents: %w", err)