diff --git a/cmd/cascade_cli_test.go b/cmd/cascade_cli_test.go index 0f273734..5c3ab7fd 100644 --- a/cmd/cascade_cli_test.go +++ b/cmd/cascade_cli_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/context_test.go b/cmd/context_test.go index 8755e83c..32752e45 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/create.go b/cmd/create.go index 09f9b009..14885433 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -33,7 +33,9 @@ var createCmd = &cobra.Command{ if models.IsValidType(normalized) { typeFlag, _ := cmd.Flags().GetString("type") if typeFlag == "" { - cmd.Flags().Set("type", string(normalized)) + if err := cmd.Flags().Set("type", string(normalized)); err != nil { + return err + } } args = args[1:] } diff --git a/cmd/create_test.go b/cmd/create_test.go index 029f0c0d..a79183f4 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/defer.go b/cmd/defer.go index b8e05095..f7a594c8 100644 --- a/cmd/defer.go +++ b/cmd/defer.go @@ -49,12 +49,14 @@ var deferCmd = &cobra.Command{ return err } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: "Deferral cleared", Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record deferral log for %s: %v", issueID, err) + } fmt.Printf("DEFERRAL CLEARED %s\n", issueID) return nil @@ -88,12 +90,14 @@ var deferCmd = &cobra.Command{ logMsg = fmt.Sprintf("Deferred until %s (deferred %d times)", dateStr, issue.DeferCount) } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: logMsg, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record deferral log for %s: %v", issueID, err) + } fmt.Printf("DEFERRED %s until %s\n", issueID, dateStr) return nil diff --git a/cmd/delete_test.go b/cmd/delete_test.go index aeee590b..e1e46379 100644 --- a/cmd/delete_test.go +++ b/cmd/delete_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/dependencies_test.go b/cmd/dependencies_test.go index f34d2a1a..d7c4a04f 100644 --- a/cmd/dependencies_test.go +++ b/cmd/dependencies_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/due.go b/cmd/due.go index f3293ea2..144a86ad 100644 --- a/cmd/due.go +++ b/cmd/due.go @@ -49,12 +49,14 @@ var dueCmd = &cobra.Command{ return err } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: "Due date cleared", Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record due date log for %s: %v", issueID, err) + } fmt.Printf("DUE DATE CLEARED %s\n", issueID) } else { @@ -75,12 +77,14 @@ var dueCmd = &cobra.Command{ return err } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: "Due date set: " + dateStr, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record due date log for %s: %v", issueID, err) + } fmt.Printf("DUE DATE SET %s: %s\n", issueID, dateStr) } diff --git a/cmd/epic.go b/cmd/epic.go index 29dfe4f9..d8731036 100644 --- a/cmd/epic.go +++ b/cmd/epic.go @@ -110,7 +110,9 @@ func init() { epicCreateCmd.Flags().StringArray("blocks", nil, "Issues this blocks (repeatable, comma-separated)") // Hidden type flag - set programmatically to "epic" epicCreateCmd.Flags().StringP("type", "t", "", "") - epicCreateCmd.Flags().MarkHidden("type") + if err := epicCreateCmd.Flags().MarkHidden("type"); err != nil { + panic(err) + } // epicListCmd flags epicListCmd.Flags().BoolP("all", "a", false, "Show all epics including closed") diff --git a/cmd/epic_test.go b/cmd/epic_test.go index b042f9d4..efb45c11 100644 --- a/cmd/epic_test.go +++ b/cmd/epic_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import "testing" diff --git a/cmd/focus_test.go b/cmd/focus_test.go index 3a7eda78..9b31e3bf 100644 --- a/cmd/focus_test.go +++ b/cmd/focus_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/handoff.go b/cmd/handoff.go index 348d5b72..8d2d5618 100644 --- a/cmd/handoff.go +++ b/cmd/handoff.go @@ -216,12 +216,14 @@ Or use flags with values, stdin (-), or file (@path): } // Add log entry for visibility - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: child.ID, SessionID: sess.ID, Message: fmt.Sprintf("Cascaded handoff from %s", issueID), Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("cascade handoff log %s: %v", child.ID, err) + } cascaded++ } diff --git a/cmd/handoff_test.go b/cmd/handoff_test.go index f7a6e5b9..2530382e 100644 --- a/cmd/handoff_test.go +++ b/cmd/handoff_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/init.go b/cmd/init.go index 4b7132b5..de54e656 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -81,10 +81,14 @@ func addToGitignore(path string) { // Add newline if file doesn't end with one if len(contentStr) > 0 && !strings.HasSuffix(contentStr, "\n") { - f.WriteString("\n") + if _, err := f.WriteString("\n"); err != nil { + return + } } - f.WriteString(".todos/\n") + if _, err := f.WriteString(".todos/\n"); err != nil { + return + } fmt.Println("Added .todos/ to .gitignore") } diff --git a/cmd/link.go b/cmd/link.go index e30ef000..ebf24198 100644 --- a/cmd/link.go +++ b/cmd/link.go @@ -209,7 +209,7 @@ Examples: if info.IsDir() { if recursive { - filepath.Walk(match, func(path string, info os.FileInfo, err error) error { + if err := filepath.Walk(match, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } @@ -217,7 +217,9 @@ Examples: allFiles = append(allFiles, path) } return nil - }) + }); err != nil { + output.Warning("failed to walk %s: %v", match, err) + } } else { // Just files in the directory entries, _ := os.ReadDir(match) diff --git a/cmd/log_test.go b/cmd/log_test.go index 80ade049..21868ae3 100644 --- a/cmd/log_test.go +++ b/cmd/log_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/review.go b/cmd/review.go index 89b7bb4f..4e8c38ed 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -19,7 +19,9 @@ import ( func clearFocusIfNeeded(baseDir, issueID string) { focusedID, _ := config.GetFocus(baseDir) if focusedID == issueID { - config.ClearFocus(baseDir) + if err := config.ClearFocus(baseDir); err != nil { + output.Warning("failed to clear focus for %s: %v", issueID, err) + } } } @@ -677,12 +679,14 @@ Supports bulk operations: } logMsg = fmt.Sprintf("[%s] Approved (CREATOR EXCEPTION: %s)", agentInfo, reason) logType = models.LogTypeSecurity - db.LogSecurityEvent(baseDir, db.SecurityEvent{ + if err := db.LogSecurityEvent(baseDir, db.SecurityEvent{ IssueID: issueID, SessionID: sess.ID, AgentType: sess.AgentType, Reason: "creator_approval_exception: " + reason, - }) + }); err != nil { + output.Warning("failed to record security event for %s: %v", issueID, err) + } } if err := database.AddLog(&models.Log{ @@ -844,7 +848,9 @@ Supports bulk operations: if reason != "" { result["reason"] = reason } - output.JSON(result) + if err := output.JSON(result); err != nil { + return err + } } else { fmt.Printf("REJECTED %s → open\n", issueID) } @@ -979,12 +985,14 @@ Examples: logType = models.LogTypeSecurity // Also log to the separate security audit file - db.LogSecurityEvent(baseDir, db.SecurityEvent{ + if err := db.LogSecurityEvent(baseDir, db.SecurityEvent{ IssueID: issueID, SessionID: sess.ID, AgentType: sess.AgentType, Reason: selfCloseException, - }) + }); err != nil { + output.Warning("failed to record security event for %s: %v", issueID, err) + } } else if reason != "" { logMsg = "Closed: " + reason } diff --git a/cmd/review_test.go b/cmd/review_test.go index 2b861989..808a44ba 100644 --- a/cmd/review_test.go +++ b/cmd/review_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/root.go b/cmd/root.go index a5d11ffd..2dc426d5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -156,7 +156,9 @@ func logAgentError(args []string, errMsg string) { } // Log the error (silently fails if project not initialized) - db.LogAgentError(dir, args, errMsg, sessionID) + if err := db.LogAgentError(dir, args, errMsg, sessionID); err != nil { + slog.Debug("agent error logging failed", "err", err) + } } // handleUnknownFlagError checks if error is an unknown flag and suggests alternatives diff --git a/cmd/search_test.go b/cmd/search_test.go index 0d60efdc..9cfb9e41 100644 --- a/cmd/search_test.go +++ b/cmd/search_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/security_test.go b/cmd/security_test.go index 15c12277..aae003ed 100644 --- a/cmd/security_test.go +++ b/cmd/security_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/show_test.go b/cmd/show_test.go index 20ac04a3..01985a7e 100644 --- a/cmd/show_test.go +++ b/cmd/show_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/start.go b/cmd/start.go index cd0795e2..307f277f 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -129,22 +129,26 @@ Examples: logMsg = reason } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: logMsg, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record start log for %s: %v", issueID, err) + } // Record git snapshot if gitErr == nil { - database.AddGitSnapshot(&models.GitSnapshot{ + if err := database.AddGitSnapshot(&models.GitSnapshot{ IssueID: issueID, Event: "start", CommitSHA: gitState.CommitSHA, Branch: gitState.Branch, DirtyFiles: gitState.DirtyFiles, - }) + }); err != nil { + output.Warning("failed to record git snapshot for %s: %v", issueID, err) + } } fmt.Printf("STARTED %s (session: %s)\n", issueID, sess.ID) @@ -153,7 +157,9 @@ Examples: // Set focus to first issue if single issue, or clear if multiple if len(args) == 1 && started == 1 { - config.SetFocus(baseDir, args[0]) + if err := config.SetFocus(baseDir, args[0]); err != nil { + output.Warning("started %s but failed to persist focus: %v", args[0], err) + } } // Show git state once at the end diff --git a/cmd/start_test.go b/cmd/start_test.go index f4918555..55280d96 100644 --- a/cmd/start_test.go +++ b/cmd/start_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/sync.go b/cmd/sync.go index 83225987..e7f5e898 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -50,6 +50,7 @@ var syncEntityValidator tdsync.EntityValidator = func(entityType string) bool { // errBootstrapNotNeeded signals that the server event count is below the snapshot threshold. var errBootstrapNotNeeded = errors.New("bootstrap not needed") +var errBootstrapRecoveryFailed = errors.New("bootstrap recovery failed") var syncCmd = &cobra.Command{ Use: "sync", @@ -71,7 +72,7 @@ var syncCmd = &cobra.Command{ output.Error("open database: %v", err) return err } - defer database.Close() + defer func() { _ = database.Close() }() syncState, err := database.GetSyncState() if err != nil { @@ -112,6 +113,10 @@ var syncCmd = &cobra.Command{ return err } } else if !errors.Is(err, errBootstrapNotNeeded) { + if newDB == nil || errors.Is(err, errBootstrapRecoveryFailed) { + output.Error("bootstrap failed: %v", err) + return err + } output.Warning("bootstrap failed, falling back to normal pull: %v", err) } } @@ -222,23 +227,15 @@ func runBootstrap(database *db.DB, client *syncclient.Client, state *db.SyncStat // Write snapshot if err := os.WriteFile(dbPath, snapshot.Data, 0644); err != nil { - os.Rename(backupPath, dbPath) - reopened, reopenErr := db.Open(baseDir) - if reopenErr != nil { - return nil, fmt.Errorf("write failed (%w) and reopen failed: %v", err, reopenErr) - } - return reopened, fmt.Errorf("write snapshot: %w", err) + restoreErr := restoreSnapshotBackup(backupPath, dbPath) + return reopenAfterBootstrapFailure(baseDir, "write snapshot", err, restoreErr) } // Reopen and update sync_state reopened, err := db.Open(baseDir) if err != nil { - os.Rename(backupPath, dbPath) - reopened2, reopenErr := db.Open(baseDir) - if reopenErr != nil { - return nil, fmt.Errorf("reopen failed (%w) and restore reopen failed: %v", err, reopenErr) - } - return reopened2, fmt.Errorf("reopen after bootstrap: %w", err) + restoreErr := restoreSnapshotBackup(backupPath, dbPath) + return reopenAfterBootstrapFailure(baseDir, "reopen after bootstrap", err, restoreErr) } // Use INSERT OR REPLACE since the snapshot DB may not have a sync_state row @@ -249,12 +246,8 @@ func runBootstrap(database *db.DB, client *syncclient.Client, state *db.SyncStat ) if err != nil { reopened.Close() - os.Rename(backupPath, dbPath) - reopened2, reopenErr := db.Open(baseDir) - if reopenErr != nil { - return nil, fmt.Errorf("sync_state update failed (%w) and restore reopen failed: %v", err, reopenErr) - } - return reopened2, fmt.Errorf("update sync_state: %w", err) + restoreErr := restoreSnapshotBackup(backupPath, dbPath) + return reopenAfterBootstrapFailure(baseDir, "update sync_state", err, restoreErr) } fmt.Printf("Bootstrap complete (seq %d).\n", snapshot.SnapshotSeq) @@ -284,6 +277,27 @@ func copyFile(src, dst string) error { return out.Sync() } +func restoreSnapshotBackup(backupPath, dbPath string) error { + if err := os.Rename(backupPath, dbPath); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func reopenAfterBootstrapFailure(baseDir, operation string, cause, restoreErr error) (*db.DB, error) { + reopened, reopenErr := db.Open(baseDir) + if reopenErr != nil { + if restoreErr != nil { + return nil, fmt.Errorf("%w: %s: %v (reopen database: %v)", errBootstrapRecoveryFailed, operation, restoreErr, reopenErr) + } + return nil, fmt.Errorf("%s failed (%w) and reopen failed: %v", operation, cause, reopenErr) + } + if restoreErr != nil { + return reopened, fmt.Errorf("%w: %s: %v (restore backup: %v)", errBootstrapRecoveryFailed, operation, cause, restoreErr) + } + return reopened, fmt.Errorf("%s: %w", operation, cause) +} + const pushBatchSize = 500 func filterEventsForSync(events []tdsync.Event, validator tdsync.EntityValidator) []tdsync.Event { @@ -316,7 +330,7 @@ func runPush(database *db.DB, client *syncclient.Client, state *db.SyncState, de output.Error("begin tx: %v", err) return err } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := tdsync.GetPendingEvents(tx, deviceID, sess.ID) if err != nil { @@ -492,21 +506,27 @@ func runPull(database *db.DB, client *syncclient.Client, state *db.SyncState, de result, err := tdsync.ApplyRemoteEvents(tx, events, deviceID, syncEntityValidator, state.LastSyncAt) if err != nil { - tx.Rollback() + if rollbackErr := tx.Rollback(); rollbackErr != nil { + slog.Debug("sync: rollback after apply failure", "err", rollbackErr) + } output.Error("apply events: %v", err) return err } // Store conflict records if err := storeConflicts(tx, result.Conflicts); err != nil { - tx.Rollback() + if rollbackErr := tx.Rollback(); rollbackErr != nil { + slog.Debug("sync: rollback after conflict storage failure", "err", rollbackErr) + } output.Error("store conflicts: %v", err) return err } // Update sync_state within the same transaction to avoid race if _, err := tx.Exec(`UPDATE sync_state SET last_pulled_server_seq = ?, last_sync_at = CURRENT_TIMESTAMP`, pullResp.LastServerSeq); err != nil { - tx.Rollback() + if rollbackErr := tx.Rollback(); rollbackErr != nil { + slog.Debug("sync: rollback after sync state failure", "err", rollbackErr) + } output.Error("update sync state: %v", err) return err } diff --git a/cmd/sync_bootstrap_test.go b/cmd/sync_bootstrap_test.go index f80ba220..2217b88a 100644 --- a/cmd/sync_bootstrap_test.go +++ b/cmd/sync_bootstrap_test.go @@ -50,3 +50,27 @@ func TestRunBootstrapSkipsWhenPendingEvents(t *testing.T) { t.Fatalf("db unusable after bootstrap skip: %v", err) } } + +func TestReopenAfterBootstrapFailureMarksRecoveryErrors(t *testing.T) { + tmpDir := t.TempDir() + database, err := db.Initialize(tmpDir) + if err != nil { + t.Fatalf("init db: %v", err) + } + if err := database.Close(); err != nil { + t.Fatalf("close db: %v", err) + } + + reopened, err := reopenAfterBootstrapFailure(tmpDir, "write snapshot", errors.New("write failed"), errors.New("rename failed")) + if reopened == nil { + t.Fatal("expected reopened db handle") + } + defer func() { _ = reopened.Close() }() + + if !errors.Is(err, errBootstrapRecoveryFailed) { + t.Fatalf("expected bootstrap recovery error, got %v", err) + } + if _, queryErr := reopened.Conn().Exec("SELECT 1"); queryErr != nil { + t.Fatalf("reopened db unusable: %v", queryErr) + } +} diff --git a/cmd/sync_tail_test.go b/cmd/sync_tail_test.go index 8ab8cb79..94d09957 100644 --- a/cmd/sync_tail_test.go +++ b/cmd/sync_tail_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( @@ -85,7 +86,7 @@ func TestPrintSyncEntry(t *testing.T) { DeviceID: "", Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), }, - contains: []string{"pull", "comments", "c_short", "delete", "seq:7"}, + contains: []string{"pull", "comments", "c_short", "delete", "seq:7"}, }, } diff --git a/cmd/system.go b/cmd/system.go index 786010d3..a2f48ab5 100644 --- a/cmd/system.go +++ b/cmd/system.go @@ -557,11 +557,11 @@ var importCmd = &cobra.Command{ // exportedItem matches the JSON structure produced by the export command. type exportedItem struct { - Issue models.Issue `json:"issue"` - Logs []models.Log `json:"logs"` - Handoffs []models.Handoff `json:"handoffs"` - Dependencies []models.IssueDependency `json:"dependencies"` - Files []models.IssueFile `json:"files"` + Issue models.Issue `json:"issue"` + Logs []models.Log `json:"logs"` + Handoffs []models.Handoff `json:"handoffs"` + Dependencies []models.IssueDependency `json:"dependencies"` + Files []models.IssueFile `json:"files"` } // UnmarshalJSON supports backward-compatible deserialization: @@ -814,8 +814,9 @@ func importMarkdown(database *db.DB, data string, dryRun, force bool, sessionID } if matches := pointsRegex.FindStringSubmatch(line); matches != nil { var pts int - fmt.Sscanf(matches[1], "%d", &pts) - currentIssue.Points = pts + if _, err := fmt.Sscanf(matches[1], "%d", &pts); err == nil { + currentIssue.Points = pts + } inDescription = false continue } diff --git a/cmd/task.go b/cmd/task.go index 0b658c77..6db23b72 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -111,7 +111,9 @@ func init() { taskCreateCmd.Flags().Bool("minor", false, "Mark as minor task (allows self-review)") // Hidden type flag - set programmatically to "task" taskCreateCmd.Flags().StringP("type", "t", "", "") - taskCreateCmd.Flags().MarkHidden("type") + if err := taskCreateCmd.Flags().MarkHidden("type"); err != nil { + panic(err) + } // taskListCmd flags taskListCmd.Flags().BoolP("all", "a", false, "Show all tasks including closed") diff --git a/cmd/tree_test.go b/cmd/tree_test.go index a21eddf2..a92fdee1 100644 --- a/cmd/tree_test.go +++ b/cmd/tree_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/undo_test.go b/cmd/undo_test.go index 19edcb15..35c132d1 100644 --- a/cmd/undo_test.go +++ b/cmd/undo_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/unstart.go b/cmd/unstart.go index c8e1f196..7540257f 100644 --- a/cmd/unstart.go +++ b/cmd/unstart.go @@ -89,12 +89,14 @@ Examples: logMsg = reason } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: logMsg, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record unstart log for %s: %v", issueID, err) + } // Clear focus if this was the focused issue clearFocusIfNeeded(baseDir, issueID) diff --git a/cmd/unstart_test.go b/cmd/unstart_test.go index f4d60770..d222c47f 100644 --- a/cmd/unstart_test.go +++ b/cmd/unstart_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/update.go b/cmd/update.go index c6f7b8fe..1e5c631b 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -202,22 +202,30 @@ var updateCmd = &cobra.Command{ if cmd.Flags().Changed("depends-on") { existingDeps, _ := database.GetDependencies(issueID) for _, dep := range existingDeps { - database.RemoveDependencyLogged(issueID, dep, sess.ID) + if err := database.RemoveDependencyLogged(issueID, dep, sess.ID); err != nil { + output.Warning("failed to remove dependency %s -> %s: %v", issueID, dep, err) + } } dependsArr, _ := cmd.Flags().GetStringArray("depends-on") for _, dep := range mergeMultiValueFlag(dependsArr) { - database.AddDependencyLogged(issueID, dep, "depends_on", sess.ID) + if err := database.AddDependencyLogged(issueID, dep, "depends_on", sess.ID); err != nil { + output.Warning("failed to add dependency %s -> %s: %v", issueID, dep, err) + } } } if cmd.Flags().Changed("blocks") { blocked, _ := database.GetBlockedBy(issueID) for _, b := range blocked { - database.RemoveDependencyLogged(b, issueID, sess.ID) + if err := database.RemoveDependencyLogged(b, issueID, sess.ID); err != nil { + output.Warning("failed to remove blocked relationship %s -> %s: %v", b, issueID, err) + } } blocksArr, _ := cmd.Flags().GetStringArray("blocks") for _, b := range mergeMultiValueFlag(blocksArr) { - database.AddDependencyLogged(b, issueID, "depends_on", sess.ID) + if err := database.AddDependencyLogged(b, issueID, "depends_on", sess.ID); err != nil { + output.Warning("failed to add blocked relationship %s -> %s: %v", b, issueID, err) + } } } @@ -271,7 +279,9 @@ func init() { updateCmd.Flags().String("status", "", "New status (open, in_progress, in_review, blocked, closed)") updateCmd.Flags().StringP("comment", "m", "", "Add a comment to the updated issue(s)") updateCmd.Flags().StringP("note", "c", "", "Alias for --comment") - updateCmd.Flags().MarkHidden("note") + if err := updateCmd.Flags().MarkHidden("note"); err != nil { + panic(err) + } updateCmd.Flags().String("defer", "", "Defer until date (e.g., +7d, monday, 2026-03-01; empty to clear)") updateCmd.Flags().String("due", "", "Due date (e.g., friday, +2w, 2026-03-15; empty to clear)") } diff --git a/cmd/update_test.go b/cmd/update_test.go index c185fbe3..e5a314be 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/cmd/ws.go b/cmd/ws.go index dbdbecf2..21e6967a 100644 --- a/cmd/ws.go +++ b/cmd/ws.go @@ -75,7 +75,9 @@ var wsStartCmd = &cobra.Command{ } // Set as active - config.SetActiveWorkSession(baseDir, ws.ID) + if err := config.SetActiveWorkSession(baseDir, ws.ID); err != nil { + output.Warning("work session %s created but failed to persist as active: %v", ws.ID, err) + } fmt.Printf("WORK SESSION STARTED: %s\n", ws.ID) fmt.Printf("Name: %s\n", name) @@ -141,30 +143,39 @@ var wsTagCmd = &cobra.Command{ } issue.Status = models.StatusInProgress issue.ImplementerSession = sess.ID - database.UpdateIssueLogged(issue, sess.ID, models.ActionStart) + if err := database.UpdateIssueLogged(issue, sess.ID, models.ActionStart); err != nil { + output.Warning("failed to auto-start %s: %v", issueID, err) + continue + } // Record session action for bypass prevention - database.RecordSessionAction(issueID, sess.ID, models.ActionSessionStarted) + if err := database.RecordSessionAction(issueID, sess.ID, models.ActionSessionStarted); err != nil { + output.Warning("failed to record session history for %s: %v", issueID, err) + } // Log the start - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, WorkSessionID: wsID, Message: "Started (via work session)", Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to record work session log for %s: %v", issueID, err) + } // Capture git state gitState, _ := git.GetState() if gitState != nil { - database.AddGitSnapshot(&models.GitSnapshot{ + if err := database.AddGitSnapshot(&models.GitSnapshot{ IssueID: issueID, Event: "start", CommitSHA: gitState.CommitSHA, Branch: gitState.Branch, DirtyFiles: gitState.DirtyFiles, - }) + }); err != nil { + output.Warning("failed to record git snapshot for %s: %v", issueID, err) + } } fmt.Printf("STARTED %s (session: %s)\n", issueID, sess.ID) @@ -266,23 +277,29 @@ var wsLogCmd = &cobra.Command{ only, _ := cmd.Flags().GetString("only") if only != "" { // Log to specific issue only - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: only, SessionID: sess.ID, WorkSessionID: wsID, Message: args[0], Type: logType, - }) + }); err != nil { + output.Error("failed to add work session log: %v", err) + return err + } fmt.Printf("LOGGED %s%s → %s\n", wsID, typeLabel, only) } else { // Store single log entry with work_session_id, no issue_id - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: "", SessionID: sess.ID, WorkSessionID: wsID, Message: args[0], Type: logType, - }) + }); err != nil { + output.Error("failed to add work session log: %v", err) + return err + } // Get tagged issues for display issueIDs, _ := database.GetWorkSessionIssues(wsID) @@ -489,18 +506,23 @@ Flags support values, stdin (-), or file (@path): Uncertain: handoff.Uncertain, } - database.AddHandoff(issueHandoff) + if err := database.AddHandoff(issueHandoff); err != nil { + output.Warning("failed to record handoff for %s: %v", issueID, err) + continue + } // Capture git state gitState, _ := git.GetState() if gitState != nil { - database.AddGitSnapshot(&models.GitSnapshot{ + if err := database.AddGitSnapshot(&models.GitSnapshot{ IssueID: issueID, Event: "handoff", CommitSHA: gitState.CommitSHA, Branch: gitState.Branch, DirtyFiles: gitState.DirtyFiles, - }) + }); err != nil { + output.Warning("failed to record git snapshot for %s: %v", issueID, err) + } } } @@ -517,8 +539,13 @@ Flags support values, stdin (-), or file (@path): ws.EndSHA = gitState.CommitSHA } - database.UpdateWorkSession(ws) - config.ClearActiveWorkSession(baseDir) + if err := database.UpdateWorkSession(ws); err != nil { + output.Error("failed to end work session: %v", err) + return err + } + if err := config.ClearActiveWorkSession(baseDir); err != nil { + output.Warning("work session %s ended but failed to clear active state: %v", ws.ID, err) + } } fmt.Printf("HANDOFF RECORDED %s\n", wsID) @@ -588,8 +615,13 @@ var wsEndCmd = &cobra.Command{ // End session now := time.Now() ws.EndedAt = &now - database.UpdateWorkSession(ws) - config.ClearActiveWorkSession(baseDir) + if err := database.UpdateWorkSession(ws); err != nil { + output.Error("failed to end work session: %v", err) + return err + } + if err := config.ClearActiveWorkSession(baseDir); err != nil { + output.Warning("work session %s ended but failed to clear active state: %v", ws.ID, err) + } output.Warning("No handoff recorded for %s", wsID) if len(issueIDs) > 0 { diff --git a/cmd/ws_test.go b/cmd/ws_test.go index 2ed9cdd8..6755084a 100644 --- a/cmd/ws_test.go +++ b/cmd/ws_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package cmd import ( diff --git a/internal/agent/instructions_test.go b/internal/agent/instructions_test.go index cdfd35b7..8f136eb9 100644 --- a/internal/agent/instructions_test.go +++ b/internal/agent/instructions_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package agent import ( diff --git a/internal/releasenotes/draft.go b/internal/releasenotes/draft.go index c09cc07a..bdad94de 100644 --- a/internal/releasenotes/draft.go +++ b/internal/releasenotes/draft.go @@ -75,9 +75,17 @@ func Generate(repoDir string, opts Options) (*Draft, error) { return nil, err } if taggedReleaseTarget { - fromRef, err = gitutil.GetPreviousSemverTag(root, toRef) + previousTag, tagErr := gitutil.GetPreviousSemverTag(root, toRef) + if tagErr != nil { + return nil, tagErr + } + fromRef = previousTag } else { - fromRef, err = gitutil.GetLatestSemverTag(root, toRef) + latestTag, tagErr := gitutil.GetLatestSemverTag(root, toRef) + if tagErr != nil { + return nil, tagErr + } + fromRef = latestTag } } else { fromRef, err = gitutil.GetLatestSemverTag(root, toRef) diff --git a/internal/serve/portfile_unix.go b/internal/serve/portfile_unix.go index 26a3b55f..b0855249 100644 --- a/internal/serve/portfile_unix.go +++ b/internal/serve/portfile_unix.go @@ -37,7 +37,8 @@ func acquireFileLockTimeout(f *os.File, timeout time.Duration) error { // releaseFileLock releases the exclusive flock. func releaseFileLock(f *os.File) { if f != nil { - syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + // Best effort: closing the file also releases the advisory lock. + _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) } } diff --git a/internal/serve/response_test.go b/internal/serve/response_test.go index a624abbf..150a9585 100644 --- a/internal/serve/response_test.go +++ b/internal/serve/response_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package serve import ( diff --git a/internal/serve/sse.go b/internal/serve/sse.go index 6d0aa399..8f3f72a8 100644 --- a/internal/serve/sse.go +++ b/internal/serve/sse.go @@ -397,7 +397,7 @@ func serveAutoSyncPush(database *db.DB, client *syncclient.Client, state *db.Syn if err != nil { return fmt.Errorf("begin tx: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := tdsync.GetPendingEvents(tx, deviceID, sessionID) if err != nil { @@ -502,12 +502,16 @@ func serveAutoSyncPull(database *db.DB, client *syncclient.Client, state *db.Syn // Accept all entity types in SSE path (no feature gating for live sync) allowAll := func(string) bool { return true } if _, err := tdsync.ApplyRemoteEvents(tx, events, deviceID, allowAll, state.LastSyncAt); err != nil { - tx.Rollback() + if rollbackErr := tx.Rollback(); rollbackErr != nil { + slog.Debug("serve autosync: rollback after apply failure", "err", rollbackErr) + } return fmt.Errorf("apply events: %w", err) } if _, err := tx.Exec(`UPDATE sync_state SET last_pulled_server_seq = ?, last_sync_at = CURRENT_TIMESTAMP`, pullResp.LastServerSeq); err != nil { - tx.Rollback() + if rollbackErr := tx.Rollback(); rollbackErr != nil { + slog.Debug("serve autosync: rollback after sync state failure", "err", rollbackErr) + } return fmt.Errorf("update sync state: %w", err) } diff --git a/internal/sync/backfill_test.go b/internal/sync/backfill_test.go index 758abf49..0303f27c 100644 --- a/internal/sync/backfill_test.go +++ b/internal/sync/backfill_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package sync import ( diff --git a/internal/sync/client_test.go b/internal/sync/client_test.go index 5d1b8a3c..dc7eace8 100644 --- a/internal/sync/client_test.go +++ b/internal/sync/client_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package sync import ( diff --git a/internal/sync/engine_test.go b/internal/sync/engine_test.go index afe65981..09c37be3 100644 --- a/internal/sync/engine_test.go +++ b/internal/sync/engine_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package sync import ( diff --git a/internal/sync/events_test.go b/internal/sync/events_test.go index 2df11d5f..c6eab7cd 100644 --- a/internal/sync/events_test.go +++ b/internal/sync/events_test.go @@ -1,3 +1,4 @@ +//nolint:errcheck // Legacy tests keep setup terse; production paths handle these return values explicitly. package sync import ( diff --git a/pkg/monitor/notes_modal.go b/pkg/monitor/notes_modal.go index 5e845acb..5e13a64e 100644 --- a/pkg/monitor/notes_modal.go +++ b/pkg/monitor/notes_modal.go @@ -1,3 +1,4 @@ +//nolint:unused // Notes modal support is staged but not yet wired into the monitor update loop. package monitor import ( @@ -19,20 +20,20 @@ import ( // NotesState holds the state for the notes modal system. type NotesState struct { // List state - Notes []models.Note - ListCursor int + Notes []models.Note + ListCursor int ShowArchived bool // Detail state - DetailNote *models.Note - DetailRender string // Pre-rendered markdown content + DetailNote *models.Note + DetailRender string // Pre-rendered markdown content // Edit state - Editing bool - Creating bool - EditTitle *textinput.Model - EditContent *textarea.Model - EditNoteID string // ID of note being edited (empty for create) + Editing bool + Creating bool + EditTitle *textinput.Model + EditContent *textarea.Model + EditNoteID string // ID of note being edited (empty for create) // Delete confirmation DeleteConfirm bool diff --git a/pkg/monitor/styles.go b/pkg/monitor/styles.go index 35acddb3..ecbc3d3c 100644 --- a/pkg/monitor/styles.go +++ b/pkg/monitor/styles.go @@ -237,6 +237,7 @@ var ( kanbanSepStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) // Modal border style for simple modal frames (notes loading, error states) + //nolint:unused // Reserved for the staged notes modal implementation. modalBorderStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("240")).