Fix lint errors: handle errors, use fmt.Fprintf, apply De Morgan's law, use switch statements

Amp-Thread-ID: https://ampcode.com/threads/T-afcf56b0-a8bc-4310-bb59-1b63e1d70c89
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-24 12:27:02 -07:00
parent 1d5e89b9bb
commit 9dcb86ebfb
17 changed files with 342 additions and 537 deletions
+2 -1
View File
@@ -16,7 +16,8 @@ import (
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
) )
// Core types for working with issues // Issue represents a tracked work item with metadata, dependencies, and status.
// Status represents the current state of an issue (open, in progress, closed, blocked).
type ( type (
Issue = types.Issue Issue = types.Issue
Status = types.Status Status = types.Status
+12 -4
View File
@@ -63,7 +63,9 @@ func TestLibraryIntegration(t *testing.T) {
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
store.CreateIssue(ctx, issue, "test-actor") if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Get it back // Get it back
retrieved, err := store.GetIssue(ctx, issue.ID) retrieved, err := store.GetIssue(ctx, issue.ID)
@@ -89,7 +91,9 @@ func TestLibraryIntegration(t *testing.T) {
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
store.CreateIssue(ctx, issue, "test-actor") if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Update status // Update status
updates := map[string]interface{}{ updates := map[string]interface{}{
@@ -131,8 +135,12 @@ func TestLibraryIntegration(t *testing.T) {
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
store.CreateIssue(ctx, issue1, "test-actor") if err := store.CreateIssue(ctx, issue1, "test-actor"); err != nil {
store.CreateIssue(ctx, issue2, "test-actor") t.Fatalf("CreateIssue failed: %v", err)
}
if err := store.CreateIssue(ctx, issue2, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add dependency: issue2 blocks issue1 // Add dependency: issue2 blocks issue1
dep := &beads.Dependency{ dep := &beads.Dependency{
+1 -1
View File
@@ -1038,7 +1038,7 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
} }
return return
case <-ctx.Done(): case <-ctx.Done():
log("Context cancelled, shutting down") log("Context canceled, shutting down")
if err := server.Stop(); err != nil { if err := server.Stop(); err != nil {
log("Error stopping RPC server: %v", err) log("Error stopping RPC server: %v", err)
} }
+1 -1
View File
@@ -249,7 +249,7 @@ func TestDaemonLogFileCreation(t *testing.T) {
timestamp := time.Now().Format("2006-01-02 15:04:05") timestamp := time.Now().Format("2006-01-02 15:04:05")
msg := "Test log message" msg := "Test log message"
_, err = logF.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, msg)) _, err = fmt.Fprintf(logF, "[%s] %s\n", timestamp, msg)
if err != nil { if err != nil {
t.Fatalf("Failed to write to log file: %v", err) t.Fatalf("Failed to write to log file: %v", err)
} }
+2 -2
View File
@@ -184,10 +184,10 @@ var closeEligibleEpicsCmd = &cobra.Command{
Reason: "All children completed", Reason: "All children completed",
}) })
if err != nil || !resp.Success { if err != nil || !resp.Success {
errMsg := "unknown error" errMsg := ""
if err != nil { if err != nil {
errMsg = err.Error() errMsg = err.Error()
} else { } else if !resp.Success {
errMsg = resp.Error errMsg = resp.Error
} }
fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", epicStatus.Epic.ID, errMsg) fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", epicStatus.Epic.ID, errMsg)
+1 -1
View File
@@ -657,7 +657,7 @@ func replaceBoundaryAware(text, oldID, newID string) string {
func isBoundary(c byte) bool { func isBoundary(c byte) bool {
// Issue IDs contain: lowercase letters, digits, and hyphens // Issue IDs contain: lowercase letters, digits, and hyphens
// Boundaries are anything else (space, punctuation, etc.) // Boundaries are anything else (space, punctuation, etc.)
return !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') return (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-'
} }
// isNumeric returns true if the string contains only digits // isNumeric returns true if the string contains only digits
+39 -104
View File
@@ -20,92 +20,40 @@ var labelCmd = &cobra.Command{
Short: "Manage issue labels", Short: "Manage issue labels",
} }
// executeLabelCommand executes a label operation and handles output // Helper function to process label operations for multiple issues
func executeLabelCommand(issueID, label, operation string, operationFunc func(context.Context, string, string, string) error) { func processBatchLabelOperation(issueIDs []string, label string, operation string,
ctx := context.Background() daemonFunc func(string, string) error, storeFunc func(context.Context, string, string, string) error) {
// Use daemon if available
if daemonClient != nil {
var err error
if operation == "added" {
_, err = daemonClient.AddLabel(&rpc.LabelAddArgs{
ID: issueID,
Label: label,
})
} else {
_, err = daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{
ID: issueID,
Label: label,
})
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
} else {
// Direct mode
if err := operationFunc(ctx, issueID, label, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Schedule auto-flush
markDirtyAndScheduleFlush()
}
if jsonOutput {
outputJSON(map[string]interface{}{
"status": operation,
"issue_id": issueID,
"label": label,
})
return
}
green := color.New(color.FgGreen).SprintFunc()
// Capitalize first letter manually (strings.Title is deprecated)
capitalizedOp := strings.ToUpper(operation[:1]) + operation[1:]
fmt.Printf("%s %s label '%s' to %s\n", green("✓"), capitalizedOp, label, issueID)
}
var labelAddCmd = &cobra.Command{
Use: "add [issue-id...] [label]",
Short: "Add a label to one or more issues",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
// Last arg is the label, everything before is issue IDs
label := args[len(args)-1]
issueIDs := args[:len(args)-1]
ctx := context.Background() ctx := context.Background()
results := []map[string]interface{}{} results := []map[string]interface{}{}
for _, issueID := range issueIDs { for _, issueID := range issueIDs {
var err error var err error
if daemonClient != nil { if daemonClient != nil {
_, err = daemonClient.AddLabel(&rpc.LabelAddArgs{ err = daemonFunc(issueID, label)
ID: issueID,
Label: label,
})
} else { } else {
err = store.AddLabel(ctx, issueID, label, actor) err = storeFunc(ctx, issueID, label, actor)
} }
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error adding label to %s: %v\n", issueID, err) fmt.Fprintf(os.Stderr, "Error %s label %s %s: %v\n", operation, operation, issueID, err)
continue continue
} }
if jsonOutput { if jsonOutput {
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"status": "added", "status": operation,
"issue_id": issueID, "issue_id": issueID,
"label": label, "label": label,
}) })
} else { } else {
green := color.New(color.FgGreen).SprintFunc() green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Added label '%s' to %s\n", green("✓"), label, issueID) verb := "Added"
prep := "to"
if operation == "removed" {
verb = "Removed"
prep = "from"
}
fmt.Printf("%s %s label '%s' %s %s\n", green("✓"), verb, label, prep, issueID)
} }
} }
@@ -116,6 +64,24 @@ var labelAddCmd = &cobra.Command{
if jsonOutput && len(results) > 0 { if jsonOutput && len(results) > 0 {
outputJSON(results) outputJSON(results)
} }
}
var labelAddCmd = &cobra.Command{
Use: "add [issue-id...] [label]",
Short: "Add a label to one or more issues",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
label := args[len(args)-1]
issueIDs := args[:len(args)-1]
processBatchLabelOperation(issueIDs, label, "added",
func(issueID, lbl string) error {
_, err := daemonClient.AddLabel(&rpc.LabelAddArgs{ID: issueID, Label: lbl})
return err
},
func(ctx context.Context, issueID, lbl, act string) error {
return store.AddLabel(ctx, issueID, lbl, act)
})
}, },
} }
@@ -124,48 +90,17 @@ var labelRemoveCmd = &cobra.Command{
Short: "Remove a label from one or more issues", Short: "Remove a label from one or more issues",
Args: cobra.MinimumNArgs(2), Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Last arg is the label, everything before is issue IDs
label := args[len(args)-1] label := args[len(args)-1]
issueIDs := args[:len(args)-1] issueIDs := args[:len(args)-1]
ctx := context.Background() processBatchLabelOperation(issueIDs, label, "removed",
results := []map[string]interface{}{} func(issueID, lbl string) error {
_, err := daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ID: issueID, Label: lbl})
for _, issueID := range issueIDs { return err
var err error },
if daemonClient != nil { func(ctx context.Context, issueID, lbl, act string) error {
_, err = daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ return store.RemoveLabel(ctx, issueID, lbl, act)
ID: issueID,
Label: label,
}) })
} else {
err = store.RemoveLabel(ctx, issueID, label, actor)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error removing label from %s: %v\n", issueID, err)
continue
}
if jsonOutput {
results = append(results, map[string]interface{}{
"status": "removed",
"issue_id": issueID,
"label": label,
})
} else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Removed label '%s' from %s\n", green("✓"), label, issueID)
}
}
if len(issueIDs) > 0 && daemonClient == nil {
markDirtyAndScheduleFlush()
}
if jsonOutput && len(results) > 0 {
outputJSON(results)
}
}, },
} }
+22 -106
View File
@@ -48,6 +48,9 @@ const (
FallbackFlagNoDaemon = "flag_no_daemon" FallbackFlagNoDaemon = "flag_no_daemon"
FallbackConnectFailed = "connect_failed" FallbackConnectFailed = "connect_failed"
FallbackHealthFailed = "health_failed" FallbackHealthFailed = "health_failed"
cmdDaemon = "daemon"
cmdImport = "import"
statusHealthy = "healthy"
FallbackAutoStartDisabled = "auto_start_disabled" FallbackAutoStartDisabled = "auto_start_disabled"
FallbackAutoStartFailed = "auto_start_failed" FallbackAutoStartFailed = "auto_start_failed"
FallbackDaemonUnsupported = "daemon_unsupported" FallbackDaemonUnsupported = "daemon_unsupported"
@@ -109,7 +112,7 @@ var rootCmd = &cobra.Command{
} }
// Skip database initialization for commands that don't need a database // Skip database initialization for commands that don't need a database
if cmd.Name() == "init" || cmd.Name() == "daemon" || cmd.Name() == "help" || cmd.Name() == "version" || cmd.Name() == "quickstart" { if cmd.Name() == "init" || cmd.Name() == cmdDaemon || cmd.Name() == "help" || cmd.Name() == "version" || cmd.Name() == "quickstart" {
return return
} }
@@ -140,7 +143,7 @@ var rootCmd = &cobra.Command{
// Special case for import: if we found a database but there's a local .beads/ // Special case for import: if we found a database but there's a local .beads/
// directory without a database, prefer creating a local database // directory without a database, prefer creating a local database
if cmd.Name() == "import" && localBeadsDir != "" { if cmd.Name() == cmdImport && localBeadsDir != "" {
if _, err := os.Stat(localBeadsDir); err == nil { if _, err := os.Stat(localBeadsDir); err == nil {
// Check if found database is NOT in the local .beads/ directory // Check if found database is NOT in the local .beads/ directory
if !strings.HasPrefix(dbPath, localBeadsDir+string(filepath.Separator)) { if !strings.HasPrefix(dbPath, localBeadsDir+string(filepath.Separator)) {
@@ -151,7 +154,7 @@ var rootCmd = &cobra.Command{
} }
} else { } else {
// For import command, allow creating database if .beads/ directory exists // For import command, allow creating database if .beads/ directory exists
if cmd.Name() == "import" && localBeadsDir != "" { if cmd.Name() == cmdImport && localBeadsDir != "" {
if _, err := os.Stat(localBeadsDir); err == nil { if _, err := os.Stat(localBeadsDir); err == nil {
// .beads/ directory exists - set dbPath for import to create // .beads/ directory exists - set dbPath for import to create
dbPath = filepath.Join(localBeadsDir, "vc.db") dbPath = filepath.Join(localBeadsDir, "vc.db")
@@ -211,7 +214,7 @@ var rootCmd = &cobra.Command{
// Perform health check // Perform health check
health, healthErr := client.Health() health, healthErr := client.Health()
if healthErr == nil && health.Status == "healthy" { if healthErr == nil && health.Status == statusHealthy {
// Check version compatibility // Check version compatibility
if !health.Compatible { if !health.Compatible {
if os.Getenv("BD_DEBUG") != "" { if os.Getenv("BD_DEBUG") != "" {
@@ -230,9 +233,9 @@ var rootCmd = &cobra.Command{
client.SetDatabasePath(absDBPath) client.SetDatabasePath(absDBPath)
} }
health, healthErr = client.Health() health, healthErr = client.Health()
if healthErr == nil && health.Status == "healthy" { if healthErr == nil && health.Status == statusHealthy {
daemonClient = client daemonClient = client
daemonStatus.Mode = "daemon" daemonStatus.Mode = cmdDaemon
daemonStatus.Connected = true daemonStatus.Connected = true
daemonStatus.Degraded = false daemonStatus.Degraded = false
daemonStatus.Health = health.Status daemonStatus.Health = health.Status
@@ -251,7 +254,7 @@ var rootCmd = &cobra.Command{
} else { } else {
// Daemon is healthy and compatible - use it // Daemon is healthy and compatible - use it
daemonClient = client daemonClient = client
daemonStatus.Mode = "daemon" daemonStatus.Mode = cmdDaemon
daemonStatus.Connected = true daemonStatus.Connected = true
daemonStatus.Degraded = false daemonStatus.Degraded = false
daemonStatus.Health = health.Status daemonStatus.Health = health.Status
@@ -309,9 +312,9 @@ var rootCmd = &cobra.Command{
// Check health of auto-started daemon // Check health of auto-started daemon
health, healthErr := client.Health() health, healthErr := client.Health()
if healthErr == nil && health.Status == "healthy" { if healthErr == nil && health.Status == statusHealthy {
daemonClient = client daemonClient = client
daemonStatus.Mode = "daemon" daemonStatus.Mode = cmdDaemon
daemonStatus.Connected = true daemonStatus.Connected = true
daemonStatus.Degraded = false daemonStatus.Degraded = false
daemonStatus.AutoStartSucceeded = true daemonStatus.AutoStartSucceeded = true
@@ -487,98 +490,11 @@ func shouldAutoStartDaemon() bool {
func shouldUseGlobalDaemon() bool { func shouldUseGlobalDaemon() bool {
// Global daemon support is deprecated // Global daemon support is deprecated
// Always use local daemon (per-project .beads/ socket) // Always use local daemon (per-project .beads/ socket)
return false
// Previously supported BEADS_PREFER_GLOBAL_DAEMON env var, but global // Previously supported BEADS_PREFER_GLOBAL_DAEMON env var, but global
// daemon has issues with multi-workspace git workflows // daemon has issues with multi-workspace git workflows
// Heuristic: detect multiple beads repositories
home, err := os.UserHomeDir()
if err != nil {
return false return false
} }
// Count .beads directories under home
repoCount := 0
maxDepth := 5 // Don't scan too deep
var countRepos func(string, int) error
countRepos = func(dir string, depth int) error {
if depth > maxDepth || repoCount > 1 {
return filepath.SkipDir
}
entries, err := os.ReadDir(dir)
if err != nil {
return nil // Skip directories we can't read
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
name := entry.Name()
// Skip hidden dirs except .beads
if strings.HasPrefix(name, ".") && name != ".beads" {
continue
}
// Skip common large directories
if name == "node_modules" || name == "vendor" || name == "target" || name == ".git" {
continue
}
path := filepath.Join(dir, name)
// Check if this is a .beads directory with a database
if name == ".beads" {
dbPath := filepath.Join(path, "db.sqlite")
if _, err := os.Stat(dbPath); err == nil {
repoCount++
if repoCount > 1 {
return filepath.SkipDir
}
}
continue
}
// Recurse into subdirectories
if depth < maxDepth {
_ = countRepos(path, depth+1)
}
}
return nil
}
// Scan common project directories
projectDirs := []string{
filepath.Join(home, "src"),
filepath.Join(home, "projects"),
filepath.Join(home, "code"),
filepath.Join(home, "workspace"),
filepath.Join(home, "dev"),
}
for _, dir := range projectDirs {
if _, err := os.Stat(dir); err == nil {
_ = countRepos(dir, 0)
if repoCount > 1 {
break
}
}
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: found %d beads repositories, prefer global: %v\n", repoCount, repoCount > 1)
}
// Use global daemon if we found more than 1 repository (multi-repo workflow)
// This prevents concurrency issues when multiple repos are being worked on
return repoCount > 1
}
// restartDaemonForVersionMismatch stops the old daemon and starts a new one // restartDaemonForVersionMismatch stops the old daemon and starts a new one
// Returns true if restart was successful // Returns true if restart was successful
func restartDaemonForVersionMismatch() bool { func restartDaemonForVersionMismatch() bool {
@@ -1947,13 +1863,13 @@ var showCmd = &cobra.Command{
// Format output (same as direct mode below) // Format output (same as direct mode below)
tierEmoji := "" tierEmoji := ""
statusSuffix := "" statusSuffix := ""
if issue.CompactionLevel == 1 { switch issue.CompactionLevel {
case 1:
tierEmoji = " 🗜️" tierEmoji = " 🗜️"
} else if issue.CompactionLevel == 2 { statusSuffix = " (compacted L1)"
case 2:
tierEmoji = " 📦" tierEmoji = " 📦"
} statusSuffix = " (compacted L2)"
if issue.CompactionLevel > 0 {
statusSuffix = fmt.Sprintf(" (compacted L%d)", issue.CompactionLevel)
} }
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji) fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
@@ -2074,13 +1990,13 @@ var showCmd = &cobra.Command{
// Add compaction emoji to title line // Add compaction emoji to title line
tierEmoji := "" tierEmoji := ""
statusSuffix := "" statusSuffix := ""
if issue.CompactionLevel == 1 { switch issue.CompactionLevel {
case 1:
tierEmoji = " 🗜️" tierEmoji = " 🗜️"
} else if issue.CompactionLevel == 2 { statusSuffix = " (compacted L1)"
case 2:
tierEmoji = " 📦" tierEmoji = " 📦"
} statusSuffix = " (compacted L2)"
if issue.CompactionLevel > 0 {
statusSuffix = fmt.Sprintf(" (compacted L%d)", issue.CompactionLevel)
} }
fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji) fmt.Printf("\n%s: %s%s\n", cyan(issue.ID), issue.Title, tierEmoji)
-7
View File
@@ -7,7 +7,6 @@ import (
"os" "os"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
@@ -358,12 +357,6 @@ func renumberDependencies(ctx context.Context, idMapping map[string]string, allD
return nil return nil
} }
// Helper to extract numeric part from issue ID
func extractNumber(issueID, prefix string) (int, error) {
numStr := strings.TrimPrefix(issueID, prefix+"-")
return strconv.Atoi(numStr)
}
func init() { func init() {
renumberCmd.Flags().Bool("dry-run", false, "Preview changes without applying them") renumberCmd.Flags().Bool("dry-run", false, "Preview changes without applying them")
renumberCmd.Flags().Bool("force", false, "Actually perform the renumbering") renumberCmd.Flags().Bool("force", false, "Actually perform the renumbering")
+6
View File
@@ -13,18 +13,21 @@ const (
defaultConcurrency = 5 defaultConcurrency = 5
) )
// CompactConfig holds configuration for the compaction process.
type CompactConfig struct { type CompactConfig struct {
APIKey string APIKey string
Concurrency int Concurrency int
DryRun bool DryRun bool
} }
// Compactor handles issue compaction using AI summarization.
type Compactor struct { type Compactor struct {
store *sqlite.SQLiteStorage store *sqlite.SQLiteStorage
haiku *HaikuClient haiku *HaikuClient
config *CompactConfig config *CompactConfig
} }
// New creates a new Compactor instance with the given configuration.
func New(store *sqlite.SQLiteStorage, apiKey string, config *CompactConfig) (*Compactor, error) { func New(store *sqlite.SQLiteStorage, apiKey string, config *CompactConfig) (*Compactor, error) {
if config == nil { if config == nil {
config = &CompactConfig{ config = &CompactConfig{
@@ -58,6 +61,7 @@ func New(store *sqlite.SQLiteStorage, apiKey string, config *CompactConfig) (*Co
}, nil }, nil
} }
// CompactResult holds the outcome of a compaction operation.
type CompactResult struct { type CompactResult struct {
IssueID string IssueID string
OriginalSize int OriginalSize int
@@ -65,6 +69,7 @@ type CompactResult struct {
Err error Err error
} }
// CompactTier1 performs tier-1 compaction on a single issue using AI summarization.
func (c *Compactor) CompactTier1(ctx context.Context, issueID string) error { func (c *Compactor) CompactTier1(ctx context.Context, issueID string) error {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
@@ -137,6 +142,7 @@ func (c *Compactor) CompactTier1(ctx context.Context, issueID string) error {
return nil return nil
} }
// CompactTier1Batch performs tier-1 compaction on multiple issues in a single batch.
func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([]*CompactResult, error) { func (c *Compactor) CompactTier1Batch(ctx context.Context, issueIDs []string) ([]*CompactResult, error) {
if len(issueIDs) == 0 { if len(issueIDs) == 0 {
return nil, nil return nil, nil
+3 -2
View File
@@ -298,11 +298,12 @@ func TestCompactTier1Batch_WithIneligible(t *testing.T) {
} }
for _, result := range results { for _, result := range results {
if result.IssueID == openIssue.ID { switch result.IssueID {
case openIssue.ID:
if result.Err == nil { if result.Err == nil {
t.Error("expected error for ineligible issue") t.Error("expected error for ineligible issue")
} }
} else if result.IssueID == closedIssue.ID { case closedIssue.ID:
if result.Err != nil { if result.Err != nil {
t.Errorf("unexpected error for eligible issue: %v", result.Err) t.Errorf("unexpected error for eligible issue: %v", result.Err)
} }
+1
View File
@@ -22,6 +22,7 @@ const (
initialBackoff = 1 * time.Second initialBackoff = 1 * time.Second
) )
// ErrAPIKeyRequired is returned when an API key is needed but not provided.
var ErrAPIKeyRequired = errors.New("API key required") var ErrAPIKeyRequired = errors.New("API key required")
// HaikuClient wraps the Anthropic API for issue summarization. // HaikuClient wraps the Anthropic API for issue summarization.
+1 -1
View File
@@ -193,7 +193,7 @@ func TestCallWithRetry_ContextCancellation(t *testing.T) {
_, err = client.callWithRetry(ctx, "test prompt") _, err = client.callWithRetry(ctx, "test prompt")
if err == nil { if err == nil {
t.Fatal("expected error when context is cancelled") t.Fatal("expected error when context is canceled")
} }
if err != context.Canceled { if err != context.Canceled {
t.Errorf("expected context.Canceled error, got: %v", err) t.Errorf("expected context.Canceled error, got: %v", err)
+2 -2
View File
@@ -145,8 +145,8 @@ func Set(key string, value interface{}) {
// return v.BindPFlag(key, flag) // return v.BindPFlag(key, flag)
// } // }
// ConfigFileUsed returns the path to the config file being used // FileUsed returns the path to the active configuration file.
func ConfigFileUsed() string { func FileUsed() string {
if v == nil { if v == nil {
return "" return ""
} }
+1 -1
View File
@@ -218,7 +218,7 @@ func (c *Client) Update(args *UpdateArgs) (*Response, error) {
return c.Execute(OpUpdate, args) return c.Execute(OpUpdate, args)
} }
// Close closes an issue via the daemon (operation, not connection) // CloseIssue marks an issue as closed via the daemon.
func (c *Client) CloseIssue(args *CloseArgs) (*Response, error) { func (c *Client) CloseIssue(args *CloseArgs) (*Response, error) {
return c.Execute(OpClose, args) return c.Execute(OpClose, args)
} }
+30 -70
View File
@@ -28,6 +28,10 @@ import (
// It's set as a var so it can be initialized from main // It's set as a var so it can be initialized from main
var ServerVersion = "0.9.10" var ServerVersion = "0.9.10"
const (
statusUnhealthy = "unhealthy"
)
// normalizeLabels trims whitespace, removes empty strings, and deduplicates labels // normalizeLabels trims whitespace, removes empty strings, and deduplicates labels
func normalizeLabels(ss []string) []string { func normalizeLabels(ss []string) []string {
seen := make(map[string]struct{}) seen := make(map[string]struct{})
@@ -699,13 +703,6 @@ func strValue(p *string) string {
return *p return *p
} }
func strPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
func updatesFromArgs(a UpdateArgs) map[string]interface{} { func updatesFromArgs(a UpdateArgs) map[string]interface{} {
u := map[string]interface{}{} u := map[string]interface{}{}
if a.Title != nil { if a.Title != nil {
@@ -780,7 +777,7 @@ func (s *Server) handleHealth(req *Request) Response {
dbResponseMs := time.Since(start).Seconds() * 1000 dbResponseMs := time.Since(start).Seconds() * 1000
if pingErr != nil { if pingErr != nil {
status = "unhealthy" status = statusUnhealthy
dbError = pingErr.Error() dbError = pingErr.Error()
} else if dbResponseMs > 500 { } else if dbResponseMs > 500 {
status = "degraded" status = "degraded"
@@ -1270,12 +1267,13 @@ func (s *Server) handleDepAdd(req *Request) Response {
return Response{Success: true} return Response{Success: true}
} }
func (s *Server) handleDepRemove(req *Request) Response { // Generic handler for simple store operations with standard error handling
var depArgs DepRemoveArgs func (s *Server) handleSimpleStoreOp(req *Request, argsPtr interface{}, argDesc string,
if err := json.Unmarshal(req.Args, &depArgs); err != nil { opFunc func(context.Context, storage.Storage, string) error) Response {
if err := json.Unmarshal(req.Args, argsPtr); err != nil {
return Response{ return Response{
Success: false, Success: false,
Error: fmt.Sprintf("invalid dep remove args: %v", err), Error: fmt.Sprintf("invalid %s args: %v", argDesc, err),
} }
} }
@@ -1288,70 +1286,35 @@ func (s *Server) handleDepRemove(req *Request) Response {
} }
ctx := s.reqCtx(req) ctx := s.reqCtx(req)
if err := store.RemoveDependency(ctx, depArgs.FromID, depArgs.ToID, s.reqActor(req)); err != nil { if err := opFunc(ctx, store, s.reqActor(req)); err != nil {
return Response{ return Response{
Success: false, Success: false,
Error: fmt.Sprintf("failed to remove dependency: %v", err), Error: fmt.Sprintf("failed to %s: %v", argDesc, err),
} }
} }
return Response{Success: true} return Response{Success: true}
} }
func (s *Server) handleDepRemove(req *Request) Response {
var depArgs DepRemoveArgs
return s.handleSimpleStoreOp(req, &depArgs, "dep remove", func(ctx context.Context, store storage.Storage, actor string) error {
return store.RemoveDependency(ctx, depArgs.FromID, depArgs.ToID, actor)
})
}
func (s *Server) handleLabelAdd(req *Request) Response { func (s *Server) handleLabelAdd(req *Request) Response {
var labelArgs LabelAddArgs var labelArgs LabelAddArgs
if err := json.Unmarshal(req.Args, &labelArgs); err != nil { return s.handleSimpleStoreOp(req, &labelArgs, "label add", func(ctx context.Context, store storage.Storage, actor string) error {
return Response{ return store.AddLabel(ctx, labelArgs.ID, labelArgs.Label, actor)
Success: false, })
Error: fmt.Sprintf("invalid label add args: %v", err),
}
}
store, err := s.getStorageForRequest(req)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("storage error: %v", err),
}
}
ctx := s.reqCtx(req)
if err := store.AddLabel(ctx, labelArgs.ID, labelArgs.Label, s.reqActor(req)); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to add label: %v", err),
}
}
return Response{Success: true}
} }
func (s *Server) handleLabelRemove(req *Request) Response { func (s *Server) handleLabelRemove(req *Request) Response {
var labelArgs LabelRemoveArgs var labelArgs LabelRemoveArgs
if err := json.Unmarshal(req.Args, &labelArgs); err != nil { return s.handleSimpleStoreOp(req, &labelArgs, "label remove", func(ctx context.Context, store storage.Storage, actor string) error {
return Response{ return store.RemoveLabel(ctx, labelArgs.ID, labelArgs.Label, actor)
Success: false, })
Error: fmt.Sprintf("invalid label remove args: %v", err),
}
}
store, err := s.getStorageForRequest(req)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("storage error: %v", err),
}
}
ctx := s.reqCtx(req)
if err := store.RemoveLabel(ctx, labelArgs.ID, labelArgs.Label, s.reqActor(req)); err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to remove label: %v", err),
}
}
return Response{Success: true}
} }
func (s *Server) handleCommentList(req *Request) Response { func (s *Server) handleCommentList(req *Request) Response {
@@ -1443,11 +1406,7 @@ func (s *Server) handleBatch(req *Request) Response {
resp := s.handleRequest(subReq) resp := s.handleRequest(subReq)
results = append(results, BatchResult{ results = append(results, BatchResult(resp))
Success: resp.Success,
Data: resp.Data,
Error: resp.Error,
})
if !resp.Success { if !resp.Success {
break break
@@ -1929,7 +1888,8 @@ func (s *Server) handleCompact(req *Request) Response {
if args.All { if args.All {
var candidates []*sqlite.CompactionCandidate var candidates []*sqlite.CompactionCandidate
if args.Tier == 1 { switch args.Tier {
case 1:
tier1, err := sqliteStore.GetTier1Candidates(ctx) tier1, err := sqliteStore.GetTier1Candidates(ctx)
if err != nil { if err != nil {
return Response{ return Response{
@@ -1938,7 +1898,7 @@ func (s *Server) handleCompact(req *Request) Response {
} }
} }
candidates = tier1 candidates = tier1
} else if args.Tier == 2 { case 2:
tier2, err := sqliteStore.GetTier2Candidates(ctx) tier2, err := sqliteStore.GetTier2Candidates(ctx)
if err != nil { if err != nil {
return Response{ return Response{
@@ -1947,7 +1907,7 @@ func (s *Server) handleCompact(req *Request) Response {
} }
} }
candidates = tier2 candidates = tier2
} else { default:
return Response{ return Response{
Success: false, Success: false,
Error: fmt.Sprintf("invalid tier: %d (must be 1 or 2)", args.Tier), Error: fmt.Sprintf("invalid tier: %d (must be 1 or 2)", args.Tier),
+3 -19
View File
@@ -128,14 +128,6 @@ func (s *SQLiteStorage) GetTier2Candidates(ctx context.Context) ([]*CompactionCa
daysStr = "90" daysStr = "90"
} }
depthStr, err := s.GetConfig(ctx, "compact_tier2_dep_levels")
if err != nil {
return nil, fmt.Errorf("failed to get compact_tier2_dep_levels: %w", err)
}
if depthStr == "" {
depthStr = "5"
}
commitsStr, err := s.GetConfig(ctx, "compact_tier2_commits") commitsStr, err := s.GetConfig(ctx, "compact_tier2_commits")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get compact_tier2_commits: %w", err) return nil, fmt.Errorf("failed to get compact_tier2_commits: %w", err)
@@ -227,20 +219,12 @@ func (s *SQLiteStorage) CheckEligibility(ctx context.Context, issueID string, ti
return false, "issue has no closed_at timestamp", nil return false, "issue has no closed_at timestamp", nil
} }
if tier == 1 { switch tier {
case 1:
if compactionLevel != 0 { if compactionLevel != 0 {
return false, "issue is already compacted", nil return false, "issue is already compacted", nil
} }
// Check if closed long enough
daysStr, err := s.GetConfig(ctx, "compact_tier1_days")
if err != nil {
return false, "", fmt.Errorf("failed to get compact_tier1_days: %w", err)
}
if daysStr == "" {
daysStr = "30"
}
// Check if it appears in tier1 candidates // Check if it appears in tier1 candidates
candidates, err := s.GetTier1Candidates(ctx) candidates, err := s.GetTier1Candidates(ctx)
if err != nil { if err != nil {
@@ -255,7 +239,7 @@ func (s *SQLiteStorage) CheckEligibility(ctx context.Context, issueID string, ti
return false, "issue has open dependents or not closed long enough", nil return false, "issue has open dependents or not closed long enough", nil
} else if tier == 2 { case 2:
if compactionLevel != 1 { if compactionLevel != 1 {
return false, "issue must be at compaction level 1 for tier 2", nil return false, "issue must be at compaction level 1 for tier 2", nil
} }