diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc5686..03efb3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' - name: Download dependencies run: go mod download @@ -44,9 +44,11 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 + # v8+ is required: .golangci.yml uses the golangci-lint v2 config schema + # (version: "2", linters.settings, formatters), which v1 cannot parse. + uses: golangci/golangci-lint-action@v8 with: - version: latest + version: v2.12.2 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d21cd68 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,67 @@ +version: "2" + +run: + timeout: 8m + +linters: + default: standard + enable: + # standard set (errcheck, govet, ineffassign, staticcheck, unused) plus: + - errorlint # error comparisons/assertions that break under wrapping + - nilerr # returning nil when a non-nil error is in hand + - gocritic # opinionated but high-signal style/perf diagnostics + - revive # doc comments, shadowed builtins, unused params + - unconvert # redundant type conversions + - misspell # typos in comments and strings + - bodyclose # unclosed HTTP response bodies + + settings: + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: increment-decrement + - name: var-naming + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: indent-error-flow + - name: errorf + - name: redefines-builtin-id + - name: unused-parameter + - name: unreachable-code + - name: superfluous-else + + exclusions: + rules: + # Mock and fake implementations must match interface signatures, so their + # parameters are structurally required even when unused. Renaming them all + # to `_` would make the test doubles harder to read, not easier. + - path: _test\.go + linters: + - revive + text: "unused-parameter" + + # stats.StatsResult, budget.BudgetEstimate and budget.BudgetSource trip + # revive's stutter check. Renaming an exported type is a cross-package API + # break (15 call sites across the repo) and does not belong in a lint-only + # change; each should be done deliberately in its own commit. + - path: (internal/stats/|internal/budget/) + linters: + - revive + text: "and that stutters" + +formatters: + enable: + - gofmt + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/cmd/nightshift/commands/budget.go b/cmd/nightshift/commands/budget.go index 424ee78..a420694 100644 --- a/cmd/nightshift/commands/budget.go +++ b/cmd/nightshift/commands/budget.go @@ -21,7 +21,7 @@ var budgetCmd = &cobra.Command{ Long: `Display current budget status and usage. Shows spending across all providers or a specific provider.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { provider, _ := cmd.Flags().GetString("provider") return runBudget(provider) }, diff --git a/cmd/nightshift/commands/config.go b/cmd/nightshift/commands/config.go index 4300470..fe781e9 100644 --- a/cmd/nightshift/commands/config.go +++ b/cmd/nightshift/commands/config.go @@ -21,7 +21,7 @@ var configCmd = &cobra.Command{ Shows current configuration merged from global and project configs. Use subcommands to get/set specific values or validate the config.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { return runConfigShow() }, } @@ -36,7 +36,7 @@ Examples: nightshift config get providers.claude.enabled nightshift config get logging.level`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runConfigGet(args[0]) }, } @@ -66,7 +66,7 @@ var configValidateCmd = &cobra.Command{ Long: `Validate the current configuration. Checks both global and project configs for errors.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { return runConfigValidate() }, } diff --git a/cmd/nightshift/commands/daemon.go b/cmd/nightshift/commands/daemon.go index d082b0e..e4204e8 100644 --- a/cmd/nightshift/commands/daemon.go +++ b/cmd/nightshift/commands/daemon.go @@ -2,6 +2,7 @@ package commands import ( "context" + "errors" "fmt" "os" "os/exec" @@ -129,7 +130,7 @@ func isDaemonRunning() (bool, int) { return isProcessRunning(pid), pid } -func runDaemonStart(cmd *cobra.Command, args []string) error { +func runDaemonStart(_ *cobra.Command, _ []string) error { // Check if already running if running, pid := isDaemonRunning(); running { return fmt.Errorf("daemon already running (pid %d)", pid) @@ -238,7 +239,7 @@ func runDaemonLoop(cfg *config.Config) error { <-ctx.Done() // Stop scheduler gracefully - if err := sched.Stop(); err != nil && err != scheduler.ErrNotRunning { + if err := sched.Stop(); err != nil && !errors.Is(err, scheduler.ErrNotRunning) { log.Errorf("stopping scheduler: %v", err) } @@ -564,28 +565,30 @@ func takeSnapshot(ctx context.Context, cfg *config.Config, database *db.DB, log if cfg.Providers.Claude.Enabled { snapshot, err := collector.TakeSnapshot(ctx, "claude") - if err != nil { + switch { + case err != nil: log.Warnf("snapshot claude: %v", err) - } else if snapshot.ScrapedPct != nil { + case snapshot.ScrapedPct != nil: log.Infof("snapshot claude: %.1f%%", *snapshot.ScrapedPct) - } else { + default: log.Info("snapshot claude: local-only") } } if cfg.Providers.Codex.Enabled { snapshot, err := collector.TakeSnapshot(ctx, "codex") - if err != nil { + switch { + case err != nil: log.Warnf("snapshot codex: %v", err) - } else if snapshot.ScrapedPct != nil { + case snapshot.ScrapedPct != nil: log.Infof("snapshot codex: %.1f%%", *snapshot.ScrapedPct) - } else { + default: log.Info("snapshot codex: local-only") } } } -func pruneSnapshots(ctx context.Context, cfg *config.Config, database *db.DB, log *logging.Logger) { +func pruneSnapshots(_ context.Context, cfg *config.Config, database *db.DB, log *logging.Logger) { collector := snapshots.NewCollector(database, nil, nil, nil, nil, weekStartDayFromConfig(cfg)) deleted, err := collector.Prune(cfg.Budget.SnapshotRetentionDays) if err != nil { @@ -609,7 +612,7 @@ func weekStartDayFromConfig(cfg *config.Config) time.Weekday { } } -func runDaemonStop(cmd *cobra.Command, args []string) error { +func runDaemonStop(_ *cobra.Command, _ []string) error { running, pid := isDaemonRunning() if !running { // Check if PID file exists but process is dead @@ -657,7 +660,7 @@ func runDaemonStop(cmd *cobra.Command, args []string) error { } } -func runDaemonStatus(cmd *cobra.Command, args []string) error { +func runDaemonStatus(_ *cobra.Command, _ []string) error { running, pid := isDaemonRunning() if !running { diff --git a/cmd/nightshift/commands/doctor.go b/cmd/nightshift/commands/doctor.go index 1a0f0ee..f5a5630 100644 --- a/cmd/nightshift/commands/doctor.go +++ b/cmd/nightshift/commands/doctor.go @@ -50,7 +50,7 @@ func init() { rootCmd.AddCommand(doctorCmd) } -func runDoctor(cmd *cobra.Command, args []string) error { +func runDoctor(_ *cobra.Command, _ []string) error { // Augment PATH the same way 'run' does so CLI checks are accurate. ensurePATH() diff --git a/cmd/nightshift/commands/init.go b/cmd/nightshift/commands/init.go index 2ade6bf..517d4a3 100644 --- a/cmd/nightshift/commands/init.go +++ b/cmd/nightshift/commands/init.go @@ -36,7 +36,7 @@ func init() { rootCmd.AddCommand(initCmd) } -func runInit(cmd *cobra.Command, args []string) error { +func runInit(cmd *cobra.Command, _ []string) error { global, _ := cmd.Flags().GetBool("global") force, _ := cmd.Flags().GetBool("force") diff --git a/cmd/nightshift/commands/install.go b/cmd/nightshift/commands/install.go index c25ad96..e553858 100644 --- a/cmd/nightshift/commands/install.go +++ b/cmd/nightshift/commands/install.go @@ -55,7 +55,7 @@ func init() { } // runInstall implements the install command -func runInstall(cmd *cobra.Command, args []string) error { +func runInstall(_ *cobra.Command, args []string) error { // Determine service type serviceType := "" if len(args) > 0 { @@ -378,7 +378,7 @@ func installCron(binaryPath string, cfg *config.Config) error { } // runUninstall implements the uninstall command -func runUninstall(cmd *cobra.Command, args []string) error { +func runUninstall(_ *cobra.Command, _ []string) error { var errors []string removed := false diff --git a/cmd/nightshift/commands/logs.go b/cmd/nightshift/commands/logs.go index c33e7f9..ff60f6f 100644 --- a/cmd/nightshift/commands/logs.go +++ b/cmd/nightshift/commands/logs.go @@ -24,7 +24,7 @@ var logsCmd = &cobra.Command{ Long: `View nightshift logs. Displays recent log entries. Use --follow to stream logs in real-time.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { tail, _ := cmd.Flags().GetInt("tail") follow, _ := cmd.Flags().GetBool("follow") export, _ := cmd.Flags().GetString("export") diff --git a/cmd/nightshift/commands/preview.go b/cmd/nightshift/commands/preview.go index 8691939..7ef7964 100644 --- a/cmd/nightshift/commands/preview.go +++ b/cmd/nightshift/commands/preview.go @@ -44,7 +44,7 @@ func init() { rootCmd.AddCommand(previewCmd) } -func runPreview(cmd *cobra.Command, args []string) error { +func runPreview(cmd *cobra.Command, _ []string) error { runs, _ := cmd.Flags().GetInt("runs") projectPath, _ := cmd.Flags().GetString("project") taskFilter, _ := cmd.Flags().GetString("task") diff --git a/cmd/nightshift/commands/preview_output.go b/cmd/nightshift/commands/preview_output.go index b3a664c..8ab46e7 100644 --- a/cmd/nightshift/commands/preview_output.go +++ b/cmd/nightshift/commands/preview_output.go @@ -83,11 +83,12 @@ func renderPreviewText(result *previewResult, opts previewTextOptions) string { fmt.Fprintln(b, line) } } - if result.TaskFilter != "" { + switch { + case result.TaskFilter != "": fmt.Fprintf(b, " Task filter: %s\n", result.TaskFilter) - } else if len(result.EnabledTasks) == 0 { + case len(result.EnabledTasks) == 0: b.WriteString(" Task filter: all enabled tasks (none explicitly enabled)\n") - } else { + default: fmt.Fprintf(b, " Task filter: enabled list (%d) [%s]\n", len(result.EnabledTasks), strings.Join(result.EnabledTasks, ", ")) } if opts.Explain && result.ProjectCount > 1 { @@ -347,7 +348,7 @@ func renderDiagnosticsText(b *strings.Builder, styles previewStyles, diagnostics renderCooldownsText(b, styles, diagnostics.Cooldowns, indent) } -func renderCooldownsText(b *strings.Builder, styles previewStyles, cooldowns []previewCooldownEntry, indent string) { +func renderCooldownsText(b *strings.Builder, _ previewStyles, cooldowns []previewCooldownEntry, indent string) { if len(cooldowns) == 0 { return } diff --git a/cmd/nightshift/commands/report.go b/cmd/nightshift/commands/report.go index 37cc9cd..1f6329c 100644 --- a/cmd/nightshift/commands/report.go +++ b/cmd/nightshift/commands/report.go @@ -47,7 +47,7 @@ var reportCmd = &cobra.Command{ Long: `View structured reports from recent nightshift runs. By default, shows a polished overview of what happened during the last night.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { opts := reportOptions{} opts.reportType, _ = cmd.Flags().GetString("report") opts.period, _ = cmd.Flags().GetString("period") @@ -895,16 +895,17 @@ func renderReportBudget(styles reportStyles, runs []reportRun) string { b.WriteString(styles.Accent.Render(header)) b.WriteString("\n") - if summary.BudgetStart > 0 { + switch { + case summary.BudgetStart > 0: fmt.Fprintf(&b, " %s %s used / %s start (%s remaining)\n", styles.Label.Render("Budget:"), formatTokensCompact(summary.TokensUsed), formatTokensCompact(summary.BudgetStart), formatTokensCompact(summary.BudgetRemaining), ) - } else if summary.TokensUsed > 0 { + case summary.TokensUsed > 0: fmt.Fprintf(&b, " %s %s\n", styles.Label.Render("Tokens:"), formatTokensCompact(summary.TokensUsed)) - } else { + default: b.WriteString(" No budget data recorded\n") } diff --git a/cmd/nightshift/commands/run.go b/cmd/nightshift/commands/run.go index 9681002..1c81b8e 100644 --- a/cmd/nightshift/commands/run.go +++ b/cmd/nightshift/commands/run.go @@ -117,7 +117,7 @@ func init() { rootCmd.AddCommand(runCmd) } -func runRun(cmd *cobra.Command, args []string) error { +func runRun(cmd *cobra.Command, _ []string) error { dryRun, _ := cmd.Flags().GetBool("dry-run") projectPath, _ := cmd.Flags().GetString("project") taskFilter, _ := cmd.Flags().GetString("task") @@ -465,7 +465,8 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) { // Select tasks var selectedTasks []tasks.ScoredTask - if p.taskFilter != "" { + switch { + case p.taskFilter != "": def, err := tasks.GetDefinition(tasks.TaskType(p.taskFilter)) if err != nil { return nil, fmt.Errorf("unknown task type: %s", p.taskFilter) @@ -475,7 +476,7 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) { Score: p.selector.ScoreTask(def.Type, projectPath), Project: projectPath, }} - } else if p.randomTask { + case p.randomTask: taskBudget := choice.allowance.Allowance if p.ignoreBudget { taskBudget = math.MaxInt64 @@ -483,7 +484,7 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) { if picked := p.selector.SelectRandom(taskBudget, projectPath); picked != nil { selectedTasks = []tasks.ScoredTask{*picked} } - } else { + default: n := p.maxTasks if n <= 0 { n = 1 diff --git a/cmd/nightshift/commands/setup.go b/cmd/nightshift/commands/setup.go index c098142..896d891 100644 --- a/cmd/nightshift/commands/setup.go +++ b/cmd/nightshift/commands/setup.go @@ -36,7 +36,7 @@ var setupCmd = &cobra.Command{ Creates/updates the global config, validates providers, runs a snapshot, previews the next run, and optionally installs/enables the daemon.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { model, err := newSetupModel() if err != nil { return err @@ -1507,11 +1507,12 @@ func renderEnvChecks(cfg *config.Config) string { // Check for Copilot CLI (gh or copilot binary) _, ghErr := execLookPath("gh") _, copilotErr := execLookPath("copilot") - if ghErr != nil && copilotErr != nil { + switch { + case ghErr != nil && copilotErr != nil: fmt.Fprintf(&b, " %s %s\n", styleWarn.Render("Note:"), "Copilot CLI not found (install via 'gh' or native 'copilot')") - } else if ghErr == nil { + case ghErr == nil: fmt.Fprintf(&b, " %s %s\n", styleOk.Render("OK:"), "gh CLI available (use 'gh copilot')") - } else { + default: fmt.Fprintf(&b, " %s %s\n", styleOk.Render("OK:"), "copilot CLI available") } if cfg.Providers.Claude.Enabled { @@ -1910,11 +1911,11 @@ func uninstallService(service string) error { func mustExecutablePath() string { path, _ := os.Executable() - real, err := filepath.EvalSymlinks(path) + resolved, err := filepath.EvalSymlinks(path) if err != nil { return path } - return real + return resolved } func writeGlobalConfig(cfg *config.Config) error { diff --git a/cmd/nightshift/commands/snapshot.go b/cmd/nightshift/commands/snapshot.go index ed19318..2d5ce30 100644 --- a/cmd/nightshift/commands/snapshot.go +++ b/cmd/nightshift/commands/snapshot.go @@ -32,7 +32,7 @@ display via tmux to get the provider's own usage percentage. Inference If both local tokens and scraped % are available, nightshift infers the weekly budget: budget = local_tokens / (scraped% / 100).`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { provider, _ := cmd.Flags().GetString("provider") localOnly, _ := cmd.Flags().GetBool("local-only") return runBudgetSnapshot(cmd, provider, localOnly) @@ -43,7 +43,7 @@ var budgetHistoryCmd = &cobra.Command{ Use: "history", Short: "Show recent budget snapshots", Long: `Show recent usage snapshots for budget calibration.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { provider, _ := cmd.Flags().GetString("provider") n, _ := cmd.Flags().GetInt("n") return runBudgetHistory(provider, n) @@ -54,7 +54,7 @@ var budgetCalibrateCmd = &cobra.Command{ Use: "calibrate", Short: "Show calibration status", Long: `Show inferred budget calibration status for providers.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { provider, _ := cmd.Flags().GetString("provider") return runBudgetCalibrate(provider) }, @@ -99,13 +99,14 @@ func runBudgetSnapshot(cmd *cobra.Command, filterProvider string, localOnly bool // Determine scraper availability and reason if disabled scraper := snapshots.UsageScraper(nil) scrapeDisabledReason := "" - if localOnly { + switch { + case localOnly: scrapeDisabledReason = "--local-only flag set" - } else if !cfg.Budget.CalibrateEnabled { + case !cfg.Budget.CalibrateEnabled: scrapeDisabledReason = "calibrate_enabled is false in config" - } else if strings.EqualFold(cfg.Budget.BillingMode, "api") { + case strings.EqualFold(cfg.Budget.BillingMode, "api"): scrapeDisabledReason = "billing_mode is 'api' (scraping only works with subscription)" - } else { + default: scraper = tmuxScraper{} } @@ -144,17 +145,18 @@ func runBudgetSnapshot(cmd *cobra.Command, filterProvider string, localOnly bool fmt.Printf(" Data source: %s\n", dataSource) // Scraping status - if scraper == nil { + switch { + case scraper == nil: fmt.Printf(" Scraping: disabled -- %s\n", scrapeDisabledReason) - } else if snapshot.ScrapeErr != nil { + case snapshot.ScrapeErr != nil: fmt.Printf(" Scraping: FAILED -- %v\n", snapshot.ScrapeErr) - } else if snapshot.ScrapedPct != nil { + case snapshot.ScrapedPct != nil: cmd := "/usage" if provName == "codex" { cmd = "/status" } fmt.Printf(" Scraped: %.1f%% used (via tmux %s)\n", *snapshot.ScrapedPct, cmd) - } else { + default: fmt.Printf(" Scraping: no data returned\n") } diff --git a/cmd/nightshift/commands/stats.go b/cmd/nightshift/commands/stats.go index c5b9c80..3fbd42d 100644 --- a/cmd/nightshift/commands/stats.go +++ b/cmd/nightshift/commands/stats.go @@ -24,7 +24,7 @@ var statsCmd = &cobra.Command{ Shows run counts, task outcomes, token usage, budget projections, and per-project breakdowns. Use --json for machine-readable output.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { jsonOutput, _ := cmd.Flags().GetBool("json") period, _ := cmd.Flags().GetString("period") return runStats(jsonOutput, period) @@ -313,7 +313,8 @@ func renderStatsHuman(result *stats.StatsResult) error { fmt.Printf(" Reset: %s\n", bp.ResetHint) } - if bp.EstExhaustAt != nil { + switch { + case bp.EstExhaustAt != nil: if time.Until(*bp.EstExhaustAt) <= 0 { fmt.Printf(" Projected: budget may already be exhausted\n") } else { @@ -326,11 +327,9 @@ func renderStatsHuman(result *stats.StatsResult) error { } } } - } else if bp.RemainingTokens <= 0 { - fmt.Printf(" At current rate: budget may be exhausted\n") - } else if bp.EstDaysRemaining > 0 { + case bp.EstDaysRemaining > 0 && bp.RemainingTokens > 0: fmt.Printf(" At current rate: ~%d days until budget exhausted\n", bp.EstDaysRemaining) - } else { + default: fmt.Printf(" At current rate: budget may be exhausted\n") } diff --git a/cmd/nightshift/commands/status.go b/cmd/nightshift/commands/status.go index 1a6883c..ba1541d 100644 --- a/cmd/nightshift/commands/status.go +++ b/cmd/nightshift/commands/status.go @@ -19,7 +19,7 @@ var statusCmd = &cobra.Command{ Long: `Display nightshift run history and activity. Shows the last N runs (default: 5) or today's activity summary.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { last, _ := cmd.Flags().GetInt("last") today, _ := cmd.Flags().GetBool("today") diff --git a/cmd/nightshift/commands/task.go b/cmd/nightshift/commands/task.go index 6b1820e..22a20fd 100644 --- a/cmd/nightshift/commands/task.go +++ b/cmd/nightshift/commands/task.go @@ -79,7 +79,7 @@ func init() { rootCmd.AddCommand(taskCmd) } -func runTaskList(cmd *cobra.Command, args []string) error { +func runTaskList(cmd *cobra.Command, _ []string) error { categoryFilter, _ := cmd.Flags().GetString("category") costFilter, _ := cmd.Flags().GetString("cost") asJSON, _ := cmd.Flags().GetBool("json") @@ -113,7 +113,7 @@ func runTaskList(cmd *cobra.Command, args []string) error { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) _, _ = fmt.Fprintln(w, "TYPE\tNAME\tCATEGORY\tCOST\tTOKENS\tRISK") for _, d := range defs { - min, max := d.EstimatedTokens() + minTokens, maxTokens := d.EstimatedTokens() typeStr := string(d.Type) if tasks.IsCustom(d.Type) { typeStr += " [custom]" @@ -123,8 +123,8 @@ func runTaskList(cmd *cobra.Command, args []string) error { d.Name, categoryShort(d.Category), costShort(d.CostTier), - formatK(min), - formatK(max), + formatK(minTokens), + formatK(maxTokens), d.RiskLevel, ) } @@ -158,12 +158,12 @@ func runTaskShow(cmd *cobra.Command, args []string) error { return printTaskShowJSON(def, prompt) } - min, max := def.EstimatedTokens() + minTokens, maxTokens := def.EstimatedTokens() fmt.Printf("Task: %s\n", def.Name) fmt.Printf("Type: %s\n", def.Type) fmt.Printf("Category: %s\n", def.Category) fmt.Printf("Cost: %s\n", def.CostTier) - fmt.Printf("Tokens: %s - %s\n", formatK(min), formatK(max)) + fmt.Printf("Tokens: %s - %s\n", formatK(minTokens), formatK(maxTokens)) fmt.Printf("Risk: %s\n", def.RiskLevel) if tasks.IsCustom(def.Type) { fmt.Printf("Custom: yes\n") @@ -251,8 +251,8 @@ func runTaskRun(cmd *cobra.Command, args []string) error { } fmt.Printf("Timeout: %s\n", timeout) - min, max := def.EstimatedTokens() - fmt.Printf("Est: %s-%s tokens\n", formatK(min), formatK(max)) + minTokens, maxTokens := def.EstimatedTokens() + fmt.Printf("Est: %s-%s tokens\n", formatK(minTokens), formatK(maxTokens)) if dryRun { fmt.Println("\n[dry-run] Would send this prompt:") @@ -434,15 +434,15 @@ type taskListEntry struct { func printTaskListJSON(defs []tasks.TaskDefinition) error { entries := make([]taskListEntry, len(defs)) for i, d := range defs { - min, max := d.EstimatedTokens() + minTokens, maxTokens := d.EstimatedTokens() entries[i] = taskListEntry{ Type: string(d.Type), Name: d.Name, Category: categoryShort(d.Category), Description: d.Description, Cost: costShort(d.CostTier), - MinTokens: min, - MaxTokens: max, + MinTokens: minTokens, + MaxTokens: maxTokens, Risk: d.RiskLevel.String(), Custom: tasks.IsCustom(d.Type), } @@ -466,15 +466,15 @@ type taskShowEntry struct { } func printTaskShowJSON(def tasks.TaskDefinition, prompt string) error { - min, max := def.EstimatedTokens() + minTokens, maxTokens := def.EstimatedTokens() entry := taskShowEntry{ Type: string(def.Type), Name: def.Name, Category: categoryShort(def.Category), Description: def.Description, Cost: costShort(def.CostTier), - MinTokens: min, - MaxTokens: max, + MinTokens: minTokens, + MaxTokens: maxTokens, Risk: def.RiskLevel.String(), Custom: tasks.IsCustom(def.Type), Prompt: prompt, diff --git a/cmd/provider-calibration/main.go b/cmd/provider-calibration/main.go index ee573b2..fd92081 100644 --- a/cmd/provider-calibration/main.go +++ b/cmd/provider-calibration/main.go @@ -1,8 +1,11 @@ +// Command provider-calibration measures real token usage for each provider +// so budget estimates can be calibrated against observed transcripts. package main import ( "bufio" "encoding/json" + "errors" "flag" "fmt" "io" @@ -226,7 +229,7 @@ func collectCodex(root, repoFilter, originatorFilter string, minUserTurns int) ( } } } - if scanErr := scanner.Err(); scanErr != nil && scanErr != io.EOF { + if scanErr := scanner.Err(); scanErr != nil && !errors.Is(scanErr, io.EOF) { return nil } @@ -349,7 +352,7 @@ func collectClaude(root, repoFilter string, minUserTurns int) ([]sessionMetrics, primary += u.InputTokens + u.OutputTokens alt += u.InputTokens + u.OutputTokens + u.CacheReadInputTokens + u.CacheCreationInputTokens } - if scanErr := scanner.Err(); scanErr != nil && scanErr != io.EOF { + if scanErr := scanner.Err(); scanErr != nil && !errors.Is(scanErr, io.EOF) { return nil } diff --git a/internal/agents/claude.go b/internal/agents/claude.go index c34d513..461cdf5 100644 --- a/internal/agents/claude.go +++ b/internal/agents/claude.go @@ -1,10 +1,12 @@ // claude.go implements the Agent interface for Claude Code CLI. + package agents import ( "bytes" "context" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -175,7 +177,8 @@ func (a *ClaudeAgent) Execute(ctx context.Context, opts ExecuteOptions) (*Execut // Check for other errors if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { result.ExitCode = exitErr.ExitCode() result.Error = stderr } else { diff --git a/internal/agents/claude_test.go b/internal/agents/claude_test.go index b8abe44..b201533 100644 --- a/internal/agents/claude_test.go +++ b/internal/agents/claude_test.go @@ -154,7 +154,7 @@ func TestClaudeAgent_Execute_Timeout(t *testing.T) { Prompt: "long task", }) - if err != context.DeadlineExceeded { + if !errors.Is(err, context.DeadlineExceeded) { t.Errorf("expected DeadlineExceeded, got %v", err) } if result.ExitCode != -1 { @@ -179,7 +179,7 @@ func TestClaudeAgent_Execute_WithOptionsTimeout(t *testing.T) { Timeout: 50 * time.Millisecond, // Short override }) - if err != context.DeadlineExceeded { + if !errors.Is(err, context.DeadlineExceeded) { t.Errorf("expected DeadlineExceeded, got %v", err) } if result == nil { diff --git a/internal/agents/codex.go b/internal/agents/codex.go index c366400..777c721 100644 --- a/internal/agents/codex.go +++ b/internal/agents/codex.go @@ -1,9 +1,11 @@ // codex.go implements the Agent interface for OpenAI Codex CLI. + package agents import ( "context" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -133,7 +135,8 @@ func (a *CodexAgent) Execute(ctx context.Context, opts ExecuteOptions) (*Execute // Check for other errors if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { result.ExitCode = exitErr.ExitCode() result.Error = stderr } else { diff --git a/internal/agents/codex_test.go b/internal/agents/codex_test.go index 0a1f5f9..f57582f 100644 --- a/internal/agents/codex_test.go +++ b/internal/agents/codex_test.go @@ -122,7 +122,7 @@ func TestCodexAgent_Execute_Timeout(t *testing.T) { Prompt: "long task", }) - if err != context.DeadlineExceeded { + if !errors.Is(err, context.DeadlineExceeded) { t.Errorf("expected DeadlineExceeded, got %v", err) } if result.ExitCode != -1 { @@ -147,7 +147,7 @@ func TestCodexAgent_Execute_WithOptionsTimeout(t *testing.T) { Timeout: 50 * time.Millisecond, // Short override }) - if err != context.DeadlineExceeded { + if !errors.Is(err, context.DeadlineExceeded) { t.Errorf("expected DeadlineExceeded, got %v", err) } if result == nil { diff --git a/internal/agents/copilot.go b/internal/agents/copilot.go index fd94226..b19afe9 100644 --- a/internal/agents/copilot.go +++ b/internal/agents/copilot.go @@ -1,9 +1,11 @@ // copilot.go implements the Agent interface for GitHub Copilot CLI. + package agents import ( "context" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -145,7 +147,8 @@ func (a *CopilotAgent) Execute(ctx context.Context, opts ExecuteOptions) (*Execu // Check for other errors if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { result.ExitCode = exitErr.ExitCode() result.Error = stderr } else { diff --git a/internal/budget/budget.go b/internal/budget/budget.go index 31a718c..c869ca6 100644 --- a/internal/budget/budget.go +++ b/internal/budget/budget.go @@ -362,7 +362,7 @@ func (m *Manager) DaysUntilWeeklyReset(provider string) (int, error) { } resetTime, err := m.codex.GetResetTime("weekly") if err != nil { - return 7, nil // Fallback on error + return 7, nil //nolint:nilerr // best-effort quota lookup; fall back to the nominal reset window } if resetTime.IsZero() { return 7, nil // No reset time available @@ -383,7 +383,7 @@ func (m *Manager) DaysUntilWeeklyReset(provider string) (int, error) { } resetTime, err := m.copilot.GetResetTime("weekly") if err != nil { - return 30, nil // Fallback on error + return 30, nil //nolint:nilerr // best-effort quota lookup; fall back to the nominal reset window } if resetTime.IsZero() { return 30, nil // No reset time available @@ -438,6 +438,7 @@ func (m *Manager) CanRun(provider string, estimatedTokens int64) (bool, error) { } // Tracker provides backward compatibility for tracking actual spend. +// // Deprecated: Use Manager for budget calculations. type Tracker struct { spent map[string]int64 @@ -445,6 +446,7 @@ type Tracker struct { } // NewTracker creates a budget tracker with the given limit. +// // Deprecated: Use NewManager instead. func NewTracker(limitCents int64) *Tracker { return &Tracker{ @@ -454,7 +456,7 @@ func NewTracker(limitCents int64) *Tracker { } // Record logs spending for a provider. -func (t *Tracker) Record(provider string, tokens int, costCents int64) { +func (t *Tracker) Record(provider string, tokens int, costCents int64) { //nolint:revive // Tracker is deprecated; the token count is part of its published signature but only cost is recorded t.spent[provider] += costCents } diff --git a/internal/calibrator/calibrator.go b/internal/calibrator/calibrator.go index d98be7d..7f4edb1 100644 --- a/internal/calibrator/calibrator.go +++ b/internal/calibrator/calibrator.go @@ -130,11 +130,12 @@ func (c *Calibrator) Calibrate(provider string) (CalibrationResult, error) { confidence = "low" } default: - if cv <= 0.10 { + switch { + case cv <= 0.10: confidence = "high" - } else if cv <= 0.15 { + case cv <= 0.15: confidence = "medium" - } else { + default: confidence = "low" } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index df34cdc..be7aa15 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -17,7 +17,7 @@ func TestValidate_CronAndInterval(t *testing.T) { }, } err := Validate(cfg) - if err != ErrCronAndInterval { + if !errors.Is(err, ErrCronAndInterval) { t.Errorf("expected ErrCronAndInterval, got %v", err) } } @@ -29,7 +29,7 @@ func TestValidate_InvalidBudgetMode(t *testing.T) { }, } err := Validate(cfg) - if err != ErrInvalidBudgetMode { + if !errors.Is(err, ErrInvalidBudgetMode) { t.Errorf("expected ErrInvalidBudgetMode, got %v", err) } } @@ -41,7 +41,7 @@ func TestValidate_InvalidBillingMode(t *testing.T) { }, } err := Validate(cfg) - if err != ErrInvalidBillingMode { + if !errors.Is(err, ErrInvalidBillingMode) { t.Errorf("expected ErrInvalidBillingMode, got %v", err) } } @@ -53,7 +53,7 @@ func TestValidate_InvalidWeekStartDay(t *testing.T) { }, } err := Validate(cfg) - if err != ErrInvalidWeekStartDay { + if !errors.Is(err, ErrInvalidWeekStartDay) { t.Errorf("expected ErrInvalidWeekStartDay, got %v", err) } } @@ -65,7 +65,7 @@ func TestValidate_InvalidMaxPercent(t *testing.T) { }, } err := Validate(cfg) - if err != ErrInvalidMaxPercent { + if !errors.Is(err, ErrInvalidMaxPercent) { t.Errorf("expected ErrInvalidMaxPercent, got %v", err) } } @@ -77,7 +77,7 @@ func TestValidate_InvalidLogLevel(t *testing.T) { }, } err := Validate(cfg) - if err != ErrInvalidLogLevel { + if !errors.Is(err, ErrInvalidLogLevel) { t.Errorf("expected ErrInvalidLogLevel, got %v", err) } } @@ -89,7 +89,7 @@ func TestValidate_InvalidLogFormat(t *testing.T) { }, } err := Validate(cfg) - if err != ErrInvalidLogFormat { + if !errors.Is(err, ErrInvalidLogFormat) { t.Errorf("expected ErrInvalidLogFormat, got %v", err) } } diff --git a/internal/db/db.go b/internal/db/db.go index 2f846a8..184b3b1 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - _ "modernc.org/sqlite" + _ "modernc.org/sqlite" // registers the pure-Go SQLite driver used by sql.Open below ) // DB wraps the SQLite connection and path. diff --git a/internal/integrations/agentsmd.go b/internal/integrations/agentsmd.go index 5af031b..d00db39 100644 --- a/internal/integrations/agentsmd.go +++ b/internal/integrations/agentsmd.go @@ -34,7 +34,7 @@ func (r *AgentsMDReader) Enabled() bool { } // Read looks for agents.md or AGENTS.md and extracts behavior preferences. -func (r *AgentsMDReader) Read(ctx context.Context, projectPath string) (*Result, error) { +func (r *AgentsMDReader) Read(_ context.Context, projectPath string) (*Result, error) { // Try multiple possible locations candidates := []string{ filepath.Join(projectPath, "AGENTS.md"), diff --git a/internal/integrations/claudemd.go b/internal/integrations/claudemd.go index 44b5ec6..c85c7a6 100644 --- a/internal/integrations/claudemd.go +++ b/internal/integrations/claudemd.go @@ -34,7 +34,7 @@ func (r *ClaudeMDReader) Enabled() bool { } // Read looks for claude.md in project root and extracts context. -func (r *ClaudeMDReader) Read(ctx context.Context, projectPath string) (*Result, error) { +func (r *ClaudeMDReader) Read(_ context.Context, projectPath string) (*Result, error) { // Try multiple possible locations candidates := []string{ filepath.Join(projectPath, "claude.md"), diff --git a/internal/integrations/github.go b/internal/integrations/github.go index 9cfcbe9..9bc691c 100644 --- a/internal/integrations/github.go +++ b/internal/integrations/github.go @@ -48,7 +48,7 @@ func (r *GitHubReader) Enabled() bool { func (r *GitHubReader) Read(ctx context.Context, projectPath string) (*Result, error) { // Check if gh is available if _, err := exec.LookPath("gh"); err != nil { - return nil, nil // gh not installed, not an error + return nil, nil //nolint:nilerr // gh CLI absent means the integration is unavailable, not failed } // Check if we're in a git repo with GitHub remote @@ -66,7 +66,7 @@ func (r *GitHubReader) Read(ctx context.Context, projectPath string) (*Result, e issues, err := r.listIssues(ctx, projectPath) if err != nil { // GitHub might not be configured or no issues - return nil, nil + return nil, nil //nolint:nilerr // issues disabled or gh unauthenticated; degrade to no tasks } result.Tasks = issues diff --git a/internal/integrations/integrations.go b/internal/integrations/integrations.go index 5b7823b..e8ea653 100644 --- a/internal/integrations/integrations.go +++ b/internal/integrations/integrations.go @@ -57,6 +57,7 @@ type Hint struct { // HintType categorizes hints. type HintType int +// Hint types recognised by the integration readers. const ( HintTaskSuggestion HintType = iota // Suggested task to run HintConvention // Coding convention diff --git a/internal/integrations/td.go b/internal/integrations/td.go index a8ed79d..7d81dbf 100644 --- a/internal/integrations/td.go +++ b/internal/integrations/td.go @@ -46,7 +46,7 @@ func (r *TDReader) Enabled() bool { func (r *TDReader) Read(ctx context.Context, projectPath string) (*Result, error) { // Check if td is available if _, err := exec.LookPath("td"); err != nil { - return nil, nil // td not installed, not an error + return nil, nil //nolint:nilerr // td CLI absent means the integration is unavailable, not failed } result := &Result{ @@ -59,7 +59,7 @@ func (r *TDReader) Read(ctx context.Context, projectPath string) (*Result, error tasks, err := r.listTasks(ctx, projectPath) if err != nil { // td might not be configured for this project - return nil, nil + return nil, nil //nolint:nilerr // td not initialised for this project; degrade to no tasks } result.Tasks = tasks diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 2f870ba..0f194fe 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -370,7 +370,7 @@ func Warn(msg string) { Get().Warn(msg) } -// Error(msg string) logs an error message to the global logger. +// Error logs an error message to the global logger. func Error(msg string) { Get().Error(msg) } diff --git a/internal/orchestrator/events.go b/internal/orchestrator/events.go index 7399672..f3626b0 100644 --- a/internal/orchestrator/events.go +++ b/internal/orchestrator/events.go @@ -5,6 +5,7 @@ import "time" // EventType classifies orchestrator lifecycle events. type EventType int +// Orchestrator lifecycle event types, emitted in roughly this order. const ( EventTaskStart EventType = iota // task execution begins EventPhaseStart // entering a phase (plan/implement/review) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 6141c95..324ae68 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -29,6 +29,7 @@ const ( // TaskStatus represents the outcome of task execution. type TaskStatus string +// Task statuses, covering the lifecycle from pending through a terminal outcome. const ( StatusPending TaskStatus = "pending" StatusPlanning TaskStatus = "planning" @@ -113,7 +114,7 @@ func DefaultConfig() Config { // Orchestrator manages agent execution using plan-implement-review loop. type Orchestrator struct { agent agents.Agent - budget *budget.Tracker + budget *budget.Tracker //nolint:staticcheck // SA1019: dead scaffolding, never read; migrating to budget.Manager is a separate API change queue *tasks.Queue config Config logger *logging.Logger @@ -132,7 +133,7 @@ func WithAgent(a agents.Agent) Option { } // WithBudget sets the budget tracker. -func WithBudget(b *budget.Tracker) Option { +func WithBudget(b *budget.Tracker) Option { //nolint:staticcheck // SA1019: no call sites; migrating to budget.Manager is a separate API change return func(o *Orchestrator) { o.budget = b } diff --git a/internal/providers/claude.go b/internal/providers/claude.go index 3f78e89..ff84396 100644 --- a/internal/providers/claude.go +++ b/internal/providers/claude.go @@ -1,4 +1,5 @@ // claude.go implements the Provider interface for Claude Code CLI. + package providers import ( @@ -102,7 +103,7 @@ func (c *Claude) Name() string { } // Execute runs a task via Claude Code CLI. -func (c *Claude) Execute(ctx context.Context, task Task) (Result, error) { +func (c *Claude) Execute(_ context.Context, _ Task) (Result, error) { // TODO: Implement - spawn claude CLI process return Result{}, nil } @@ -409,7 +410,7 @@ func (c *Claude) scanTokensSince(cutoffDate string, extraMtimeDays int) (int64, // mtime filter: skip files not modified since cutoff info, err := d.Info() if err != nil { - return nil // skip unreadable + return nil //nolint:nilerr // skip unreadable entry so one bad file cannot abort the scan } if info.ModTime().Before(mtimeCutoff) { return nil @@ -417,7 +418,7 @@ func (c *Claude) scanTokensSince(cutoffDate string, extraMtimeDays int) (int64, tokens, err := scanFileTokens(path, cutoffDate) if err != nil { - return nil // skip corrupt files + return nil //nolint:nilerr // skip corrupt transcript so one bad file cannot abort the scan } total += tokens return nil diff --git a/internal/providers/codex.go b/internal/providers/codex.go index eb948ed..b69fb08 100644 --- a/internal/providers/codex.go +++ b/internal/providers/codex.go @@ -1,4 +1,5 @@ // codex.go implements the Provider interface for OpenAI Codex CLI. + package providers import ( @@ -84,7 +85,7 @@ func (c *Codex) Name() string { } // Execute runs a task via Codex CLI. -func (c *Codex) Execute(ctx context.Context, task Task) (Result, error) { +func (c *Codex) Execute(_ context.Context, _ Task) (Result, error) { // TODO: Implement - spawn codex CLI process return Result{}, nil } diff --git a/internal/providers/copilot.go b/internal/providers/copilot.go index fe69acf..74c8c3e 100644 --- a/internal/providers/copilot.go +++ b/internal/providers/copilot.go @@ -1,4 +1,5 @@ // copilot.go implements the Provider interface for GitHub Copilot CLI. + package providers import ( @@ -56,7 +57,7 @@ func (c *Copilot) Name() string { // Execute runs a task via GitHub Copilot CLI. // Implementation note: GitHub Copilot CLI uses 'gh copilot' commands. -func (c *Copilot) Execute(ctx context.Context, task Task) (Result, error) { +func (c *Copilot) Execute(_ context.Context, _ Task) (Result, error) { // TODO: Implement - spawn gh copilot CLI process // According to GitHub docs, commands are: // - gh copilot explain diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 278ca71..d1bdf58 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -134,7 +134,7 @@ func (s *Scheduler) SetCron(expr string) error { parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) _, err := parser.Parse(expr) if err != nil { - return fmt.Errorf("%w: %v", ErrInvalidCron, err) + return fmt.Errorf("%w: %w", ErrInvalidCron, err) } s.mu.Lock() s.cronExpr = expr @@ -159,18 +159,18 @@ func (s *Scheduler) SetInterval(d time.Duration) error { func (s *Scheduler) SetWindow(cfg *config.WindowConfig) error { start, err := ParseTimeOfDay(cfg.Start) if err != nil { - return fmt.Errorf("%w: start: %v", ErrInvalidWindow, err) + return fmt.Errorf("%w: start: %w", ErrInvalidWindow, err) } end, err := ParseTimeOfDay(cfg.End) if err != nil { - return fmt.Errorf("%w: end: %v", ErrInvalidWindow, err) + return fmt.Errorf("%w: end: %w", ErrInvalidWindow, err) } loc := time.Local if cfg.Timezone != "" { loc, err = time.LoadLocation(cfg.Timezone) if err != nil { - return fmt.Errorf("%w: %v", ErrInvalidTimezone, err) + return fmt.Errorf("%w: %w", ErrInvalidTimezone, err) } } @@ -200,7 +200,8 @@ func (s *Scheduler) Start(ctx context.Context) error { s.stopCh = make(chan struct{}) s.doneCh = make(chan struct{}) - if s.cronExpr != "" { + switch { + case s.cronExpr != "": // Cron-based scheduling s.cron = cron.New(cron.WithLocation(s.location)) entryID, err := s.cron.AddFunc(s.cronExpr, func() { @@ -222,13 +223,14 @@ func (s *Scheduler) Start(ctx context.Context) error { s.cron.Stop() close(s.doneCh) }() - } else if s.interval > 0 { + case s.interval > 0: // Interval-based scheduling s.updateNextRunLocked() s.mu.Unlock() go s.intervalLoop(ctx) - } else { + + default: s.running = false s.mu.Unlock() return ErrNoSchedule @@ -329,7 +331,7 @@ func (s *Scheduler) NextRuns(n int) ([]time.Time, error) { parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) schedule, err := parser.Parse(cronExpr) if err != nil { - return nil, fmt.Errorf("%w: %v", ErrInvalidCron, err) + return nil, fmt.Errorf("%w: %w", ErrInvalidCron, err) } current := now for i := 0; i < n; i++ { @@ -449,12 +451,13 @@ func (s *Scheduler) IsRunning() bool { } // ScheduleCron adds a recurring job using cron expression. +// // Deprecated: Use SetCron and AddJob instead. func (s *Scheduler) ScheduleCron(expr string, job func()) error { if err := s.SetCron(expr); err != nil { return err } - s.AddJob(func(ctx context.Context) error { + s.AddJob(func(_ context.Context) error { job() return nil }) @@ -466,7 +469,7 @@ func (s *Scheduler) ScheduleInterval(d time.Duration, job func()) error { if err := s.SetInterval(d); err != nil { return err } - s.AddJob(func(ctx context.Context) error { + s.AddJob(func(_ context.Context) error { job() return nil }) @@ -475,7 +478,7 @@ func (s *Scheduler) ScheduleInterval(d time.Duration, job func()) error { // Schedule adds a one-time job to run at the specified time. func (s *Scheduler) Schedule(at time.Time, job func()) { - s.AddJob(func(ctx context.Context) error { + s.AddJob(func(_ context.Context) error { if time.Now().After(at) { job() } diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go index e28a714..ce42c92 100644 --- a/internal/scheduler/scheduler_test.go +++ b/internal/scheduler/scheduler_test.go @@ -2,6 +2,7 @@ package scheduler import ( "context" + "errors" "fmt" "sync/atomic" "testing" @@ -208,7 +209,7 @@ func TestNewFromConfig_NoSchedule(t *testing.T) { cfg := &config.ScheduleConfig{} _, err := NewFromConfig(cfg) - if err != ErrNoSchedule { + if !errors.Is(err, ErrNoSchedule) { t.Errorf("NewFromConfig() error = %v, want %v", err, ErrNoSchedule) } } @@ -321,7 +322,7 @@ func TestScheduler_StartStop_Cron(t *testing.T) { } // Starting again should fail - if err := s.Start(ctx); err != ErrAlreadyRunning { + if err := s.Start(ctx); !errors.Is(err, ErrAlreadyRunning) { t.Errorf("Start() twice error = %v, want %v", err, ErrAlreadyRunning) } @@ -334,7 +335,7 @@ func TestScheduler_StartStop_Cron(t *testing.T) { } // Stopping again should fail - if err := s.Stop(); err != ErrNotRunning { + if err := s.Stop(); !errors.Is(err, ErrNotRunning) { t.Errorf("Stop() twice error = %v, want %v", err, ErrNotRunning) } } @@ -365,7 +366,7 @@ func TestScheduler_StartNoSchedule(t *testing.T) { s := New() ctx := context.Background() - if err := s.Start(ctx); err != ErrNoSchedule { + if err := s.Start(ctx); !errors.Is(err, ErrNoSchedule) { t.Errorf("Start() error = %v, want %v", err, ErrNoSchedule) } } diff --git a/internal/security/audit.go b/internal/security/audit.go index 69e93c6..b899f1c 100644 --- a/internal/security/audit.go +++ b/internal/security/audit.go @@ -14,6 +14,7 @@ import ( // AuditEventType categorizes audit events. type AuditEventType string +// Audit event types recorded to the audit log. const ( AuditAgentStart AuditEventType = "agent_start" AuditAgentComplete AuditEventType = "agent_complete" diff --git a/internal/security/sandbox.go b/internal/security/sandbox.go index 50a4bde..2ba20d7 100644 --- a/internal/security/sandbox.go +++ b/internal/security/sandbox.go @@ -4,6 +4,7 @@ package security import ( "context" + "errors" "fmt" "io" "os" @@ -132,7 +133,8 @@ func (s *Sandbox) Execute(ctx context.Context, name string, args ...string) (*Ex } if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { result.ExitCode = exitErr.ExitCode() } else { result.Error = err.Error() diff --git a/internal/security/security.go b/internal/security/security.go index 1166e57..9c5b788 100644 --- a/internal/security/security.go +++ b/internal/security/security.go @@ -237,9 +237,10 @@ func ValidateProjectPath(path string) error { return nil } -// Operation types for safety checks. +// OperationType identifies a class of agent operation subject to safety checks. type OperationType string +// Operation types that a safety policy can allow or deny. const ( OpAgentInvoke OperationType = "agent_invoke" OpFileRead OperationType = "file_read" diff --git a/internal/setup/presets.go b/internal/setup/presets.go index 4927c06..9692a18 100644 --- a/internal/setup/presets.go +++ b/internal/setup/presets.go @@ -12,6 +12,7 @@ import ( // Preset identifies a task selection profile (safe, balanced, aggressive). type Preset string +// Available task selection presets, ordered from most to least conservative. const ( PresetBalanced Preset = "balanced" PresetSafe Preset = "safe" diff --git a/internal/tasks/selector.go b/internal/tasks/selector.go index 7a5c4d3..ae0ebc6 100644 --- a/internal/tasks/selector.go +++ b/internal/tasks/selector.go @@ -98,8 +98,8 @@ func (s *Selector) FilterEnabled(tasks []TaskDefinition) []TaskDefinition { func (s *Selector) FilterByBudget(tasks []TaskDefinition, budget int64) []TaskDefinition { filtered := make([]TaskDefinition, 0, len(tasks)) for _, t := range tasks { - _, max := t.EstimatedTokens() - if int64(max) <= budget { + _, maxTokens := t.EstimatedTokens() + if int64(maxTokens) <= budget { filtered = append(filtered, t) } } @@ -243,8 +243,8 @@ func (s *Selector) SelectNext(budget int64, project string) *ScoredTask { // Select top task that fits remaining budget for _, st := range scored { - _, max := st.Definition.EstimatedTokens() - if int64(max) <= budget { + _, maxTokens := st.Definition.EstimatedTokens() + if int64(maxTokens) <= budget { return &st } } diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go index 2c7dabb..d39ce7a 100644 --- a/internal/tasks/tasks.go +++ b/internal/tasks/tasks.go @@ -12,6 +12,7 @@ import ( // CostTier represents the estimated token cost for a task. type CostTier int +// Cost tiers, ordered from cheapest to most expensive. const ( CostLow CostTier = iota // 10-50k tokens CostMedium // 50-150k tokens @@ -36,7 +37,7 @@ func (c CostTier) String() string { } // TokenRange returns the min and max estimated tokens for this tier. -func (c CostTier) TokenRange() (min, max int) { +func (c CostTier) TokenRange() (minTokens, maxTokens int) { switch c { case CostLow: return 10_000, 50_000 @@ -54,6 +55,7 @@ func (c CostTier) TokenRange() (min, max int) { // RiskLevel represents the risk associated with a task. type RiskLevel int +// Risk levels, ordered from least to most risky. const ( RiskLow RiskLevel = iota RiskMedium @@ -242,7 +244,7 @@ func DefaultIntervalForCategory(cat TaskCategory) time.Duration { } // EstimatedTokens returns the token range for this task definition. -func (d TaskDefinition) EstimatedTokens() (min, max int) { +func (d TaskDefinition) EstimatedTokens() (minTokens, maxTokens int) { return d.CostTier.TokenRange() } @@ -856,13 +858,13 @@ func GetDefinition(taskType TaskType) (TaskDefinition, error) { } // GetCostEstimate returns the estimated token cost range for a task type. -func GetCostEstimate(taskType TaskType) (min, max int, err error) { +func GetCostEstimate(taskType TaskType) (minTokens, maxTokens int, err error) { def, err := GetDefinition(taskType) if err != nil { return 0, 0, err } - min, max = def.EstimatedTokens() - return min, max, nil + minTokens, maxTokens = def.EstimatedTokens() + return minTokens, maxTokens, nil } // GetTasksByCategory returns all task definitions in a category. @@ -996,7 +998,7 @@ func NewQueue() *Queue { } // Add queues a task. -func (q *Queue) Add(t Task) { +func (q *Queue) Add(t Task) { //nolint:revive // unimplemented stub; the parameter documents the intended signature // TODO: Implement } diff --git a/internal/tasks/tasks_test.go b/internal/tasks/tasks_test.go index 03cdf18..c7412a0 100644 --- a/internal/tasks/tasks_test.go +++ b/internal/tasks/tasks_test.go @@ -36,10 +36,10 @@ func TestCostTierTokenRange(t *testing.T) { {CostTier(99), 0, 0}, } for _, tt := range tests { - min, max := tt.tier.TokenRange() - if min != tt.wantMin || max != tt.wantMax { + minTokens, maxTokens := tt.tier.TokenRange() + if minTokens != tt.wantMin || maxTokens != tt.wantMax { t.Errorf("CostTier(%d).TokenRange() = (%d, %d), want (%d, %d)", - tt.tier, min, max, tt.wantMin, tt.wantMax) + tt.tier, minTokens, maxTokens, tt.wantMin, tt.wantMax) } } } @@ -106,21 +106,21 @@ func TestGetDefinition(t *testing.T) { func TestGetCostEstimate(t *testing.T) { // Low cost task - min, max, err := GetCostEstimate(TaskLintFix) + minTokens, maxTokens, err := GetCostEstimate(TaskLintFix) if err != nil { t.Fatalf("GetCostEstimate(TaskLintFix) error: %v", err) } - if min != 10_000 || max != 50_000 { - t.Errorf("GetCostEstimate(TaskLintFix) = (%d, %d), want (10000, 50000)", min, max) + if minTokens != 10_000 || maxTokens != 50_000 { + t.Errorf("GetCostEstimate(TaskLintFix) = (%d, %d), want (10000, 50000)", minTokens, maxTokens) } // Very high cost task - min, max, err = GetCostEstimate(TaskMigrationRehearsal) + minTokens, maxTokens, err = GetCostEstimate(TaskMigrationRehearsal) if err != nil { t.Fatalf("GetCostEstimate(TaskMigrationRehearsal) error: %v", err) } - if min != 500_000 || max != 1_000_000 { - t.Errorf("GetCostEstimate(TaskMigrationRehearsal) = (%d, %d), want (500000, 1000000)", min, max) + if minTokens != 500_000 || maxTokens != 1_000_000 { + t.Errorf("GetCostEstimate(TaskMigrationRehearsal) = (%d, %d), want (500000, 1000000)", minTokens, maxTokens) } // Unknown task @@ -237,9 +237,9 @@ func TestAllDefinitions(t *testing.T) { func TestTaskDefinitionEstimatedTokens(t *testing.T) { def, _ := GetDefinition(TaskLintFix) - min, max := def.EstimatedTokens() - if min != 10_000 || max != 50_000 { - t.Errorf("TaskDefinition.EstimatedTokens() = (%d, %d), want (10000, 50000)", min, max) + minTokens, maxTokens := def.EstimatedTokens() + if minTokens != 10_000 || maxTokens != 50_000 { + t.Errorf("TaskDefinition.EstimatedTokens() = (%d, %d), want (10000, 50000)", minTokens, maxTokens) } }