From 9f865754438aaba846bcef80c2b0224ebd668b01 Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:06:26 -0700 Subject: [PATCH 1/7] build: add curated golangci-lint configuration Enables errorlint, nilerr, gocritic, revive, unconvert, misspell and bodyclose on top of the standard linter set. Two exclusions are recorded in-config with justification: revive's unused-parameter is skipped in test files (mock signatures require the parameters), and the stats.StatsResult stutter warning is left for a dedicated API change. Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- .golangci.yml | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..03939c0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,66 @@ +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 trips revive's stutter check. Renaming an exported type + # is a cross-package API break and does not belong in a lint-only change; + # it should be done deliberately in its own commit. + - path: internal/stats/ + linters: + - revive + text: "type name will be used as stats.StatsResult by other packages" + +formatters: + enable: + - gofmt + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 From 462970716b56b9fab5e5d8c0ba0f45596cb6922e Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:06:38 -0700 Subject: [PATCH 2/7] fix: compare and unwrap errors through the error chain Replaces sentinel `!=` comparisons with errors.Is, *exec.ExitError type assertions with errors.As, and non-wrapping %v verbs with %w in fmt.Errorf. These are latent bugs: each site silently stops matching as soon as an error anywhere below it is wrapped. Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- cmd/nightshift/commands/daemon.go | 3 ++- cmd/provider-calibration/main.go | 5 +++-- internal/agents/claude.go | 4 +++- internal/agents/claude_test.go | 4 ++-- internal/agents/codex.go | 4 +++- internal/agents/codex_test.go | 4 ++-- internal/agents/copilot.go | 4 +++- internal/config/config_test.go | 14 +++++++------- internal/scheduler/scheduler.go | 10 +++++----- internal/scheduler/scheduler_test.go | 9 +++++---- internal/security/sandbox.go | 4 +++- 11 files changed, 38 insertions(+), 27 deletions(-) diff --git a/cmd/nightshift/commands/daemon.go b/cmd/nightshift/commands/daemon.go index d082b0e..95a911d 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" @@ -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) } diff --git a/cmd/provider-calibration/main.go b/cmd/provider-calibration/main.go index ee573b2..9efd2fe 100644 --- a/cmd/provider-calibration/main.go +++ b/cmd/provider-calibration/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "encoding/json" + "errors" "flag" "fmt" "io" @@ -226,7 +227,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 +350,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..b0a1320 100644 --- a/internal/agents/claude.go +++ b/internal/agents/claude.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -175,7 +176,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..86cf13e 100644 --- a/internal/agents/codex.go +++ b/internal/agents/codex.go @@ -4,6 +4,7 @@ package agents import ( "context" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -133,7 +134,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..4c16df9 100644 --- a/internal/agents/copilot.go +++ b/internal/agents/copilot.go @@ -4,6 +4,7 @@ package agents import ( "context" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -145,7 +146,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/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/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 278ca71..6767afe 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) } } @@ -329,7 +329,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++ { 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/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() From 7e50225abba23df37e5b503c667f2c19bc1b3f50 Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:07:31 -0700 Subject: [PATCH 3/7] style: document why nilerr sites intentionally swallow errors All eight sites are deliberate graceful degradation (missing optional CLI, unreadable transcript file, best-effort quota lookup). None was a dropped error, so no control flow changed; each now carries a //nolint:nilerr directive stating why nil is the correct return. Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- internal/budget/budget.go | 4 ++-- internal/integrations/github.go | 4 ++-- internal/integrations/td.go | 4 ++-- internal/providers/claude.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/budget/budget.go b/internal/budget/budget.go index 31a718c..2013e6a 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 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/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/providers/claude.go b/internal/providers/claude.go index 3f78e89..ded3bec 100644 --- a/internal/providers/claude.go +++ b/internal/providers/claude.go @@ -409,7 +409,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 +417,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 From eaa360a061113ada9b20b306ff0d6247ae20bcb2 Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:09:46 -0700 Subject: [PATCH 4/7] style: address gocritic findings Rewrites eleven if-else chains as switch statements and moves three `Deprecated:` notices into their own comment paragraph so godoc renders them as deprecation markers. In stats.go two identical branches of the projection chain collapse into a single default. Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- cmd/nightshift/commands/daemon.go | 14 ++++++++------ cmd/nightshift/commands/preview_output.go | 7 ++++--- cmd/nightshift/commands/report.go | 7 ++++--- cmd/nightshift/commands/run.go | 7 ++++--- cmd/nightshift/commands/setup.go | 7 ++++--- cmd/nightshift/commands/snapshot.go | 18 ++++++++++-------- cmd/nightshift/commands/stats.go | 9 ++++----- internal/budget/budget.go | 2 ++ internal/calibrator/calibrator.go | 7 ++++--- internal/scheduler/scheduler.go | 9 ++++++--- 10 files changed, 50 insertions(+), 37 deletions(-) diff --git a/cmd/nightshift/commands/daemon.go b/cmd/nightshift/commands/daemon.go index 95a911d..3671643 100644 --- a/cmd/nightshift/commands/daemon.go +++ b/cmd/nightshift/commands/daemon.go @@ -565,22 +565,24 @@ 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") } } diff --git a/cmd/nightshift/commands/preview_output.go b/cmd/nightshift/commands/preview_output.go index b3a664c..72a2b52 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 { diff --git a/cmd/nightshift/commands/report.go b/cmd/nightshift/commands/report.go index 37cc9cd..1358837 100644 --- a/cmd/nightshift/commands/report.go +++ b/cmd/nightshift/commands/report.go @@ -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..5378bb3 100644 --- a/cmd/nightshift/commands/run.go +++ b/cmd/nightshift/commands/run.go @@ -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..f761815 100644 --- a/cmd/nightshift/commands/setup.go +++ b/cmd/nightshift/commands/setup.go @@ -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 { diff --git a/cmd/nightshift/commands/snapshot.go b/cmd/nightshift/commands/snapshot.go index ed19318..08cab84 100644 --- a/cmd/nightshift/commands/snapshot.go +++ b/cmd/nightshift/commands/snapshot.go @@ -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..b277024 100644 --- a/cmd/nightshift/commands/stats.go +++ b/cmd/nightshift/commands/stats.go @@ -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/internal/budget/budget.go b/internal/budget/budget.go index 2013e6a..733340a 100644 --- a/internal/budget/budget.go +++ b/internal/budget/budget.go @@ -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{ 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/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 6767afe..e72d202 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -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 @@ -449,6 +451,7 @@ 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 { From 5ed0f94d4f01ccb4206d9b0d950a16ae47199176 Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:11:30 -0700 Subject: [PATCH 5/7] style: rename unused parameters and identifiers shadowing builtins Renames 43 structurally-required-but-unused parameters to _ (cobra handlers, interface-mandated ctx) and renames the min/max/real locals that shadow Go builtins to minTokens/maxTokens/resolved. Two unused parameters are kept and annotated instead of renamed: Tracker.Record's tokens (deprecated published signature) and Queue.Add's t (an unimplemented stub with no call sites, so the discarded argument is scaffolding rather than a bug). Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- cmd/nightshift/commands/budget.go | 2 +- cmd/nightshift/commands/config.go | 6 ++--- cmd/nightshift/commands/daemon.go | 8 +++---- cmd/nightshift/commands/doctor.go | 2 +- cmd/nightshift/commands/init.go | 2 +- cmd/nightshift/commands/install.go | 4 ++-- cmd/nightshift/commands/logs.go | 2 +- cmd/nightshift/commands/preview.go | 2 +- cmd/nightshift/commands/preview_output.go | 2 +- cmd/nightshift/commands/report.go | 2 +- cmd/nightshift/commands/run.go | 2 +- cmd/nightshift/commands/setup.go | 6 ++--- cmd/nightshift/commands/snapshot.go | 6 ++--- cmd/nightshift/commands/stats.go | 2 +- cmd/nightshift/commands/status.go | 2 +- cmd/nightshift/commands/task.go | 28 +++++++++++------------ internal/budget/budget.go | 2 +- internal/integrations/agentsmd.go | 2 +- internal/integrations/claudemd.go | 2 +- internal/providers/claude.go | 2 +- internal/providers/codex.go | 2 +- internal/providers/copilot.go | 2 +- internal/scheduler/scheduler.go | 6 ++--- internal/tasks/selector.go | 8 +++---- internal/tasks/tasks.go | 12 +++++----- internal/tasks/tasks_test.go | 24 +++++++++---------- 26 files changed, 70 insertions(+), 70 deletions(-) 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 3671643..e4204e8 100644 --- a/cmd/nightshift/commands/daemon.go +++ b/cmd/nightshift/commands/daemon.go @@ -130,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) @@ -588,7 +588,7 @@ func takeSnapshot(ctx context.Context, cfg *config.Config, database *db.DB, log } } -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 { @@ -612,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 @@ -660,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 72a2b52..8ab46e7 100644 --- a/cmd/nightshift/commands/preview_output.go +++ b/cmd/nightshift/commands/preview_output.go @@ -348,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 1358837..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") diff --git a/cmd/nightshift/commands/run.go b/cmd/nightshift/commands/run.go index 5378bb3..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") diff --git a/cmd/nightshift/commands/setup.go b/cmd/nightshift/commands/setup.go index f761815..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 @@ -1911,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 08cab84..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) }, diff --git a/cmd/nightshift/commands/stats.go b/cmd/nightshift/commands/stats.go index b277024..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) 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/internal/budget/budget.go b/internal/budget/budget.go index 733340a..c869ca6 100644 --- a/internal/budget/budget.go +++ b/internal/budget/budget.go @@ -456,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/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/providers/claude.go b/internal/providers/claude.go index ded3bec..d200523 100644 --- a/internal/providers/claude.go +++ b/internal/providers/claude.go @@ -102,7 +102,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 } diff --git a/internal/providers/codex.go b/internal/providers/codex.go index eb948ed..663f7dc 100644 --- a/internal/providers/codex.go +++ b/internal/providers/codex.go @@ -84,7 +84,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..4510a6f 100644 --- a/internal/providers/copilot.go +++ b/internal/providers/copilot.go @@ -56,7 +56,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 e72d202..d1bdf58 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -457,7 +457,7 @@ 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 }) @@ -469,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 }) @@ -478,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/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..277bbc0 100644 --- a/internal/tasks/tasks.go +++ b/internal/tasks/tasks.go @@ -36,7 +36,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 @@ -242,7 +242,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 +856,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 +996,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) } } From 7b7a5e299fcd3553483be3091fb7286ad99a408c Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:13:35 -0700 Subject: [PATCH 6/7] docs: add missing package, const-block and blank-import comments Adds doc comments to eight exported const blocks, fixes two malformed doc comments, justifies the blank modernc.org/sqlite import, gives cmd/provider-calibration a command comment, and demotes the per-file headers in internal/agents and internal/providers so they no longer read as duplicate package comments. Moving the `Deprecated:` markers into their own paragraph made staticcheck recognise them, surfacing two uses of the deprecated budget.Tracker in the orchestrator. Both are dead scaffolding (WithBudget has no call sites and o.budget is never read); they are annotated rather than migrated, since replacing them with budget.Manager is an API change rather than a lint fix. Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- .golangci.yml | 11 ++++++----- cmd/provider-calibration/main.go | 2 ++ internal/agents/claude.go | 1 + internal/agents/codex.go | 1 + internal/agents/copilot.go | 1 + internal/db/db.go | 2 +- internal/integrations/integrations.go | 1 + internal/logging/logging.go | 2 +- internal/orchestrator/events.go | 1 + internal/orchestrator/orchestrator.go | 5 +++-- internal/providers/claude.go | 1 + internal/providers/codex.go | 1 + internal/providers/copilot.go | 1 + internal/security/audit.go | 1 + internal/security/security.go | 3 ++- internal/setup/presets.go | 1 + internal/tasks/tasks.go | 2 ++ 17 files changed, 27 insertions(+), 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 03939c0..d21cd68 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -49,13 +49,14 @@ linters: - revive text: "unused-parameter" - # stats.StatsResult trips revive's stutter check. Renaming an exported type - # is a cross-package API break and does not belong in a lint-only change; - # it should be done deliberately in its own commit. - - path: internal/stats/ + # 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: "type name will be used as stats.StatsResult by other packages" + text: "and that stutters" formatters: enable: diff --git a/cmd/provider-calibration/main.go b/cmd/provider-calibration/main.go index 9efd2fe..fd92081 100644 --- a/cmd/provider-calibration/main.go +++ b/cmd/provider-calibration/main.go @@ -1,3 +1,5 @@ +// Command provider-calibration measures real token usage for each provider +// so budget estimates can be calibrated against observed transcripts. package main import ( diff --git a/internal/agents/claude.go b/internal/agents/claude.go index b0a1320..461cdf5 100644 --- a/internal/agents/claude.go +++ b/internal/agents/claude.go @@ -1,4 +1,5 @@ // claude.go implements the Agent interface for Claude Code CLI. + package agents import ( diff --git a/internal/agents/codex.go b/internal/agents/codex.go index 86cf13e..777c721 100644 --- a/internal/agents/codex.go +++ b/internal/agents/codex.go @@ -1,4 +1,5 @@ // codex.go implements the Agent interface for OpenAI Codex CLI. + package agents import ( diff --git a/internal/agents/copilot.go b/internal/agents/copilot.go index 4c16df9..b19afe9 100644 --- a/internal/agents/copilot.go +++ b/internal/agents/copilot.go @@ -1,4 +1,5 @@ // copilot.go implements the Agent interface for GitHub Copilot CLI. + package agents import ( 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/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/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 d200523..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 ( diff --git a/internal/providers/codex.go b/internal/providers/codex.go index 663f7dc..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 ( diff --git a/internal/providers/copilot.go b/internal/providers/copilot.go index 4510a6f..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 ( 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/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/tasks.go b/internal/tasks/tasks.go index 277bbc0..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 @@ -54,6 +55,7 @@ func (c CostTier) TokenRange() (minTokens, maxTokens 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 From f45f4f4b681a9b5760eb37c1f7425d3cc4a2fdcf Mon Sep 17 00:00:00 2001 From: Greg Gardner Date: Mon, 7 Sep 2026 02:21:43 -0700 Subject: [PATCH 7/7] ci: run golangci-lint v2 and build against Go 1.24 The new .golangci.yml uses the golangci-lint v2 config schema, which golangci-lint-action@v6 (a v1 driver) cannot parse. Bump the action to v8 and pin the linter to v2.12.2. Also raise the CI Go version to 1.24 to match the go directive in go.mod. Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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