diff --git a/.golangci.yml b/.golangci.yml index 6410390731..b271c82c10 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,13 +2,23 @@ version: "2" linters: enable: + - copyloopvar - errcheck - errorlint - govet - ineffassign + - intrange + - modernize - staticcheck - unused + - usestdlibvars settings: + modernize: + # omitempty -> omitzero changes which fields are written for nested + # structs. That is a wire-format change to session files and the + # extension protocol, not a refactor, so it lands on its own. + disable: + - omitzero errcheck: check-type-assertions: false exclude-functions: diff --git a/benchmarks/context-maintenance-e2e/main.go b/benchmarks/context-maintenance-e2e/main.go index fda902a80e..ac553392bc 100644 --- a/benchmarks/context-maintenance-e2e/main.go +++ b/benchmarks/context-maintenance-e2e/main.go @@ -55,7 +55,7 @@ func fakeGoFile(nonce string, i, size int) string { func seedSession(nonce string) *agent.Session { s := agent.NewSession("You are a terse coding agent reviewing a Go codebase.") s.Add(provider.Message{Role: provider.RoleUser, Content: "Review every file in module " + nonce + " one by one. Keep notes short."}) - for i := 0; i < fatResults; i++ { + for i := range fatResults { id := fmt.Sprintf("c%02d", i) name := fmt.Sprintf("src/file%02d.go", i) s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: fmt.Sprintf(`{"path":%q}`, name)}}}) @@ -180,7 +180,7 @@ func resume(dir string) { func comprehension(trials int) { p := prov() pass := 0 - for t := 0; t < trials; t++ { + for t := range trials { dir, err := os.MkdirTemp("", "cm-e2e-") if err != nil { fmt.Fprintln(os.Stderr, err) @@ -198,7 +198,7 @@ func comprehension(trials int) { s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "r1", Name: "read_file", Arguments: `{"path":"config.go"}`}}}) s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "r1", Name: "read_file", Content: content}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "Noted the constants in config.go."}) - for i := 0; i < 4; i++ { + for i := range 4 { s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("ack %d", i)}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "ok"}) } diff --git a/cmd/e2ebench/diff.go b/cmd/e2ebench/diff.go index 3cf6664d31..b608719939 100644 --- a/cmd/e2ebench/diff.go +++ b/cmd/e2ebench/diff.go @@ -48,10 +48,7 @@ func runDiff(o diffOpts) string { pkgs := packagesOf(srcFiles) prompt := buildDiffPrompt(srcFiles, pkgs, truncate(gitOut(o.repo, "diff", o.base+"...HEAD", "--"))) - attempts := o.attempts - if attempts < 1 { - attempts = 1 - } + attempts := max(o.attempts, 1) var best diffReport made := 0 for i := 1; i <= attempts; i++ { @@ -406,7 +403,7 @@ func parseCoverProfile(repo, path string) map[string][]coverBlock { return nil } out := map[string][]coverBlock{} - for _, ln := range strings.Split(string(data), "\n") { + for ln := range strings.SplitSeq(string(data), "\n") { if ln == "" || strings.HasPrefix(ln, "mode:") { continue } @@ -433,8 +430,8 @@ func repoRelFromModulePath(p string) string { if strings.HasPrefix(p, prefix) { return p[len(prefix):] } - if i := strings.IndexByte(p, '/'); i >= 0 { - return p[i+1:] + if _, after, ok := strings.Cut(p, "/"); ok { + return after } return p } @@ -447,7 +444,7 @@ func changedLineSet(repo, base string, srcFiles []string) map[string]map[int]boo out := map[string]map[int]bool{} file := "" newLine := 0 - for _, ln := range strings.Split(diff, "\n") { + for ln := range strings.SplitSeq(diff, "\n") { // '-' (deletion) lines are intentionally unhandled: they don't advance the // new-side line counter, so they fall through with no case. switch { @@ -457,10 +454,10 @@ func changedLineSet(repo, base string, srcFiles []string) map[string]map[int]boo case strings.HasPrefix(ln, "@@"): // @@ -a,b +c,d @@ — start collecting at new-side line c. // Digit-only cut: malformed headers (e.g. `@@ +abc @@`) fail closed. - if plus := strings.Index(ln, "+"); plus >= 0 { - num := ln[plus+1:] + if _, after, ok := strings.Cut(ln, "+"); ok { + num := after end := len(num) - for i := 0; i < len(num); i++ { + for i := range len(num) { if num[i] < '0' || num[i] > '9' { end = i break @@ -503,9 +500,9 @@ func countAssertionPins(ps []pinResult) int { func parseNewTests(diff string) []testRef { var refs []testRef pkg := "" - for _, ln := range strings.Split(diff, "\n") { - if strings.HasPrefix(ln, "+++ b/") { - pkg = "./" + filepath.ToSlash(filepath.Dir(strings.TrimPrefix(ln, "+++ b/"))) + for ln := range strings.SplitSeq(diff, "\n") { + if after, ok := strings.CutPrefix(ln, "+++ b/"); ok { + pkg = "./" + filepath.ToSlash(filepath.Dir(after)) continue } if !strings.HasPrefix(ln, "+") || strings.HasPrefix(ln, "+++") { @@ -519,11 +516,11 @@ func parseNewTests(diff string) []testRef { // Method form `(r T) Name(...)` starts with '('; parse the receiver out before the name. var name string if sig[0] == '(' { - close := strings.IndexByte(sig, ')') - if close < 0 { + _, after, ok := strings.Cut(sig, ")") + if !ok { continue } - rest := strings.TrimSpace(sig[close+1:]) + rest := strings.TrimSpace(after) methodParen := strings.IndexByte(rest, '(') if methodParen <= 0 { continue @@ -545,7 +542,7 @@ func parseNewTests(diff string) []testRef { func countAdded(diff string) int { n := 0 - for _, ln := range strings.Split(diff, "\n") { + for ln := range strings.SplitSeq(diff, "\n") { if strings.HasPrefix(ln, "+") && !strings.HasPrefix(ln, "+++") { n++ } @@ -557,7 +554,7 @@ func countAdded(diff string) int { func failingTestNames(out string) []string { var names []string seen := map[string]bool{} - for _, ln := range strings.Split(out, "\n") { + for ln := range strings.SplitSeq(out, "\n") { ln = strings.TrimSpace(ln) if !strings.HasPrefix(ln, "--- FAIL:") { continue @@ -603,7 +600,7 @@ func changedGoFilesWorktree(repo string, includeTests bool) []string { func filterGo(out string, includeTests bool) []string { var keep []string - for _, f := range strings.Fields(strings.ReplaceAll(out, "\n", " ")) { + for f := range strings.FieldsSeq(strings.ReplaceAll(out, "\n", " ")) { if strings.HasSuffix(f, "_test.go") && !includeTests { continue } diff --git a/cmd/e2ebench/main.go b/cmd/e2ebench/main.go index 5854ed1901..0d553bea50 100644 --- a/cmd/e2ebench/main.go +++ b/cmd/e2ebench/main.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "time" @@ -526,7 +527,7 @@ func median(ms []int64) int64 { return 0 } sorted := append([]int64(nil), ms...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + slices.Sort(sorted) return sorted[len(sorted)/2] } diff --git a/cmd/e2ebench/mutation.go b/cmd/e2ebench/mutation.go index 5a214ccd73..0d0847f7c1 100644 --- a/cmd/e2ebench/mutation.go +++ b/cmd/e2ebench/mutation.go @@ -129,7 +129,7 @@ func mutantBody(fset *token.FileSet, results *ast.FieldList) string { if n == 0 { n = 1 } - for i := 0; i < n; i++ { + for range n { rets = append(rets, t) } } diff --git a/cmd/e2ebench/swebench.go b/cmd/e2ebench/swebench.go index e7fa288373..03c34aad9e 100644 --- a/cmd/e2ebench/swebench.go +++ b/cmd/e2ebench/swebench.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "slices" "strings" "reasonix/internal/ablation" @@ -137,30 +138,20 @@ func swebenchReportPath(model, runID string) string { // taxonomy. An id the grader never mentions is reported as unknown rather than // silently counted as unsolved. func (r swebenchReport) gradedClass(instanceID string) string { - for _, id := range r.ResolvedIDs { - if id == instanceID { - return "solved" - } + if slices.Contains(r.ResolvedIDs, instanceID) { + return "solved" } - for _, id := range r.EmptyPatchIDs { - if id == instanceID { - return "no_patch" - } + if slices.Contains(r.EmptyPatchIDs, instanceID) { + return "no_patch" } - for _, id := range r.ErrorIDs { - if id == instanceID { - return "grader_error" - } + if slices.Contains(r.ErrorIDs, instanceID) { + return "grader_error" } - for _, id := range r.IncompleteIDs { - if id == instanceID { - return "eval_timeout" - } + if slices.Contains(r.IncompleteIDs, instanceID) { + return "eval_timeout" } - for _, id := range r.UnresolvedIDs { - if id == instanceID { - return "wrong_patch" - } + if slices.Contains(r.UnresolvedIDs, instanceID) { + return "wrong_patch" } return "ungraded" } diff --git a/cmd/e2ebench/swebench_run.go b/cmd/e2ebench/swebench_run.go index f76061fb69..a50da15471 100644 --- a/cmd/e2ebench/swebench_run.go +++ b/cmd/e2ebench/swebench_run.go @@ -296,8 +296,8 @@ func proxyEnv(url string) []string { } func firstLine(s string) string { - if i := strings.IndexByte(s, '\n'); i >= 0 { - return strings.TrimSpace(s[:i]) + if before, _, ok := strings.Cut(s, "\n"); ok { + return strings.TrimSpace(before) } return strings.TrimSpace(s) } diff --git a/desktop/app.go b/desktop/app.go index cf3dea3c79..779861c2e1 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "log/slog" + "maps" "mime" "net/http" "net/url" @@ -20,6 +21,7 @@ import ( "path/filepath" "regexp" goruntime "runtime" + "slices" "sort" "strconv" "strings" @@ -2635,6 +2637,7 @@ func (a *App) CheckpointsForTab(tabID string) []CheckpointMeta { canCodeAfter := true codeFileSet := make(map[string]bool, len(metas)*2) codeFilePreview := []string{} + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(out) - 1; i >= 0; i-- { if len(out[i].Files) > 0 { hasCodeAfter = true @@ -5681,10 +5684,7 @@ func historyPageFromMessages(messages []HistoryMessage, beforeTurn, limit int) H if beforeTurn <= 0 || beforeTurn > totalTurns { beforeTurn = totalTurns } - startTurn := beforeTurn - limit - if startTurn < 0 { - startTurn = 0 - } + startTurn := max(beforeTurn-limit, 0) page := HistoryPage{ StartTurn: startTurn, EndTurn: beforeTurn, @@ -6033,10 +6033,7 @@ func historyPageFromProviderMessages( if beforeTurn <= 0 || beforeTurn > totalTurns { beforeTurn = totalTurns } - startTurn := beforeTurn - limit - if startTurn < 0 { - startTurn = 0 - } + startTurn := max(beforeTurn-limit, 0) page := HistoryPage{ StartTurn: startTurn, EndTurn: beforeTurn, @@ -6323,7 +6320,7 @@ func historyLineCount(s string) int { func historyNonEmptyLineCount(s string) int { count := 0 - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if strings.TrimSpace(line) != "" { count++ } @@ -6622,9 +6619,9 @@ func updateHistoryToolCallSummary(out []HistoryMessage, callID, output string) { if callID == "" { return } - for i := len(out) - 1; i >= 0; i-- { - for j := range out[i].ToolCalls { - call := &out[i].ToolCalls[j] + for _, v := range slices.Backward(out) { + for j := range v.ToolCalls { + call := &v.ToolCalls[j] if call.ID != callID { continue } @@ -8160,9 +8157,7 @@ func (a *App) mcpServersView() []ServerView { } ctrl := tab.Ctrl disabled := make(map[string]ServerView, len(tab.disabledMCP)) - for name, s := range tab.disabledMCP { - disabled[name] = s - } + maps.Copy(disabled, tab.disabledMCP) order := append([]string(nil), tab.mcpOrder...) workspaceRoot := tab.WorkspaceRoot tabID := tab.ID @@ -9447,9 +9442,7 @@ func cloneStringIntMap(values map[string]int) map[string]int { return nil } out := make(map[string]int, len(values)) - for key, value := range values { - out[key] = value - } + maps.Copy(out, values) return out } @@ -11327,7 +11320,7 @@ func saveExclusiveExportPayloads(targets []string, payloadCount int, payloadAt f // while bytes are staged; the caller restores finalMode only after the payload // has been completely written and synced. func createExportTempFile(dir string) (*os.File, os.FileMode, error) { - for attempt := 0; attempt < exportTempCreateAttempts; attempt++ { + for range exportTempCreateAttempts { var suffix [12]byte if _, err := rand.Read(suffix[:]); err != nil { return nil, 0, fmt.Errorf("generate export temp name: %w", err) diff --git a/desktop/app_autosave_test.go b/desktop/app_autosave_test.go index 8fe0aa632d..9b7dfe9bfe 100644 --- a/desktop/app_autosave_test.go +++ b/desktop/app_autosave_test.go @@ -124,12 +124,10 @@ func TestScheduleSnapshotCoalesces(t *testing.T) { _ = a var wg sync.WaitGroup - for i := 0; i < 64; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 64 { + wg.Go(func() { tab.sink.Emit(event.Event{Kind: event.TurnDone}) - }() + }) } wg.Wait() @@ -335,11 +333,11 @@ func TestDesktopSnapshotConflictRecoveryRequiresRecoveryLease(t *testing.T) { detachedSessions: map[string]*WorkspaceTab{}, activeTabID: "recovery_tab", } - app.runtimeEvents.emit = func(ctx context.Context, name string, payload ...interface{}) { + app.runtimeEvents.emit = func(ctx context.Context, name string, payload ...any) { runtimeEvents <- runtimeEventEnvelope{ ctx: ctx, name: name, - payload: append([]interface{}(nil), payload...), + payload: append([]any(nil), payload...), } } tab := &WorkspaceTab{ diff --git a/desktop/app_session_dedup_test.go b/desktop/app_session_dedup_test.go index 1b4e96d010..8612b0f3d5 100644 --- a/desktop/app_session_dedup_test.go +++ b/desktop/app_session_dedup_test.go @@ -35,7 +35,7 @@ func TestCarriedRebuildsKeepOneSession(t *testing.T) { t.Fatal(err) } - for i := 0; i < 5; i++ { + for range 5 { prevPath := ctrl.SessionPath() carried := ctrl.History() ctrl.Close() diff --git a/desktop/app_test.go b/desktop/app_test.go index 5717d693e8..128b29c2b6 100644 --- a/desktop/app_test.go +++ b/desktop/app_test.go @@ -13,6 +13,7 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" "strconv" "strings" "sync" @@ -1058,14 +1059,14 @@ func TestBackgroundRestorePlanAvoidsNormalWindowFlash(t *testing.T) { func TestEmitReadyInvokesReadyHook(t *testing.T) { app := NewApp() - var calls int32 + var calls atomic.Int32 app.readyHook = func() { - atomic.AddInt32(&calls, 1) + calls.Add(1) } app.emitReady(context.TODO()) - if got := atomic.LoadInt32(&calls); got != 1 { + if got := calls.Load(); got != 1 { t.Fatalf("ready hook calls = %d, want 1", got) } } @@ -1227,7 +1228,7 @@ func BenchmarkDesktopSettingsPayloads(b *testing.B) { b.Setenv("SHARED_PROVIDER_KEY", "sk-test") cfg := config.LoadForEdit(config.UserConfigPath()) - for i := 0; i < 40; i++ { + for i := range 40 { cfg.Providers = append(cfg.Providers, config.ProviderEntry{ Name: fmt.Sprintf("custom-%02d", i), Kind: "openai", @@ -1243,12 +1244,12 @@ func BenchmarkDesktopSettingsPayloads(b *testing.B) { app := NewApp() b.Run("Settings", func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { _ = app.Settings() } }) b.Run("DesktopStartupSettings", func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { _ = app.DesktopStartupSettings() } }) @@ -2073,7 +2074,6 @@ func TestResetProviderPresetAccessRejectsMissingSameNameProvider(t *testing.T) { func TestAddEveryProviderPresetAccessInstallsTemplate(t *testing.T) { for _, preset := range config.CuratedProviderPresets() { - preset := preset t.Run(preset.ID, func(t *testing.T) { isolateDesktopUserDirs(t) @@ -2890,7 +2890,7 @@ func TestSetEffortForTabReanchorsDepthCapRecoveryBranch(t *testing.T) { app := NewApp() app.ctx = context.Background() - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} tab := &WorkspaceTab{ ID: "tab_depth_cap_effort", Scope: "global", @@ -3566,7 +3566,7 @@ func TestSetModelForTabContinuesRecoveryPathAfterSnapshotConflict(t *testing.T) app := NewApp() app.ctx = context.Background() - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} tab := &WorkspaceTab{ ID: "tab_recovery_model", Scope: "global", @@ -4339,16 +4339,13 @@ func runQuickClickWorkspaceReconcileTest(t *testing.T, layoutStyle string) { errs := make(chan error, len(actions)) var wg sync.WaitGroup for _, action := range actions { - action := action - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { ready <- struct{}{} <-start if err := action.run(); err != nil { errs <- fmt.Errorf("%s: %w", action.name, err) } - }() + }) } for range actions { <-ready @@ -6793,7 +6790,7 @@ func assertSingleTeardownTimeoutNotice(t *testing.T, notices <-chan event.Event, t.Fatal("missing background-job teardown timeout notice") } var waited time.Duration - for _, field := range strings.Fields(notice.Detail) { + for field := range strings.FieldsSeq(notice.Detail) { if !strings.HasPrefix(field, "waited=") { continue } @@ -7297,7 +7294,7 @@ func BenchmarkDesktopListSessionsScoped(b *testing.B) { if err := os.MkdirAll(dir, 0o755); err != nil { b.Fatalf("mkdir %s: %v", dir, err) } - for i := 0; i < 120; i++ { + for i := range 120 { path := filepath.Join(dir, fmt.Sprintf("session-%03d.jsonl", i)) body := fmt.Sprintf(`{"role":"user","content":"session %03d"}`+"\n", i) if err := os.WriteFile(path, []byte(body), 0o644); err != nil { @@ -7312,7 +7309,7 @@ func BenchmarkDesktopListSessionsScoped(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { sessions := app.ListSessions() if len(sessions) != 120 { b.Fatalf("ListSessions len = %d, want 120", len(sessions)) @@ -7563,12 +7560,12 @@ func TestDesktopSharedHostProjectMCPConnectsWithoutLaunchApproval(t *testing.T) srv := desktopMCPHTTPServer(t) defer srv.Close() - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" type = "http" url = %q -`, srv.URL)), 0o644); err != nil { +`, srv.URL), 0o644); err != nil { t.Fatal(err) } @@ -7759,12 +7756,12 @@ func TestSetMCPServerEnabledSharedHostPreservesSiblingTabs(t *testing.T) { srv := desktopMCPHTTPServer(t) defer srv.Close() - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" type = "http" url = %q -`, srv.URL)), 0o644); err != nil { +`, srv.URL), 0o644); err != nil { t.Fatal(err) } @@ -7909,12 +7906,12 @@ func TestReconnectMCPServerUsesEffectiveProjectConfigWhenUserNameIsShadowed(t *t if err := userCfg.SaveTo(config.UserConfigPath()); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" type = "http" url = %q -`, projectServer.URL)), 0o644); err != nil { +`, projectServer.URL), 0o644); err != nil { t.Fatal(err) } @@ -8055,7 +8052,7 @@ func newGatedDesktopMCPLaunchFixture(t *testing.T, startGateAddr string) gatedDe gateConfig = fmt.Sprintf("DESKTOP_MCP_START_GATE_ADDR = %q\n", startGateAddr) } helperArgs := []string{"-test.run=TestDesktopMCPHelperProcess", "--"} - if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(dir, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "h" command = %q @@ -8067,7 +8064,7 @@ DESKTOP_MCP_SINGLE_INSTANCE_ADDR = %q %s [sandbox] network = true -`, exe, singleInstanceAddr, gateConfig)), 0o644); err != nil { +`, exe, singleInstanceAddr, gateConfig), 0o644); err != nil { t.Fatal(err) } @@ -8305,13 +8302,13 @@ func installGatedTestPluginPackage(t *testing.T, mcpServerName string) string { if err := os.MkdirAll(root, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), []byte(fmt.Sprintf(`{ + if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), fmt.Appendf(nil, `{ "name": "review-helper", "version": "1.0.0", "mcpServers": { %q: { "type": "stdio", "command": "helper" } } -}`, mcpServerName)), 0o644); err != nil { +}`, mcpServerName), 0o644); err != nil { t.Fatal(err) } if err := pluginpkg.Upsert(reasonixHome, pluginpkg.InstalledPlugin{ @@ -9082,13 +9079,13 @@ func TestRemoveMCPServerRejectsPluginManagedServerWithoutDisconnecting(t *testin if err := os.MkdirAll(root, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), []byte(fmt.Sprintf(`{ + if err := os.WriteFile(filepath.Join(root, pluginpkg.NativeManifest), fmt.Appendf(nil, `{ "name": "superpowers", "version": "1.0.0", "mcpServers": { "helper": { "type": "http", "url": %q } } -}`, srv.URL)), 0o644); err != nil { +}`, srv.URL), 0o644); err != nil { t.Fatal(err) } if err := pluginpkg.Upsert(reasonixHome, pluginpkg.InstalledPlugin{ @@ -10127,7 +10124,7 @@ func TestRunShellForTabStaysBoundDuringRapidProjectTabSwitching(t *testing.T) { waitForShellDispatch(t, shellEvents, marker) waitForFile(t, filepath.Join(projectA, marker), "shell") - for i := 0; i < 8; i++ { + for range 8 { if err := app.SetActiveTab("project-b"); err != nil { t.Fatalf("SetActiveTab(project-b): %v", err) } @@ -10281,12 +10278,7 @@ func newBackgroundJobController(t *testing.T, label string) *control.Controller } func hasLevel(levels []string, want string) bool { - for _, level := range levels { - if level == want { - return true - } - } - return false + return slices.Contains(levels, want) } func hasCommand(cmds []CommandInfo, name string) bool { @@ -10616,14 +10608,14 @@ func TestScanPromptHistoryFromDirUsesSessionActivityBeforeEventInterleaving(t *t early := filepath.Join(dir, "early.jsonl") late := filepath.Join(dir, "late.jsonl") - if err := os.WriteFile(early, []byte(fmt.Sprintf(`{"role":"user","content":"early first","time":%d} + if err := os.WriteFile(early, fmt.Appendf(nil, `{"role":"user","content":"early first","time":%d} {"role":"assistant","content":"ok"} {"role":"user","content":"early second","time":%d} -`, base.UnixMilli(), base.Add(time.Minute).UnixMilli())), 0o644); err != nil { +`, base.UnixMilli(), base.Add(time.Minute).UnixMilli()), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(late, []byte(fmt.Sprintf(`{"role":"user","content":"late newest","time":%d} -`, base.Add(2*time.Minute).UnixMilli())), 0o644); err != nil { + if err := os.WriteFile(late, fmt.Appendf(nil, `{"role":"user","content":"late newest","time":%d} +`, base.Add(2*time.Minute).UnixMilli()), 0o644); err != nil { t.Fatal(err) } // Invert file mtimes: session activity should keep each session grouped @@ -10792,8 +10784,8 @@ func TestScanPromptHistoryPaginatesCurrentSessionBeforeCrossSession(t *testing.T other := filepath.Join(dir, "other.jsonl") var lines []byte for i := range 55 { - lines = append(lines, []byte(fmt.Sprintf(`{"role":"user","content":"current %d"} -`, i))...) + lines = append(lines, fmt.Appendf(nil, `{"role":"user","content":"current %d"} +`, i)...) } if err := os.WriteFile(current, lines, 0o644); err != nil { t.Fatal(err) @@ -10867,8 +10859,8 @@ func TestScanPromptHistoryFromDirReadsAllEntriesForInternalHelper(t *testing.T) dir := t.TempDir() var lines []byte for i := range 250 { - lines = append(lines, []byte(fmt.Sprintf(`{"role":"user","content":"prompt %d"} -`, i))...) + lines = append(lines, fmt.Appendf(nil, `{"role":"user","content":"prompt %d"} +`, i)...) } if err := os.WriteFile(filepath.Join(dir, "many.jsonl"), lines, 0o644); err != nil { t.Fatal(err) diff --git a/desktop/bot_bridge_test.go b/desktop/bot_bridge_test.go index 4935fe969e..36840ab068 100644 --- a/desktop/bot_bridge_test.go +++ b/desktop/bot_bridge_test.go @@ -155,7 +155,7 @@ func TestBridgeTakeoverSwitchAnnouncesReleaseToOldTab(t *testing.T) { t.Fatalf("switch to B: %v", err) } seen := map[string]bool{} - for i := 0; i < 2; i++ { + for range 2 { select { case got := <-env.announced: seen[got[0]] = true diff --git a/desktop/bot_connection_app.go b/desktop/bot_connection_app.go index c664b6ce96..c9032d814f 100644 --- a/desktop/bot_connection_app.go +++ b/desktop/bot_connection_app.go @@ -693,7 +693,7 @@ func postFeishuInstallFormResult(base string, body map[string]string) (map[strin for k, v := range body { reqBody.Set(k, v) } - req, err := http.NewRequestWithContext(ctx, "POST", strings.TrimRight(base, "/")+"/oauth/v1/app/registration", strings.NewReader(reqBody.Encode())) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(base, "/")+"/oauth/v1/app/registration", strings.NewReader(reqBody.Encode())) if err != nil { return nil, 0, err } diff --git a/desktop/bound_array_contract_test.go b/desktop/bound_array_contract_test.go index d5b1f43cb1..c479b3d20d 100644 --- a/desktop/bound_array_contract_test.go +++ b/desktop/bound_array_contract_test.go @@ -114,12 +114,12 @@ func assertRequiredJSONSlicesNonNil(t *testing.T, path string, value reflect.Val if value.Kind() == reflect.Slice && value.IsNil() { t.Fatalf("%s is a nil slice; JSON contract requires []", path) } - for i := 0; i < value.Len(); i++ { + for i := range value.Len() { assertRequiredJSONSlicesNonNil(t, path, value.Index(i)) } case reflect.Struct: typ := value.Type() - for i := 0; i < value.NumField(); i++ { + for i := range value.NumField() { fieldType := typ.Field(i) if fieldType.PkgPath != "" { continue diff --git a/desktop/checkpoints_cancode_test.go b/desktop/checkpoints_cancode_test.go index 9353101c9f..e936aa3a5b 100644 --- a/desktop/checkpoints_cancode_test.go +++ b/desktop/checkpoints_cancode_test.go @@ -230,7 +230,7 @@ func TestCheckpointsForTabLimitsCumulativeFilePreview(t *testing.T) { } content := "old" files := make([]checkpoint.FileSnap, 0, checkpointFilePreviewLimit+5) - for i := 0; i < checkpointFilePreviewLimit+5; i++ { + for i := range checkpointFilePreviewLimit + 5 { files = append(files, checkpoint.FileSnap{Path: "file-" + strconv.Itoa(1000+i) + ".txt", Content: &content}) } now := time.Now() diff --git a/desktop/cmd/update-helper/main_windows.go b/desktop/cmd/update-helper/main_windows.go index c803661e34..f378baae7f 100644 --- a/desktop/cmd/update-helper/main_windows.go +++ b/desktop/cmd/update-helper/main_windows.go @@ -534,7 +534,7 @@ func cleanupOwnedWindowsUpdateDirectory(path string, owner os.FileInfo) error { if path == "" || owner == nil || !owner.IsDir() { return fmt.Errorf("Windows update cleanup identity is incomplete") } - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { cleanup := fmt.Sprintf("%s.reasonix-cleanup-%d-%d", path, time.Now().UTC().UnixNano(), attempt) from, err := windows.UTF16PtrFromString(path) if err != nil { diff --git a/desktop/crash_app.go b/desktop/crash_app.go index 043cf60c82..18b5f5ca5a 100644 --- a/desktop/crash_app.go +++ b/desktop/crash_app.go @@ -155,7 +155,7 @@ func baseCrashReport(kind string) crashReport { } func topFrameFromStack(stack string) string { - for _, line := range strings.Split(stack, "\n") { + for line := range strings.SplitSeq(stack, "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/desktop/crash_pending_test.go b/desktop/crash_pending_test.go index f82cb92c77..7f9cb35f3e 100644 --- a/desktop/crash_pending_test.go +++ b/desktop/crash_pending_test.go @@ -107,11 +107,9 @@ func TestWritePendingReportQueueIsBoundedUnderConcurrentWriters(t *testing.T) { var done sync.WaitGroup var successes atomic.Int32 - for i := 0; i < writers; i++ { + for range writers { ready.Add(1) - done.Add(1) - go func() { - defer done.Done() + done.Go(func() { report := baseCrashReport("performance") report.Source = "native.watchdog" report.Label = "mac.main_thread.hang" @@ -121,7 +119,7 @@ func TestWritePendingReportQueueIsBoundedUnderConcurrentWriters(t *testing.T) { if writePendingReport(report, false) { successes.Add(1) } - }() + }) } ready.Wait() close(start) diff --git a/desktop/deferred_rebuild.go b/desktop/deferred_rebuild.go index 94fe9649d0..41de8e5002 100644 --- a/desktop/deferred_rebuild.go +++ b/desktop/deferred_rebuild.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "maps" "strings" "sync" "time" @@ -163,9 +164,7 @@ func (a *App) deferredRebuildTick(markIdle bool) bool { return true } pending := make(map[string]string, len(d.pending)) - for id, setting := range d.pending { - pending[id] = setting - } + maps.Copy(pending, d.pending) d.mu.Unlock() for tabID, setting := range pending { diff --git a/desktop/devinfo.go b/desktop/devinfo.go index 945967ea02..8277a6517d 100644 --- a/desktop/devinfo.go +++ b/desktop/devinfo.go @@ -28,7 +28,7 @@ func collectDeviceInfo() deviceInfo { } func parseCPUModel(cpuinfo string) string { - for _, line := range strings.Split(cpuinfo, "\n") { + for line := range strings.SplitSeq(cpuinfo, "\n") { if name, ok := strings.CutPrefix(line, "model name"); ok { if _, v, ok := strings.Cut(name, ":"); ok { return strings.TrimSpace(v) @@ -39,7 +39,7 @@ func parseCPUModel(cpuinfo string) string { } func parseMemTotalBytes(meminfo string) uint64 { - for _, line := range strings.Split(meminfo, "\n") { + for line := range strings.SplitSeq(meminfo, "\n") { if rest, ok := strings.CutPrefix(line, "MemTotal:"); ok { kb, err := strconv.ParseUint(strings.TrimSuffix(strings.TrimSpace(rest), " kB"), 10, 64) if err != nil { @@ -52,7 +52,7 @@ func parseMemTotalBytes(meminfo string) uint64 { } func parseOSReleasePrettyName(osRelease string) string { - for _, line := range strings.Split(osRelease, "\n") { + for line := range strings.SplitSeq(osRelease, "\n") { if v, ok := strings.CutPrefix(line, "PRETTY_NAME="); ok { return strings.Trim(strings.TrimSpace(v), `"`) } diff --git a/desktop/export_test.go b/desktop/export_test.go index 4dff32465c..93a311977a 100644 --- a/desktop/export_test.go +++ b/desktop/export_test.go @@ -81,7 +81,6 @@ func TestExportErrorsDoNotExposeSelectedDirectory(t *testing.T) { }, } for _, test := range tests { - test := test t.Run(test.name, func(t *testing.T) { err := test.run(test.path) if err == nil { @@ -290,7 +289,6 @@ func TestConcurrentMultipartExportsHaveSingleCompleteWinner(t *testing.T) { var ready sync.WaitGroup ready.Add(len(batches)) for _, batch := range batches { - batch := batch go func() { ready.Done() <-start diff --git a/desktop/external_opener_darwin.go b/desktop/external_opener_darwin.go index de991a26d2..7ab19986ee 100644 --- a/desktop/external_opener_darwin.go +++ b/desktop/external_opener_darwin.go @@ -48,7 +48,7 @@ func darwinInstalledApplicationIndex() map[string]string { defer cancel() out, err := exec.CommandContext(ctx, "/usr/bin/mdfind", "kMDItemContentType == 'com.apple.application-bundle'").Output() if err == nil { - for _, line := range strings.Split(string(out), "\n") { + for line := range strings.SplitSeq(string(out), "\n") { add(line) } } diff --git a/desktop/hang_watchdog.go b/desktop/hang_watchdog.go index 6e0eaf11e1..ea5563fb71 100644 --- a/desktop/hang_watchdog.go +++ b/desktop/hang_watchdog.go @@ -24,10 +24,7 @@ var ( ) func recordMainThreadHeartbeat(t time.Time) { - elapsed := t.Sub(mainThreadClockBase) - if elapsed < 0 { - elapsed = 0 - } + elapsed := max(t.Sub(mainThreadClockBase), 0) mainThreadLastHeartbeatElapsed.Store(int64(elapsed)) mainThreadLastHeartbeatWall.Store(t.UnixNano()) } @@ -38,10 +35,7 @@ func mainThreadHeartbeatAge(now time.Time) (time.Duration, time.Time, bool) { if lastWall <= 0 { return 0, time.Time{}, false } - age := now.Sub(mainThreadClockBase) - lastElapsed - if age < 0 { - age = 0 - } + age := max(now.Sub(mainThreadClockBase)-lastElapsed, 0) return age, time.Unix(0, lastWall), true } diff --git a/desktop/hang_watchdog_windows.go b/desktop/hang_watchdog_windows.go index 209060bed1..226a5021d8 100644 --- a/desktop/hang_watchdog_windows.go +++ b/desktop/hang_watchdog_windows.go @@ -113,10 +113,7 @@ func windowClassName(hwnd uintptr) string { } func windowMessageLoopResponsive(hwnd uintptr, timeout time.Duration) bool { - timeoutMS := timeout.Milliseconds() - if timeoutMS < 250 { - timeoutMS = 250 - } + timeoutMS := max(timeout.Milliseconds(), 250) var result uintptr ok, _, _ := sendMessageTimeoutProc.Call( hwnd, diff --git a/desktop/hang_watchdog_windows_test.go b/desktop/hang_watchdog_windows_test.go index cca9a81d11..430fc06c54 100644 --- a/desktop/hang_watchdog_windows_test.go +++ b/desktop/hang_watchdog_windows_test.go @@ -12,7 +12,7 @@ func TestCurrentProcessTopLevelWindowReusesEnumCallback(t *testing.T) { // Go's Windows runtime currently supports a finite callback table. The old // implementation allocated one callback per poll and exhausted that table // during a normal long-running session. - for i := 0; i < 2100; i++ { + for range 2100 { _ = currentProcessTopLevelWindow() } } diff --git a/desktop/heartbeat.go b/desktop/heartbeat.go index 4596bce7da..317286473a 100644 --- a/desktop/heartbeat.go +++ b/desktop/heartbeat.go @@ -319,7 +319,7 @@ func (e *HeartbeatEngine) executeTask(t HeartbeatTask) HeartbeatTask { // Wait for the tab's controller to be built (it's started // asynchronously in a goroutine by openTopicTab). var ctrl heartbeatRuntimeStatus - for i := 0; i < 40; i++ { + for range 40 { if candidate := e.app.ctrlByTabID(tabMeta.ID); candidate != nil { ctrl = candidate break @@ -597,11 +597,11 @@ type heartbeatSchedule struct { } func parseHeartbeatSchedule(interval string) (heartbeatSchedule, bool) { - idx := strings.Index(interval, "|") - if idx < 0 { + _, after, ok0 := strings.Cut(interval, "|") + if !ok0 { return heartbeatSchedule{}, false } - raw := strings.TrimSpace(interval[idx+1:]) + raw := strings.TrimSpace(after) if raw == "" { return heartbeatSchedule{}, false } @@ -625,7 +625,7 @@ func parseHeartbeatSchedule(interval string) (heartbeatSchedule, bool) { case "daily": return s, true case "weekly", "biweekly": - for _, part := range strings.Split(rule, ",") { + for part := range strings.SplitSeq(rule, ",") { if wd, ok := parseHeartbeatWeekday(part); ok { s.days = append(s.days, wd) } @@ -681,7 +681,7 @@ func previousHeartbeatScheduleAt(t HeartbeatTask, now time.Time) (time.Time, boo func previousHeartbeatWeeklyAt(s heartbeatSchedule, now time.Time, windowDays int, anchor time.Time) (time.Time, bool) { var best time.Time - for offset := 0; offset < windowDays; offset++ { + for offset := range windowDays { day := now.AddDate(0, 0, -offset) for _, wd := range s.days { if day.Weekday() != wd { diff --git a/desktop/heartbeat_test.go b/desktop/heartbeat_test.go index d187dc4aa1..b69b8729f0 100644 --- a/desktop/heartbeat_test.go +++ b/desktop/heartbeat_test.go @@ -155,7 +155,7 @@ func TestHeartbeatExecuteTaskPersistsFreshConversationTopicID(t *testing.T) { app := NewApp() app.ctx = context.Background() app.readyHook = func() {} - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} engine := &HeartbeatEngine{ app: app, pendingTopics: map[string]heartbeatPendingTopic{}, @@ -236,7 +236,7 @@ func TestHeartbeatExecuteTaskSkipsPendingPrompt(t *testing.T) { app := NewApp() app.ctx = context.Background() app.readyHook = func() {} - app.runtimeEvents.emit = func(context.Context, string, ...interface{}) {} + app.runtimeEvents.emit = func(context.Context, string, ...any) {} engine := &HeartbeatEngine{ app: app, pendingTopics: map[string]heartbeatPendingTopic{}, diff --git a/desktop/history_perf_test.go b/desktop/history_perf_test.go index f7d2eef51b..f1cf8ed795 100644 --- a/desktop/history_perf_test.go +++ b/desktop/history_perf_test.go @@ -23,7 +23,7 @@ func syntheticHistoryMessages(turns, outputSize int, toolName string, failed boo if failed { output = "error: " + output } - for i := 0; i < turns; i++ { + for i := range turns { callID := fmt.Sprintf("call_%d", i) msgs = append(msgs, provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("prompt %d", i)}, @@ -66,7 +66,7 @@ func BenchmarkHistoryMessagesSynthetic(b *testing.B) { b.ReportAllocs() b.SetBytes(inputBytes) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { got := historyMessages(msgs, func(content string) string { return content }) if len(got) == 0 { b.Fatal("empty history") @@ -84,7 +84,7 @@ func BenchmarkHistoryMessagesMarshalSynthetic(b *testing.B) { b.ReportAllocs() b.SetBytes(inputBytes) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { got := historyMessages(msgs, func(content string) string { return content }) encoded, err := json.Marshal(got) if err != nil { diff --git a/desktop/hooks_settings_app.go b/desktop/hooks_settings_app.go index bc0e96710d..b25a50a3e5 100644 --- a/desktop/hooks_settings_app.go +++ b/desktop/hooks_settings_app.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" fileencoding "reasonix/internal/fileutil/encoding" @@ -127,12 +128,7 @@ func hookEventNames() []string { } func validHookEvent(event hook.Event) bool { - for _, e := range hook.Events { - if event == e { - return true - } - } - return false + return slices.Contains(hook.Events, event) } func hookConfigView(event hook.Event, cfg hook.HookConfig) HookConfigView { diff --git a/desktop/main_test.go b/desktop/main_test.go index 06bab18acc..e7bc984fcb 100644 --- a/desktop/main_test.go +++ b/desktop/main_test.go @@ -64,7 +64,7 @@ func TestMain(m *testing.M) { // contexts tests use, killing the process from any emitting code path. // Tests that assert on runtime events install their own capture through // the per-instance runtimeEvents.emit hook, which takes precedence. - runtimeEventsEmitFallback = func(context.Context, string, ...interface{}) {} + runtimeEventsEmitFallback = func(context.Context, string, ...any) {} code := m.Run() os.RemoveAll(dir) os.Exit(code) diff --git a/desktop/metrics_app.go b/desktop/metrics_app.go index 9de5ba8ce3..ac7dcc9cd0 100644 --- a/desktop/metrics_app.go +++ b/desktop/metrics_app.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" "sync" @@ -102,10 +103,8 @@ func countBucket(n int) string { func knownBucket(value string, allowed ...string) string { value = strings.ToLower(strings.TrimSpace(value)) - for _, ok := range allowed { - if value == ok { - return value - } + if slices.Contains(allowed, value) { + return value } return "other" } @@ -433,7 +432,7 @@ func (m *metricsAggregator) observeRecoveryMetrics(stats recovery.Metrics) { return } add := func(signal string, n int64) { - for i := int64(0); i < n; i++ { + for range n { m.inc(signal, "total") } } diff --git a/desktop/race_regression_test.go b/desktop/race_regression_test.go index 47320554e0..a401ec128e 100644 --- a/desktop/race_regression_test.go +++ b/desktop/race_regression_test.go @@ -36,14 +36,14 @@ func TestSessionLeaseHelpersConcurrentAccess(t *testing.T) { wg.Add(3) go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { _ = tabA.ensureSessionLease(path) _ = tabA.sessionLeaseRuntimeKey() } }() go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { // The applyRuntimeTab transfer shape: move A's lease to B and back. tabB.adoptSessionLease(tabA.takeSessionLease()) tabA.adoptSessionLease(tabB.takeSessionLease()) @@ -51,7 +51,7 @@ func TestSessionLeaseHelpersConcurrentAccess(t *testing.T) { }() go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { tabA.releaseSessionLease() } }() @@ -142,13 +142,13 @@ func TestTabEventSinkEmitConcurrentRebind(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for range iterations { sink.Emit(event.Event{Kind: event.Notice, Text: "hammer"}) } }() go func() { defer wg.Done() - for i := 0; i < iterations; i++ { + for i := range iterations { sink.setBinding(fmt.Sprintf("tab-%d", i%2), nil) sink.clearContext() } @@ -170,7 +170,7 @@ func TestModeRebuildAndABANavigationFenceLeaseFailureAndOldEpochEvent(t *testing continueCommit := make(chan struct{}) oldEvent := make(chan wireEventTab, 1) var capturedOldEvent wireEventTab - oldSink.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) { + oldSink.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) { if name != eventChannel || len(payload) != 1 { return } @@ -573,7 +573,7 @@ func TestMetaForTabConcurrentWithBuildSwap(t *testing.T) { done := make(chan struct{}) go func() { defer close(done) - for i := 0; i < iterations; i++ { + for i := range iterations { app.mu.Lock() tab.Ready = !tab.Ready tab.Label = fmt.Sprintf("model-%d", i) @@ -584,7 +584,7 @@ func TestMetaForTabConcurrentWithBuildSwap(t *testing.T) { app.mu.Unlock() } }() - for i := 0; i < iterations; i++ { + for range iterations { meta := app.MetaForTab("tab") if meta.EventChannel == "" { t.Fatal("MetaForTab returned zero meta for a live tab") diff --git a/desktop/recovery_gc_test.go b/desktop/recovery_gc_test.go index 321f567a36..97da77e20a 100644 --- a/desktop/recovery_gc_test.go +++ b/desktop/recovery_gc_test.go @@ -1,7 +1,6 @@ package main import ( - "context" "os" "path/filepath" "testing" @@ -106,8 +105,7 @@ func TestRecoveryGCFirstSweepWaitsForTabRestore(t *testing.T) { } _, branchPath := forkCoveredRecoveryBranch(t, dir, "startup") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() app := &App{ ctx: ctx, tabs: map[string]*WorkspaceTab{}, diff --git a/desktop/reload_runtime_test.go b/desktop/reload_runtime_test.go index a10b29483c..7105a17ce1 100644 --- a/desktop/reload_runtime_test.go +++ b/desktop/reload_runtime_test.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "slices" "sync" "testing" "time" @@ -77,7 +78,7 @@ func TestReloadRuntimeSwapsAndClosesOldAfterSwap(t *testing.T) { app.readyHook = func() {} var fenceMu sync.Mutex var fenceNames []string - app.runtimeEvents.emit = func(_ context.Context, name string, _ ...interface{}) { + app.runtimeEvents.emit = func(_ context.Context, name string, _ ...any) { fenceMu.Lock() fenceNames = append(fenceNames, name) fenceMu.Unlock() @@ -131,13 +132,7 @@ func TestReloadRuntimeSwapsAndClosesOldAfterSwap(t *testing.T) { deadline := time.Now().Add(2 * time.Second) for { fenceMu.Lock() - found := false - for _, name := range fenceNames { - if name == "runtime:rebuilt" { - found = true - break - } - } + found := slices.Contains(fenceNames, "runtime:rebuilt") fenceMu.Unlock() if found { break diff --git a/desktop/remote_app_test.go b/desktop/remote_app_test.go index ba045ec5cd..00b5767497 100644 --- a/desktop/remote_app_test.go +++ b/desktop/remote_app_test.go @@ -165,7 +165,7 @@ func TestConfirmRemoteSecretDelegatesWithoutPersisting(t *testing.T) { func TestRemoteStatusBridgesToAsyncEmitter(t *testing.T) { a := &App{ctx: context.Background()} events := make(chan runtimeEventEnvelope, 4) - a.runtimeEvents.emit = func(ctx context.Context, name string, payload ...interface{}) { + a.runtimeEvents.emit = func(ctx context.Context, name string, payload ...any) { events <- runtimeEventEnvelope{ctx: ctx, name: name, payload: payload} } a.onStatus(RemoteConnectionStatusView{HostID: "box", State: "connected"}) diff --git a/desktop/remote_hosts.go b/desktop/remote_hosts.go index d9f7b99ae2..bf0c3704e6 100644 --- a/desktop/remote_hosts.go +++ b/desktop/remote_hosts.go @@ -194,7 +194,7 @@ func canonicalRemoteSSHHost(host string) (string, error) { if trimmed == "" || len(trimmed) > 253 { return "", errors.New("remote SSH host is invalid") } - for _, label := range strings.Split(trimmed, ".") { + for label := range strings.SplitSeq(trimmed, ".") { if !remoteDNSLabelPattern.MatchString(label) { return "", errors.New("remote SSH host is invalid") } diff --git a/desktop/remote_hosts_test.go b/desktop/remote_hosts_test.go index 553de986b6..ec902c4cf2 100644 --- a/desktop/remote_hosts_test.go +++ b/desktop/remote_hosts_test.go @@ -100,10 +100,10 @@ func TestRemoteHostStoreLoadsV1AsConfigAndWritesV2OnMutation(t *testing.T) { if err != nil { t.Fatal(err) } - raw := []byte(fmt.Sprintf( + raw := fmt.Appendf(nil, `{"version":1,"hosts":[{"id":%q,"alias":%q,"label":%q,"sshConfigPath":%q,"clientInstanceId":%q,"resumeLeaseId":"lease_legacy","layoutRef":"layout_legacy"}]}`, entry.ID, entry.Alias, entry.Label, configPath, entry.ClientInstanceID, - )) + ) if err := os.WriteFile(path, raw, 0o600); err != nil { t.Fatal(err) } @@ -213,7 +213,7 @@ func TestRemoteHostStoreConcurrentInstancesDoNotLoseUpdates(t *testing.T) { const count = 48 var wg sync.WaitGroup errorsCh := make(chan error, count) - for i := 0; i < count; i++ { + for i := range count { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/desktop/remote_lifecycle_test.go b/desktop/remote_lifecycle_test.go index 7124ff2fe7..97bdf67bf0 100644 --- a/desktop/remote_lifecycle_test.go +++ b/desktop/remote_lifecycle_test.go @@ -373,15 +373,13 @@ func TestHostKeyPromptsAreSerializedForGlobalDialog(t *testing.T) { prompts = append(prompts, pendingPrompt{hostID: hostID, prompt: mgr.hostKeyPrompt(hostID, mh)}) } for _, pending := range prompts { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { _, _ = pending.prompt(ctx, remote.HostKeyQuestion{ Address: pending.hostID + ":22", KeyType: "ssh-ed25519", Fingerprint: pending.hostID, }) - }() + }) } first := <-sink.statuses diff --git a/desktop/remote_window.go b/desktop/remote_window.go index ab1580c8de..ab82342861 100644 --- a/desktop/remote_window.go +++ b/desktop/remote_window.go @@ -574,8 +574,8 @@ func (a *App) domReadyRemoteWindow() { func (a *App) secondInstanceRemoteWindow(data options.SecondInstanceData) { ticket := "" for _, arg := range data.Args { - if strings.HasPrefix(arg, remoteWindowTicketArgPrefix) { - ticket = strings.TrimPrefix(arg, remoteWindowTicketArgPrefix) + if after, ok := strings.CutPrefix(arg, remoteWindowTicketArgPrefix); ok { + ticket = after break } } diff --git a/desktop/remote_window_test.go b/desktop/remote_window_test.go index 8e97992bd1..fde86c0b90 100644 --- a/desktop/remote_window_test.go +++ b/desktop/remote_window_test.go @@ -289,8 +289,7 @@ func TestRemoteWindowOwnerWaitDetectsParentExit(t *testing.T) { if err := cmd.Start(); err != nil { t.Fatal(err) } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() exited := make(chan bool, 1) go func() { exited <- waitForRemoteWindowOwnerExit(ctx, cmd.Process.Pid) }() if err := cmd.Process.Kill(); err != nil { diff --git a/desktop/runtime_rebuilt_event_test.go b/desktop/runtime_rebuilt_event_test.go index 4a4c4f088f..35f7562194 100644 --- a/desktop/runtime_rebuilt_event_test.go +++ b/desktop/runtime_rebuilt_event_test.go @@ -59,14 +59,14 @@ func TestRuntimeRebuildsEmitRuntimeRebuiltForTab(t *testing.T) { // The App-level queue must stay silent: ordering against the tab's agent // events only holds when the notice rides the tab sink's own queue, so a // notice showing up here means the routing regressed to the fallback. - app.runtimeEvents.emit = func(_ context.Context, name string, _ ...interface{}) { + app.runtimeEvents.emit = func(_ context.Context, name string, _ ...any) { if name == "runtime:rebuilt" { mu.Lock() rebuilt = append(rebuilt, "VIA-APP-QUEUE") mu.Unlock() } } - sinkEmit := func(_ context.Context, name string, payload ...interface{}) { + sinkEmit := func(_ context.Context, name string, payload ...any) { if name != "runtime:rebuilt" { return } diff --git a/desktop/session_prompt_test.go b/desktop/session_prompt_test.go index 9cd76f1acf..4cf988bfb4 100644 --- a/desktop/session_prompt_test.go +++ b/desktop/session_prompt_test.go @@ -172,7 +172,7 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) tabOrder := make([]string, 0, tabCount) controllers := make([]*control.Controller, 0, tabCount) - for tabIndex := 0; tabIndex < tabCount; tabIndex++ { + for tabIndex := range tabCount { id := fmt.Sprintf("parallel-%02d", tabIndex) path := filepath.Join(dir, id+".jsonl") prov := &capturingProvider{} @@ -207,25 +207,20 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) errs := make(chan error, tabCount+1) var wg sync.WaitGroup for tabIndex, ctrl := range controllers { - tabIndex, ctrl := tabIndex, ctrl - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start - for turn := 0; turn < turnsPerTab; turn++ { + for turn := range turnsPerTab { input := fmt.Sprintf("tab-%02d-turn-%02d", tabIndex, turn) if err := ctrl.RunTurn(context.Background(), input); err != nil { errs <- fmt.Errorf("%s: %w", input, err) return } } - }() + }) } - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start - for round := 0; round < 3; round++ { + for range 3 { for _, id := range tabOrder { if err := app.SetActiveTab(id); err != nil { errs <- fmt.Errorf("switch to %s: %w", id, err) @@ -233,7 +228,7 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) } } } - }() + }) close(start) wg.Wait() @@ -269,7 +264,7 @@ func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) if msgs[0].Role != provider.RoleSystem || msgs[0].Content != systemText { t.Fatalf("%s system message = %+v", id, msgs[0]) } - for turn := 0; turn < turnsPerTab; turn++ { + for turn := range turnsPerTab { user := msgs[1+turn*2] assistant := msgs[2+turn*2] wantUser := fmt.Sprintf("tab-%02d-turn-%02d", tabIndex, turn) diff --git a/desktop/session_recovery_cleanup_test.go b/desktop/session_recovery_cleanup_test.go index eb3dee625a..037aebeaf1 100644 --- a/desktop/session_recovery_cleanup_test.go +++ b/desktop/session_recovery_cleanup_test.go @@ -17,7 +17,7 @@ import ( func saveSnapshotTurns(t *testing.T, path string, turns int) *agent.Session { t.Helper() s := agent.NewSession("sys") - for i := 0; i < turns; i++ { + for i := range turns { s.Add(provider.Message{Role: provider.RoleUser, Content: "prompt " + string(rune('a'+i))}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "reply"}) if err := s.SaveSnapshot(path); err != nil { diff --git a/desktop/sessions.go b/desktop/sessions.go index bcf5c40e83..8d38d720c9 100644 --- a/desktop/sessions.go +++ b/desktop/sessions.go @@ -354,7 +354,7 @@ func reserveUniqueSessionTrashItemDir(dir, key string) (string, error) { return "", err } stem := strings.TrimSuffix(key, ".jsonl") - for i := 0; i < 100; i++ { + for i := range 100 { name := fmt.Sprintf("%s.jsonl-deleted-%d-%02d", stem, time.Now().UnixNano(), i) itemDir := filepath.Join(root, name) if err := os.Mkdir(itemDir, 0o755); err == nil { diff --git a/desktop/sessions_test.go b/desktop/sessions_test.go index ab744e7abc..d61167b312 100644 --- a/desktop/sessions_test.go +++ b/desktop/sessions_test.go @@ -30,7 +30,7 @@ func occupyReadFileWithTimeoutSlots(t *testing.T) func() { return } released = true - for i := 0; i < filled; i++ { + for range filled { <-readFileWithTimeoutSlots } } @@ -1208,7 +1208,7 @@ func TestRecordSessionPlannerDisplayConcurrentPreservesEverySession(t *testing.T start := make(chan struct{}) errs := make(chan error, writers) var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(i int) { defer wg.Done() @@ -1232,7 +1232,7 @@ func TestRecordSessionPlannerDisplayConcurrentPreservesEverySession(t *testing.T if len(got) != writers { t.Fatalf("planner display sessions = %d, want %d", len(got), writers) } - for i := 0; i < writers; i++ { + for i := range writers { key := fmt.Sprintf("session-%02d.jsonl", i) if len(got[key]) != 1 || len(got[key][0].Messages) != 1 || got[key][0].Messages[0].Content != fmt.Sprintf("answer-%02d", i) { t.Fatalf("planner display %s = %+v", key, got[key]) @@ -1504,7 +1504,7 @@ func TestRecordSessionDisplaySerializesConcurrentTabs(t *testing.T) { const tabs = 32 errs := make(chan error, tabs) var wg sync.WaitGroup - for i := 0; i < tabs; i++ { + for i := range tabs { wg.Add(1) go func(i int) { defer wg.Done() @@ -1523,7 +1523,7 @@ func TestRecordSessionDisplaySerializesConcurrentTabs(t *testing.T) { } got := loadSessionDisplays(dir) - for i := 0; i < tabs; i++ { + for i := range tabs { key := fmt.Sprintf("tab-%02d.jsonl", i) content := fmt.Sprintf("expanded-%02d", i) if display := got[key][messageDisplayKey(content)]; display != fmt.Sprintf("display-%02d", i) { diff --git a/desktop/settings_app.go b/desktop/settings_app.go index 155bd83e55..e86249451b 100644 --- a/desktop/settings_app.go +++ b/desktop/settings_app.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" @@ -1687,13 +1688,13 @@ func configDeclaresProviderAccess(path string) bool { if err != nil { return false } - for _, line := range strings.Split(string(body), "\n") { + for line := range strings.SplitSeq(string(body), "\n") { if before, _, ok := strings.Cut(line, "#"); ok { line = before } line = strings.TrimSpace(line) - if strings.HasPrefix(line, "provider_access") { - rest := strings.TrimSpace(strings.TrimPrefix(line, "provider_access")) + if after, ok := strings.CutPrefix(line, "provider_access"); ok { + rest := strings.TrimSpace(after) return strings.HasPrefix(rest, "=") } } @@ -2321,10 +2322,8 @@ func providerVisionModels(models, visionModels []string) []string { func providerDefaultForModels(currentDefault string, models []string) string { currentDefault = strings.TrimSpace(currentDefault) if currentDefault != "" { - for _, model := range models { - if model == currentDefault { - return currentDefault - } + if slices.Contains(models, currentDefault) { + return currentDefault } } if len(models) > 0 { diff --git a/desktop/shared_host.go b/desktop/shared_host.go index 3126a707a3..1c18144d3e 100644 --- a/desktop/shared_host.go +++ b/desktop/shared_host.go @@ -73,7 +73,7 @@ func (a *App) reapOrphanCodeGraph() { ours := map[int]bool{} out, err := exec.Command("pgrep", "-P", strconv.Itoa(myPID)).Output() if err == nil { - for _, f := range strings.Fields(string(out)) { + for f := range strings.FieldsSeq(string(out)) { if pid, err := strconv.Atoi(f); err == nil { ours[pid] = true } @@ -85,7 +85,7 @@ func (a *App) reapOrphanCodeGraph() { if err != nil { return } - for _, f := range strings.Fields(string(out)) { + for f := range strings.FieldsSeq(string(out)) { pid, err := strconv.Atoi(f) if err != nil || pid == myPID || ours[pid] { continue diff --git a/desktop/tab_event_sink_test.go b/desktop/tab_event_sink_test.go index df543dbca8..f24ec5e02a 100644 --- a/desktop/tab_event_sink_test.go +++ b/desktop/tab_event_sink_test.go @@ -110,7 +110,7 @@ func TestTabEventSinkDoesNotBlockOnRuntimeEventsEmit(t *testing.T) { var calls atomic.Int32 sink := &tabEventSink{tabID: "tab", ctx: context.Background()} - sink.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) { + sink.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) { if name != eventChannel { t.Errorf("event name = %q, want %q", name, eventChannel) } @@ -170,7 +170,7 @@ func TestEmitProjectTreeChangedDoesNotBlockOnRuntimeEventsEmit(t *testing.T) { var calls atomic.Int32 app := &App{ctx: context.Background()} - app.runtimeEvents.emit = func(_ context.Context, name string, payload ...interface{}) { + app.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) { if name != "project-tree:changed" { t.Errorf("event name = %q, want project-tree:changed", name) } @@ -221,7 +221,7 @@ func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) { var calls atomic.Int32 emitter := &asyncRuntimeEmitter{} - emitter.emit = func(_ context.Context, _ string, payload ...interface{}) { + emitter.emit = func(_ context.Context, _ string, payload ...any) { if len(payload) != 1 { t.Errorf("payload count = %d, want 1", len(payload)) return @@ -239,7 +239,7 @@ func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) { } ctx := context.Background() - for i := 0; i < backlog; i++ { + for i := range backlog { emitter.Emit(ctx, "agent:event", strconv.Itoa(i)) } @@ -250,7 +250,7 @@ func TestAsyncRuntimeEmitterDrainsBacklogInOrder(t *testing.T) { } close(release) - for i := 0; i < backlog; i++ { + for i := range backlog { select { case got := <-delivered: if want := strconv.Itoa(i); got != want { diff --git a/desktop/tabs.go b/desktop/tabs.go index 42d7b6e2f6..7271780db7 100644 --- a/desktop/tabs.go +++ b/desktop/tabs.go @@ -9,8 +9,10 @@ import ( "errors" "fmt" "log/slog" + "maps" "os" "path/filepath" + "slices" "sort" "strings" "sync" @@ -308,15 +310,11 @@ func cloneSessionUsageStats(in sessionUsageStats) sessionUsageStats { out := in if len(in.Sources) > 0 { out.Sources = make(map[string]usageSourceStats, len(in.Sources)) - for source, stats := range in.Sources { - out.Sources[source] = stats - } + maps.Copy(out.Sources, in.Sources) } if len(in.sourceSessionCache) > 0 { out.sourceSessionCache = make(map[string]sourceSessionCacheCounters, len(in.sourceSessionCache)) - for source, counters := range in.sourceSessionCache { - out.sourceSessionCache[source] = counters - } + maps.Copy(out.sourceSessionCache, in.sourceSessionCache) } return out } @@ -1197,9 +1195,7 @@ func (t *WorkspaceTab) telemetrySnapshot() tabTelemetrySnapshot { } if len(t.usageTelemetry.Sources) > 0 { usage.Sources = make(map[string]usageSourceStats, len(t.usageTelemetry.Sources)) - for source, stats := range t.usageTelemetry.Sources { - usage.Sources[source] = stats - } + maps.Copy(usage.Sources, t.usageTelemetry.Sources) } usage.activeTurnStartedAt = 0 usage.sourceSessionCache = nil @@ -1408,9 +1404,9 @@ func updateBufferedHistoryToolCallSummary(messages []*bufferedHistoryMessage, ca if callID == "" { return } - for i := len(messages) - 1; i >= 0; i-- { - for j := range messages[i].message.ToolCalls { - call := &messages[i].message.ToolCalls[j] + for _, v := range slices.Backward(messages) { + for j := range v.message.ToolCalls { + call := &v.message.ToolCalls[j] if call.ID != callID { continue } @@ -1737,7 +1733,7 @@ func (s *tabEventSink) context() context.Context { return s.ctx } -func (s *tabEventSink) emitRuntimeEvent(name string, payload ...interface{}) { +func (s *tabEventSink) emitRuntimeEvent(name string, payload ...any) { if s == nil { return } @@ -1748,12 +1744,12 @@ func (s *tabEventSink) emitRuntimeEvent(name string, payload ...interface{}) { s.runtimeEvents.Emit(ctx, name, payload...) } -type runtimeEventEmitFunc func(context.Context, string, ...interface{}) +type runtimeEventEmitFunc func(context.Context, string, ...any) type runtimeEventEnvelope struct { ctx context.Context name string - payload []interface{} + payload []any } // asyncRuntimeEmitter decouples Wails' runtime event bridge from agent @@ -1775,14 +1771,14 @@ type asyncRuntimeEmitter struct { running bool } -func (e *asyncRuntimeEmitter) Emit(ctx context.Context, name string, payload ...interface{}) { +func (e *asyncRuntimeEmitter) Emit(ctx context.Context, name string, payload ...any) { if ctx == nil { return } item := runtimeEventEnvelope{ ctx: ctx, name: name, - payload: append([]interface{}(nil), payload...), + payload: append([]any(nil), payload...), } e.mu.Lock() e.queue = append(e.queue, item) @@ -2077,8 +2073,8 @@ func lastHistoryMessageIsUser(history []provider.Message) bool { } func hasPendingInterruptedRecovery(history []provider.Message) bool { - for i := len(history) - 1; i >= 0; i-- { - m := history[i] + for _, v := range slices.Backward(history) { + m := v if m.LocalOnly && m.InterruptedTurn != nil { return m.InterruptedTurn.Pending } @@ -2104,9 +2100,9 @@ func (s *tabEventSink) eventTabAndController() (*WorkspaceTab, control.SessionAP } func lastUserMessageContent(msgs []provider.Message) string { - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleUser { - return agent.UserMessageText(msgs[i]) + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleUser { + return agent.UserMessageText(v) } } return "" @@ -2892,7 +2888,7 @@ func createEmptySessionFile(dir, model string) (string, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } - for i := 0; i < 3; i++ { + for range 3 { path := agent.NewSessionPath(dir, model) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644) if err == nil { @@ -3232,10 +3228,7 @@ func (a *App) closeTab(tabID string, allowDetach bool) error { if wasActive { a.activeTabID = "" if len(a.tabOrder) > 0 { - nextIndex := closedIndex - if nextIndex < 0 { - nextIndex = 0 - } + nextIndex := max(closedIndex, 0) if nextIndex >= len(a.tabOrder) { nextIndex = len(a.tabOrder) - 1 } @@ -4673,7 +4666,7 @@ func autoTopicTitleProposalFromSession(path string) autoTopicTitleProposal { if title == "" { return autoTopicTitleProposal{} } - sum := sha256.Sum256([]byte(fmt.Sprintf("%d\x00%s", stage, strings.Join(basis, "\x00")))) + sum := sha256.Sum256(fmt.Appendf(nil, "%d\x00%s", stage, strings.Join(basis, "\x00"))) return autoTopicTitleProposal{ Title: title, Stage: stage, @@ -4799,10 +4792,7 @@ func topicTitleFromUserTurns(users []string) string { continue } runes := len([]rune(title)) - score := runes - if score > 24 { - score = 24 - } + score := min(runes, 24) if i == 0 { score += 3 } @@ -5443,12 +5433,7 @@ func containsDesktopString(values []string, value string) bool { if value == "" { return false } - for _, item := range uniqueStrings(values) { - if item == value { - return true - } - } - return false + return slices.Contains(uniqueStrings(values), value) } func pinnedTopicIDs(topicIDs []string, pinned []string) []string { @@ -7484,7 +7469,7 @@ func (a *App) emitProjectTreeChangedEvent() { a.emitRuntimeEvent("project-tree:changed") } -func (a *App) emitRuntimeEvent(name string, payload ...interface{}) { +func (a *App) emitRuntimeEvent(name string, payload ...any) { if a == nil || a.ctx == nil { return } @@ -7875,7 +7860,7 @@ func (a *App) ListProjectTree() []ProjectNode { continue } pendingLoads++ - dir := dir // capture + // capture cacheToken := projectSessionCache.versionToken(dir) go func() { result := sessionDirLoadResult{dir: dir} @@ -8078,17 +8063,14 @@ func (a *App) ListProjectTree() []ProjectNode { projectTopicResults := make([]projectTopics, len(f.Projects)) var topicLoadWg sync.WaitGroup for i, p := range f.Projects { - i, p := i, p - topicLoadWg.Add(1) - go func() { - defer topicLoadWg.Done() + topicLoadWg.Go(func() { projectTopicResults[i] = projectTopics{ project: p, titles: loadTopicTitles(p.Root), sources: loadTopicTitleSources(p.Root), createdAts: loadTopicCreatedAts(p.Root), } - }() + }) } topicLoadWg.Wait() for _, loaded := range projectTopicResults { diff --git a/desktop/tabs_display_buffer_test.go b/desktop/tabs_display_buffer_test.go index 96493109e3..04db67cedc 100644 --- a/desktop/tabs_display_buffer_test.go +++ b/desktop/tabs_display_buffer_test.go @@ -57,9 +57,9 @@ func TestDisplayTurnBufferStreamingAllocationsStayNearLinear(t *testing.T) { chunk := strings.Repeat("x", chunkSize) result := testing.Benchmark(func(b *testing.B) { b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { var buffer displayTurnBuffer - for part := 0; part < chunks; part++ { + for range chunks { recordHistoryDisplayEvent(&buffer, event.Event{Kind: event.Text, Text: chunk}) } messages := buffer.materialize() diff --git a/desktop/tabs_order_test.go b/desktop/tabs_order_test.go index 7b982301f5..1d1f3c350b 100644 --- a/desktop/tabs_order_test.go +++ b/desktop/tabs_order_test.go @@ -33,7 +33,7 @@ func testAppWithOrderedTabs(t *testing.T, active string, ids ...string) *App { } func installNoopRuntimeEvents(app *App, sinks ...*tabEventSink) { - emit := func(context.Context, string, ...interface{}) {} + emit := func(context.Context, string, ...any) {} if app != nil { app.runtimeEvents.emit = emit } @@ -664,18 +664,16 @@ func TestListTabsRepairsStaleOrderWithoutRacing(t *testing.T) { if testing.Short() { iterations = 5 } - for i := 0; i < 8; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 8 { + wg.Go(func() { <-start - for j := 0; j < iterations; j++ { + for range iterations { if got := strings.Join(tabIDs(app.ListTabs()), ","); got != "a,b,c" { errs <- got return } } - }() + }) } close(start) wg.Wait() diff --git a/desktop/tabs_runtime_status_test.go b/desktop/tabs_runtime_status_test.go index b3bcaed6ab..5527856f5a 100644 --- a/desktop/tabs_runtime_status_test.go +++ b/desktop/tabs_runtime_status_test.go @@ -224,10 +224,10 @@ func TestProjectTreeShowsBackgroundJobStatus(t *testing.T) { func TestBackgroundJobNoticeForcesProjectTreeRefresh(t *testing.T) { isolateDesktopUserDirs(t) - var refreshes int32 + var refreshes atomic.Int32 app := NewApp() app.projectTreeChangedHook = func() { - atomic.AddInt32(&refreshes, 1) + refreshes.Add(1) } app.tabs["job"] = &WorkspaceTab{ ID: "job", @@ -239,7 +239,7 @@ func TestBackgroundJobNoticeForcesProjectTreeRefresh(t *testing.T) { sink := &tabEventSink{tabID: "job", app: app} sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "background bash finished: bash-1"}) - if got := atomic.LoadInt32(&refreshes); got != 1 { + if got := refreshes.Load(); got != 1 { t.Fatalf("project-tree refreshes = %d, want 1 for background job finish notice", got) } } diff --git a/desktop/tabs_sink_context_test.go b/desktop/tabs_sink_context_test.go index a5a68013bc..affd2bb765 100644 --- a/desktop/tabs_sink_context_test.go +++ b/desktop/tabs_sink_context_test.go @@ -19,7 +19,7 @@ func TestTabEventSinkClearContextStopsEmission(t *testing.T) { var mu sync.Mutex var emitted int s := &tabEventSink{tabID: "t"} - s.runtimeEvents.emit = func(context.Context, string, ...interface{}) { + s.runtimeEvents.emit = func(context.Context, string, ...any) { mu.Lock() emitted++ mu.Unlock() diff --git a/desktop/tabs_topic_test.go b/desktop/tabs_topic_test.go index ef257a0479..90233538a2 100644 --- a/desktop/tabs_topic_test.go +++ b/desktop/tabs_topic_test.go @@ -3971,18 +3971,16 @@ func TestLegacyMigrationConcurrentRunsHaveNoLostUpdates(t *testing.T) { } const n = 8 want := make(map[string]bool, n) - for i := 0; i < n; i++ { + for i := range n { p := writeLegacySession(t, dir, fmt.Sprintf("legacy-%d.jsonl", i), "hi", time.Now()) want[legacySessionTopicID(p)] = true } var wg sync.WaitGroup - for i := 0; i < n; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range n { + wg.Go(func() { migrateLegacySessionsIntoGlobalTopics(dir) - }() + }) } wg.Wait() @@ -4148,17 +4146,15 @@ func TestEnsureTopicIndexedConcurrentRunsHaveNoLostProjectUpdates(t *testing.T) const n = 12 start := make(chan struct{}) var wg sync.WaitGroup - for i := 0; i < n; i++ { - i := i - wg.Add(1) - go func() { - defer wg.Done() + for i := range n { + + wg.Go(func() { <-start topicID := fmt.Sprintf("topic_recovered_%02d", i) if err := ensureTopicIndexed("project", projectRoot, topicID, fmt.Sprintf("Recovered %02d", i), topicTitleSourceManual); err != nil { t.Errorf("ensure topic indexed: %v", err) } - }() + }) } close(start) wg.Wait() @@ -4171,7 +4167,7 @@ func TestEnsureTopicIndexedConcurrentRunsHaveNoLostProjectUpdates(t *testing.T) for _, child := range nodes[0].Children { got[child.TopicID] = true } - for i := 0; i < n; i++ { + for i := range n { topicID := fmt.Sprintf("topic_recovered_%02d", i) if !got[topicID] { t.Fatalf("concurrent topic index recovery lost %q; children=%#v", topicID, nodes[0].Children) diff --git a/desktop/task_monitor_test.go b/desktop/task_monitor_test.go index ec218d7911..d047e8602c 100644 --- a/desktop/task_monitor_test.go +++ b/desktop/task_monitor_test.go @@ -43,12 +43,10 @@ func TestTaskControlConcurrentInitializationReturnsOneService(t *testing.T) { const callers = 16 services := make(chan *taskmonitor.ControlService, callers) var wg sync.WaitGroup - for i := 0; i < callers; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range callers { + wg.Go(func() { services <- app.taskControl() - }() + }) } wg.Wait() close(services) diff --git a/desktop/temphelper_test.go b/desktop/temphelper_test.go index f6d09ca6a2..2115f1dac7 100644 --- a/desktop/temphelper_test.go +++ b/desktop/temphelper_test.go @@ -23,7 +23,7 @@ func robustTempDir(t *testing.T) string { } t.Cleanup(func() { var rmErr error - for i := 0; i < 100; i++ { + for range 100 { if rmErr = os.RemoveAll(dir); rmErr == nil { return } diff --git a/desktop/terminal_test.go b/desktop/terminal_test.go index aef1b92964..5f1f1468bc 100644 --- a/desktop/terminal_test.go +++ b/desktop/terminal_test.go @@ -291,13 +291,13 @@ func TestTerminalManagerCountsConcurrentStartsTowardLimit(t *testing.T) { type result struct{ err error } results := make(chan result, maxTerminalsPerWorkspace+1) - for i := 0; i < maxTerminalsPerWorkspace+1; i++ { + for range maxTerminalsPerWorkspace + 1 { go func() { _, err := manager.create("tab", "workspace", ".", terminalCommand{path: "shell", label: "shell"}) results <- result{err: err} }() } - for i := 0; i < maxTerminalsPerWorkspace; i++ { + for range maxTerminalsPerWorkspace { select { case <-entered: case <-time.After(time.Second): @@ -307,7 +307,7 @@ func TestTerminalManagerCountsConcurrentStartsTowardLimit(t *testing.T) { close(release) succeeded, limited := 0, 0 - for i := 0; i < maxTerminalsPerWorkspace+1; i++ { + for range maxTerminalsPerWorkspace + 1 { result := <-results switch { case result.err == nil: diff --git a/desktop/theme_image.go b/desktop/theme_image.go index 6dc529c58e..c75467c8fd 100644 --- a/desktop/theme_image.go +++ b/desktop/theme_image.go @@ -131,12 +131,12 @@ func decodeDataURLImage(dataURL string) (filename string, data []byte, err error } // data:[][;base64], rest := dataURL[len(prefix):] - comma := strings.IndexByte(rest, ',') - if comma < 0 { + before, after, ok := strings.Cut(rest, ",") + if !ok { return "", nil, fmt.Errorf("invalid data URL") } - meta := rest[:comma] - payload := rest[comma+1:] + meta := before + payload := after parts := strings.Split(meta, ";") mime := strings.TrimSpace(parts[0]) base64Enc := false diff --git a/desktop/theme_pack.go b/desktop/theme_pack.go index a15ab60cb2..ad047683aa 100644 --- a/desktop/theme_pack.go +++ b/desktop/theme_pack.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "maps" "path/filepath" "regexp" "strings" @@ -84,7 +85,7 @@ type ThemePackManifest struct { Recipes ThemePackRecipes `json:"recipes"` Background *ThemePackBackground `json:"background,omitempty"` TaskBackground *ThemePackSceneBackground `json:"taskBackground,omitempty"` - Extra map[string]interface{} `json:"-"` // rejected on parse when present as unknown top-level + Extra map[string]any `json:"-"` // rejected on parse when present as unknown top-level } // ThemePackTokens holds optional light/dark semantic color overrides. @@ -603,9 +604,7 @@ func copyStringMap(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } diff --git a/desktop/theme_pack_test.go b/desktop/theme_pack_test.go index ee63240924..43a3a95ef9 100644 --- a/desktop/theme_pack_test.go +++ b/desktop/theme_pack_test.go @@ -631,8 +631,8 @@ func TestManifestJSONRoundTrip(t *testing.T) { func writeTestPNG(t *testing.T, path string, w, h int) string { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { + for y := range h { + for x := range w { img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 40, A: 255}) } } diff --git a/desktop/theme_plugin_test.go b/desktop/theme_plugin_test.go index 315e536f90..c2bda84f59 100644 --- a/desktop/theme_plugin_test.go +++ b/desktop/theme_plugin_test.go @@ -45,8 +45,8 @@ func testPluginThemeManifest(id, name string) *ThemePackManifest { func testPNGBytes(t *testing.T) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, 8, 8)) - for y := 0; y < 8; y++ { - for x := 0; x < 8; x++ { + for y := range 8 { + for x := range 8 { img.Set(x, y, color.RGBA{R: uint8(x * 30), G: uint8(y * 30), B: 40, A: 255}) } } diff --git a/desktop/tray_loop_windows_test.go b/desktop/tray_loop_windows_test.go index 8924ae9470..d7a0d2bfe5 100644 --- a/desktop/tray_loop_windows_test.go +++ b/desktop/tray_loop_windows_test.go @@ -13,7 +13,7 @@ func TestDesktopTrayLoopRunsOnLockedOSThread(t *testing.T) { done := make(chan struct{}) runDesktopTrayLoop(func() { first := windows.GetCurrentThreadId() - for i := 0; i < 100; i++ { + for range 100 { runtime.Gosched() } if got := windows.GetCurrentThreadId(); got != first { diff --git a/desktop/updater_deb_linux.go b/desktop/updater_deb_linux.go index 6bbd0de88e..166c28b543 100644 --- a/desktop/updater_deb_linux.go +++ b/desktop/updater_deb_linux.go @@ -145,7 +145,7 @@ func helperErrorMessage(stdout, stderr []byte) string { } // Strip protocol lines from stderr diagnostics. var kept []string - for _, line := range strings.Split(string(stderr), "\n") { + for line := range strings.SplitSeq(string(stderr), "\n") { if _, ok := parseHelperPhaseLine(line); ok { continue } diff --git a/desktop/updater_install_linux.go b/desktop/updater_install_linux.go index b5ab80e404..37d3334175 100644 --- a/desktop/updater_install_linux.go +++ b/desktop/updater_install_linux.go @@ -112,7 +112,7 @@ func isDpkgOwnedReasonix(absPath string) bool { return false } // dpkg-query may return multiple lines for diversions; require an exact package hit. - for _, raw := range strings.Split(line, "\n") { + for raw := range strings.SplitSeq(line, "\n") { raw = strings.TrimSpace(raw) pkg, path, ok := strings.Cut(raw, ":") if !ok { diff --git a/desktop/updater_mac.go b/desktop/updater_mac.go index 07038464c9..e5c2154bdb 100644 --- a/desktop/updater_mac.go +++ b/desktop/updater_mac.go @@ -611,7 +611,7 @@ func completeMacHandoffHandshake(cfg macUpdateHandoffConfig) error { } func retainMacHandoffNode(path, suffix string) (string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { retained := fmt.Sprintf( "%s.%s-%d-%d", path, @@ -636,7 +636,7 @@ func cleanupOwnedMacUpdateDirectory(path string, owner os.FileInfo) error { if strings.TrimSpace(path) == "" || owner == nil || !owner.IsDir() { return fmt.Errorf("macOS update cleanup identity is incomplete") } - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { cleanup := fmt.Sprintf("%s.reasonix-cleanup-%d-%d", path, time.Now().UTC().UnixNano(), attempt) err := unix.RenameatxNp(unix.AT_FDCWD, path, unix.AT_FDCWD, cleanup, unix.RENAME_EXCL) if err != nil { diff --git a/desktop/updater_test.go b/desktop/updater_test.go index ed277b5cd7..ba69c40801 100644 --- a/desktop/updater_test.go +++ b/desktop/updater_test.go @@ -93,7 +93,7 @@ func TestValidateAssetInstallLayout(t *testing.T) { } func TestUpdaterWailsMethodContracts(t *testing.T) { - appType := reflect.TypeOf((*App)(nil)) + appType := reflect.TypeFor[*App]() tests := []struct { name string numIn int @@ -1256,9 +1256,9 @@ func fastRetry(t *testing.T) { func TestDownloadRecoversFromMidStreamReset(t *testing.T) { fastRetry(t) const body = "complete-installer-bytes" - var calls int32 + var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - if atomic.AddInt32(&calls, 1) < int32(downloadAttempts) { + if calls.Add(1) < int32(downloadAttempts) { // Mid-stream reset: promise 100 bytes, send a few, drop the socket — // the client's body read fails with unexpected EOF, exactly the CN-IPv6 // "forcibly closed" case the retry exists for. @@ -1283,16 +1283,16 @@ func TestDownloadRecoversFromMidStreamReset(t *testing.T) { if string(data) != body { t.Fatalf("got %q, want %q", data, body) } - if n := atomic.LoadInt32(&calls); n != int32(downloadAttempts) { + if n := calls.Load(); n != int32(downloadAttempts) { t.Fatalf("made %d attempts, want %d", n, downloadAttempts) } } func TestDownloadGivesUpAfterCap(t *testing.T) { fastRetry(t) - var calls int32 + var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - atomic.AddInt32(&calls, 1) + calls.Add(1) conn, _, err := w.(http.Hijacker).Hijack() if err != nil { t.Errorf("hijack: %v", err) @@ -1305,7 +1305,7 @@ func TestDownloadGivesUpAfterCap(t *testing.T) { if _, err := download(context.Background(), srv.Client(), nil, srv.URL, 0, nil); err == nil { t.Fatal("download should fail after exhausting retries") } - if n := atomic.LoadInt32(&calls); n != int32(downloadAttempts) { + if n := calls.Load(); n != int32(downloadAttempts) { t.Fatalf("made %d attempts, want %d", n, downloadAttempts) } } @@ -1329,10 +1329,10 @@ func TestDownloadResumesWithRange(t *testing.T) { fastRetry(t) full := bytes.Repeat([]byte("0123456789"), 50) // 500 bytes const cut = 200 - var calls int32 + var calls atomic.Int32 rangeCh := make(chan string, 4) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if atomic.AddInt32(&calls, 1) == 1 { + if calls.Add(1) == 1 { // First attempt: promise the whole file, send a prefix, drop the socket. conn, bw, err := w.(http.Hijacker).Hijack() if err != nil { @@ -1379,9 +1379,9 @@ func TestDownloadFallsBackToSecondClient(t *testing.T) { primary := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("connection reset (ipv6)") })} - var fbCalls int32 + var fbCalls atomic.Int32 fallback := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { - atomic.AddInt32(&fbCalls, 1) + fbCalls.Add(1) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), @@ -1397,7 +1397,7 @@ func TestDownloadFallsBackToSecondClient(t *testing.T) { if string(data) != body { t.Fatalf("got %q, want %q", data, body) } - if atomic.LoadInt32(&fbCalls) == 0 { + if fbCalls.Load() == 0 { t.Fatal("fallback client was never used after the primary failed") } } @@ -1480,9 +1480,9 @@ func TestFetchBytesFallbackEscapesStalledPrimary(t *testing.T) { func TestFetchBytesDoesNotRetryPermanentHTTPStatus(t *testing.T) { fastRetry(t) - var calls int32 + var calls atomic.Int32 client := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { - atomic.AddInt32(&calls, 1) + calls.Add(1) return &http.Response{ StatusCode: http.StatusForbidden, Status: "403 Forbidden", @@ -1494,7 +1494,7 @@ func TestFetchBytesDoesNotRetryPermanentHTTPStatus(t *testing.T) { if _, err := fetchBytes(context.Background(), client, "https://example.invalid/latest.json"); err == nil { t.Fatal("fetchBytes should return a permanent HTTP error") } - if got := atomic.LoadInt32(&calls); got != 1 { + if got := calls.Load(); got != 1 { t.Fatalf("permanent HTTP error made %d requests, want 1", got) } } @@ -1502,9 +1502,9 @@ func TestFetchBytesDoesNotRetryPermanentHTTPStatus(t *testing.T) { func TestFetchBytesRejectsOversizeResponsesWithoutRetry(t *testing.T) { fastRetry(t) t.Run("declared content length", func(t *testing.T) { - var calls int32 + var calls atomic.Int32 client := &http.Client{Transport: rtFunc(func(*http.Request) (*http.Response, error) { - atomic.AddInt32(&calls, 1) + calls.Add(1) return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", @@ -1523,7 +1523,7 @@ func TestFetchBytesRejectsOversizeResponsesWithoutRetry(t *testing.T) { ); !errors.Is(err, errUpdateResponseTooLarge) { t.Fatalf("declared oversize error = %v, want errUpdateResponseTooLarge", err) } - if got := atomic.LoadInt32(&calls); got != 1 { + if got := calls.Load(); got != 1 { t.Fatalf("declared oversize response made %d requests, want 1", got) } }) diff --git a/desktop/window_state_test.go b/desktop/window_state_test.go index e8b7a91b93..30996329c1 100644 --- a/desktop/window_state_test.go +++ b/desktop/window_state_test.go @@ -187,7 +187,7 @@ func TestSaveWindowStateConcurrentReports(t *testing.T) { app := NewApp() var wg sync.WaitGroup - for i := 0; i < 32; i++ { + for i := range 32 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/desktop/workspace_changes.go b/desktop/workspace_changes.go index 66de5a93b0..d5ed95a7de 100644 --- a/desktop/workspace_changes.go +++ b/desktop/workspace_changes.go @@ -351,7 +351,7 @@ func workspaceCurrentText(path string) (string, bool, bool, error) { func tallyUnifiedPatch(patch string) (added, removed int) { inHunk := false - for _, line := range strings.Split(patch, "\n") { + for line := range strings.SplitSeq(patch, "\n") { switch { case strings.HasPrefix(line, "@@"): inHunk = true diff --git a/desktop/workspace_test.go b/desktop/workspace_test.go index a9f99326da..4940d8fc01 100644 --- a/desktop/workspace_test.go +++ b/desktop/workspace_test.go @@ -568,7 +568,7 @@ func BenchmarkDesktopSessionDir(b *testing.B) { b.Fatal(err) } b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { if desktopSessionDir(root) == "" { b.Fatal("empty session dir") } @@ -950,7 +950,7 @@ func TestMediaTokenMaxEviction(t *testing.T) { // Fill beyond max to trigger eviction of oldest. var oldestToken string - for i := 0; i < mediaTokenMax+1; i++ { + for i := range mediaTokenMax + 1 { tok := store.create(dir+"/test.png", "test.png", "image/png", "image", 4, time.Time{}) if i == 0 { oldestToken = tok diff --git a/internal/acp/protocol_test.go b/internal/acp/protocol_test.go index 9cb96bbbdf..87c59b204b 100644 --- a/internal/acp/protocol_test.go +++ b/internal/acp/protocol_test.go @@ -116,7 +116,7 @@ func TestNewSessionID(t *testing.T) { func TestNewSessionIDUnique(t *testing.T) { seen := map[string]bool{} - for i := 0; i < 100; i++ { + for range 100 { id, err := newSessionID() if err != nil { t.Fatalf("newSessionID: %v", err) diff --git a/internal/acp/server.go b/internal/acp/server.go index 07dda83615..07900ba171 100644 --- a/internal/acp/server.go +++ b/internal/acp/server.go @@ -208,18 +208,14 @@ func (c *Conn) dispatch(ctx context.Context, line []byte) { hasID := len(in.ID) > 0 switch { case in.Method != "" && hasID: - c.wg.Add(1) - go func() { - defer c.wg.Done() + c.wg.Go(func() { c.serveRequest(ctx, in.ID, in.Method, in.Params) - }() + }) case in.Method != "" && !hasID: if h := c.notH[in.Method]; h != nil { - c.wg.Add(1) - go func() { - defer c.wg.Done() + c.wg.Go(func() { h(ctx, in.Params) - }() + }) } case in.Method == "" && hasID: c.resolve(in) diff --git a/internal/acp/service.go b/internal/acp/service.go index 53681d2199..098662fce5 100644 --- a/internal/acp/service.go +++ b/internal/acp/service.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "log/slog" + "maps" "os" "path/filepath" "sort" @@ -2955,9 +2956,7 @@ func mapString(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } diff --git a/internal/acp/service_lock_test.go b/internal/acp/service_lock_test.go index 9f09894914..c537bf956d 100644 --- a/internal/acp/service_lock_test.go +++ b/internal/acp/service_lock_test.go @@ -411,7 +411,7 @@ func TestACPCtrlReadPathsDoNotRaceWithRebuild(t *testing.T) { done := make(chan struct{}) go func() { defer close(done) - for i := 0; i < rebuilds; i++ { + for i := range rebuilds { if err := svc.rebuildSession(context.Background(), sess, SessionConfigState{Model: models[i%len(models)]}, []sessionConfigDelta{{axis: "model", model: models[i%len(models)]}}); err != nil { t.Errorf("rebuildSession %d: %v", i, err) return diff --git a/internal/acp/status.go b/internal/acp/status.go index 4745277624..14644765d0 100644 --- a/internal/acp/status.go +++ b/internal/acp/status.go @@ -6,6 +6,7 @@ import ( "errors" "math" "runtime" + "slices" "strings" "sync" @@ -710,9 +711,9 @@ func finalAssistantSummary(ctrl acpController) string { return "" } history := ctrl.History() - for i := len(history) - 1; i >= 0; i-- { - if history[i].Role == provider.RoleAssistant && strings.TrimSpace(history[i].Content) != "" { - return history[i].Content + for _, v := range slices.Backward(history) { + if v.Role == provider.RoleAssistant && strings.TrimSpace(v.Content) != "" { + return v.Content } } return "" diff --git a/internal/agent/agent.go b/internal/agent/agent.go index bf55fda04a..44a2f7b425 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1187,10 +1187,7 @@ func New(prov provider.Provider, tools *tool.Registry, session *Session, opts Op } else { maxSubagentDepth = NormalizeMaxSubagentDepth(maxSubagentDepth) } - subagentDepth := opts.SubagentDepth - if subagentDepth < 0 { - subagentDepth = 0 - } + subagentDepth := max(opts.SubagentDepth, 0) reasoningByteLimit := opts.ReasoningByteLimit if reasoningByteLimit == 0 { reasoningByteLimit = defaultReasoningByteLimit @@ -3297,7 +3294,7 @@ launch: <-sem break } - i := i + wg.Add(1) ranUntil = i + 1 go func() { @@ -4016,8 +4013,8 @@ func (a *Agent) toolReadOnly(name string) bool { // firstLine returns s up to its first newline — a one-line failure summary for // the display Err, while the full error stays in the model-facing output. func firstLine(s string) string { - if i := strings.IndexByte(s, '\n'); i >= 0 { - return s[:i] + if before, _, ok := strings.Cut(s, "\n"); ok { + return before } return s } diff --git a/internal/agent/branch.go b/internal/agent/branch.go index 09a5d59090..7c4a124abc 100644 --- a/internal/agent/branch.go +++ b/internal/agent/branch.go @@ -302,10 +302,7 @@ func MarkSessionInFlightTurn(sessionPath string, startMessageIndex int, preserve // what lets crash recovery relocate the turn after an in-turn compaction has // rewritten its original message index. func SetSessionInFlightTurn(sessionPath string, marker InFlightTurnMeta) error { - startMessageIndex := marker.StartMessageIndex - if startMessageIndex < 0 { - startMessageIndex = 0 - } + startMessageIndex := max(marker.StartMessageIndex, 0) // The sidecar is read-modify-write; the per-path save lock keeps concurrent // writers (autosave's UpdateSessionMeta, listing backfill) from dropping // each other's fields. diff --git a/internal/agent/cache_shape.go b/internal/agent/cache_shape.go index 3b86acb2c4..59d6e100b0 100644 --- a/internal/agent/cache_shape.go +++ b/internal/agent/cache_shape.go @@ -26,7 +26,7 @@ type PrefixShape struct { // every call site, while still assigning to event.Event.CacheDiagnostics. type CacheDiagnostics = event.CacheDiagnostics -func shortHash(v interface{}) string { +func shortHash(v any) string { b, _ := json.Marshal(v) h := sha256.Sum256(b) return fmt.Sprintf("%x", h[:8]) @@ -39,7 +39,7 @@ func CaptureShape(systemPrompt string, schemas []provider.ToolSchema, rewriteVer return PrefixShape{ SystemHash: shortHash(systemPrompt), ToolsHash: shortHash(string(toolsJSON)), - PrefixHash: shortHash(map[string]interface{}{ + PrefixHash: shortHash(map[string]any{ "system": systemPrompt, "tools": string(toolsJSON), }), diff --git a/internal/agent/cachehit_e2e_test.go b/internal/agent/cachehit_e2e_test.go index 013d60854b..0680db9917 100644 --- a/internal/agent/cachehit_e2e_test.go +++ b/internal/agent/cachehit_e2e_test.go @@ -199,7 +199,7 @@ func TestCacheHitClimbsWithoutCompaction(t *testing.T) { a, sink := newAgent(t, srv.URL, mock.tools(), 0 /*no compaction*/, 0) const turns = 14 - for i := 0; i < turns; i++ { + for i := range turns { userMsg := "Turn " + fmt.Sprint(i) + ": " + strings.Repeat("please consider this requirement. ", 6) if err := a.Run(context.Background(), userMsg); err != nil { t.Fatalf("Run %d: %v", i, err) @@ -286,7 +286,7 @@ func TestReasoningRoundTripCost(t *testing.T) { defer srv.Close() a, sink := newAgent(t, srv.URL, mock.tools(), 0, 0) const turns = 12 - for i := 0; i < turns; i++ { + for i := range turns { if err := a.Run(context.Background(), strings.Repeat("please consider this requirement. ", 6)); err != nil { t.Fatalf("Run %d: %v", i, err) } @@ -329,7 +329,7 @@ func TestSessionAggregateCacheRate(t *testing.T) { a, sink := newAgent(t, srv.URL, mock.tools(), 0, 0) const turns = 8 - for i := 0; i < turns; i++ { + for i := range turns { if err := a.Run(context.Background(), strings.Repeat("please consider this requirement. ", 6)); err != nil { t.Fatalf("Run %d: %v", i, err) } @@ -409,7 +409,7 @@ func TestReleaseCacheHitGuard(t *testing.T) { name: "mixed-message-sizes", run: func(t *testing.T) []int { msgs := make([]string, 0, 20) - for i := 0; i < 20; i++ { + for i := range 20 { repeats := 4 if i%3 == 2 { repeats = 20 @@ -482,7 +482,7 @@ func cacheCurve(t *testing.T, mock *mockDeepSeek, turns int) []int { func repeatedMessages(turns, repeats int) []string { msgs := make([]string, 0, turns) - for i := 0; i < turns; i++ { + for i := range turns { msgs = append(msgs, "Turn "+fmt.Sprint(i)+": "+strings.Repeat("please consider this requirement. ", repeats)) } return msgs diff --git a/internal/agent/cancel_test.go b/internal/agent/cancel_test.go index 94930f378d..b56fd0fd7a 100644 --- a/internal/agent/cancel_test.go +++ b/internal/agent/cancel_test.go @@ -130,7 +130,7 @@ func (closedStreamProvider) Stream(context.Context, provider.Request) (<-chan pr } func TestCanceledContextClosedProviderStreamReturnsCancel(t *testing.T) { - for i := 0; i < 100; i++ { + for i := range 100 { ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -574,7 +574,7 @@ func TestCancelInsideLargeParallelBatchStopsSchedulingNewTools(t *testing.T) { reg.Add(trackingTool{name: "readonly_tracking", readOnly: true}) var calls []provider.ToolCall - for i := 0; i < 12; i++ { + for i := range 12 { calls = append(calls, provider.ToolCall{ ID: fmt.Sprintf("call-%02d", i), Name: "readonly_tracking", diff --git a/internal/agent/compact_loop_e2e_test.go b/internal/agent/compact_loop_e2e_test.go index 691da1f14e..e57dac5afb 100644 --- a/internal/agent/compact_loop_e2e_test.go +++ b/internal/agent/compact_loop_e2e_test.go @@ -109,7 +109,7 @@ func compactionsPerTurn(t *testing.T, windowTok int, blob, finalText string, tur }) perTurn = make([]int, turns) - for i := 0; i < turns; i++ { + for i := range turns { before := started if err := a.Run(context.Background(), fmt.Sprintf("turn %d: keep going, continue the work", i)); err != nil { t.Fatalf("Run %d: %v", i, err) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 1f8882aef1..f22520dd73 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "time" @@ -521,17 +522,17 @@ func plannerPlanRequestsApproval(plan string) bool { } // Match per line so a nearby negation ("无需等待用户批准", "no need to wait // for approval") exempts only its own phrase, not the whole plan. - for _, rawLine := range strings.Split(lower, "\n") { + for rawLine := range strings.SplitSeq(lower, "\n") { line := strings.TrimSpace(rawLine) if line == "" { continue } for _, phrase := range plannerApprovalPhrases { - idx := strings.Index(line, phrase) - if idx < 0 { + before, _, ok := strings.Cut(line, phrase) + if !ok { continue } - if approvalMentionNegated(line[:idx]) { + if approvalMentionNegated(before) { continue } return true @@ -633,7 +634,7 @@ func parsePlannerAskBlock(plan string) (event.AskQuestion, bool) { block := plan[start+len(plannerAskStartMarker) : end] var question string var options []event.AskOption - for _, raw := range strings.Split(block, "\n") { + for raw := range strings.SplitSeq(block, "\n") { line := strings.TrimSpace(raw) if line == "" { continue @@ -671,8 +672,8 @@ func parsePlannerAskBlock(plan string) (event.AskQuestion, bool) { func plannerQuestionPrompt(plan string) string { lines := strings.Split(plan, "\n") - for i := len(lines) - 1; i >= 0; i-- { - line := strings.TrimSpace(strings.Trim(lines[i], "-* \t")) + for _, v := range slices.Backward(lines) { + line := strings.TrimSpace(strings.Trim(v, "-* \t")) if line == "" { continue } @@ -780,8 +781,8 @@ func isNoOpPlan(plan string) bool { func lastNonEmptyLine(s string) string { lines := strings.Split(s, "\n") - for i := len(lines) - 1; i >= 0; i-- { - if t := strings.TrimSpace(lines[i]); t != "" { + for _, v := range slices.Backward(lines) { + if t := strings.TrimSpace(v); t != "" { return t } } @@ -1076,11 +1077,11 @@ func HandoffTask(s string) string { return s } const header = "Original task:\n" - i := strings.Index(trimmed, header) - if i < 0 { + _, after, ok := strings.Cut(trimmed, header) + if !ok { return s } - rest := trimmed[i+len(header):] + rest := after if j := strings.Index(rest, "\n\nPlanner output:"); j >= 0 { rest = rest[:j] } diff --git a/internal/agent/coordinator_test.go b/internal/agent/coordinator_test.go index 572420680f..1741a1b0ef 100644 --- a/internal/agent/coordinator_test.go +++ b/internal/agent/coordinator_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "reasonix/internal/event" + "slices" "strings" "testing" @@ -43,9 +44,9 @@ func (m *mockProvider) Stream(ctx context.Context, req provider.Request) (<-chan } func lastUser(req provider.Request) string { - for i := len(req.Messages) - 1; i >= 0; i-- { - if req.Messages[i].Role == provider.RoleUser { - return req.Messages[i].Content + for _, v := range slices.Backward(req.Messages) { + if v.Role == provider.RoleUser { + return v.Content } } return "" @@ -1274,17 +1275,12 @@ func toolSchemaNames(schemas []provider.ToolSchema) []string { } func contains(items []string, want string) bool { - for _, item := range items { - if item == want { - return true - } - } - return false + return slices.Contains(items, want) } func BenchmarkPlannerToolRegistry(b *testing.B) { parentReg := tool.NewRegistry() - for i := 0; i < 200; i++ { + for i := range 200 { parentReg.Add(coordinatorTestTool{ name: fmt.Sprintf("tool_%03d", i), readOnly: i%3 != 0, @@ -1294,7 +1290,7 @@ func BenchmarkPlannerToolRegistry(b *testing.B) { parentReg.Add(coordinatorTestTool{name: "write_file", readOnly: false}) b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { reg := PlannerToolRegistry(parentReg) if reg.Len() == 0 { b.Fatal("planner registry should retain read-only research tools") @@ -1727,7 +1723,7 @@ func TestCoordinatorHandoffSurvivesPlannerCompaction(t *testing.T) { // below) its pre-turn length, which is what strands a boundary based on // the pre-turn message count. filler := strings.Repeat("planner history filler. ", 150) - for i := 0; i < 3; i++ { + for range 3 { plannerSess.Add(provider.Message{Role: provider.RoleUser, Content: filler}) plannerSess.Add(provider.Message{Role: provider.RoleAssistant, Content: filler}) } @@ -1873,7 +1869,7 @@ func TestCoordinatorFailedTurnRollbackKeepsCompaction(t *testing.T) { executor := New(exec, tool.NewRegistry(), NewSession("exec-sys"), Options{}, event.Discard) plannerSess := NewSession("planner-sys") filler := strings.Repeat("planner history filler. ", 150) - for i := 0; i < 3; i++ { + for range 3 { plannerSess.Add(provider.Message{Role: provider.RoleUser, Content: filler}) plannerSess.Add(provider.Message{Role: provider.RoleAssistant, Content: filler}) } diff --git a/internal/agent/empty_final_test.go b/internal/agent/empty_final_test.go index a969fc5b9b..9b43876ad6 100644 --- a/internal/agent/empty_final_test.go +++ b/internal/agent/empty_final_test.go @@ -180,7 +180,7 @@ func BenchmarkHasVisibleFinalAnswer(b *testing.B) { b.Run(tc.name, func(b *testing.B) { b.ReportAllocs() var got bool - for i := 0; i < b.N; i++ { + for range b.N { got = hasVisibleFinalAnswer(tc.text) } _ = got diff --git a/internal/agent/fleet.go b/internal/agent/fleet.go index 0fdf8c5b71..4b5a30210c 100644 --- a/internal/agent/fleet.go +++ b/internal/agent/fleet.go @@ -323,9 +323,7 @@ func (f *FleetTool) runFleet(ctx context.Context, sink event.Sink, specs []Profi }, }) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { // Each fleet item runs as its own task-shaped execution so // transcripts, evidence, and scheduler claims stay independent. itemCtx := withCallContext(ctx, subID, subSinkFor(subID, sink), nil, false) @@ -350,7 +348,7 @@ func (f *FleetTool) runFleet(ctx context.Context, sink event.Sink, specs []Profi }) } doneCh <- res - }() + }) } // Mark only genuinely unstarted items skipped. Started items always publish diff --git a/internal/agent/fleet_test.go b/internal/agent/fleet_test.go index 25e16349a5..d9320e7c54 100644 --- a/internal/agent/fleet_test.go +++ b/internal/agent/fleet_test.go @@ -356,7 +356,7 @@ func TestFleetParallelDisjointWriters(t *testing.T) { f := NewFleetTool(task) tasks := make([]map[string]any, 0, 4) - for i := 0; i < 4; i++ { + for i := range 4 { path := filepath.Join("docs", "f"+string(rune('0'+i))+".md") tasks = append(tasks, map[string]any{ "prompt": "handle " + path, diff --git a/internal/agent/goal_display.go b/internal/agent/goal_display.go index c8afd55d0f..4705c21b61 100644 --- a/internal/agent/goal_display.go +++ b/internal/agent/goal_display.go @@ -56,11 +56,11 @@ func StripAutoResearchEvidenceBlocks(text string) string { } b.WriteString(rest[:start]) afterOpen := rest[start+len(autoResearchEvidenceOpen):] - end := strings.Index(afterOpen, autoResearchEvidenceClose) - if end < 0 { + _, after, ok := strings.Cut(afterOpen, autoResearchEvidenceClose) + if !ok { return strings.TrimSpace(b.String()) } - rest = afterOpen[end+len(autoResearchEvidenceClose):] + rest = after } } diff --git a/internal/agent/interrupted_recovery.go b/internal/agent/interrupted_recovery.go index c0f1db1d05..4410075e4d 100644 --- a/internal/agent/interrupted_recovery.go +++ b/internal/agent/interrupted_recovery.go @@ -3,6 +3,7 @@ package agent import ( "fmt" "html" + "slices" "strings" "reasonix/internal/provider" @@ -24,8 +25,8 @@ func (a *Agent) pendingInterruptedRecovery() *provider.InterruptedTurnRecovery { return nil } msgs := a.session.Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - m := msgs[i] + for _, v := range slices.Backward(msgs) { + m := v if m.LocalOnly && m.InterruptedTurn != nil && m.InterruptedTurn.Pending { copy := *m.InterruptedTurn copy.CompletedTools = append([]provider.InterruptedToolSummary(nil), copy.CompletedTools...) diff --git a/internal/agent/loop_e2e_test.go b/internal/agent/loop_e2e_test.go index be01839ec0..20dcb10de1 100644 --- a/internal/agent/loop_e2e_test.go +++ b/internal/agent/loop_e2e_test.go @@ -417,7 +417,7 @@ func TestRunStreamRetryRequestCountIsLinearNotTriangular(t *testing.T) { func TestRunExhaustedStreamRetriesPersistPendingLocalOnly(t *testing.T) { interrupted := &provider.StreamInterruptedError{Err: errors.New("eof"), Reason: provider.StreamInterruptPrematureEOF} turns := make([]testutil.Turn, 0, maxSamplingAttempts) - for i := 0; i < maxSamplingAttempts; i++ { + for range maxSamplingAttempts { turns = append(turns, testutil.Turn{Text: "half", ChunkError: interrupted}) } mp := testutil.NewMock("m", turns...) @@ -1026,7 +1026,7 @@ func TestHealthyToolCallReasoningRetriesTransientStateWriteFailure(t *testing.T) t.Fatal(err) } permissionsRestored = true - for healthy := 0; healthy < missingReasoningHealthyResolveStreak-1; healthy++ { + for healthy := range missingReasoningHealthyResolveStreak - 1 { if missing, retry := a.observeMissingToolCallReasoning(calls, "healthy reasoning"); missing || retry { t.Fatalf("healthy recovery observation %d = missing:%v retry:%v, want false/false", healthy+1, missing, retry) } diff --git a/internal/agent/parallel_tasks.go b/internal/agent/parallel_tasks.go index fb7ab516fa..809ce9b347 100644 --- a/internal/agent/parallel_tasks.go +++ b/internal/agent/parallel_tasks.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "sync" @@ -177,9 +178,7 @@ func (p *ParallelTasksTool) Execute(ctx context.Context, args json.RawMessage) ( }, }) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { modelRef, effortRef := p.taskTool.effectiveProfile(t.Model, t.Effort) itemCtx := withCallContext(ctx, subID, subSinkFor(subID, sink), nil, PlanModeFromContext(ctx)) // Route through TaskTool's unified runner so persisted parent sessions @@ -221,7 +220,7 @@ func (p *ParallelTasksTool) Execute(ctx context.Context, args json.RawMessage) ( }) answer, ref := splitSubagentRunResult(output) doneCh <- subResult{index: idx, output: answer, ref: ref} - }() + }) } markCancelled := func(err error) { @@ -300,10 +299,8 @@ func parallelGroupTerminalPhase(ctx context.Context, err error, statuses []paral if ctx.Err() != nil { return subagentPhaseCancelled } - for _, st := range statuses { - if st == parallelTaskFailed { - return subagentPhaseFailed - } + if slices.Contains(statuses, parallelTaskFailed) { + return subagentPhaseFailed } if err != nil { return subagentPhaseFailed diff --git a/internal/agent/parallel_tasks_test.go b/internal/agent/parallel_tasks_test.go index a30f73e699..f849ad1bc5 100644 --- a/internal/agent/parallel_tasks_test.go +++ b/internal/agent/parallel_tasks_test.go @@ -377,9 +377,9 @@ func (parallelLongResultProvider) Stream(_ context.Context, req provider.Request func subagentRefsFromText(text string) []string { var refs []string - for _, line := range strings.Split(text, "\n") { - if strings.HasPrefix(line, "Subagent reference: ") { - refs = append(refs, strings.TrimSpace(strings.TrimPrefix(line, "Subagent reference: "))) + for line := range strings.SplitSeq(text, "\n") { + if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok { + refs = append(refs, strings.TrimSpace(after)) } } return refs diff --git a/internal/agent/prune_test.go b/internal/agent/prune_test.go index fc5cbff235..c15246317c 100644 --- a/internal/agent/prune_test.go +++ b/internal/agent/prune_test.go @@ -98,7 +98,7 @@ func TestPruneNeverRewritesLocalInterruptedDisplay(t *testing.T) { func TestSnipStaleToolResults(t *testing.T) { var lines []string - for i := 0; i < 1000; i++ { + for range 1000 { lines = append(lines, "line") } big := strings.Join(lines, "\n") @@ -376,7 +376,7 @@ func TestSnipUsesRegisteredToolHint(t *testing.T) { // 600 numbered lines; a SnipHinter keeping head=3, tail=2 must yield exactly // those boundary lines, which no default geometry would produce. var lines []string - for i := 0; i < 600; i++ { + for i := range 600 { lines = append(lines, fmt.Sprintf("L%d", i)) } content := strings.Join(lines, "\n") @@ -403,7 +403,7 @@ func TestSnipUsesRegisteredToolHint(t *testing.T) { func TestSnipFallsBackByReadOnlyTier(t *testing.T) { var lines []string - for i := 0; i < 600; i++ { + for i := range 600 { lines = append(lines, fmt.Sprintf("L%d", i)) } content := strings.Join(lines, "\n") diff --git a/internal/agent/reasoning_language.go b/internal/agent/reasoning_language.go index ce7f7b8e81..6187340098 100644 --- a/internal/agent/reasoning_language.go +++ b/internal/agent/reasoning_language.go @@ -242,11 +242,11 @@ func hasOpenTag(s, tag string) bool { func trimLeadingTransientBlock(content, tag string) (string, bool) { closeTag := "" - i := strings.Index(content, closeTag) - if i < 0 { + _, after, ok := strings.Cut(content, closeTag) + if !ok { return content, false } - return strings.TrimLeft(content[i+len(closeTag):], " \t\r\n"), true + return strings.TrimLeft(after, " \t\r\n"), true } // WithResponseLanguagePreference carries the runtime final-answer language diff --git a/internal/agent/reasoning_warn_state.go b/internal/agent/reasoning_warn_state.go index 0fd1f06b0b..003a76fdf4 100644 --- a/internal/agent/reasoning_warn_state.go +++ b/internal/agent/reasoning_warn_state.go @@ -214,13 +214,7 @@ func normalizeMissingReasoningIncident(incident missingReasoningIncident, now ti } func (incident missingReasoningIncident) lastEventUnixNano() int64 { - lastEvent := incident.LastMissingUnixNano - if incident.LastResolvedAtUnixNano > lastEvent { - lastEvent = incident.LastResolvedAtUnixNano - } - if incident.LastHealthyAtUnixNano > lastEvent { - lastEvent = incident.LastHealthyAtUnixNano - } + lastEvent := max(incident.LastHealthyAtUnixNano, max(incident.LastResolvedAtUnixNano, incident.LastMissingUnixNano)) return lastEvent } diff --git a/internal/agent/reasoning_warn_state_test.go b/internal/agent/reasoning_warn_state_test.go index 5df319dd5b..56bf6bfb29 100644 --- a/internal/agent/reasoning_warn_state_test.go +++ b/internal/agent/reasoning_warn_state_test.go @@ -406,14 +406,12 @@ func TestMissingReasoningWarnStateConcurrentSameIncidentWarnsOnce(t *testing.T) var warned atomic.Int64 var wg sync.WaitGroup for range 8 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if newMissingReasoningWarnState(dir).claimAt(fingerprint, now) { warned.Add(1) } - }() + }) } close(start) wg.Wait() @@ -490,14 +488,12 @@ func TestMissingReasoningWarnStateConcurrentClaimsKeepEveryConfiguration(t *test var wg sync.WaitGroup for _, label := range labels { fingerprint := warningFingerprint(label) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if !newMissingReasoningWarnState(dir).claimAt(fingerprint, now) { t.Errorf("fresh configuration %q did not claim its notice", label) } - }() + }) } close(start) wg.Wait() diff --git a/internal/agent/recovery_gc.go b/internal/agent/recovery_gc.go index 229f242cd0..a5b7a8e12e 100644 --- a/internal/agent/recovery_gc.go +++ b/internal/agent/recovery_gc.go @@ -457,7 +457,7 @@ func publishRecoveryTrashStage(dir, key, stageDir string) (string, error) { root := filepath.Join(dir, recoveryTrashDir) stem := strings.TrimSuffix(key, filepath.Ext(key)) stamp := time.Now().UTC().UnixMilli() - for i := 0; i < 1000; i++ { + for i := range 1000 { name := key if i > 0 { name = fmt.Sprintf("%s-recovery-%d-%d", stem, stamp, i) @@ -523,7 +523,7 @@ func reserveRecoveryTrashItemDir(dir, key string) (string, string, error) { return "", "", err } stem := strings.TrimSuffix(key, filepath.Ext(key)) - for i := 0; i < 1000; i++ { + for i := range 1000 { name := key if i > 0 { name = fmt.Sprintf("%s-recovery-%d-%d", stem, time.Now().UTC().UnixMilli(), i) diff --git a/internal/agent/repeat_guard_test.go b/internal/agent/repeat_guard_test.go index 9e1c41323e..f8fc7a682b 100644 --- a/internal/agent/repeat_guard_test.go +++ b/internal/agent/repeat_guard_test.go @@ -303,7 +303,7 @@ func TestRepeatGuardRetainsStaleEditFailuresAcrossGoalScope(t *testing.T) { TaskText: "fix prompt.txt", }) - for i := 0; i < 3; i++ { + for i := range 3 { if err := a.Run(ctx, "continue goal"); err != nil { t.Fatalf("Run %d: %v", i+1, err) } @@ -335,7 +335,7 @@ func TestRepeatGuardClearsOrdinaryWriteFailureAcrossGoalRuns(t *testing.T) { TaskText: "fix prompt.txt", }) - for i := 0; i < 3; i++ { + for i := range 3 { if err := a.Run(ctx, "continue goal"); err != nil { t.Fatalf("Run %d: %v", i+1, err) } diff --git a/internal/agent/run_loop.go b/internal/agent/run_loop.go index 790ce9fc16..351c2ad048 100644 --- a/internal/agent/run_loop.go +++ b/internal/agent/run_loop.go @@ -435,10 +435,7 @@ func (a *Agent) streamWithSamplingRecovery(ctx context.Context, turn int) stream before := provider.RequestAttemptCount(ctx) result := a.streamWithFrozen(ctx, turn, sink, &frozen, attemptID) after := provider.RequestAttemptCount(ctx) - delta := after - before - if delta < 0 { - delta = 0 - } + delta := max(after-before, 0) // httpRequests=0 means the provider does not use SendWithRetry // (extension/custom), or it failed before issuing an HTTP request. // Only overwrite RequestCount when the built-in counter observed POSTs; @@ -581,13 +578,7 @@ var streamRetrySleep = sleepStreamRetryBackoff // Returns false when ctx is cancelled during the wait. func sleepStreamRetryBackoff(ctx context.Context, attempt int) bool { // attempt is 1-based for the failed attempt about to be retried. - shift := attempt - 1 - if shift < 0 { - shift = 0 - } - if shift > 4 { - shift = 4 - } + shift := min(max(attempt-1, 0), 4) base := time.Duration(1< SessionRecoveryMaxDepth { + recoveryDepth := min(parentDepth+1, // A shutdown copy is allowed even when the ordinary conflict chain is // capped because losing the only in-memory transcript is worse than one // additional branch. Keep the saturated depth so later ordinary saves // still enforce the existing anti-cascade policy. - recoveryDepth = SessionRecoveryMaxDepth - } + SessionRecoveryMaxDepth) recoveryPath := recoverySessionPath(originalPath, digest) if shutdown { @@ -808,8 +807,8 @@ func recoveryParentStem(parent string) string { return "session" } sum := sha256.Sum256([]byte(parent)) - if idx := strings.Index(parent, "-recovery-"); idx >= 0 { - base := strings.Trim(parent[:idx], "-_. ") + if before, _, ok := strings.Cut(parent, "-recovery-"); ok { + base := strings.Trim(before, "-_. ") if base == "" { base = "session" } @@ -1265,8 +1264,8 @@ func resolvePathThroughExistingAncestor(path string) string { missing := make([]string, 0, 4) for { if resolved, err := filepath.EvalSymlinks(current); err == nil { - for i := len(missing) - 1; i >= 0; i-- { - resolved = filepath.Join(resolved, missing[i]) + for _, v := range slices.Backward(missing) { + resolved = filepath.Join(resolved, v) } return resolved } diff --git a/internal/agent/save_test.go b/internal/agent/save_test.go index 8fe8e596e4..8eb0af1701 100644 --- a/internal/agent/save_test.go +++ b/internal/agent/save_test.go @@ -2117,7 +2117,7 @@ func TestSaveSnapshotSameRevisionAllowsOwnedNonPrefixAppend(t *testing.T) { // Simulate recovery: load the saved transcript into a new session, then // add more messages. Every subsequent SaveSnapshot must succeed without a // diverged conflict while the persisted digest still proves ownership. - for turn := 0; turn < 5; turn++ { + for turn := range 5 { s, err := LoadSession(path) if err != nil { t.Fatalf("LoadSession turn %d: %v", turn, err) diff --git a/internal/agent/scheduler_test.go b/internal/agent/scheduler_test.go index 9b9dbd7e10..01c4a03491 100644 --- a/internal/agent/scheduler_test.go +++ b/internal/agent/scheduler_test.go @@ -16,10 +16,8 @@ func TestSchedulerTotalConcurrencyQueues(t *testing.T) { var wg sync.WaitGroup barrier := make(chan struct{}) - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 4 { + wg.Go(func() { release, err := s.Acquire(context.Background(), AcquireRequest{Writer: false}) if err != nil { t.Errorf("acquire: %v", err) @@ -35,7 +33,7 @@ func TestSchedulerTotalConcurrencyQueues(t *testing.T) { <-barrier started.Add(-1) release() - }() + }) } // Wait until at least 2 are running, then release them. diff --git a/internal/agent/session.go b/internal/agent/session.go index 68fabfce5b..bfd74c7353 100644 --- a/internal/agent/session.go +++ b/internal/agent/session.go @@ -86,6 +86,7 @@ func (s *Session) AddDecisionReceipt(receipt *provider.DecisionReceipt) { } s.mu.Lock() defer s.mu.Unlock() + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(s.Messages) - 1; i >= 0; i-- { if s.Messages[i].Role == provider.RoleUser && !s.Messages[i].LocalOnly { break @@ -123,6 +124,7 @@ func (s *Session) UpdateToolCallPreview(call provider.ToolCall) bool { } s.mu.Lock() defer s.mu.Unlock() + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(s.Messages) - 1; i >= 0; i-- { if s.Messages[i].Role != provider.RoleAssistant { continue @@ -159,6 +161,7 @@ func (s *Session) UpdateToolCallResolution(call provider.ToolCall) bool { } s.mu.Lock() defer s.mu.Unlock() + //nolint:modernize // slices.Backward yields element copies; this body writes through the index. for i := len(s.Messages) - 1; i >= 0; i-- { if s.Messages[i].Role != provider.RoleAssistant { continue diff --git a/internal/agent/session_concurrency_test.go b/internal/agent/session_concurrency_test.go index a3d5aee7b3..0df89afc79 100644 --- a/internal/agent/session_concurrency_test.go +++ b/internal/agent/session_concurrency_test.go @@ -26,26 +26,22 @@ func TestSessionConcurrentAddAndRead(t *testing.T) { var wg sync.WaitGroup // One writer mimicking the turn goroutine. - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < appends; i++ { + wg.Go(func() { + for range appends { s.Add(provider.Message{Role: provider.RoleUser, Content: "msg"}) } - }() + }) // Many readers mimicking frontends polling history. - for r := 0; r < readers; r++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < reads; i++ { + for range readers { + wg.Go(func() { + for range reads { snap := s.Snapshot() for _, m := range snap { // iterate the copy: must never tear _ = m.Content } _ = s.HasContent() } - }() + }) } wg.Wait() diff --git a/internal/agent/session_events_test.go b/internal/agent/session_events_test.go index 018f751da5..a0a4206f93 100644 --- a/internal/agent/session_events_test.go +++ b/internal/agent/session_events_test.go @@ -16,7 +16,7 @@ import ( func sessionWithTurns(t *testing.T, path string, turns int) *Session { t.Helper() s := NewSession("sys") - for i := 0; i < turns; i++ { + for i := range turns { s.Add(provider.Message{Role: provider.RoleUser, Content: "prompt " + strings.Repeat("x", 8)}) s.Add(provider.Message{Role: provider.RoleAssistant, Content: "reply"}) if err := s.SaveSnapshot(path); err != nil { @@ -566,7 +566,7 @@ func TestEventLogCompactionBoundsGrowth(t *testing.T) { filler := strings.Repeat("y", 8<<10) // Repeated rewrites (each a full replace event) must not grow the log // without bound: once past the threshold the log folds to one event. - for i := 0; i < 60; i++ { + for i := range 60 { s.Replace([]provider.Message{ {Role: provider.RoleSystem, Content: "sys"}, {Role: provider.RoleUser, Content: filler}, @@ -642,7 +642,7 @@ func TestConcurrentLoadDuringAppendsStaysConsistent(t *testing.T) { } } }() - for i := 0; i < 40; i++ { + for i := range 40 { s.Add(provider.Message{Role: provider.RoleAssistant, Content: strings.Repeat("a", 512)}) if err := s.SaveSnapshot(path); err != nil { t.Fatalf("SaveSnapshot %d: %v", i, err) diff --git a/internal/agent/session_lease_test.go b/internal/agent/session_lease_test.go index ff97b3dee9..465c0b7047 100644 --- a/internal/agent/session_lease_test.go +++ b/internal/agent/session_lease_test.go @@ -179,14 +179,12 @@ func TestSessionLeaseConcurrentReclaimSingleWinner(t *testing.T) { leases := make(chan *SessionLease, attempts) start := make(chan struct{}) for range attempts { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if lease, err := TryReclaimCurrentProcessSessionLease(userPath); err == nil && lease != nil { leases <- lease } - }() + }) } close(start) wg.Wait() diff --git a/internal/agent/session_meta_ledger_test.go b/internal/agent/session_meta_ledger_test.go index 16936c54bf..c875f39bd3 100644 --- a/internal/agent/session_meta_ledger_test.go +++ b/internal/agent/session_meta_ledger_test.go @@ -314,9 +314,7 @@ func TestSessionMetaConcurrentWritersKeepRevisionMonotonic(t *testing.T) { metaErrCh := make(chan error, 2) var regressed atomic.Bool - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for i := 0; ; i++ { select { case <-stop: @@ -338,10 +336,8 @@ func TestSessionMetaConcurrentWritersKeepRevisionMonotonic(t *testing.T) { return } } - }() - wg.Add(1) - go func() { - defer wg.Done() + }) + wg.Go(func() { last := int64(0) for { select { @@ -357,7 +353,7 @@ func TestSessionMetaConcurrentWritersKeepRevisionMonotonic(t *testing.T) { last = meta.Revision } } - }() + }) for i := 1; i <= saves; i++ { s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn %d", i)}) @@ -445,10 +441,8 @@ func TestConcurrentSnapshotSaversNeverConflict(t *testing.T) { stop := make(chan struct{}) errCh := make(chan error, 64) var wg sync.WaitGroup - for i := 0; i < 2; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 2 { + wg.Go(func() { for { if err := s.SaveSnapshot(path); err != nil { select { @@ -462,7 +456,7 @@ func TestConcurrentSnapshotSaversNeverConflict(t *testing.T) { default: } } - }() + }) } for i := 1; i <= adds; i++ { s.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn %d", i)}) diff --git a/internal/agent/storm_test.go b/internal/agent/storm_test.go index 78557476a4..fb9caeae38 100644 --- a/internal/agent/storm_test.go +++ b/internal/agent/storm_test.go @@ -59,7 +59,7 @@ func TestStormBreakerEscalatesRepeatedFailure(t *testing.T) { args := []string{`{"content":"Mountains are`, `{"path":"n.txt","content":"Peaks rise`, `{}`} var last string - for i := 0; i < stormBreakThreshold; i++ { + for i := range stormBreakThreshold { call := provider.ToolCall{Name: "write_file", Arguments: args[i]} last = executeBatchOutputs(a, context.Background(), []provider.ToolCall{call})[0] } @@ -97,7 +97,7 @@ func TestStormBreakerEscalatesRepeatedBlockedPermission(t *testing.T) { `{"command":"ls -la"}`, } var last string - for i := 0; i < stormBreakThreshold; i++ { + for i := range stormBreakThreshold { call := provider.ToolCall{Name: "bash", Arguments: args[i]} last = executeBatchOutputs(a, context.Background(), []provider.ToolCall{call})[0] } @@ -191,7 +191,7 @@ func TestStormBreakerEscalatesRepeatedBatch(t *testing.T) { {Name: "write_b", Arguments: `{"content":"y`}, } var first string - for i := 0; i < stormBreakThreshold; i++ { + for range stormBreakThreshold { first = executeBatchOutputs(a, context.Background(), batch)[0] } @@ -221,7 +221,7 @@ func TestStormBreakerBatchResetsOnPartialSuccess(t *testing.T) { {Name: "read_file", Arguments: `{"path":"x"}`}, } var first string - for i := 0; i < stormBreakThreshold+2; i++ { + for range stormBreakThreshold + 2 { first = executeBatchOutputs(a, context.Background(), batch)[0] } @@ -243,7 +243,7 @@ func TestStormBreakerSilentBelowThreshold(t *testing.T) { call := provider.ToolCall{Name: "write_file", Arguments: `{"content":"x`} var last string - for i := 0; i < stormBreakThreshold-1; i++ { + for range stormBreakThreshold - 1 { last = executeBatchOutputs(a, context.Background(), []provider.ToolCall{call})[0] } diff --git a/internal/agent/subagent_progress.go b/internal/agent/subagent_progress.go index b0696c6d37..e550228e72 100644 --- a/internal/agent/subagent_progress.go +++ b/internal/agent/subagent_progress.go @@ -2,6 +2,7 @@ package agent import ( "context" + "slices" "sync" "time" "unicode/utf8" @@ -376,7 +377,7 @@ func (m *subagentProgressMerger) stepLocked() bool { return false } now := m.clock.Now() - for i := 0; i < n; i++ { + for i := range n { idx := (m.rr + i) % n childID := m.order[idx] if m.tokens < 1 { @@ -548,10 +549,8 @@ func (m *subagentProgressMerger) pendingBytesLocked(childID string) int { } func (m *subagentProgressMerger) ensureOrderLocked(childID string) { - for _, id := range m.order { - if id == childID { - return - } + if slices.Contains(m.order, childID) { + return } m.order = append(m.order, childID) } diff --git a/internal/agent/subagent_progress_test.go b/internal/agent/subagent_progress_test.go index ca7c6255d6..fa061f5a4c 100644 --- a/internal/agent/subagent_progress_test.go +++ b/internal/agent/subagent_progress_test.go @@ -476,7 +476,7 @@ func TestSubagentProgressGroupBudgetBoundsBurstAndServesAll(t *testing.T) { t.Cleanup(merger.Close) const n = 64 - for i := 0; i < n; i++ { + for i := range n { child := "child-" + string(rune('0'+i/10)) + string(rune('0'+i%10)) // Ordinary phase transitions share the budget with previews: 64 // status changes alone must not exceed the 32 events/s contract. @@ -501,7 +501,7 @@ func TestSubagentProgressGroupBudgetBoundsBurstAndServesAll(t *testing.T) { // Once the budget refills, every child is served exactly once — no child // starves behind a high-activity sibling. var rest []event.Event - for i := 0; i < 16; i++ { + for range 16 { clock.Advance(time.Second) rest = append(rest, collectFor(t, ch, 50*time.Millisecond)...) } diff --git a/internal/agent/subagent_result.go b/internal/agent/subagent_result.go index f5585e5087..d30f931d8a 100644 --- a/internal/agent/subagent_result.go +++ b/internal/agent/subagent_result.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "strings" "unicode/utf8" @@ -91,10 +92,7 @@ func (t *SubagentResultTool) Execute(ctx context.Context, args json.RawMessage) if p.OffsetBytes < len(answer) && !utf8.RuneStart(answer[p.OffsetBytes]) { return "", fmt.Errorf("offset_bytes %d is not at a UTF-8 character boundary; use next_offset_bytes from the previous page", p.OffsetBytes) } - end := p.OffsetBytes + p.LimitBytes - if end > len(answer) { - end = len(answer) - } + end := min(p.OffsetBytes+p.LimitBytes, len(answer)) for end > p.OffsetBytes && end < len(answer) && !utf8.RuneStart(answer[end]) { end-- } @@ -154,9 +152,9 @@ func (s *SubagentStore) ReadFinalAnswer(ref, parentSession, workspaceRoot string return "", meta.Status, fmt.Errorf("load subagent transcript %q: %w", ref, err) } msgs := sess.Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleAssistant && strings.TrimSpace(msgs[i].Content) != "" { - return msgs[i].Content, meta.Status, nil + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleAssistant && strings.TrimSpace(v.Content) != "" { + return v.Content, meta.Status, nil } } return "", meta.Status, fmt.Errorf("subagent reference %q has no final assistant answer", ref) @@ -183,10 +181,7 @@ func formatBoundedSubagentAggregate(prefix string, items []subagentAggregateItem completed++ } } - available := subagentAggregateBudgetBytes - baseBytes - if available < 0 { - available = 0 - } + available := max(subagentAggregateBudgetBytes-baseBytes, 0) perAnswer := 0 if completed > 0 { perAnswer = available / completed @@ -280,8 +275,8 @@ func splitSubagentRunResult(output string) (answer, ref string) { return strings.TrimSpace(output), "" } const marker = "\n\nFinal answer:\n" - if idx := strings.Index(output, marker); idx >= 0 { - return strings.TrimSpace(output[idx+len(marker):]), ref + if _, after, ok := strings.Cut(output, marker); ok { + return strings.TrimSpace(after), ref } return strings.TrimSpace(output), ref } diff --git a/internal/agent/task.go b/internal/agent/task.go index 62a1c16b52..9ed0e9c416 100644 --- a/internal/agent/task.go +++ b/internal/agent/task.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "strconv" "strings" "time" @@ -670,10 +671,7 @@ func (t *TaskTool) childMaxSteps(requested int) int { if t.maxSteps <= 0 { return 0 } - half := t.maxSteps / 2 - if half < 5 { - half = 5 - } + half := max(t.maxSteps/2, 5) return half } @@ -2045,8 +2043,8 @@ func latestAssistantAnswer(sess *Session) string { if sess == nil { return "" } - for i := len(sess.Messages) - 1; i >= 0; i-- { - m := sess.Messages[i] + for _, v := range slices.Backward(sess.Messages) { + m := v if m.Role == provider.RoleAssistant && strings.TrimSpace(m.Content) != "" { return m.Content } diff --git a/internal/agent/task_test.go b/internal/agent/task_test.go index 578e1bc78e..f3f429711a 100644 --- a/internal/agent/task_test.go +++ b/internal/agent/task_test.go @@ -825,7 +825,7 @@ func TestTaskToolBackgroundCapRefusesFanOut(t *testing.T) { // Saturate the cap with still-running task jobs owned by this session. release := make(chan struct{}) var ids []string - for i := 0; i < maxConcurrentBackgroundTasks; i++ { + for range maxConcurrentBackgroundTasks { j := jm.StartForSession("parent-session", "task", "busy", func(jctx context.Context, _ io.Writer) (string, error) { select { case <-release: @@ -1124,9 +1124,9 @@ func extractJobID(msg string) string { func subagentRefFromOutput(t *testing.T, out string) string { t.Helper() - for _, line := range strings.Split(out, "\n") { - if strings.HasPrefix(line, "Subagent reference: ") { - return strings.TrimSpace(strings.TrimPrefix(line, "Subagent reference: ")) + for line := range strings.SplitSeq(out, "\n") { + if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok { + return strings.TrimSpace(after) } } t.Fatalf("no subagent reference in output:\n%s", out) diff --git a/internal/agent/textsink.go b/internal/agent/textsink.go index 6bca3fb57f..e26c5d97e3 100644 --- a/internal/agent/textsink.go +++ b/internal/agent/textsink.go @@ -166,7 +166,7 @@ func (s *TextSink) Emit(e event.Event) { break // aborted pass — the caller's Notice already explained why } fmt.Fprintln(s.out, dimText(fmt.Sprintf(" ⋯ compacted %d messages (%s)", c.Messages, c.Trigger))) - for _, ln := range strings.Split(strings.TrimRight(c.Summary, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(c.Summary, "\n"), "\n") { fmt.Fprintln(s.out, dimText(" "+ln)) } s.wroteAnything = true diff --git a/internal/agent/todo_progress_guard_test.go b/internal/agent/todo_progress_guard_test.go index 5dea115c4e..bbdb0b64dc 100644 --- a/internal/agent/todo_progress_guard_test.go +++ b/internal/agent/todo_progress_guard_test.go @@ -21,7 +21,7 @@ func TestTodoProgressGuardPausesSemanticToolDrift(t *testing.T) { Arguments: `{"todos":[{"content":"finish the task","status":"in_progress"}]}`, }}}} // The first unique read renews the lease; exact repeats after it do not. - for i := 0; i < maxTodoStallRounds+1; i++ { + for i := range maxTodoStallRounds + 1 { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ ID: fmt.Sprintf("read-%d", i), Name: "inspect", Arguments: `{"path":"same"}`, }}}) @@ -51,7 +51,7 @@ func TestTodoProgressGuardRenewsOnUniqueHostWork(t *testing.T) { ID: "todo", Name: "todo_write", Arguments: `{"todos":[{"content":"finish the task","status":"in_progress"}]}`, }}}} - for i := 0; i < todoProgressNudgeRounds-1; i++ { + for i := range todoProgressNudgeRounds - 1 { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ ID: fmt.Sprintf("read-a-%d", i), Name: "inspect", Arguments: `{"path":"same"}`, }}}) @@ -59,7 +59,7 @@ func TestTodoProgressGuardRenewsOnUniqueHostWork(t *testing.T) { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ID: "write", Name: "write_file", Arguments: `{"path":"result.txt","content":"done"}`}}}, ) - for i := 0; i < todoProgressNudgeRounds-1; i++ { + for i := range todoProgressNudgeRounds - 1 { turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ ID: fmt.Sprintf("read-b-%d", i), Name: "inspect", Arguments: `{"path":"same"}`, }}}) diff --git a/internal/agent/usecapability.go b/internal/agent/usecapability.go index fb75ef0fde..f373840823 100644 --- a/internal/agent/usecapability.go +++ b/internal/agent/usecapability.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "sort" "strings" "sync" @@ -330,9 +331,7 @@ func cloneMCPSpec(in plugin.Spec) plugin.Spec { out.Headers = cloneStringMap(in.Headers) if in.ToolTimeouts != nil { out.ToolTimeouts = make(map[string]time.Duration, len(in.ToolTimeouts)) - for name, timeout := range in.ToolTimeouts { - out.ToolTimeouts[name] = timeout - } + maps.Copy(out.ToolTimeouts, in.ToolTimeouts) } return out } @@ -342,9 +341,7 @@ func cloneStringMap(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for key, value := range in { - out[key] = value - } + maps.Copy(out, in) return out } diff --git a/internal/agent/usecapability_test.go b/internal/agent/usecapability_test.go index 3991ae42a2..623ae6c3ec 100644 --- a/internal/agent/usecapability_test.go +++ b/internal/agent/usecapability_test.go @@ -1666,7 +1666,7 @@ func TestMCPCapabilityRuntimeConcurrentUpdatesAndSnapshots(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - for i := 0; i < 100; i++ { + for i := range 100 { entry.URL = fmt.Sprintf("http://127.0.0.1:%d", 10000+i) runtime.UpsertServer(entry, plugin.Spec{Name: "race", Type: "http", URL: entry.URL, Authorized: true}, true) runtime.state.setLiveTools("race", []plugin.CachedTool{{Name: "query", ReadOnly: true}}) @@ -1678,7 +1678,7 @@ func TestMCPCapabilityRuntimeConcurrentUpdatesAndSnapshots(t *testing.T) { }() go func() { defer wg.Done() - for i := 0; i < 100; i++ { + for range 100 { _, _ = frontend.Execute(context.Background(), json.RawMessage(`{"action":"list"}`)) _, _, _, _, _ = runtime.CapabilityCatalogState() } diff --git a/internal/agent/width.go b/internal/agent/width.go index 42498422f2..a5607c8d91 100644 --- a/internal/agent/width.go +++ b/internal/agent/width.go @@ -31,7 +31,7 @@ func streamedRows(s string, width int) int { width = 80 } rows := 0 - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if w := visibleWidth(line); w > 0 { rows += (w - 1) / width } diff --git a/internal/agent/write_claims.go b/internal/agent/write_claims.go index 1b333a3b55..b351abbbde 100644 --- a/internal/agent/write_claims.go +++ b/internal/agent/write_claims.go @@ -134,7 +134,7 @@ func (s WritePathSet) Overlaps(other WritePathSet) bool { // ValidateNonOverlappingWriteClaims fails if any pair of claims overlaps. // Used by fleet preflight so no task starts when path division is invalid. func ValidateNonOverlappingWriteClaims(claims []WritePathSet) error { - for i := 0; i < len(claims); i++ { + for i := range claims { if claims[i].Empty() { continue } diff --git a/internal/autoresearch/bounded_store_test.go b/internal/autoresearch/bounded_store_test.go index 5b31dad158..03f4a95cc0 100644 --- a/internal/autoresearch/bounded_store_test.go +++ b/internal/autoresearch/bounded_store_test.go @@ -157,7 +157,7 @@ func TestTailJSONLLinesMatchesFullScan(t *testing.T) { } // Write enough heartbeats that the log exceeds one 64KiB chunk. long := strings.Repeat("x", 700) - for i := 0; i < 150; i++ { + for i := range 150 { if err := store.AppendHeartbeat(task.ID, Heartbeat{ Status: HeartbeatTurnDone, Iteration: i + 1, @@ -211,7 +211,7 @@ func TestFindingsBoundedTailMatchesFullScan(t *testing.T) { if err != nil { t.Fatalf("CreateTask: %v", err) } - for i := 0; i < 25; i++ { + for i := range 25 { if err := store.AppendFinding(task.ID, Finding{ ID: fmt.Sprintf("f%03d", i), Kind: FindingKindManual, diff --git a/internal/autoresearch/store.go b/internal/autoresearch/store.go index a3b35daa6e..6016ce998c 100644 --- a/internal/autoresearch/store.go +++ b/internal/autoresearch/store.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" "sync" @@ -823,10 +824,7 @@ func tailJSONLLines(root *os.Root, path string, limit int) ([][]byte, error) { off = info.Size() ) for off > 0 { - readLen := int64(chunkSize) - if off < readLen { - readLen = off - } + readLen := min(off, int64(chunkSize)) off -= readLen chunk := make([]byte, readLen) if _, err := f.ReadAt(chunk, off); err != nil { @@ -923,12 +921,7 @@ func validateHeartbeat(h Heartbeat) error { } func stringSliceContains(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } func cloneCriteria(in []SuccessCriterion) []SuccessCriterion { diff --git a/internal/autoresearch/store_test.go b/internal/autoresearch/store_test.go index 87f3bc51fe..d156415db2 100644 --- a/internal/autoresearch/store_test.go +++ b/internal/autoresearch/store_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "sync" "testing" @@ -124,7 +125,7 @@ func TestCreateTaskReservesIDsAtomicallyAcrossStores(t *testing.T) { start := make(chan struct{}) var ready sync.WaitGroup ready.Add(workers) - for i := 0; i < workers; i++ { + for range workers { go func() { store := NewStore(root) ready.Done() @@ -137,7 +138,7 @@ func TestCreateTaskReservesIDsAtomicallyAcrossStores(t *testing.T) { close(start) ids := map[string]string{} - for i := 0; i < workers; i++ { + for range workers { res := <-results if res.err != nil { t.Fatalf("CreateTask worker failed: %v", res.err) @@ -482,7 +483,7 @@ func TestConcurrentDirectionWritesAreSerializedPerTask(t *testing.T) { const writers = 20 var wg sync.WaitGroup errs := make(chan error, writers) - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(i int) { defer wg.Done() @@ -657,12 +658,7 @@ func containsValidationError(errors []ValidationError, file, field string) bool } func containsString(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } func ptrString(s string) *string { diff --git a/internal/boot/boot.go b/internal/boot/boot.go index 3d448919d5..8b8cffd7e3 100644 --- a/internal/boot/boot.go +++ b/internal/boot/boot.go @@ -17,6 +17,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strconv" "strings" "sync/atomic" @@ -896,10 +897,7 @@ func build(ctx context.Context, opts Options) (*BuildResult, error) { cleanup = func() { prev(); lspMgr.Close() } } - maxSteps := 0 - if opts.MaxSteps > 0 { - maxSteps = opts.MaxSteps - } + maxSteps := max(opts.MaxSteps, 0) subagentStore, err := newSubagentStore(sessionDir, opts.SubagentParentLive) if err != nil { return nil, err @@ -1407,7 +1405,6 @@ func build(ctx context.Context, opts Options) (*BuildResult, error) { var slashEntries []command.SlashEntry if includeSkills { for _, sk := range skillStore.SlashList() { - sk := sk slashEntries = append(slashEntries, command.SlashEntry{ Name: sk.SlashName(), Description: sk.Description, @@ -1419,7 +1416,7 @@ func build(ctx context.Context, opts Options) (*BuildResult, error) { if cmd.Hidden { continue } - cmd := cmd + slashEntries = append(slashEntries, command.SlashEntry{ Name: cmd.Name, Description: cmd.Description, @@ -2349,13 +2346,7 @@ func SubagentModelKeys(name string) []string { if alias == "" { continue } - seen := false - for _, key := range keys { - if key == alias { - seen = true - break - } - } + seen := slices.Contains(keys, alias) if !seen { keys = append(keys, alias) } diff --git a/internal/boot/boot_test.go b/internal/boot/boot_test.go index 49e6fa57c8..b1dd389fdf 100644 --- a/internal/boot/boot_test.go +++ b/internal/boot/boot_test.go @@ -905,9 +905,9 @@ func (p *bootSubagentTestProvider) requestsSnapshot() []provider.Request { } func bootLastUser(req provider.Request) string { - for i := len(req.Messages) - 1; i >= 0; i-- { - if req.Messages[i].Role == provider.RoleUser { - return req.Messages[i].Content + for _, v := range slices.Backward(req.Messages) { + if v.Role == provider.RoleUser { + return v.Content } } return "" @@ -919,9 +919,9 @@ func subagentRefFromHistory(t *testing.T, msgs []provider.Message) string { if msg.Role != provider.RoleTool { continue } - for _, line := range strings.Split(msg.Content, "\n") { - if strings.HasPrefix(line, "Subagent reference: ") { - return strings.TrimSpace(strings.TrimPrefix(line, "Subagent reference: ")) + for line := range strings.SplitSeq(msg.Content, "\n") { + if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok { + return strings.TrimSpace(after) } } } @@ -967,20 +967,20 @@ model = "x" t.Fatalf("headless run should keep an empty session path, got %q", got) } - var toolContent string + var toolContent strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool { - toolContent += "\n" + msg.Content + toolContent.WriteString("\n" + msg.Content) } } - if strings.Contains(toolContent, "parent session is required") { - t.Fatalf("task subagent failed in headless run mode: %s", toolContent) + if strings.Contains(toolContent.String(), "parent session is required") { + t.Fatalf("task subagent failed in headless run mode: %s", toolContent.String()) } - if !strings.Contains(toolContent, "subagent answer") { - t.Fatalf("task tool result = %q, want sub-agent answer", toolContent) + if !strings.Contains(toolContent.String(), "subagent answer") { + t.Fatalf("task tool result = %q, want sub-agent answer", toolContent.String()) } - if strings.Contains(toolContent, "Subagent reference") { - t.Fatalf("ephemeral headless run should not persist a transcript reference: %s", toolContent) + if strings.Contains(toolContent.String(), "Subagent reference") { + t.Fatalf("ephemeral headless run should not persist a transcript reference: %s", toolContent.String()) } } @@ -2740,17 +2740,17 @@ READ ONLY SKILL BODY`) t.Fatalf("read_only_skill child request should hide %q; tools=%v", forbidden, toolSchemaNames(subReq.Tools)) } } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) if strings.Contains(msg.Content, "blocked:") { t.Fatalf("connect_tool_source should not block read_only_skill in plan mode, got:\n%s", msg.Content) } } } - if !strings.Contains(toolOutput, "readonlydig") || !strings.Contains(toolOutput, "# Skills") { - t.Fatalf("read_only_skill source result should include the skill index, got:\n%s", toolOutput) + if !strings.Contains(toolOutput.String(), "readonlydig") || !strings.Contains(toolOutput.String(), "# Skills") { + t.Fatalf("read_only_skill source result should include the skill index, got:\n%s", toolOutput.String()) } } @@ -2954,17 +2954,17 @@ command = "reasonix-missing-mockmcp" if len(reqs) != 2 { t.Fatalf("requests = %d, want 2", len(reqs)) } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) } } - if strings.TrimSpace(toolOutput) == "" { + if strings.TrimSpace(toolOutput.String()) == "" { t.Fatalf("connect_tool_source(%s) returned empty tool output", tt.source) } - if strings.Contains(toolOutput, "blocked:") { - t.Fatalf("connect_tool_source(%s) should load capability metadata in Plan, got %q", tt.source, toolOutput) + if strings.Contains(toolOutput.String(), "blocked:") { + t.Fatalf("connect_tool_source(%s) should load capability metadata in Plan, got %q", tt.source, toolOutput.String()) } for _, enabled := range tt.enabledTools { if !requestHasTool(reqs[1], enabled) { @@ -3024,17 +3024,17 @@ model = "x" if requestHasTool(reqs[1], "complete_step") { t.Fatalf("plan-mode workflow connect must not expose complete_step; tools=%v", toolSchemaNames(reqs[1].Tools)) } - var planConnectOutput string + var planConnectOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - planConnectOutput += msg.Content + planConnectOutput.WriteString(msg.Content) } } - if strings.Contains(planConnectOutput, "blocked:") { - t.Fatalf("workflow source should not be blocked in plan mode, got:\n%s", planConnectOutput) + if strings.Contains(planConnectOutput.String(), "blocked:") { + t.Fatalf("workflow source should not be blocked in plan mode, got:\n%s", planConnectOutput.String()) } - if !strings.Contains(planConnectOutput, "complete_step stays blocked in plan mode") { - t.Fatalf("plan-mode workflow connect should explain the deferred complete_step, got:\n%s", planConnectOutput) + if !strings.Contains(planConnectOutput.String(), "complete_step stays blocked in plan mode") { + t.Fatalf("plan-mode workflow connect should explain the deferred complete_step, got:\n%s", planConnectOutput.String()) } ctrl.SetPlanMode(false) @@ -3137,14 +3137,14 @@ model = "x" if requestHasTool(reqs[1], "web_fetch") { t.Fatalf("disabled web_fetch should not be exposed after connect_tool_source; tools=%v", toolSchemaNames(reqs[1].Tools)) } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) } } - if !strings.Contains(toolOutput, "web_fetch is disabled by [tools].enabled") { - t.Fatalf("connector should explain disabled web_fetch, got:\n%s", toolOutput) + if !strings.Contains(toolOutput.String(), "web_fetch is disabled by [tools].enabled") { + t.Fatalf("connector should explain disabled web_fetch, got:\n%s", toolOutput.String()) } } @@ -3194,14 +3194,14 @@ model = "x" t.Fatalf("second request should expose %q after connect_tool_source; tools=%v", name, toolSchemaNames(reqs[1].Tools)) } } - var toolOutput string + var toolOutput strings.Builder for _, msg := range ctrl.History() { if msg.Role == provider.RoleTool && msg.Name == "connect_tool_source" { - toolOutput += msg.Content + toolOutput.WriteString(msg.Content) } } - if !strings.Contains(toolOutput, "projskill") || !strings.Contains(toolOutput, "# Skills") { - t.Fatalf("skills source result should include the skill index, got:\n%s", toolOutput) + if !strings.Contains(toolOutput.String(), "projskill") || !strings.Contains(toolOutput.String(), "# Skills") { + t.Fatalf("skills source result should include the skill index, got:\n%s", toolOutput.String()) } } @@ -3357,8 +3357,8 @@ api_key_env = "REASONIX_TEST_KEY_UNSET" // user-global REASONIX.md in the real config dir could append; the test // environment has none, so the base stands alone.) base := sys - if i := strings.Index(sys, "\n\n# Skills"); i >= 0 { - base = sys[:i] + if before, _, ok := strings.Cut(sys, "\n\n# Skills"); ok { + base = before } // The language policy, user-decision policy, and current-workspace line are // always appended at boot; strip them so this assertion is purely about @@ -3521,8 +3521,8 @@ func stripLanguagePolicy(s string) string { } func stripEnvironmentBlock(s string) string { - if i := strings.Index(s, "\n\n## Environment"); i >= 0 { - return s[:i] + if before, _, ok := strings.Cut(s, "\n\n## Environment"); ok { + return before } return s } @@ -3695,7 +3695,7 @@ func TestRememberPermissionRuleSerializesConcurrentWriters(t *testing.T) { start := make(chan struct{}) results := make(chan control.RememberResult, writers) var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(n int) { defer wg.Done() @@ -3713,7 +3713,7 @@ func TestRememberPermissionRuleSerializesConcurrentWriters(t *testing.T) { } got := config.LoadForEdit(filepath.Join(workspace, "reasonix.toml")) - for i := 0; i < writers; i++ { + for i := range writers { rule := fmt.Sprintf("Edit(file-%02d)", i) if !hasPermissionRule(got.Permissions.Allow, rule) { t.Errorf("permissions.allow missing %q: %v", rule, got.Permissions.Allow) @@ -3731,7 +3731,7 @@ func TestRememberPermissionRuleSerializesCrossProcessWriters(t *testing.T) { const rulesPerWorker = 8 commands := make([]*exec.Cmd, 0, workers) outputs := make([]bytes.Buffer, workers) - for worker := 0; worker < workers; worker++ { + for worker := range workers { cmd := exec.Command(os.Args[0], "-test.run=^TestRememberPermissionRuleProcessHelper$") cmd.Stdout = &outputs[worker] cmd.Stderr = &outputs[worker] @@ -3778,8 +3778,8 @@ func TestRememberPermissionRuleSerializesCrossProcessWriters(t *testing.T) { } got := config.LoadForEdit(filepath.Join(workspace, "reasonix.toml")) - for worker := 0; worker < workers; worker++ { - for n := 0; n < rulesPerWorker; n++ { + for worker := range workers { + for n := range rulesPerWorker { rule := fmt.Sprintf("Edit(process-%d-file-%02d)", worker, n) if !hasPermissionRule(got.Permissions.Allow, rule) { t.Errorf("permissions.allow missing %q: %v", rule, got.Permissions.Allow) @@ -3817,7 +3817,7 @@ func TestRememberPermissionRuleProcessHelper(t *testing.T) { } time.Sleep(10 * time.Millisecond) } - for n := 0; n < rules; n++ { + for n := range rules { rule := fmt.Sprintf("Edit(process-%d-file-%02d)", worker, n) result := rememberPermissionRule(workspace, rule) if result.Err != nil || !result.Saved { @@ -4008,12 +4008,7 @@ plan_mode_read_only_commands = ["gh issue view"] } func hasPermissionRule(rules []string, want string) bool { - for _, rule := range rules { - if rule == want { - return true - } - } - return false + return slices.Contains(rules, want) } func hasPlanModeReadOnlyCommand(commands []string, want string) bool { @@ -4907,7 +4902,7 @@ func TestBuildMigratesLegacyEagerBeforeStatsDemotion(t *testing.T) { // Three samples above 2*budget — the rule in stats.go's Recommend triggers // when the trailing window is entirely over the threshold. Use 30s so even // future budget bumps stay below the threshold. - for i := 0; i < 3; i++ { + for i := range 3 { if err := plugin.RecordStartup("slowserver", 30*time.Second); err != nil { t.Fatalf("RecordStartup #%d: %v", i, err) } @@ -4990,8 +4985,7 @@ func TestBuildExtraPluginProbeKeepsSessionProcessAlive(t *testing.T) { workspace := robustTempDir(t) t.Chdir(workspace) - sessionCtx, cancelSession := context.WithCancel(context.Background()) - defer cancelSession() + sessionCtx := t.Context() ctrl, err := Build(sessionCtx, Options{ SessionDir: filepath.Join(t.TempDir(), "sessions"), Sink: event.Discard, diff --git a/internal/boot/extension_dispatch_test.go b/internal/boot/extension_dispatch_test.go index 8185fa4fcd..324b73ecd5 100644 --- a/internal/boot/extension_dispatch_test.go +++ b/internal/boot/extension_dispatch_test.go @@ -260,7 +260,7 @@ func TestBootSystemPromptBuildEventObserved(t *testing.T) { if err != nil { return false } - for _, candidate := range strings.Split(strings.TrimSpace(string(data)), "\n") { + for candidate := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { if strings.HasPrefix(candidate, "system_prompt.build ") { line = candidate return true diff --git a/internal/boot/extension_firstboot_test.go b/internal/boot/extension_firstboot_test.go index 759df3a2fc..aecc1353fb 100644 --- a/internal/boot/extension_firstboot_test.go +++ b/internal/boot/extension_firstboot_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "os" "path/filepath" "strconv" @@ -54,9 +55,7 @@ func installProviderFake(t *testing.T, home, name string, extraEnv map[string]st bootFakeEnvPluginName: name, bootFakeEnvProvider: "1", } - for k, v := range extraEnv { - env[k] = v - } + maps.Copy(env, extraEnv) installBootFakePlugin(t, home, name, map[string]any{ "capabilities": []string{"providers"}, "env": env, diff --git a/internal/boot/extension_provider_test.go b/internal/boot/extension_provider_test.go index 0f4d754ee5..33ff07db82 100644 --- a/internal/boot/extension_provider_test.go +++ b/internal/boot/extension_provider_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "os" "strings" "testing" @@ -32,9 +33,7 @@ func bootWithProviderPlugin(t *testing.T, name string, runtime map[string]any) * bootFakeEnvProvider: "1", } if extra, ok := runtime["env"].(map[string]string); ok { - for k, v := range extra { - env[k] = v - } + maps.Copy(env, extra) } runtime["env"] = env return bootWithFakePlugin(t, name, runtime) diff --git a/internal/boot/extension_sidecar_test.go b/internal/boot/extension_sidecar_test.go index e98deae4c1..9eeca1801b 100644 --- a/internal/boot/extension_sidecar_test.go +++ b/internal/boot/extension_sidecar_test.go @@ -82,7 +82,7 @@ func TestExtensionFakeSidecarHelperProcess(t *testing.T) { func runBootFakeSidecar(stdin io.Reader, stdout io.Writer) { if pidFile := strings.TrimSpace(os.Getenv(bootFakeEnvPIDFile)); pidFile != "" { - _ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o644) + _ = os.WriteFile(pidFile, fmt.Appendf(nil, "%d", os.Getpid()), 0o644) } if os.Getenv(bootFakeEnvExitImmediately) == "1" { os.Exit(0) diff --git a/internal/boot/prompt_stability_test.go b/internal/boot/prompt_stability_test.go index 9148243f0f..014bdab6a0 100644 --- a/internal/boot/prompt_stability_test.go +++ b/internal/boot/prompt_stability_test.go @@ -61,25 +61,13 @@ api_key_env = "REASONIX_TEST_KEY_UNSET" // firstDivergence returns a small window around the first differing byte so a // failure names the drifting prompt section instead of dumping both prompts. func firstDivergence(a, b string) string { - limit := len(a) - if len(b) < limit { - limit = len(b) - } + limit := min(len(b), len(a)) i := 0 for i < limit && a[i] == b[i] { i++ } - start := i - 40 - if start < 0 { - start = 0 - } - endA := i + 40 - if endA > len(a) { - endA = len(a) - } - endB := i + 40 - if endB > len(b) { - endB = len(b) - } + start := max(i-40, 0) + endA := min(i+40, len(a)) + endB := min(i+40, len(b)) return "..." + a[start:endA] + "... vs ..." + b[start:endB] + "..." } diff --git a/internal/boot/resolver.go b/internal/boot/resolver.go index 385bf4065a..d0ccf3688e 100644 --- a/internal/boot/resolver.go +++ b/internal/boot/resolver.go @@ -222,8 +222,8 @@ func syntheticEntryFromResolver(r provider.Resolver, ref string) *config.Provide func splitProviderRef(ref string) (string, string) { ref = strings.TrimSpace(ref) - if i := strings.IndexByte(ref, '/'); i >= 0 { - return ref[:i], ref[i+1:] + if before, after, ok := strings.Cut(ref, "/"); ok { + return before, after } return ref, "" } diff --git a/internal/boot/temphelper_test.go b/internal/boot/temphelper_test.go index a1918872d8..d09a01eec9 100644 --- a/internal/boot/temphelper_test.go +++ b/internal/boot/temphelper_test.go @@ -25,7 +25,7 @@ func robustTempDir(t *testing.T) string { } t.Cleanup(func() { var rmErr error - for i := 0; i < 100; i++ { + for range 100 { if rmErr = os.RemoveAll(dir); rmErr == nil { return } diff --git a/internal/bot/feishu/feishu.go b/internal/bot/feishu/feishu.go index fa81a927c6..02e7cb3ca8 100644 --- a/internal/bot/feishu/feishu.go +++ b/internal/bot/feishu/feishu.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "log/slog" + "maps" "net/http" "os" "strings" @@ -888,24 +889,22 @@ func (a *adapter) AddPendingReaction(ctx context.Context, messageID string) (fun func (a *adapter) sendCard(ctx context.Context, msg bot.OutboundMessage) (bot.SendResult, error) { card := msg.Card - elements := make([]map[string]interface{}, 0) + elements := make([]map[string]any, 0) for _, el := range card.Elements { - item := map[string]interface{}{"tag": el.Tag} + item := map[string]any{"tag": el.Tag} if el.Content != "" { item["content"] = el.Content } if actions, ok := el.Extra["actions"]; ok && el.Tag == "action" { item["actions"] = actions } else { - for k, v := range el.Extra { - item[k] = v - } + maps.Copy(item, el.Extra) } elements = append(elements, item) } - cardPayload := map[string]interface{}{ - "header": map[string]interface{}{ + cardPayload := map[string]any{ + "header": map[string]any{ "title": map[string]string{ "tag": "plain_text", "content": card.Header, @@ -954,7 +953,7 @@ func (a *adapter) runWebhook(ctx context.Context) { mux.HandleFunc("/feishu/event", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 1024*1024)) if err != nil { - http.Error(w, "bad request", 400) + http.Error(w, "bad request", http.StatusBadRequest) return } var challenge struct { @@ -977,7 +976,7 @@ func (a *adapter) runWebhook(ctx context.Context) { var evt feishuEvent if err := json.Unmarshal(body, &evt); err != nil { - http.Error(w, "bad request", 400) + http.Error(w, "bad request", http.StatusBadRequest) return } if !a.verificationTokenValid(evt.Header.Token) { @@ -989,7 +988,7 @@ func (a *adapter) runWebhook(ctx context.Context) { raw, _ := json.Marshal(evt) a.handleWSEvent(ctx, raw) } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) server := &http.Server{ diff --git a/internal/bot/feishu/feishu_test.go b/internal/bot/feishu/feishu_test.go index f3089b194e..6508a264ad 100644 --- a/internal/bot/feishu/feishu_test.go +++ b/internal/bot/feishu/feishu_test.go @@ -53,7 +53,7 @@ func TestMarkSeenConcurrent(t *testing.T) { a := &adapter{seen: make(map[string]bool)} var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for i := range 100 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/bot/gateway.go b/internal/bot/gateway.go index 05a61ecd08..5188c7b059 100644 --- a/internal/bot/gateway.go +++ b/internal/bot/gateway.go @@ -442,11 +442,9 @@ func (gw *BotGateway) Start(ctx context.Context) (err error) { // 合并所有适配器的消息通道 for _, binding := range gw.adapters { - gw.gatewayWG.Add(1) - go func() { - defer gw.gatewayWG.Done() + gw.gatewayWG.Go(func() { gw.dispatchLoop(runCtx, binding) - }() + }) } return nil @@ -836,11 +834,9 @@ func (gw *BotGateway) handleMessage(ctx context.Context, binding AdapterBinding, // would unblock it: the session wedges until restart (#4701, #4863, #4402). // Per-session serialization is still held by the session lock (active[key]), // which the deferred Release inside runTurn clears. - gw.turnWG.Add(1) - go func() { - defer gw.turnWG.Done() + gw.turnWG.Go(func() { gw.runTurn(ctx, binding.Adapter, key, msg, cleanup) - }() + }) } func (gw *BotGateway) queueMode(key string, msg InboundMessage) string { @@ -2801,7 +2797,7 @@ func parseAskAnswers(questions []event.AskQuestion, raw string) []event.AskAnswe } answerMap := make(map[string][]string, len(questions)) if strings.Contains(raw, "=") { - for _, part := range strings.Split(raw, ";") { + for part := range strings.SplitSeq(raw, ";") { k, v, ok := strings.Cut(part, "=") if !ok { continue diff --git a/internal/bot/gateway_lock_test.go b/internal/bot/gateway_lock_test.go index 32fdc3d6eb..10d8f92765 100644 --- a/internal/bot/gateway_lock_test.go +++ b/internal/bot/gateway_lock_test.go @@ -384,7 +384,7 @@ func TestBotGatewayToolApprovalModeConcurrentWithConfigReaders(t *testing.T) { defer close(writerDone) <-start modes := []string{control.ToolApprovalYolo, control.ToolApprovalAsk, control.ToolApprovalAuto} - for i := 0; i < iterations; i++ { + for i := range iterations { mode := modes[i%len(modes)] gw.UpdateConnectionToolApprovalMode("feishu-lark", mode) gw.mu.Lock() @@ -397,7 +397,7 @@ func TestBotGatewayToolApprovalModeConcurrentWithConfigReaders(t *testing.T) { close(start) connMsg := InboundMessage{Platform: PlatformFeishu, ConnectionID: "feishu-lark", ChatType: ChatDM, ChatID: "chat", UserID: "user"} - for i := 0; i < iterations; i++ { + for range iterations { gw.sessionOptionsForMessage(connMsg) gw.sessionOptionsForMessage(InboundMessage{Platform: PlatformFeishu}) projects := gw.buildProjectIndex() diff --git a/internal/bot/gateway_test.go b/internal/bot/gateway_test.go index d2c28daccc..2e991a2f6f 100644 --- a/internal/bot/gateway_test.go +++ b/internal/bot/gateway_test.go @@ -1353,8 +1353,7 @@ func TestGatewayApprovalReplyUnblocksWedgedTurn(t *testing.T) { pendingAsks: make(map[string][]event.AskQuestion), } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go gw.dispatchLoop(ctx, binding) adapter.msgCh <- msg @@ -1408,8 +1407,7 @@ func TestGatewayAskReplyUnblocksWedgedTurn(t *testing.T) { pendingAsks: make(map[string][]event.AskQuestion), } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go gw.dispatchLoop(ctx, binding) adapter.msgCh <- msg diff --git a/internal/bot/pairing.go b/internal/bot/pairing.go index 3544ede231..c5a481edf4 100644 --- a/internal/bot/pairing.go +++ b/internal/bot/pairing.go @@ -343,7 +343,7 @@ func pairingRequestMatches(req PairingRequest, msg InboundMessage) bool { func newPairingCode() (string, error) { var b strings.Builder max := big.NewInt(int64(len(pairingAlphabet))) - for i := 0; i < 8; i++ { + for range 8 { n, err := rand.Int(rand.Reader, max) if err != nil { return "", err diff --git a/internal/bot/pairing_test.go b/internal/bot/pairing_test.go index 036ced07e2..7fd92a1648 100644 --- a/internal/bot/pairing_test.go +++ b/internal/bot/pairing_test.go @@ -17,7 +17,7 @@ func TestCreateOrRefreshPairingRequestConcurrent(t *testing.T) { const workers = 8 var wg sync.WaitGroup errs := make(chan error, workers) - for i := 0; i < workers; i++ { + for i := range workers { wg.Add(1) go func(i int) { defer wg.Done() @@ -27,7 +27,7 @@ func TestCreateOrRefreshPairingRequestConcurrent(t *testing.T) { ChatID: fmt.Sprintf("chat-%d", i), UserID: fmt.Sprintf("user-%d", i), } - for j := 0; j < 5; j++ { + for range 5 { if _, _, err := CreateOrRefreshPairingRequest(msg, cfg); err != nil { errs <- err return diff --git a/internal/bot/project_index.go b/internal/bot/project_index.go index 0498034b4f..04ce390c93 100644 --- a/internal/bot/project_index.go +++ b/internal/bot/project_index.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "time" @@ -143,10 +144,8 @@ func (c *botProjectCollector) add(root, source string) { if source == "" { return } - for _, existing := range entry.Sources { - if existing == source { - return - } + if slices.Contains(entry.Sources, source) { + return } entry.Sources = append(entry.Sources, source) } @@ -367,8 +366,8 @@ func botSessionPathFromTarget(target string) string { if target == "" { return "" } - if strings.HasPrefix(target, "path:") { - return canonicalBotPath(strings.TrimPrefix(target, "path:")) + if after, ok := strings.CutPrefix(target, "path:"); ok { + return canonicalBotPath(after) } if filepath.IsAbs(target) && strings.HasSuffix(target, ".jsonl") { return canonicalBotPath(target) @@ -499,7 +498,7 @@ func formatBotProjects(projects []botProjectEntry, query string, limit int) stri } var b strings.Builder fmt.Fprintf(&b, "项目索引(%d/%d):", limit, len(matches)) - for i := 0; i < limit; i++ { + for i := range limit { project := matches[i] fmt.Fprintf(&b, "\n%s %s — %s", project.ID, project.Name, displayBotPath(project.Root)) if len(project.Sources) > 0 { @@ -525,7 +524,7 @@ func formatBotSessions(sessions []botSessionEntry, query string, limit int) stri } var b strings.Builder fmt.Fprintf(&b, "会话索引(%d/%d):", limit, len(matches)) - for i := 0; i < limit; i++ { + for i := range limit { session := matches[i] project := firstNonEmptyString(session.ProjectName, "global") fmt.Fprintf(&b, "\n%s %s", session.ID, project) @@ -559,7 +558,7 @@ func formatBotProjectSearchResults(results []botProjectSearchResult, limit int) } var b strings.Builder fmt.Fprintf(&b, "跨项目检索结果(%d/%d):", limit, len(results)) - for i := 0; i < limit; i++ { + for i := range limit { result := results[i] project := firstNonEmptyString(result.ProjectName, result.ProjectID) fmt.Fprintf(&b, "\n- %s %s:%d: %s", project, displayBotPath(result.Path), result.Line, singleLineBotText(result.Text, 120)) diff --git a/internal/bot/qq/adapter.go b/internal/bot/qq/adapter.go index b46cd7d788..4993ef3ca8 100644 --- a/internal/bot/qq/adapter.go +++ b/internal/bot/qq/adapter.go @@ -64,11 +64,9 @@ func (a *adapter) Start(ctx context.Context) error { } ctx, a.cancel = context.WithCancel(ctx) - a.loopWG.Add(1) - go func() { - defer a.loopWG.Done() + a.loopWG.Go(func() { a.gatewayLoop(ctx) - }() + }) return nil } diff --git a/internal/bot/qq/adapter_stop_test.go b/internal/bot/qq/adapter_stop_test.go index 48b08391de..f097af46af 100644 --- a/internal/bot/qq/adapter_stop_test.go +++ b/internal/bot/qq/adapter_stop_test.go @@ -33,9 +33,7 @@ func TestStopClosesTrackedConnAndWaitsForLoop(t *testing.T) { a.cancel = cancel tracked := make(chan struct{}) decodeReturned := make(chan struct{}) - a.loopWG.Add(1) - go func() { - defer a.loopWG.Done() + a.loopWG.Go(func() { if !a.trackConn(ctx, conn) { conn.Close() return @@ -45,7 +43,7 @@ func TestStopClosesTrackedConnAndWaitsForLoop(t *testing.T) { var payload gatewayPayload _ = json.NewDecoder(conn).Decode(&payload) // blocks like connectGateway's reads close(decodeReturned) - }() + }) select { case <-tracked: case <-time.After(time.Second): @@ -101,15 +99,13 @@ func TestStopUnblocksStalledHandshakeDial(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) a.cancel = cancel dialErr := make(chan error, 1) - a.loopWG.Add(1) - go func() { - defer a.loopWG.Done() + a.loopWG.Go(func() { conn, err := a.dialGateway(ctx, "ws://"+ln.Addr().String(), "test-token") if err == nil { conn.Close() } dialErr <- err - }() + }) var srvConn net.Conn select { diff --git a/internal/bot/qq/gateway.go b/internal/bot/qq/gateway.go index afb5f2ca99..ec90cb9f67 100644 --- a/internal/bot/qq/gateway.go +++ b/internal/bot/qq/gateway.go @@ -146,7 +146,7 @@ func (a *adapter) getAccessToken(ctx context.Context) (string, error) { return "", err } - req, err := http.NewRequestWithContext(ctx, "POST", qqTokenURL, bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, qqTokenURL, bytes.NewReader(body)) if err != nil { return "", err } @@ -421,7 +421,7 @@ func (a *adapter) apiBaseURL() string { } func (a *adapter) getGatewayURL(ctx context.Context, token string) (string, error) { - req, err := http.NewRequestWithContext(ctx, "GET", a.apiBaseURL()+"/gateway", nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.apiBaseURL()+"/gateway", nil) if err != nil { return "", err } @@ -616,7 +616,7 @@ func (a *adapter) sendMessageChunk(ctx context.Context, msg bot.OutboundMessage, } rows = append(rows, map[string]any{"buttons": buttons}) } - payload["keyboard"] = map[string]interface{}{ + payload["keyboard"] = map[string]any{ "content": rows, } return a.sendMessagePayload(ctx, msg, payload, seq) @@ -643,7 +643,7 @@ func (a *adapter) sendMessagePayload(ctx context.Context, msg bot.OutboundMessag url := a.qqSendURL(msg) body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return bot.SendResult{}, err } diff --git a/internal/bot/render_test.go b/internal/bot/render_test.go index fb93805b76..8e28d037d4 100644 --- a/internal/bot/render_test.go +++ b/internal/bot/render_test.go @@ -253,7 +253,7 @@ func TestRenderSinkLimitsProgressMessages(t *testing.T) { adapter := newFakeAdapter(PlatformWeixin, "fake-weixin") sink := newRenderSink(context.Background(), adapter, "weixin-weixin", "weixin", "chat-1", ChatDM, "user-1", "msg-1", slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil) - for i := 0; i < renderMaxProgressMessages+2; i++ { + for range renderMaxProgressMessages + 2 { sink.lastProgress = time.Now().Add(-renderProgressMinInterval) sink.Emit(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: "tool", Name: "bash"}}) } diff --git a/internal/bot/session.go b/internal/bot/session.go index 8f7dcd9efa..ed8dbe14e7 100644 --- a/internal/bot/session.go +++ b/internal/bot/session.go @@ -319,11 +319,8 @@ func (sm *SessionManager) consumeDroppedPrefixLocked(key, text string) string { fmt.Fprintf(&b, "[Queue note: %d older pending message(s) were dropped because this bot session reached its queue cap.", len(dropped)) if len(dropped) > 0 { b.WriteString(" Dropped summaries:") - limit := len(dropped) - if limit > 3 { - limit = 3 - } - for i := 0; i < limit; i++ { + limit := min(len(dropped), 3) + for i := range limit { fmt.Fprintf(&b, "\n- %s", dropped[i]) } if len(dropped) > limit { diff --git a/internal/bot/turn_dispatch_test.go b/internal/bot/turn_dispatch_test.go index 5c583cc164..bc8da7882a 100644 --- a/internal/bot/turn_dispatch_test.go +++ b/internal/bot/turn_dispatch_test.go @@ -89,8 +89,7 @@ func TestGatewayApprovalReplyUnblocksTurnOffDispatchGoroutine(t *testing.T) { // building a real one via boot.Build. gw.controllers[key] = &sessionState{ctrl: ctrl, sink: &sessionEventSink{}} - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go gw.dispatchLoop(ctx, binding) // First message: a normal turn that will block on approval inside RunTurn. diff --git a/internal/bot/types.go b/internal/bot/types.go index adaeee4c7b..f5b45d1df2 100644 --- a/internal/bot/types.go +++ b/internal/bot/types.go @@ -4,6 +4,7 @@ package bot import ( "context" + "slices" "strings" ) @@ -144,10 +145,8 @@ func (r SendResult) DeliveredMessageIDs() []string { if id == "" { return } - for _, existing := range ids { - if existing == id { - return - } + if slices.Contains(ids, id) { + return } ids = append(ids, id) } @@ -162,13 +161,7 @@ func (r SendResult) DeliveredMessageIDs() []string { // last delivered ID for callers using the legacy singular field. func (r *SendResult) Merge(delivered SendResult) { for _, id := range delivered.DeliveredMessageIDs() { - duplicate := false - for _, existing := range r.MessageIDs { - if existing == id { - duplicate = true - break - } - } + duplicate := slices.Contains(r.MessageIDs, id) if !duplicate { r.MessageIDs = append(r.MessageIDs, id) } diff --git a/internal/bot/weixin/weixin.go b/internal/bot/weixin/weixin.go index 31e543084f..f045ca212f 100644 --- a/internal/bot/weixin/weixin.go +++ b/internal/bot/weixin/weixin.go @@ -289,7 +289,7 @@ func (a *adapter) saveContextTokens() { } func ilinkGET(ctx context.Context, baseURL, endpoint string) (map[string]any, error) { - req, err := http.NewRequestWithContext(ctx, "GET", strings.TrimRight(baseURL, "/")+"/"+strings.TrimLeft(endpoint, "/"), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(baseURL, "/")+"/"+strings.TrimLeft(endpoint, "/"), nil) if err != nil { return nil, err } @@ -358,7 +358,7 @@ func (a *adapter) getUpdates(ctx context.Context) ([]ilinkUpdate, error) { url := a.apiBase() + getUpdatesPath a.mu.Lock() - payload := map[string]interface{}{ + payload := map[string]any{ "get_updates_buf": a.syncBuf, "base_info": map[string]string{ "channel_version": ilinkChannelVersion, @@ -367,7 +367,7 @@ func (a *adapter) getUpdates(ctx context.Context) ([]ilinkUpdate, error) { a.mu.Unlock() body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return nil, err } @@ -542,9 +542,9 @@ func setIlinkHeaders(req *http.Request, token string, body []byte) { func randomWechatUIN() string { var b [4]byte if _, err := rand.Read(b[:]); err != nil { - return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + return base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", time.Now().UnixNano())) } - return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", uint32(b[0])<<24|uint32(b[1])<<16|uint32(b[2])<<8|uint32(b[3])))) + return base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", uint32(b[0])<<24|uint32(b[1])<<16|uint32(b[2])<<8|uint32(b[3]))) } func firstNonEmptyString(vals ...string) string { @@ -565,27 +565,27 @@ func (a *adapter) sendMessage(ctx context.Context, msg bot.OutboundMessage) (bot url := a.apiBase() + sendMessagePath - payload := map[string]interface{}{ + payload := map[string]any{ "base_info": map[string]string{"channel_version": ilinkChannelVersion}, - "msg": map[string]interface{}{ + "msg": map[string]any{ "from_user_id": "", "to_user_id": msg.ChatID, "client_id": fmt.Sprintf("reasonix-%d", time.Now().UnixNano()), "message_type": weixinMsgTypeBot, "message_state": weixinMsgStateDone, - "item_list": []map[string]interface{}{ + "item_list": []map[string]any{ {"type": weixinItemText, "text_item": map[string]string{"text": msg.Text}}, }, }, } if contextToken := a.contextToken(msg.ChatID); contextToken != "" { - if m, ok := payload["msg"].(map[string]interface{}); ok { + if m, ok := payload["msg"].(map[string]any); ok { m["context_token"] = contextToken } } body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return bot.SendResult{}, err } @@ -627,7 +627,7 @@ func (a *adapter) sendTyping(ctx context.Context, chatID string) error { url := a.apiBase() + sendTypingPath - payload := map[string]interface{}{ + payload := map[string]any{ "base_info": map[string]string{"channel_version": ilinkChannelVersion}, "ilink_user_id": chatID, "status": 1, @@ -637,7 +637,7 @@ func (a *adapter) sendTyping(ctx context.Context, chatID string) error { } body, _ := json.Marshal(payload) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) if err != nil { return err } diff --git a/internal/capability/capability.go b/internal/capability/capability.go index 23724176e4..c1bf98329c 100644 --- a/internal/capability/capability.go +++ b/internal/capability/capability.go @@ -218,10 +218,7 @@ func limitRouteCandidates(candidates []RouteCandidate) []RouteCandidate { suggested = append(suggested, candidate) } } - slots := targetCandidates - len(strong) - if slots < 0 { - slots = 0 - } + slots := max(targetCandidates-len(strong), 0) if len(suggested) > slots { suggested = suggested[:slots] } diff --git a/internal/capability/capability_test.go b/internal/capability/capability_test.go index 98e01678ca..357d5e0da6 100644 --- a/internal/capability/capability_test.go +++ b/internal/capability/capability_test.go @@ -71,7 +71,7 @@ func TestRouteRespectsSkillAutoUseMetadata(t *testing.T) { func TestRouteKeepsAllStrongCandidatesBeforeSuggestBudget(t *testing.T) { entries := make([]Entry, 0, 8) - for i := 0; i < 6; i++ { + for i := range 6 { entries = append(entries, Entry{ID: fmt.Sprintf("skill:required-%d", i), Kind: KindSkill, Name: fmt.Sprintf("required-%d", i), AutoUse: AutoUsePrefer, Triggers: []string{"ship"}}) } entries = append(entries, diff --git a/internal/capability/semantic.go b/internal/capability/semantic.go index 3cd051b028..07ef888e67 100644 --- a/internal/capability/semantic.go +++ b/internal/capability/semantic.go @@ -106,7 +106,7 @@ func semanticPool(text string, entries []Entry) []Entry { // bounded built-in/high-policy Skill set so English metadata does not make // the semantic router blind to Chinese requests. matched := false - for _, tok := range strings.Fields(text) { + for tok := range strings.FieldsSeq(text) { if len(tok) < 3 { continue } diff --git a/internal/checkpoint/checkpoint.go b/internal/checkpoint/checkpoint.go index 73cab16894..ca0909ca0f 100644 --- a/internal/checkpoint/checkpoint.go +++ b/internal/checkpoint/checkpoint.go @@ -20,6 +20,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "sort" "strings" "sync" @@ -522,9 +523,6 @@ func (s *Store) CaptureAfter(path string, opts CaptureAfterOpts) { } // Update after fingerprint on the most recent snap of this path. updated := false - for i := len(s.curFilesLocked()) - 1; i >= 0; i-- { - // search in cur first, then done reverse - } if s.cur != nil { for i := range s.cur.Files { if NormalizeRelPath(s.root, s.cur.Files[i].Path) != pathKey { @@ -546,8 +544,8 @@ func (s *Store) CaptureAfter(path string, opts CaptureAfterOpts) { // Path might only appear in earlier turns; still record after on earliest? // Ownership after is per-path last write — update the latest checkpoint that // has this path. - for i := len(s.done) - 1; i >= 0; i-- { - c := s.done[i] + for _, v := range slices.Backward(s.done) { + c := v for j := range c.Files { if NormalizeRelPath(s.root, c.Files[j].Path) != pathKey { continue @@ -563,13 +561,6 @@ func (s *Store) CaptureAfter(path string, opts CaptureAfterOpts) { } } -func (s *Store) curFilesLocked() []FileSnap { - if s.cur == nil { - return nil - } - return s.cur.Files -} - // RecordGap appends a coverage gap to the current checkpoint. func (s *Store) RecordGap(gap CoverageGap) { if s == nil { diff --git a/internal/checkpoint/checkpoint_test.go b/internal/checkpoint/checkpoint_test.go index 44e40208cb..73e23ae526 100644 --- a/internal/checkpoint/checkpoint_test.go +++ b/internal/checkpoint/checkpoint_test.go @@ -510,7 +510,7 @@ func BenchmarkRestoreGB18030Encoding(b *testing.B) { b.SetBytes(int64(len(originalRaw))) b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { if err := os.WriteFile(a, editedRaw, 0o644); err != nil { b.Fatal(err) } diff --git a/internal/checkpoint/transaction.go b/internal/checkpoint/transaction.go index 10c7064830..74dac05d56 100644 --- a/internal/checkpoint/transaction.go +++ b/internal/checkpoint/transaction.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "sort" "strings" "time" @@ -971,8 +972,8 @@ func (s *Store) publishTarget(t *TransactionTarget) error { func (s *Store) compensatePublished(targets []TransactionTarget, stages []FileStage) error { var first error - for i := len(targets) - 1; i >= 0; i-- { - t := targets[i] + for _, v := range slices.Backward(targets) { + t := v if !t.Published { if t.PublishTmp != "" { _ = secureRemove(s.root, t.PublishTmp) @@ -1519,7 +1520,7 @@ func (s *Store) restoreCheckpointBackup(backup []byte) error { } func newID(prefix string) string { - return fmt.Sprintf("%s-%d-%s", prefix, time.Now().UnixNano(), Digest([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))[:8]) + return fmt.Sprintf("%s-%d-%s", prefix, time.Now().UnixNano(), Digest(fmt.Appendf(nil, "%d", time.Now().UnixNano()))[:8]) } // RestoreCheckpointBackupPublic reloads backed-up checkpoints after an undo. diff --git a/internal/cli/bot_test.go b/internal/cli/bot_test.go index 9b82a32b5f..34e18726b3 100644 --- a/internal/cli/bot_test.go +++ b/internal/cli/bot_test.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "strings" "testing" @@ -492,10 +493,5 @@ func isolateBotUserConfig(t *testing.T) { } func hasTestString(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } diff --git a/internal/cli/box.go b/internal/cli/box.go index 7c25b8722e..37481d38e3 100644 --- a/internal/cli/box.go +++ b/internal/cli/box.go @@ -44,10 +44,7 @@ func boxed(lines []string) string { b.WriteString(accent("╭" + bar + "╮")) b.WriteByte('\n') for _, l := range lines { - gap := inner - visibleWidth(l) - 2 - if gap < 0 { - gap = 0 - } + gap := max(inner-visibleWidth(l)-2, 0) b.WriteString(accent("│")) b.WriteByte(' ') b.WriteString(l) diff --git a/internal/cli/chat_ghost_test.go b/internal/cli/chat_ghost_test.go index 339124c728..c492b1ae77 100644 --- a/internal/cli/chat_ghost_test.go +++ b/internal/cli/chat_ghost_test.go @@ -17,7 +17,7 @@ func TestClampWidth(t *testing.T) { // Over width: every resulting line fits, content is preserved. long := strings.Repeat("x", 200) out := clampWidth(long, 40) - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { if visibleWidth(line) > 40 { t.Errorf("clamped line exceeds 40: width=%d", visibleWidth(line)) } diff --git a/internal/cli/chat_render_test.go b/internal/cli/chat_render_test.go index 300b41a5cb..d8eccd7dd3 100644 --- a/internal/cli/chat_render_test.go +++ b/internal/cli/chat_render_test.go @@ -488,7 +488,7 @@ func TestTodoPanelKeepsLastSuccessfulTodoWrite(t *testing.T) { func TestToolProgressTailCap(t *testing.T) { m := newTestChatTUI() m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: "b1", Name: "bash", Args: `{"command":"x"}`}}) - for i := 0; i < toolStreamTailLines+5; i++ { + for i := range toolStreamTailLines + 5 { m.ingestEvent(event.Event{Kind: event.ToolProgress, Tool: event.Tool{ID: "b1", Output: "line" + string(rune('A'+i)) + "\n"}}) } block := m.transcript[m.toolStreamIdx] @@ -504,7 +504,7 @@ func TestToolProgressTailCap(t *testing.T) { // long stream — the fix for the O(n²)/multi-GB re-render of the full thought. func TestReasoningViewBounded(t *testing.T) { m := newTestChatTUI() - for i := 0; i < 5000; i++ { + for range 5000 { m.ingestEvent(event.Event{Kind: event.Reasoning, Text: "some thinking text token "}) } if len(m.reasoningView) > reasoningViewMax { diff --git a/internal/cli/chat_tui.go b/internal/cli/chat_tui.go index 382b468971..9d12371a98 100644 --- a/internal/cli/chat_tui.go +++ b/internal/cli/chat_tui.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" "time" @@ -1669,8 +1670,8 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { } // "!" runs a shell command directly, bypassing the model. - if strings.HasPrefix(line, "!") { - cmd := strings.TrimPrefix(line, "!") + if after, ok := strings.CutPrefix(line, "!"); ok { + cmd := after if strings.TrimSpace(cmd) == "" { m.input.Reset() m.pastedBlocks = nil @@ -1746,7 +1747,7 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { // when bash output or reasoning floods in. Capped so a sustained flood // still yields to render periodically. drain: - for drained := 0; drained < maxEventDrain; drained++ { + for range maxEventDrain { select { case e2 := <-m.eventCh: m.noteWatchdogHeartbeat(watchdogAgentSource(e2.Kind)) @@ -2089,10 +2090,7 @@ func chunkLines(s string, n int) []string { } var out []string for i := 0; i < len(lines); i += n { - end := i + n - if end > len(lines) { - end = len(lines) - } + end := min(i+n, len(lines)) out = append(out, strings.Join(lines[i:end], "\n")) } return out @@ -2315,13 +2313,10 @@ func (m *chatTUI) streamReasoning(chunk string) { // positive maxLines keeps only the trailing visual lines (the live view); 0 // renders all (verbose collapse). func reasoningBlock(raw string, width, maxLines int) string { - w := width - len([]rune(connector)) - if w < 8 { - w = 8 - } + w := max(width-len([]rune(connector)), 8) var lines []string - for _, ln := range strings.Split(strings.TrimRight(raw, "\n"), "\n") { - for _, wl := range strings.Split(ansi.Wrap(expandTabs(ln), w, ""), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(raw, "\n"), "\n") { + for wl := range strings.SplitSeq(ansi.Wrap(expandTabs(ln), w, ""), "\n") { lines = append(lines, dim(wl)) } } @@ -2617,18 +2612,15 @@ func (m *chatTUI) subagentProgressBlock(id string, sp *cliSubagentProgress) stri // subagentPreviewBlock renders a bounded trailing window of a preview channel // as dim, width-wrapped lines carrying a small glyph marker. func subagentPreviewBlock(glyph, raw string, width, maxLines int) string { - w := width - len([]rune(connector)) - if w < 8 { - w = 8 - } + w := max(width-len([]rune(connector)), 8) var lines []string first := true - for _, ln := range strings.Split(strings.TrimRight(raw, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(raw, "\n"), "\n") { if first { ln = glyph + " " + ln first = false } - for _, wl := range strings.Split(ansi.Wrap(expandTabs(ln), w, ""), "\n") { + for wl := range strings.SplitSeq(ansi.Wrap(expandTabs(ln), w, ""), "\n") { lines = append(lines, dim(wl)) } } @@ -2712,7 +2704,7 @@ func (m *chatTUI) collapseToolOutput(id, resultOutput string) { total := len(lines) if total > shellPreviewLines { preview := make([]string, shellPreviewLines+1) - for i := 0; i < shellPreviewLines; i++ { + for i := range shellPreviewLines { preview[i] = dim(clampPlain(lines[i], m.width-len([]rune(connector)))) } preview[shellPreviewLines] = dim(fmt.Sprintf("… %d more lines (Ctrl+B)", total-shellPreviewLines)) @@ -2801,7 +2793,7 @@ func (m *chatTUI) collapseShellSlot(id string, idx int, resultOutput string) { total := len(lines) if total > shellPreviewLines { preview := make([]string, shellPreviewLines+1) - for i := 0; i < shellPreviewLines; i++ { + for i := range shellPreviewLines { preview[i] = dim(clampPlain(lines[i], m.width-len([]rune(connector)))) } preview[shellPreviewLines] = dim(fmt.Sprintf("… %d more lines (Ctrl+B)", total-shellPreviewLines)) @@ -2851,7 +2843,7 @@ func (m *chatTUI) toggleShellOutput() { m.shellExpanded[lastID] = false if total > shellPreviewLines { preview := make([]string, shellPreviewLines+1) - for i := 0; i < shellPreviewLines; i++ { + for i := range shellPreviewLines { preview[i] = dim(clampPlain(lines[i], innerW)) } preview[shellPreviewLines] = dim(fmt.Sprintf("… %d more lines (Ctrl+B)", total-shellPreviewLines)) @@ -2860,12 +2852,9 @@ func (m *chatTUI) toggleShellOutput() { } else { // Expand: show up to shellExpandMaxLines lines. m.shellExpanded[lastID] = true - show := total - if show > shellExpandMaxLines { - show = shellExpandMaxLines - } + show := min(total, shellExpandMaxLines) rendered := make([]string, show) - for i := 0; i < show; i++ { + for i := range show { rendered[i] = dim(clampPlain(lines[i], innerW)) } if total > shellExpandMaxLines { @@ -3140,7 +3129,7 @@ func approvalChoiceLabels(a *event.Approval) []string { choices = fmt.Sprintf(i18n.M.BashPrefixChoices, prefixRule, prefixRule) } var labels []string - for _, line := range strings.Split(choices, "\n") { + for line := range strings.SplitSeq(choices, "\n") { line = strings.TrimSpace(line) if len(line) < 3 || line[0] < '1' || line[0] > '9' || line[1] != '.' { continue @@ -3331,10 +3320,7 @@ func (m chatTUI) View() tea.View { } return v } - boxW := m.width - if boxW < 10 { - boxW = 10 - } + boxW := max(m.width, 10) hideComposer := m.hideComposer() shellMode := strings.HasPrefix(strings.TrimSpace(m.input.Value()), "!") cancelRequested := m.cancelRequested() @@ -3522,7 +3508,7 @@ func compactionCardLines(c event.Compaction) []string { } header := fmt.Sprintf("%s · %d %s · %s", i18n.M.CompactionTitle, c.Messages, i18n.M.CompactionUnit, trigger) lines := []string{accent("◆ " + header)} - for _, ln := range strings.Split(strings.TrimRight(c.Summary, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(c.Summary, "\n"), "\n") { lines = append(lines, dim(" │ "+ln)) } if c.Archive != "" { @@ -3556,10 +3542,7 @@ func (m chatTUI) contextTag() string { } threshold := int(ratio * 100) // Headroom to the compaction point, as a percentage of the window (clamped at 0). - left := threshold - pct - if left < 0 { - left = 0 - } + left := max(threshold-pct, 0) body := fmt.Sprintf("%s ctx (%d%%) · %d%% to compact", shortTokens(used), pct, left) switch { case pct >= threshold: @@ -3674,10 +3657,7 @@ func shortTokens(n int) string { // renderApprovalBanner is the slim notice shown above the input while a tool // call (or a plan) awaits the user's decision. func (m chatTUI) renderApprovalBanner() string { - w := m.width - if w < 10 { - w = 10 - } + w := max(m.width, 10) if m.pendingApproval == nil { return "" } @@ -3736,10 +3716,7 @@ func approvalSubjectBody(full, preview string, width int) string { if full == "" || full == preview { return "" } - wrapWidth := width - 4 - if wrapWidth < 20 { - wrapWidth = 20 - } + wrapWidth := max(width-4, 20) lines := strings.Split(wrapStatusLine(full, wrapWidth), "\n") if len(lines) > maxApprovalSubjectLines { lines = lines[:maxApprovalSubjectLines] @@ -3878,10 +3855,7 @@ func todoPanelWindow(todos []todoPanelTodo) (int, int) { if active < 0 { return 0, todoPanelMaxRows } - start := active - todoPanelMaxRows/2 - if start < 0 { - start = 0 - } + start := max(active-todoPanelMaxRows/2, 0) if maxStart := len(todos) - todoPanelMaxRows; start > maxStart { start = maxStart } @@ -4010,13 +3984,7 @@ func (m *chatTUI) growInputToFit() { if m.input.DynamicHeight { return } - lines := strings.Count(m.input.Value(), "\n") + 1 - if lines < 1 { - lines = 1 - } - if lines > maxInputRows { - lines = maxInputRows - } + lines := min(max(strings.Count(m.input.Value(), "\n")+1, 1), maxInputRows) if lines != m.input.Height() { m.input.SetHeight(lines) } @@ -4952,7 +4920,7 @@ func (m *chatTUI) runCopyCommand(input string) tea.Cmd { // firstLine returns the first non-empty line of s, truncated to 80 runes. func firstLine(s string) string { - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if t := strings.TrimSpace(line); t != "" { runes := []rune(t) if len(runes) > 80 { @@ -4969,8 +4937,8 @@ func firstLine(s string) string { // The result is chronological (oldest first). func copyAssistantParts(msgs []provider.Message) []string { lastUserIdx := -1 - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleUser { + for i, v := range slices.Backward(msgs) { + if v.Role == provider.RoleUser { lastUserIdx = i break } diff --git a/internal/cli/chat_tui_paste.go b/internal/cli/chat_tui_paste.go index 71b3d8e12e..5702abeb38 100644 --- a/internal/cli/chat_tui_paste.go +++ b/internal/cli/chat_tui_paste.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" @@ -129,11 +130,11 @@ func recoverOrphanedPasteLabelsFromHistory(sent string, knownBlocks []pastedBloc var recovered string found := false ambiguous := false - for i := len(history) - 1; i >= 0; i-- { - if history[i].Role != provider.RoleUser { + for _, v := range slices.Backward(history) { + if v.Role != provider.RoleUser { continue } - for _, body := range expandedPasteBodies(history[i].Content, label) { + for _, body := range expandedPasteBodies(v.Content, label) { if !found { recovered = body found = true @@ -246,10 +247,7 @@ func (m *chatTUI) takeNextPasteID() int { if m.ctrl != nil { m.syncPasteIDStateFromHistory(m.ctrl.History()) } - candidate := m.nextPasteID - if candidate < 1 { - candidate = 1 - } + candidate := max(m.nextPasteID, 1) if m.usedPasteIDs == nil { m.usedPasteIDs = make(map[int]struct{}) } @@ -519,7 +517,7 @@ func splitPastePathTokens(s string) []string { b.Reset() } } - for i := 0; i < len(s); i++ { + for i := range len(s) { ch := s[i] switch { case escaped: @@ -548,7 +546,7 @@ func splitPastePathTokens(s string) []string { func nonEmptyPasteLines(text string) []string { var out []string - for _, line := range strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n") { + for line := range strings.SplitSeq(strings.ReplaceAll(text, "\r\n", "\n"), "\n") { line = strings.TrimSpace(line) if line != "" { out = append(out, line) @@ -629,7 +627,7 @@ func pastedImagePathForOS(src, goos string) (string, bool) { func hasUnescapedPathWhitespace(s string) bool { escaped := false - for i := 0; i < len(s); i++ { + for i := range len(s) { ch := s[i] if escaped { escaped = false diff --git a/internal/cli/chat_tui_test.go b/internal/cli/chat_tui_test.go index c67ab41376..5bc317b7cf 100644 --- a/internal/cli/chat_tui_test.go +++ b/internal/cli/chat_tui_test.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -547,7 +548,7 @@ func TestTranscriptResizeRerendersCommittedMarkdownAtNewWidth(t *testing.T) { newLines := strings.Count(newRendered, "\n") + 1 ruleWidth := 0 - for _, line := range strings.Split(newRendered, "\n") { + for line := range strings.SplitSeq(newRendered, "\n") { trimmed := strings.TrimSpace(line) if trimmed != "" && strings.Trim(trimmed, "─") == "" { ruleWidth = visibleWidth(trimmed) @@ -988,7 +989,7 @@ func TestRewindPickerWindowsLongSession(t *testing.T) { if strings.Contains(card, "more") { t.Fatalf("short session must not show more markers: %q", card) } - for i := 0; i < 4; i++ { + for i := range 4 { if !strings.Contains(card, fmt.Sprintf("turn %d", i)) { t.Fatalf("short session row %d missing: %q", i, card) } @@ -1497,13 +1498,7 @@ func TestInsertNewlineKeyBinding(t *testing.T) { ctrl := control.New(control.Options{}) m := newChatTUI(ctrl, "", make(chan event.Event, 1), 80) keys := m.input.KeyMap.InsertNewline.Keys() - found := false - for _, k := range keys { - if k == "shift+enter" { - found = true - break - } - } + found := slices.Contains(keys, "shift+enter") if !found { t.Errorf("newChatTUI InsertNewline should include shift+enter, got %v", keys) } @@ -1519,7 +1514,7 @@ func TestCtrlHomeEndScrollKeyBindings(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = adv(cur, notice) } // Viewport should be at the bottom after output. @@ -1550,7 +1545,7 @@ func TestMouseWheelAndPageKeysScrollTranscript(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } if !cur.viewport.AtBottom() { @@ -1593,7 +1588,7 @@ func TestRunningStreamPreservesScrolledReadingPosition(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } cur.state = tuiRunning @@ -1630,7 +1625,7 @@ func TestTranscriptScrollbarClickAndDrag(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } cur.viewport.GotoTop() @@ -3028,7 +3023,7 @@ func TestTranscriptTailFollow(t *testing.T) { notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { // overflow the short viewport so there's room to scroll + for range 12 { // overflow the short viewport so there's room to scroll cur = adv(cur, notice) } if !cur.viewport.AtBottom() { @@ -3061,7 +3056,7 @@ func TestEmptyEnterScrollsToBottom(t *testing.T) { // idle state t.Run("idle", func(t *testing.T) { cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = adv(cur, notice) } // Scroll up to leave the bottom. @@ -3079,7 +3074,7 @@ func TestEmptyEnterScrollsToBottom(t *testing.T) { // running state t.Run("running", func(t *testing.T) { cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = adv(cur, notice) } cur.state = tuiRunning @@ -3110,7 +3105,7 @@ func TestForceGotoBottomScrollsWithoutTranscriptChange(t *testing.T) { } cur := next(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = next(cur, notice) } if !cur.viewport.AtBottom() { @@ -3151,7 +3146,7 @@ func TestSessionSwitchSuppressesOneClearScreen(t *testing.T) { } cur := next(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) - for i := 0; i < 12; i++ { + for range 12 { cur = next(cur, notice) } cur = next(cur, tea.MouseWheelMsg{Button: tea.MouseWheelUp}) @@ -4062,10 +4057,7 @@ func TestTruncateSubject(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := truncateSubject(tc.input, tc.width) - wantMax := tc.width - 28 - if wantMax < 16 { - wantMax = 16 - } + wantMax := max(tc.width-28, 16) w := ansi.StringWidth(got) if w > wantMax { t.Errorf("truncateSubject(%q, %d) = %q (width %d), want visible width <= %d", tc.input, tc.width, got, w, wantMax) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index a4d9fa5cc9..083b4a3b4c 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -19,6 +19,7 @@ import ( "os" "os/signal" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -1446,7 +1447,7 @@ func prepareNativeScrollback(w io.Writer, rows int) { } func reserveNativeScrollbackFrame(w io.Writer, rows int) { - for i := 0; i < rows; i++ { + for range rows { fmt.Fprintln(w) } } @@ -1783,12 +1784,7 @@ func buildFamilyEntry(probe config.ProviderEntry, selected []string) config.Prov } func containsString(xs []string, v string) bool { - for _, x := range xs { - if x == v { - return true - } - } - return false + return slices.Contains(xs, v) } // filterStaleCustomEntries drops the wizard's own magic-name entries @@ -2317,7 +2313,7 @@ func appendEnv(path string, lines []string) error { var kept []string if data, err := fileencoding.ReadFileUTF8(path); err == nil { - for _, raw := range strings.Split(string(data), "\n") { + for raw := range strings.SplitSeq(string(data), "\n") { trimmed := strings.TrimSpace(raw) check := strings.TrimPrefix(trimmed, "export ") if k, _, ok := strings.Cut(check, "="); ok && target[strings.TrimSpace(k)] { diff --git a/internal/cli/cli_flags.go b/internal/cli/cli_flags.go index 23cc930706..6a6777177f 100644 --- a/internal/cli/cli_flags.go +++ b/internal/cli/cli_flags.go @@ -198,7 +198,7 @@ func looksLikeMachineSessionID(query string) bool { if len(hexPart) != 32 { return false } - for i := 0; i < len(hexPart); i++ { + for i := range len(hexPart) { c := hexPart[i] if (c < '0' || c > '9') && (c < 'a' || c > 'f') { return false diff --git a/internal/cli/complete.go b/internal/cli/complete.go index 416a9d78e3..2244a9dc21 100644 --- a/internal/cli/complete.go +++ b/internal/cli/complete.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" @@ -144,8 +145,8 @@ func renameSlashItem(items []compItem, oldLabel, newLabel string) []compItem { continue } items[i].label = newLabel - if strings.HasPrefix(items[i].insert, oldLabel) { - items[i].insert = newLabel + strings.TrimPrefix(items[i].insert, oldLabel) + if after, ok := strings.CutPrefix(items[i].insert, oldLabel); ok { + items[i].insert = newLabel + after } break } @@ -225,10 +226,7 @@ func (m *chatTUI) inputCursorByteOffset() int { // cur.X is screen-relative and includes the "❯ " prompt // gutter (composerPromptWidth columns). Subtract it so // we measure content columns only. - col := cur.X - composerPromptWidth - if col < 0 { - col = 0 - } + col := max(cur.X-composerPromptWidth, 0) visual := 0 for _, cell := range row.cells { w := rw.RuneWidth(cell.r) @@ -505,13 +503,7 @@ func activeAtToken(val string, cursor int) (at, end int, query string, ok bool) case '@': if i == 0 || val[i-1] == ' ' || val[i-1] == '\t' || val[i-1] == '\n' { end = tokenEnd(val, i+1) - queryEnd := cursor - if queryEnd < i+1 { - queryEnd = i + 1 - } - if queryEnd > end { - queryEnd = end - } + queryEnd := min(max(cursor, i+1), end) return i, end, val[i+1 : queryEnd], true } return 0, 0, "", false @@ -608,10 +600,7 @@ func (m *chatTUI) fileItems(token string) []compItem { for _, it := range items { seen[strings.TrimPrefix(it.insert, "@")] = true } - remaining := maxCompItems - len(items) - if remaining > maxFileSearchItems { - remaining = maxFileSearchItems - } + remaining := min(maxCompItems-len(items), maxFileSearchItems) results := m.searchFileRefs(fsFrag) if len(results) > remaining { results = results[:remaining] @@ -669,12 +658,7 @@ func (m *chatTUI) isMCPServer(name string) bool { if m.host == nil { return false } - for _, s := range m.host.ServerNames() { - if s == name { - return true - } - } - return false + return slices.Contains(m.host.ServerNames(), name) } // resourceItems lists MCP resources as @server:uri completions. When server is @@ -835,18 +819,9 @@ func (m chatTUI) renderCompletion() string { items := m.completion.items start := 0 if len(items) > maxCompRows { - start = m.completion.sel - maxCompRows/2 - if start < 0 { - start = 0 - } - if start > len(items)-maxCompRows { - start = len(items) - maxCompRows - } - } - end := start + maxCompRows - if end > len(items) { - end = len(items) + start = min(max(m.completion.sel-maxCompRows/2, 0), len(items)-maxCompRows) } + end := min(start+maxCompRows, len(items)) var b strings.Builder for i := start; i < end; i++ { diff --git a/internal/cli/complete_test.go b/internal/cli/complete_test.go index bb62f9ca81..4f618a1362 100644 --- a/internal/cli/complete_test.go +++ b/internal/cli/complete_test.go @@ -367,7 +367,7 @@ func TestFileItemsSearchRespectsMenuCap(t *testing.T) { defer os.Chdir(orig) dir := t.TempDir() - for i := 0; i < maxCompItems; i++ { + for i := range maxCompItems { writeAt(t, dir, filepath.Join("aa-dir-"+fmt.Sprintf("%03d", i), "file.txt"), "x") } writeAt(t, dir, "nested/aa-deep.js", "y") diff --git a/internal/cli/composer_selection.go b/internal/cli/composer_selection.go index 655a4c1d81..397e92d71e 100644 --- a/internal/cli/composer_selection.go +++ b/internal/cli/composer_selection.go @@ -383,10 +383,7 @@ func (m *chatTUI) composerCaretAt(screenX, screenY int, clamp bool) (composerCar relY = m.input.Height() - 1 } rows := m.composerRows() - visualRow := m.composerViewOffset() + relY - if visualRow < 0 { - visualRow = 0 - } + visualRow := max(m.composerViewOffset()+relY, 0) if visualRow >= len(rows) { visualRow = len(rows) - 1 } @@ -398,7 +395,7 @@ func (m *chatTUI) setComposerCursor(offset int) { rows := m.composerRows() caret := composerCaretForOffset(rows, offset) m.input.MoveToBegin() - for i := 0; i < caret.visualRow; i++ { + for range caret.visualRow { m.input.CursorDown() } m.input.SetCursorColumn(caret.logicalCol) diff --git a/internal/cli/consecutive_tool_markers_test.go b/internal/cli/consecutive_tool_markers_test.go index 80206e3f3b..8cd49e711d 100644 --- a/internal/cli/consecutive_tool_markers_test.go +++ b/internal/cli/consecutive_tool_markers_test.go @@ -21,7 +21,7 @@ func TestParallelBashMarkersKeepOwnLineCount(t *testing.T) { m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: id, Name: "bash", Args: `{"command":"ls"}`, Partial: false}}) } for _, id := range ids { - for i := 0; i < 22; i++ { + for range 22 { m.ingestEvent(event.Event{Kind: event.ToolProgress, Tool: event.Tool{ID: id, Output: "line\n"}}) } } diff --git a/internal/cli/diffview.go b/internal/cli/diffview.go index 23ec0fcf41..e4dcd50c0b 100644 --- a/internal/cli/diffview.go +++ b/internal/cli/diffview.go @@ -144,19 +144,13 @@ func diffBody(d event.FileDiff, path string, width, maxLines int) []string { // bar mid-line — and padded to the bar width so it runs edge to edge. func diffBar(sign byte, code, path string, width int, bg, signFg string, lineNo, gw int) string { gutter := dim(lpad(strconv.Itoa(lineNo), gw)) - barW := width - 2 - gw - 1 - if barW < 4 { - barW = 4 - } + barW := max(width-2-gw-1, 4) code = clampPlain(code, barW-2) if !colorOn() { return " " + gutter + " " + string(sign) + " " + code } hl := reapplyBG(highlightCode(path, code), bg) - pad := barW - 2 - visibleWidth(code) - if pad < 0 { - pad = 0 - } + pad := max(barW-2-visibleWidth(code), 0) return " " + gutter + " " + bg + signFg + string(sign) + ansiReset + bg + " " + hl + strings.Repeat(" ", pad) + ansiReset } @@ -232,7 +226,7 @@ func expandTabs(s string) string { for _, r := range s { if r == '\t' { n := tabWidth - col%tabWidth - for i := 0; i < n; i++ { + for range n { b.WriteByte(' ') } col += n diff --git a/internal/cli/diffview_test.go b/internal/cli/diffview_test.go index 62c5994b82..dce587667b 100644 --- a/internal/cli/diffview_test.go +++ b/internal/cli/diffview_test.go @@ -26,7 +26,7 @@ func TestDiffBodyDropsHeadersKeepsLineNumbers(t *testing.T) { func TestDiffBodyFolds(t *testing.T) { var b strings.Builder b.WriteString("--- a/x\n+++ b/x\n@@ -1,8 +1,8 @@\n") - for i := 0; i < 8; i++ { + for range 8 { b.WriteString("+line\n") } body := diffBody(event.FileDiff{Diff: b.String()}, "x", 80, 5) diff --git a/internal/cli/extension_surface.go b/internal/cli/extension_surface.go index dfc28d7091..b88943632b 100644 --- a/internal/cli/extension_surface.go +++ b/internal/cli/extension_surface.go @@ -98,7 +98,7 @@ func extensionCardLines(pluginID string, c *event.ExtensionCardView, width int) body = c.Markdown } } - for _, ln := range strings.Split(strings.TrimRight(body, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(body, "\n"), "\n") { if ln == "" { lines = append(lines, "") continue @@ -131,7 +131,7 @@ func extensionFormLines(pluginID string, f *event.ExtensionFormView) []string { title = pluginID } lines := []string{accent("◆ " + title)} - for _, ln := range strings.Split(strings.TrimRight(f.Message, "\n"), "\n") { + for ln := range strings.SplitSeq(strings.TrimRight(f.Message, "\n"), "\n") { if ln == "" { continue } diff --git a/internal/cli/gitstatus.go b/internal/cli/gitstatus.go index 3c4d2a0348..8fc7a09469 100644 --- a/internal/cli/gitstatus.go +++ b/internal/cli/gitstatus.go @@ -84,7 +84,7 @@ func runGit(ctx context.Context, cwd string, args ...string) (string, error) { } func parseGitNumstat(out string) (added int, removed int) { - for _, line := range strings.Split(strings.TrimSpace(out), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { if line == "" { continue } @@ -108,7 +108,7 @@ func parseGitNumstat(out string) (added int, removed int) { func countUntracked(out string) int { n := 0 - for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { + for line := range strings.SplitSeq(strings.TrimRight(out, "\n"), "\n") { if strings.HasPrefix(line, "?? ") { n++ } diff --git a/internal/cli/machine_identity_test.go b/internal/cli/machine_identity_test.go index 8c1ac4dda8..5f9716c075 100644 --- a/internal/cli/machine_identity_test.go +++ b/internal/cli/machine_identity_test.go @@ -35,13 +35,11 @@ func TestMachineIdentityKeyInitializesOnceAcrossConcurrentReaders(t *testing.T) const readers = 16 results := make(chan result, readers) var wg sync.WaitGroup - for i := 0; i < readers; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range readers { + wg.Go(func() { key, err := loadMachineIdentityKey() results <- result{key: key, err: err} - }() + }) } wg.Wait() close(results) diff --git a/internal/cli/mcp_manager.go b/internal/cli/mcp_manager.go index 5ec0376351..934a20c6d4 100644 --- a/internal/cli/mcp_manager.go +++ b/internal/cli/mcp_manager.go @@ -428,10 +428,7 @@ func visibleRange(total, sel, limit int) (int, int) { if sel >= total { sel = total - 1 } - start := sel - limit/2 - if start < 0 { - start = 0 - } + start := max(sel-limit/2, 0) if start+limit > total { start = total - limit } diff --git a/internal/cli/mcp_manager_view.go b/internal/cli/mcp_manager_view.go index 195b11c745..d577377292 100644 --- a/internal/cli/mcp_manager_view.go +++ b/internal/cli/mcp_manager_view.go @@ -177,10 +177,7 @@ func (p *mcpManager) renderTools(width int) string { if len(v.ToolList) == 0 { b.WriteString(viewMeta("Current connection did not return tool details.") + "\n") } else { - limit := len(v.ToolList) - if limit > mcpToolMaxRows { - limit = mcpToolMaxRows - } + limit := min(len(v.ToolList), mcpToolMaxRows) for _, t := range v.ToolList[:limit] { desc := t.Description if t.SchemaError != "" { diff --git a/internal/cli/mcp_test.go b/internal/cli/mcp_test.go index 5f1f0e61cd..192c4a0b0a 100644 --- a/internal/cli/mcp_test.go +++ b/internal/cli/mcp_test.go @@ -430,7 +430,7 @@ func TestRenderMCPStatusGroupsAndCompactsResources(t *testing.T) { func TestRenderMCPStatusCapsLongSections(t *testing.T) { var resources []plugin.Resource - for i := 0; i < mcpMaxItemsPerSection+2; i++ { + for range mcpMaxItemsPerSection + 2 { resources = append(resources, plugin.Resource{Server: "fs", URI: "file:///tmp/resource.md"}) } got := renderMCPStatus(80, @@ -648,7 +648,7 @@ func TestRenderMCPManagerListCompactsLongNames(t *testing.T) { {Name: "@modelcontextprotocol/server-sequential-thinking", Transport: "stdio", Status: "deferred", Configured: true, Tier: "background"}, }}} got := p.renderList(80) - for _, line := range strings.Split(got, "\n") { + for line := range strings.SplitSeq(got, "\n") { if visibleWidth(line) > 80 { t.Fatalf("line exceeds width 80 (%d): %q\n%s", visibleWidth(line), line, got) } @@ -790,7 +790,7 @@ func TestRenderMCPManagerDetailCompactsConfigPath(t *testing.T) { }, } got := p.renderDetail(80) - for _, line := range strings.Split(got, "\n") { + for line := range strings.SplitSeq(got, "\n") { if visibleWidth(line) > 80 { t.Fatalf("detail line exceeds width 80 (%d): %q\n%s", visibleWidth(line), line, got) } diff --git a/internal/cli/mcp_view.go b/internal/cli/mcp_view.go index 0e8b79be2a..6a4c009d47 100644 --- a/internal/cli/mcp_view.go +++ b/internal/cli/mcp_view.go @@ -87,10 +87,7 @@ func writeMCPServer(b *strings.Builder, width int, s plugin.ServerStatus, prompt } if len(invalidTools) > 0 { b.WriteString(viewSubhead(" unavailable tools") + "\n") - limit := len(invalidTools) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(invalidTools), mcpMaxItemsPerSection) for _, t := range invalidTools[:limit] { writeMCPItem(b, width, " ", sanitizeExternalDisplayText(t.Name), sanitizeExternalDisplayText(t.SchemaError)) } @@ -109,10 +106,7 @@ func writeMCPToolList(b *strings.Builder, width int, s plugin.ServerStatus, tool return } b.WriteString(viewSubhead(" tools") + "\n") - limit := len(tools) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(tools), mcpMaxItemsPerSection) src := sanitizeExternalDisplayText(s.ConfigSource) for _, t := range tools[:limit] { detail := sanitizeExternalDisplayText(t.Description) @@ -162,10 +156,7 @@ func writeMCPFailure(b *strings.Builder, width int, f plugin.Failure) { func writeMCPPromptList(b *strings.Builder, width int, prompts []plugin.Prompt) { b.WriteString(viewSubhead(" prompts") + "\n") - limit := len(prompts) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(prompts), mcpMaxItemsPerSection) for _, p := range prompts[:limit] { writeMCPItem(b, width, " ", "/"+sanitizeExternalDisplayText(p.Name), sanitizeExternalDisplayText(p.Description)) } @@ -176,10 +167,7 @@ func writeMCPPromptList(b *strings.Builder, width int, prompts []plugin.Prompt) func writeMCPResourceList(b *strings.Builder, width int, resources []plugin.Resource) { b.WriteString(viewSubhead(" resources") + "\n") - limit := len(resources) - if limit > mcpMaxItemsPerSection { - limit = mcpMaxItemsPerSection - } + limit := min(len(resources), mcpMaxItemsPerSection) for _, r := range resources[:limit] { label := sanitizeExternalDisplayText(r.Name) if label == "" { diff --git a/internal/cli/md.go b/internal/cli/md.go index 4583ee5047..3dd220a742 100644 --- a/internal/cli/md.go +++ b/internal/cli/md.go @@ -215,10 +215,7 @@ func (r *mdRenderer) renderBlock(buf *strings.Builder, node ast.Node, src []byte case *extast.Table: r.renderTable(buf, n, src, indent) case *ast.ThematicBreak: - w := r.width - indent - if w < 8 { - w = 8 - } + w := max(r.width-indent, 8) buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(dim(strings.Repeat("─", w))) buf.WriteString("\n\n") @@ -256,7 +253,7 @@ func (r *mdRenderer) renderInlineBlock(buf *strings.Builder, n ast.Node, src []b inline := r.collectInline(n, src) prefix := strings.Repeat(" ", indent) wrapped := wrapAnsi(inline, r.width-indent) - for _, line := range strings.Split(wrapped, "\n") { + for line := range strings.SplitSeq(wrapped, "\n") { buf.WriteString(prefix) buf.WriteString(line) buf.WriteString("\n") @@ -311,7 +308,7 @@ func (r *mdRenderer) renderList(buf *strings.Builder, n *ast.List, src []byte, i func (r *mdRenderer) renderFenced(buf *strings.Builder, n ast.Node, src []byte, indent int) { prefix := strings.Repeat(" ", indent) + dim("│ ") - for i := 0; i < n.Lines().Len(); i++ { + for i := range n.Lines().Len() { l := n.Lines().At(i) line := strings.TrimRight(string(l.Value(src)), "\n") buf.WriteString(prefix) @@ -325,7 +322,7 @@ func (r *mdRenderer) renderBlockquote(buf *strings.Builder, n *ast.Blockquote, s var inner strings.Builder r.renderBlocks(&inner, n, src, 0) prefix := strings.Repeat(" ", indent) + dim("▎ ") - for _, line := range strings.Split(strings.TrimRight(inner.String(), "\n"), "\n") { + for line := range strings.SplitSeq(strings.TrimRight(inner.String(), "\n"), "\n") { buf.WriteString(prefix) buf.WriteString(dim(line)) buf.WriteString("\n") @@ -448,20 +445,14 @@ func (r *mdRenderer) renderTable(buf *strings.Builder, n *extast.Table, src []by // widths + separators (3 chars each) + indent. Distribute the budget // proportionally to the natural widths so columns with rich content // keep more space than narrow ones. - available := r.width - indent - 3*(cols-1) - if available < cols*3 { - available = cols * 3 - } + available := max(r.width-indent-3*(cols-1), cols*3) total := 0 for _, w := range widths { total += w } if total > available { for i := range widths { - widths[i] = widths[i] * available / total - if widths[i] < 3 { - widths[i] = 3 - } + widths[i] = max(widths[i]*available/total, 3) } } @@ -493,7 +484,7 @@ func (r *mdRenderer) renderTableRow(buf *strings.Builder, prefix, sep string, ce cols := len(widths) wrapped := make([][]string, cols) maxLines := 1 - for i := 0; i < cols; i++ { + for i := range cols { var text string if i < len(cells) { text = cells[i] @@ -503,9 +494,9 @@ func (r *mdRenderer) renderTableRow(buf *strings.Builder, prefix, sep string, ce maxLines = len(wrapped[i]) } } - for line := 0; line < maxLines; line++ { + for line := range maxLines { buf.WriteString(prefix) - for i := 0; i < cols; i++ { + for i := range cols { if i > 0 { buf.WriteString(sep) } diff --git a/internal/cli/mouse_reenable.go b/internal/cli/mouse_reenable.go index f02c068725..5a051615ad 100644 --- a/internal/cli/mouse_reenable.go +++ b/internal/cli/mouse_reenable.go @@ -52,10 +52,7 @@ func (m *chatTUI) armMouseReenableTimer(now time.Time) tea.Cmd { if m.mouseReenableTimerArmed { return nil } - wait := mouseReenableMinInterval - now.Sub(m.lastMouseReenable) - if wait < 0 { - wait = 0 - } + wait := max(mouseReenableMinInterval-now.Sub(m.lastMouseReenable), 0) m.mouseReenableTimerArmed = true return tea.Tick(wait, func(time.Time) tea.Msg { return mouseReenableMsg{} diff --git a/internal/cli/quick_picker.go b/internal/cli/quick_picker.go index 6c780d0ff4..9c2ecea873 100644 --- a/internal/cli/quick_picker.go +++ b/internal/cli/quick_picker.go @@ -165,10 +165,7 @@ func quickPickerWindow(total, selected int) (int, int) { if total <= quickPickerMaxVisible { return 0, total } - start := selected - quickPickerMaxVisible/2 - if start < 0 { - start = 0 - } + start := max(selected-quickPickerMaxVisible/2, 0) if maxStart := total - quickPickerMaxVisible; start > maxStart { start = maxStart } diff --git a/internal/cli/quick_picker_test.go b/internal/cli/quick_picker_test.go index 1ed8933ed2..73ba8b2bb9 100644 --- a/internal/cli/quick_picker_test.go +++ b/internal/cli/quick_picker_test.go @@ -83,7 +83,7 @@ func TestQuickPickerRendersWithinNarrowPanel(t *testing.T) { }}, } out := p.render(32) - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { if got := visibleWidth(line); got > 32 { t.Fatalf("rendered line width = %d, want <= 32: %q", got, line) } diff --git a/internal/cli/remote_cmd_test.go b/internal/cli/remote_cmd_test.go index b484c1dbd3..994c4290b0 100644 --- a/internal/cli/remote_cmd_test.go +++ b/internal/cli/remote_cmd_test.go @@ -86,7 +86,7 @@ func TestRemoteRemoveCleansGeneratedCredentialsButKeepsUserManagedOnes(t *testin if _, err := config.SetCredential(key, value); err != nil { t.Fatal(err) } - key := key + t.Cleanup(func() { _ = config.RemoveCredential(key) }) } if err := editUserConfig(func(c *config.Config) error { diff --git a/internal/cli/resume_test.go b/internal/cli/resume_test.go index 95271f78f2..e8e32ee69a 100644 --- a/internal/cli/resume_test.go +++ b/internal/cli/resume_test.go @@ -182,7 +182,7 @@ func TestRunResumeKeepsCompletedIndexStableAcrossRecoveryGC(t *testing.T) { func TestCapResumeSessionGroupsDoesNotSplitRecoveryFamily(t *testing.T) { base := time.Date(2026, 8, 3, 12, 0, 0, 0, time.UTC) sessions := make([]agent.SessionInfo, 0, 12) - for i := 0; i < 9; i++ { + for i := range 9 { sessions = append(sessions, agent.SessionInfo{ Path: filepath.Join("/sessions", "standalone-"+strconv.Itoa(i)+".jsonl"), ModTime: base.Add(time.Duration(20-i) * time.Minute), @@ -337,7 +337,7 @@ func TestResumeDispatchSwitchesAndReplays(t *testing.T) { func TestResumeWhileScrolledUpPinsViewportToBottom(t *testing.T) { dir := t.TempDir() active := agent.NewSession("sys") - for i := 0; i < 18; i++ { + for i := range 18 { active.Add(provider.Message{Role: provider.RoleUser, Content: "active prompt " + strconv.Itoa(i)}) } exec := agent.New(nil, nil, active, agent.Options{}, event.Discard) diff --git a/internal/cli/run_metrics.go b/internal/cli/run_metrics.go index 2732f45600..334740b9b4 100644 --- a/internal/cli/run_metrics.go +++ b/internal/cli/run_metrics.go @@ -2,6 +2,7 @@ package cli import ( "encoding/json" + "maps" "os" "strings" "sync" @@ -154,9 +155,7 @@ func cloneCounts(in map[string]int) map[string]int { return nil } out := make(map[string]int, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } @@ -165,9 +164,7 @@ func cloneSourceUsage(in map[string]SourceUsage) map[string]SourceUsage { return nil } out := make(map[string]SourceUsage, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } diff --git a/internal/cli/run_metrics_partial_test.go b/internal/cli/run_metrics_partial_test.go index a2b7194d18..83e6fb30eb 100644 --- a/internal/cli/run_metrics_partial_test.go +++ b/internal/cli/run_metrics_partial_test.go @@ -89,7 +89,7 @@ func TestSnapshotsAreThrottled(t *testing.T) { snapshotEvery: time.Minute, clock: func() time.Time { return now }, } - for i := 0; i < 50; i++ { + for range 50 { s.Emit(usageEvent(event.UsageSourceExecutor, 10, 1)) } raw, err := os.ReadFile(s.partialPath) @@ -189,26 +189,22 @@ func TestConcurrentEmitAndSnapshotAreRaceFree(t *testing.T) { const emitters, each = 8, 50 var wg sync.WaitGroup - for i := 0; i < emitters; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < each; j++ { + for range emitters { + wg.Go(func() { + for range each { s.Emit(usageEventWithCacheReason("compact_auto")) s.Emit(event.Event{Kind: event.ToolResult, Tool: event.Tool{Name: "bash"}}) } - }() + }) } - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 200; i++ { + wg.Go(func() { + for range 200 { if _, err := json.Marshal(s.Snapshot()); err != nil { t.Errorf("marshal snapshot: %v", err) return } } - }() + }) wg.Wait() m := s.Snapshot() diff --git a/internal/cli/scroll_state_test.go b/internal/cli/scroll_state_test.go index 9ba50a8c83..ec6f8cf36a 100644 --- a/internal/cli/scroll_state_test.go +++ b/internal/cli/scroll_state_test.go @@ -24,7 +24,7 @@ func TestModalOpenDoesNotDisableTailFollow(t *testing.T) { notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 12}) - for i := 0; i < 20; i++ { + for range 20 { cur = adv(cur, notice) } if !cur.shouldFollowTail() || !cur.viewport.AtBottom() { @@ -56,7 +56,7 @@ func TestUserScrollBreaksAndEmptyEnterRestoresFollow(t *testing.T) { notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 30; i++ { + for range 30 { cur = adv(cur, notice) } cur = adv(cur, tea.MouseWheelMsg{Button: tea.MouseWheelUp}) @@ -83,7 +83,7 @@ func TestScrollbarDragMotionSyncsBeforeRelease(t *testing.T) { } notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 10}) - for i := 0; i < 40; i++ { + for range 40 { cur = adv(cur, notice) } if !cur.shouldFollowTail() { @@ -118,7 +118,7 @@ func TestWrapCacheAppendOnlyMatchesFullRebuild(t *testing.T) { m := newTestChatTUI() m.width = 40 cw := 40 - for i := 0; i < 5; i++ { + for i := range 5 { m.transcript = append(m.transcript, fmt.Sprintf("block-%d %s", i, strings.Repeat("word ", 20))) } m.rebuildWrappedLinesFull(cw) @@ -126,7 +126,7 @@ func TestWrapCacheAppendOnlyMatchesFullRebuild(t *testing.T) { m2 := newTestChatTUI() m2.width = 40 - for i := 0; i < 5; i++ { + for i := range 5 { m2.transcript = append(m2.transcript, m.transcript[i]) m2.syncWrappedLines(cw, i == 0) } @@ -150,7 +150,7 @@ func TestStreamAnswerSuffixInvalidationNotFullRebuild(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 20}) // Seed history blocks. - for i := 0; i < 50; i++ { + for i := range 50 { cur = adv(cur, agentEventMsg(event.Event{ Kind: event.Notice, Level: event.LevelInfo, Text: fmt.Sprintf("hist-%d %s", i, strings.Repeat("x", 30)), @@ -279,14 +279,14 @@ func TestLongTranscriptStreamAnswerScalesSuffixOnly(t *testing.T) { } cur := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 20}) const hist = 1000 - for i := 0; i < hist; i++ { + for i := range hist { cur = adv(cur, agentEventMsg(event.Event{ Kind: event.Notice, Level: event.LevelInfo, Text: fmt.Sprintf("h-%d-%s", i, strings.Repeat("y", 40)), })) } cur.state = tuiRunning - for i := 0; i < 30; i++ { + for i := range 30 { cur = adv(cur, agentEventMsg(event.Event{ Kind: event.Text, Text: fmt.Sprintf("stream paragraph %d with enough text to flush.\n\n", i), @@ -311,7 +311,7 @@ func BenchmarkStreamAnswerSuffixWrap(b *testing.B) { return n.(chatTUI) } base := adv(newChatTUI(ctrl, "", make(chan event.Event, 1), 80), tea.WindowSizeMsg{Width: 80, Height: 24}) - for i := 0; i < hist; i++ { + for i := range hist { base = adv(base, agentEventMsg(event.Event{ Kind: event.Notice, Level: event.LevelInfo, Text: fmt.Sprintf("h-%d-%s", i, strings.Repeat("z", 48)), @@ -319,7 +319,7 @@ func BenchmarkStreamAnswerSuffixWrap(b *testing.B) { } b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { m := base // copy model value (shallow; OK for bench of Update path) m.state = tuiRunning m.pending = &strings.Builder{} diff --git a/internal/cli/select.go b/internal/cli/select.go index 3eb5811a7c..f6d41098c7 100644 --- a/internal/cli/select.go +++ b/internal/cli/select.go @@ -42,10 +42,7 @@ func fixedLines(searching bool) int { // maxViewport calculates how many menu item rows fit after subtracting the // fixed lines from the available terminal rows, leaving at least 1 row. func maxViewport(totalItems, termRows int, searching bool) int { - avail := termRows - fixedLines(searching) - if avail < 1 { - avail = 1 - } + avail := max(termRows-fixedLines(searching), 1) if totalItems < avail { return totalItems } @@ -124,10 +121,7 @@ func selectOne(label string, items []menuItem) (int, error) { } // menu rows - end := scroll + vp - if end > n { - end = n - } + end := min(scroll+vp, n) for i := scroll; i < end; i++ { it := filtered[i] name := fmt.Sprintf("%-10s", it.name) @@ -308,10 +302,7 @@ func selectMany(label string, items []menuItem) ([]int, error) { fmt.Fprintf(w, "\r\033[K\r\n") } - end := scroll + vp - if end > n { - end = n - } + end := min(scroll+vp, n) for i := scroll; i < end; i++ { it := filtered[i] origIdx := filterIdx[i] diff --git a/internal/cli/select_test.go b/internal/cli/select_test.go index 77bdd0f1c3..02094a25f2 100644 --- a/internal/cli/select_test.go +++ b/internal/cli/select_test.go @@ -78,7 +78,7 @@ func TestFrameLinesNeverExceedsTerminal(t *testing.T) { // Otherwise the terminal scrolls and cursor repositioning drifts. termRows := 24 for _, searching := range []bool{false, true} { - for n := 0; n <= 200; n++ { + for n := range 201 { lines := FrameLines(n, termRows, searching) if lines > termRows { t.Errorf("FrameLines(%d, %d, searching=%v) = %d, exceeds terminal", diff --git a/internal/cli/setup_manager.go b/internal/cli/setup_manager.go index 91405ec015..8541e76b31 100644 --- a/internal/cli/setup_manager.go +++ b/internal/cli/setup_manager.go @@ -631,7 +631,7 @@ func promptOptionalAPIKeyEnvName(in *bufio.Scanner, w io.Writer, label, def stri func splitModels(raw string) []string { seen := map[string]bool{} var models []string - for _, model := range strings.Split(raw, ",") { + for model := range strings.SplitSeq(raw, ",") { model = strings.TrimSpace(model) if model != "" && !seen[model] { seen[model] = true diff --git a/internal/cli/shell_completion.go b/internal/cli/shell_completion.go index 0f4d84e79c..607812378b 100644 --- a/internal/cli/shell_completion.go +++ b/internal/cli/shell_completion.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "slices" "strconv" "strings" @@ -448,10 +449,7 @@ func cliCompletionCandidatesWithValues(root cliCompletionSpec, cword int, words if cword < len(words) { current = words[cword] } - limit := cword - if limit > len(words) { - limit = len(words) - } + limit := min(cword, len(words)) ctx := &root positionalSeen := false @@ -537,10 +535,8 @@ func cliCompletionLookupFlag(spec *cliCompletionSpec, token string) (*cliComplet inline = true } for i := range spec.flags { - for _, candidate := range spec.flags[i].names { - if candidate == name { - return &spec.flags[i], inline - } + if slices.Contains(spec.flags[i].names, name) { + return &spec.flags[i], inline } } return nil, inline @@ -552,10 +548,8 @@ func cliCompletionLookupSubcommand(spec *cliCompletionSpec, token string) *cliCo if child.name == token { return child } - for _, alias := range child.aliases { - if alias == token { - return child - } + if slices.Contains(child.aliases, token) { + return child } } return nil diff --git a/internal/cli/shell_completion_test.go b/internal/cli/shell_completion_test.go index 9a35707603..4df004dffe 100644 --- a/internal/cli/shell_completion_test.go +++ b/internal/cli/shell_completion_test.go @@ -5,6 +5,7 @@ import ( "go/parser" "go/token" "reflect" + "slices" "strconv" "strings" "testing" @@ -296,12 +297,7 @@ func TestCLICompletionPathFlagReturnsEmptyForShellFallback(t *testing.T) { } func containsCompletionValue(values []string, target string) bool { - for _, value := range values { - if value == target { - return true - } - } - return false + return slices.Contains(values, target) } func completionRegistryKnowsRootToken(root *cliCompletionSpec, token string) bool { diff --git a/internal/cli/skill_hooks.go b/internal/cli/skill_hooks.go index 7ae6d34d63..5e76b31b14 100644 --- a/internal/cli/skill_hooks.go +++ b/internal/cli/skill_hooks.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "os" + "slices" "strings" tea "charm.land/bubbletea/v2" @@ -313,10 +314,5 @@ func (m *chatTUI) hooksList(cwd string) { } func containsArg(args []string, flag string) bool { - for _, a := range args { - if a == flag { - return true - } - } - return false + return slices.Contains(args, flag) } diff --git a/internal/cli/skill_picker_test.go b/internal/cli/skill_picker_test.go index 22e5eaac06..2f2fdce5ba 100644 --- a/internal/cli/skill_picker_test.go +++ b/internal/cli/skill_picker_test.go @@ -202,7 +202,7 @@ func TestSkillPickerRenderDialogStyle(t *testing.T) { t.Cleanup(func() { i18n.DetectLanguage("en") }) var skills []skill.Skill - for i := 0; i < 24; i++ { + for i := range 24 { skills = append(skills, skill.Skill{ Name: "skill-" + strings.Repeat("x", i%4) + string(rune('a'+i)), Description: "this long description should stay out of the default picker list", diff --git a/internal/cli/skill_picker_view.go b/internal/cli/skill_picker_view.go index c44ec5004c..2c279f486f 100644 --- a/internal/cli/skill_picker_view.go +++ b/internal/cli/skill_picker_view.go @@ -123,10 +123,7 @@ func skillListWindow(sel, total, limit int) (int, int) { if sel >= total { sel = total - 1 } - start := sel - limit/2 - if start < 0 { - start = 0 - } + start := max(sel-limit/2, 0) if start+limit > total { start = total - limit } diff --git a/internal/cli/slash_catalog_test.go b/internal/cli/slash_catalog_test.go index 7828c49f78..2393422cc9 100644 --- a/internal/cli/slash_catalog_test.go +++ b/internal/cli/slash_catalog_test.go @@ -18,7 +18,7 @@ func TestSlashCatalogCachesAcrossKeystrokes(t *testing.T) { ctrl := control.New(control.Options{}) m := newChatTUI(ctrl, "", make(chan event.Event, 1), 80) m.skills = make([]skill.Skill, 0, 50) - for i := 0; i < 50; i++ { + for i := range 50 { m.skills = append(m.skills, skill.Skill{ Name: fmt.Sprintf("skill-%03d", i), Description: strings.Repeat("description text for catalog build ", 20), @@ -221,7 +221,7 @@ func BenchmarkSlashCompletionKeystroke(b *testing.B) { ctrl := control.New(control.Options{}) m := newChatTUI(ctrl, "", make(chan event.Event, 1), 80) m.skills = make([]skill.Skill, 0, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { m.skills = append(m.skills, skill.Skill{ Name: fmt.Sprintf("bench-skill-%04d", i), Description: "benchmark skill description " + strings.Repeat("x", 80), @@ -230,7 +230,7 @@ func BenchmarkSlashCompletionKeystroke(b *testing.B) { _ = m.slashItems() // warm catalog once b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { m.input.SetValue("/be") m.updateCompletion() if !m.completion.active { diff --git a/internal/cli/slash_registry.go b/internal/cli/slash_registry.go index 88077fa254..f7e426abc1 100644 --- a/internal/cli/slash_registry.go +++ b/internal/cli/slash_registry.go @@ -1,6 +1,7 @@ package cli import ( + "slices" "strings" "reasonix/internal/i18n" @@ -93,10 +94,8 @@ func canonicalBuiltinSlashCommand(name string) string { if name == spec.name { return spec.name } - for _, alias := range spec.aliases { - if name == alias { - return spec.name - } + if slices.Contains(spec.aliases, name) { + return spec.name } } return name diff --git a/internal/cli/status_footer.go b/internal/cli/status_footer.go index 03a227db03..a97f6512d4 100644 --- a/internal/cli/status_footer.go +++ b/internal/cli/status_footer.go @@ -292,7 +292,7 @@ func (m chatTUI) renderStatusBlock(primary string, width int) string { // the optional shortcut help yields space to the composer. func hideStatusHintWhenKeyNamesCannotFit(primary string, width int) string { hint := i18n.M.ChatStatusCycleHintCompact - for _, group := range strings.Split(hint, " · ") { + for group := range strings.SplitSeq(hint, " · ") { if visibleWidth(statusFooterIndent+group) > width { return strings.Replace(primary, " · "+footerHint(hint), "", 1) } diff --git a/internal/cli/subagent.go b/internal/cli/subagent.go index 385fcb6bf1..0fde9aa185 100644 --- a/internal/cli/subagent.go +++ b/internal/cli/subagent.go @@ -558,7 +558,7 @@ func profileFlagsChanged(values subagentProfileFlags) bool { func parseToolList(raw string) []string { seen := map[string]bool{} var tools []string - for _, item := range strings.Split(raw, ",") { + for item := range strings.SplitSeq(raw, ",") { name := strings.TrimSpace(item) if name == "" || seen[name] { continue diff --git a/internal/cli/theme.go b/internal/cli/theme.go index 7604a3f3fc..5ba6d34ca9 100644 --- a/internal/cli/theme.go +++ b/internal/cli/theme.go @@ -256,11 +256,11 @@ func (c terminalRGB) looksLight() bool { } func parseOSC11Response(s string) (terminalRGB, bool) { - idx := strings.Index(s, "]11;") - if idx < 0 { + _, after, ok := strings.Cut(s, "]11;") + if !ok { return terminalRGB{}, false } - payload := s[idx+len("]11;"):] + payload := after if end := strings.IndexByte(payload, '\a'); end >= 0 { payload = payload[:end] } else if end := strings.Index(payload, "\x1b\\"); end >= 0 { @@ -272,8 +272,8 @@ func parseOSC11Response(s string) (terminalRGB, bool) { return terminalRGB{r, g, b}, ok } for _, prefix := range []string{"rgb:", "rgba:"} { - if strings.HasPrefix(payload, prefix) { - return parseOSCColorTriplet(strings.TrimPrefix(payload, prefix)) + if after, ok := strings.CutPrefix(payload, prefix); ok { + return parseOSCColorTriplet(after) } } return terminalRGB{}, false diff --git a/internal/cli/toolcard.go b/internal/cli/toolcard.go index 24251616d3..673bc8bd68 100644 --- a/internal/cli/toolcard.go +++ b/internal/cli/toolcard.go @@ -24,11 +24,12 @@ func connectorBlock(lines []string) string { return "" } indent := strings.Repeat(" ", len([]rune(connector))) - out := dim(connector) + lines[0] + var out strings.Builder + out.WriteString(dim(connector) + lines[0]) for _, ln := range lines[1:] { - out += "\n" + indent + ln + out.WriteString("\n" + indent + ln) } - return out + return out.String() } // toolVerb maps a tool's snake_case id to the verb shown in its card. diff --git a/internal/cli/transcript.go b/internal/cli/transcript.go index f6c193728c..a3bc768630 100644 --- a/internal/cli/transcript.go +++ b/internal/cli/transcript.go @@ -317,7 +317,7 @@ func (a transcriptResizeAnchor) yOffset(blocks []string, width int) int { } block := min(max(a.block, 0), len(blocks)-1) offset := 0 - for i := 0; i < block; i++ { + for i := range block { offset += transcriptBlockLineCount(blocks[i], width) } lines := transcriptBlockLineCount(blocks[block], width) @@ -473,7 +473,7 @@ func (m chatTUI) renderTranscript() string { rows := make([]string, h) bar := make([]string, h) - for r := 0; r < h; r++ { + for r := range h { idx := yoff + r line := blank // off-content rows fill to width if idx >= 0 && idx < total { @@ -717,15 +717,9 @@ func scrollbarThumb(height, yoff, total int) (start, size int) { if total <= height { return 0, 0 // no overflow → no thumb } - size = height * height / total - if size < 1 { - size = 1 - } + size = max(height*height/total, 1) maxYoff := total - height - start = yoff * (height - size) / maxYoff - if start > height-size { - start = height - size - } + start = min(yoff*(height-size)/maxYoff, height-size) return start, size } @@ -738,13 +732,7 @@ func scrollbarYOffset(height, row, total, grabOffset int) int { if maxTop <= 0 { return 0 } - top := row - grabOffset - if top < 0 { - top = 0 - } - if top > maxTop { - top = maxTop - } + top := min(max(row-grabOffset, 0), maxTop) maxYoff := total - height return (top*maxYoff + maxTop/2) / maxTop } diff --git a/internal/cli/transcript_test.go b/internal/cli/transcript_test.go index aba8d71d3c..8ff0feed9f 100644 --- a/internal/cli/transcript_test.go +++ b/internal/cli/transcript_test.go @@ -166,13 +166,13 @@ func TestSelectedTextRestoresMathWithoutReusingRawColumns(t *testing.T) { } plain := ansi.Strip(m.wrappedLines[lineIndex]) - formulaByte := strings.Index(plain, "α") - afterByte := strings.Index(plain, "after") - if formulaByte < 0 || afterByte < 0 { + before, _, ok := strings.Cut(plain, "α") + before0, _, ok0 := strings.Cut(plain, "after") + if !ok || !ok0 { t.Fatalf("math line = %q", plain) } - formulaCol := ansi.StringWidth(plain[:formulaByte]) - afterCol := ansi.StringWidth(plain[:afterByte]) + formulaCol := ansi.StringWidth(before) + afterCol := ansi.StringWidth(before0) m.sel = selection{ active: true, @@ -218,12 +218,12 @@ func TestSelectedTextRestoresMathFromReplayBundle(t *testing.T) { formulaCol := -1 for i, line := range m.wrappedLines { plain := ansi.Strip(line) - formulaByte := strings.Index(plain, "α") - if formulaByte < 0 { + before, _, ok := strings.Cut(plain, "α") + if !ok { continue } lineIndex = i - formulaCol = ansi.StringWidth(plain[:formulaByte]) + formulaCol = ansi.StringWidth(before) break } if lineIndex < 0 { @@ -283,12 +283,12 @@ func TestSelectedTextPreservesProseAroundMath(t *testing.T) { for i, line := range m.wrappedLines { plain := ansi.Strip(line) - startByte := strings.Index(plain, "before") + before, _, ok := strings.Cut(plain, "before") endByte := strings.Index(plain, " after") - if startByte < 0 || endByte < 0 { + if !ok || endByte < 0 { continue } - startCol := ansi.StringWidth(plain[:startByte]) + startCol := ansi.StringWidth(before) endCol := ansi.StringWidth(plain[:endByte+len(" after")]) m.sel = selection{ active: true, diff --git a/internal/cli/tui_diagnostics.go b/internal/cli/tui_diagnostics.go index 46de20147f..53f7e89a12 100644 --- a/internal/cli/tui_diagnostics.go +++ b/internal/cli/tui_diagnostics.go @@ -290,11 +290,9 @@ func (d *tuiDiagnostics) StartWatchdog(p *tea.Program) { d.lastHeartbeatSource = "watchdog_armed" } d.mu.Unlock() - d.watchWG.Add(1) - go func() { - defer d.watchWG.Done() + d.watchWG.Go(func() { d.watch() - }() + }) }) } diff --git a/internal/cli/tui_diagnostics_test.go b/internal/cli/tui_diagnostics_test.go index 03ed423e0d..96a8a7a52e 100644 --- a/internal/cli/tui_diagnostics_test.go +++ b/internal/cli/tui_diagnostics_test.go @@ -200,7 +200,7 @@ func TestWatchdogIdleNeverEscalates(t *testing.T) { t.Fatalf("phase = %s, want idle", d.phaseForTest()) } // Idle for well over the stall threshold. - for i := 0; i < 30; i++ { + for range 30 { clock.now = clock.now.Add(time.Second) d.onTick(clock.now) } @@ -244,7 +244,7 @@ func TestWatchdogRunningElapsedHeartbeatPreventsKill(t *testing.T) { d.NoteBooted() d.NoteRunning(func() {}) // Simulate a long turn with a heartbeat every second. - for i := 0; i < 60; i++ { + for range 60 { clock.now = clock.now.Add(time.Second) d.NoteActiveHeartbeat("elapsed_tick") d.onTick(clock.now) diff --git a/internal/cli/upgrade.go b/internal/cli/upgrade.go index 74dfab729a..35ab7a1323 100644 --- a/internal/cli/upgrade.go +++ b/internal/cli/upgrade.go @@ -529,7 +529,7 @@ func fetchLatestRelease(c *http.Client, channel cliReleaseChannel) (*ghRelease, return pointerRelease, nil } - req, err := http.NewRequest("GET", ghAPIReleases, nil) + req, err := http.NewRequest(http.MethodGet, ghAPIReleases, nil) if err != nil { return nil, err } @@ -560,7 +560,7 @@ func fetchLatestRelease(c *http.Client, channel cliReleaseChannel) (*ghRelease, } func fetchCLIReleasePointer(c *http.Client, pointerURL string, channel cliReleaseChannel) (*ghRelease, error) { - req, err := http.NewRequest("GET", pointerURL, nil) + req, err := http.NewRequest(http.MethodGet, pointerURL, nil) if err != nil { return nil, err } @@ -617,7 +617,7 @@ func verifyChecksum(data []byte, fileName string, checksumFile []byte) error { sum := sha256.Sum256(data) got := hex.EncodeToString(sum[:]) - for _, line := range strings.Split(strings.TrimSpace(string(checksumFile)), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(string(checksumFile)), "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/internal/cli/upgrade_test.go b/internal/cli/upgrade_test.go index 9e6f960f3b..963748e949 100644 --- a/internal/cli/upgrade_test.go +++ b/internal/cli/upgrade_test.go @@ -48,21 +48,21 @@ func TestVerifyChecksum(t *testing.T) { hash := hex.EncodeToString(sum[:]) t.Run("match", func(t *testing.T) { - checksumFile := []byte(fmt.Sprintf("%s reasonix-linux-amd64.tar.gz\n", hash)) + checksumFile := fmt.Appendf(nil, "%s reasonix-linux-amd64.tar.gz\n", hash) if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err != nil { t.Errorf("unexpected error: %v", err) } }) t.Run("mismatch", func(t *testing.T) { - checksumFile := []byte(fmt.Sprintf("%s reasonix-linux-amd64.tar.gz\n", "0000000000000000000000000000000000000000000000000000000000000000")) + checksumFile := fmt.Appendf(nil, "%s reasonix-linux-amd64.tar.gz\n", "0000000000000000000000000000000000000000000000000000000000000000") if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err == nil { t.Error("expected checksum mismatch error") } }) t.Run("not found", func(t *testing.T) { - checksumFile := []byte(fmt.Sprintf("%s reasonix-darwin-arm64.tar.gz\n", hash)) + checksumFile := fmt.Appendf(nil, "%s reasonix-darwin-arm64.tar.gz\n", hash) if err := verifyChecksum(content, "reasonix-linux-amd64.tar.gz", checksumFile); err == nil { t.Error("expected not-found error") } diff --git a/internal/cli/width_test.go b/internal/cli/width_test.go index f93882082e..de77e3cba6 100644 --- a/internal/cli/width_test.go +++ b/internal/cli/width_test.go @@ -37,7 +37,7 @@ func TestVisibleWidthGraphemeClusters(t *testing.T) { // preserves ANSI escapes as zero width, and leaves in-width lines untouched. func TestClampWidthHardwrap(t *testing.T) { // CJK hard-breaks at the column boundary (each is 2 cols); no line over width. - for _, line := range strings.Split(clampWidth("中文字", 4), "\n") { + for line := range strings.SplitSeq(clampWidth("中文字", 4), "\n") { if visibleWidth(line) > 4 { t.Errorf("cjk line %q exceeds width 4 (got %d)", line, visibleWidth(line)) } @@ -55,7 +55,7 @@ func TestClampWidthHardwrap(t *testing.T) { } // Every wrapped line stays within the column budget. - for _, line := range strings.Split(clampWidth(strings.Repeat("中", 10), 6), "\n") { + for line := range strings.SplitSeq(clampWidth(strings.Repeat("中", 10), 6), "\n") { if visibleWidth(line) > 6 { t.Errorf("line %q exceeds width 6 (got %d)", line, visibleWidth(line)) } diff --git a/internal/cli/wrap_cache.go b/internal/cli/wrap_cache.go index 5f66d3d744..a6a2e9f05b 100644 --- a/internal/cli/wrap_cache.go +++ b/internal/cli/wrap_cache.go @@ -63,7 +63,7 @@ func (m *chatTUI) rebuildWrappedLinesFull(contentW int) bool { } n := len(m.transcript) m.wrapBlockLines = make([][]string, n) - for i := 0; i < n; i++ { + for i := range n { m.wrapBlockLines[i] = wrapBlockLines(m.transcript[i], contentW) } // Prefer per-block flatten over join-then-wrap so streaming suffix rebuilds diff --git a/internal/command/inspect.go b/internal/command/inspect.go index 5da316b6fe..430b82bc1e 100644 --- a/internal/command/inspect.go +++ b/internal/command/inspect.go @@ -3,6 +3,7 @@ package command import ( "os" "path/filepath" + "slices" "sort" "strings" ) @@ -89,8 +90,8 @@ func Inspect(dirs ...string) Inspection { for name, list := range byName { // Find last non-error candidate as winner; errors stay as errors. winIdx := -1 - for i := len(list) - 1; i >= 0; i-- { - if list[i].Status != CandidateError { + for i, v := range slices.Backward(list) { + if v.Status != CandidateError { winIdx = i break } diff --git a/internal/command/slashtool_test.go b/internal/command/slashtool_test.go index bbeecb7e64..d4a2d81b7a 100644 --- a/internal/command/slashtool_test.go +++ b/internal/command/slashtool_test.go @@ -144,7 +144,7 @@ func TestPluginSlashToolShowsOnlyCanonicalQualifiedName(t *testing.T) { if cmd.Hidden { continue } - cmd := cmd + out = append(out, SlashEntry{Name: cmd.Name, Description: cmd.Description, ArgHint: cmd.ArgHint, Render: func(args []string) string { return cmd.Render(args) }}) } return out diff --git a/internal/config/backfill_test.go b/internal/config/backfill_test.go index e2d4f2aa86..79977bed6f 100644 --- a/internal/config/backfill_test.go +++ b/internal/config/backfill_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -14,10 +15,8 @@ import ( func hasModel(c *Config, model string) *ProviderEntry { for i := range c.Providers { - for _, m := range c.Providers[i].ModelList() { - if m == model { - return &c.Providers[i] - } + if slices.Contains(c.Providers[i].ModelList(), model) { + return &c.Providers[i] } } return nil @@ -572,7 +571,7 @@ func TestNormalizeLegacyKimiK3CatalogMigratesOnlyOfficialUntouchedPresets(t *tes if !normalizeLegacyKimiK3Catalog(c) { t.Fatal("legacy Kimi direct catalogs did not report a change") } - for i := 0; i < 2; i++ { + for i := range 2 { got := &c.Providers[i] if !got.HasModel("kimi-k3") || !got.HasVisionModel("kimi-k3") { t.Fatalf("migrated Kimi provider %d = %+v, want K3 with vision", i, got) diff --git a/internal/config/config.go b/internal/config/config.go index 35d4e0c757..93df15988d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strings" fileencoding "reasonix/internal/fileutil/encoding" @@ -1492,12 +1493,7 @@ func (e *ProviderEntry) DefaultModel() string { // HasModel reports whether m is one of the provider's models. func (e *ProviderEntry) HasModel(m string) bool { - for _, x := range e.ModelList() { - if x == m { - return true - } - } - return false + return slices.Contains(e.ModelList(), m) } // PriceForModel returns the configured per-1M-token price for model. Per-model diff --git a/internal/config/credentials_keyring_unix.go b/internal/config/credentials_keyring_unix.go index afe5bbf7b0..df55b68d34 100644 --- a/internal/config/credentials_keyring_unix.go +++ b/internal/config/credentials_keyring_unix.go @@ -5,6 +5,7 @@ package config import ( "context" "fmt" + "slices" "strings" dbus "github.com/godbus/dbus/v5" @@ -185,10 +186,8 @@ func ssResolveLoginCollection(ctx context.Context, svc dbus.BusObject) (dbus.Obj return dbus.ObjectPath(ssLoginAlias), nil } paths, _ := val.Value().([]dbus.ObjectPath) - for _, p := range paths { - if p == path { - return path, nil - } + if slices.Contains(paths, path) { + return path, nil } return dbus.ObjectPath(ssLoginAlias), nil } diff --git a/internal/config/dotenv.go b/internal/config/dotenv.go index c482697e0f..ec4b3c2851 100644 --- a/internal/config/dotenv.go +++ b/internal/config/dotenv.go @@ -172,7 +172,7 @@ func detectDotEnvDuplicateKeys(path string) []string { } seen := map[string]bool{} dups := map[string]bool{} - for _, line := range strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n") { + for line := range strings.SplitSeq(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n") { values, err := godotenv.Unmarshal(line) if err != nil { continue diff --git a/internal/config/edit.go b/internal/config/edit.go index 8e0ef5c472..bef515c9c3 100644 --- a/internal/config/edit.go +++ b/internal/config/edit.go @@ -676,10 +676,8 @@ func (c *Config) AddPermissionRule(list, rule string) error { if _, ok := permission.ParseRule(rule); !ok { return fmt.Errorf("invalid permission rule %q (want \"ToolName\" or \"ToolName(glob)\")", rule) } - for _, existing := range *target { - if existing == rule { - return nil // already present - } + if slices.Contains(*target, rule) { + return nil // already present } *target = append(*target, rule) return nil @@ -1704,7 +1702,7 @@ func mergeTOMLDelta(body, delta string) string { } func mergeTOMLTopLevelFields(body, fields string) string { - for _, line := range strings.Split(fields, "\n") { + for line := range strings.SplitSeq(fields, "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/internal/config/effort.go b/internal/config/effort.go index dd6b31a3e8..006c943bcd 100644 --- a/internal/config/effort.go +++ b/internal/config/effort.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "slices" "strings" "reasonix/internal/provider/openai" @@ -477,12 +478,7 @@ func effortNotConfigurableError(e *ProviderEntry) error { } func containsString(haystack []string, needle string) bool { - for _, s := range haystack { - if s == needle { - return true - } - } - return false + return slices.Contains(haystack, needle) } func normalizeEffortLevel(s string) string { diff --git a/internal/config/fetch.go b/internal/config/fetch.go index f1befa1661..51ccbd318d 100644 --- a/internal/config/fetch.go +++ b/internal/config/fetch.go @@ -4,6 +4,7 @@ package config import ( "context" "fmt" + "slices" "strings" "reasonix/internal/provider/openai" @@ -125,13 +126,7 @@ func uniqueStrings(in []string) []string { if s == "" { continue } - seen := false - for _, existing := range out { - if existing == s { - seen = true - break - } - } + seen := slices.Contains(out, s) if !seen { out = append(out, s) } diff --git a/internal/config/load.go b/internal/config/load.go index db3ff02d46..90428257f0 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -3,9 +3,11 @@ package config import ( "fmt" "log/slog" + "maps" "net/url" "os" "path/filepath" + "slices" "strings" "github.com/BurntSushi/toml" @@ -278,9 +280,7 @@ func cloneStringMap(in map[string]string) map[string]string { return nil } out := make(map[string]string, len(in)) - for k, v := range in { - out[k] = v - } + maps.Copy(out, in) return out } @@ -1946,15 +1946,11 @@ func mergeModelLists(primary, extra []string) []string { func firstKnownModel(current string, models []string, fallback string) string { current = strings.TrimSpace(current) - for _, model := range models { - if model == current { - return current - } + if slices.Contains(models, current) { + return current } - for _, model := range models { - if model == fallback { - return fallback - } + if slices.Contains(models, fallback) { + return fallback } if len(models) > 0 { return models[0] diff --git a/internal/config/mcp_activation_test.go b/internal/config/mcp_activation_test.go index 41a5485f42..c25c56104f 100644 --- a/internal/config/mcp_activation_test.go +++ b/internal/config/mcp_activation_test.go @@ -88,9 +88,7 @@ func TestMCPActivationStoreConcurrentIndependentWriters(t *testing.T) { start := make(chan struct{}) var wg sync.WaitGroup for i, store := range stores { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-start if err := store.SetEnabled(MCPActivationOverride{ Scope: MCPActivationGlobal, @@ -99,7 +97,7 @@ func TestMCPActivationStoreConcurrentIndependentWriters(t *testing.T) { }); err != nil { t.Errorf("SetEnabled(%d): %v", i, err) } - }() + }) } close(start) wg.Wait() diff --git a/internal/config/migrate.go b/internal/config/migrate.go index eee6f146b8..ea55f0cfea 100644 --- a/internal/config/migrate.go +++ b/internal/config/migrate.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "maps" "os" "path/filepath" "sort" @@ -670,12 +671,8 @@ func mergeEnv(base, overlay map[string]string) map[string]string { return nil } out := make(map[string]string, len(base)+len(overlay)) - for k, v := range base { - out[k] = v - } - for k, v := range overlay { - out[k] = v - } + maps.Copy(out, base) + maps.Copy(out, overlay) return out } diff --git a/internal/config/mutate.go b/internal/config/mutate.go index 4d1ca92005..5e9dd012e2 100644 --- a/internal/config/mutate.go +++ b/internal/config/mutate.go @@ -8,6 +8,7 @@ import ( osuser "os/user" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" @@ -164,16 +165,16 @@ func lockConfigFilesEdits(paths ...string) (func(), error) { unlockFiles := make([]func(), 0, len(lockPaths)) for _, lockPath := range lockPaths { if err := os.MkdirAll(filepath.Dir(lockPath), 0o700); err != nil { - for i := len(unlockFiles) - 1; i >= 0; i-- { - unlockFiles[i]() + for _, v := range slices.Backward(unlockFiles) { + v() } userEditMu.Unlock() return nil, fmt.Errorf("lock config edits: create lock directory: %w", err) } unlockFile, err := acquireConfigEditLockPath(ctx, lockPath) if err != nil { - for i := len(unlockFiles) - 1; i >= 0; i-- { - unlockFiles[i]() + for _, v := range slices.Backward(unlockFiles) { + v() } userEditMu.Unlock() return nil, fmt.Errorf("lock config edits: %w", err) @@ -186,8 +187,8 @@ func lockConfigFilesEdits(paths ...string) (func(), error) { return func() { once.Do(func() { clearPins() - for i := len(unlockFiles) - 1; i >= 0; i-- { - unlockFiles[i]() + for _, v := range slices.Backward(unlockFiles) { + v() } userEditMu.Unlock() }) diff --git a/internal/config/mutate_test.go b/internal/config/mutate_test.go index a19974c7b3..70d51a6893 100644 --- a/internal/config/mutate_test.go +++ b/internal/config/mutate_test.go @@ -31,7 +31,7 @@ func TestLockUserConfigEditsSerializesRMW(t *testing.T) { const writers = 8 var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(n int) { defer wg.Done() diff --git a/internal/config/path_access.go b/internal/config/path_access.go index 8688beaea5..67c825e5ec 100644 --- a/internal/config/path_access.go +++ b/internal/config/path_access.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" ) @@ -65,8 +66,8 @@ func evalSymlinksAllowMissing(path string) (string, error) { if err != nil { return "", err } - for i := len(suffix) - 1; i >= 0; i-- { - resolved = filepath.Join(resolved, suffix[i]) + for _, v := range slices.Backward(suffix) { + resolved = filepath.Join(resolved, v) } return filepath.Clean(resolved), nil } else if !os.IsNotExist(err) { diff --git a/internal/config/paths.go b/internal/config/paths.go index bf09026d2d..c680f856fd 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "unicode/utf8" @@ -540,8 +541,8 @@ var ConventionDirs = []string{".reasonix", ".agents", ".agent", ".claude"} // highest-priority entry — command.Load lets a later directory win on a clash. func conventionSubdirsAsc(base, sub string) []string { out := make([]string, 0, len(ConventionDirs)) - for i := len(ConventionDirs) - 1; i >= 0; i-- { - out = append(out, filepath.Join(base, ConventionDirs[i], sub)) + for _, v := range slices.Backward(ConventionDirs) { + out = append(out, filepath.Join(base, v, sub)) } return out } diff --git a/internal/config/remote_credentials.go b/internal/config/remote_credentials.go index ec956de7bb..b89cf10932 100644 --- a/internal/config/remote_credentials.go +++ b/internal/config/remote_credentials.go @@ -3,6 +3,7 @@ package config import ( "crypto/sha256" "fmt" + "slices" "strings" ) @@ -96,8 +97,8 @@ func EditUserConfigWithCredentials(mutate func(*Config) ([]CredentialChange, err applied := make([]string, 0, len(changes)) rollback := func() { seen := map[string]bool{} - for i := len(applied) - 1; i >= 0; i-- { - key := applied[i] + for _, v := range slices.Backward(applied) { + key := v if seen[key] { continue } diff --git a/internal/config/render_test.go b/internal/config/render_test.go index 0d9a9fa585..0dcd2d3daa 100644 --- a/internal/config/render_test.go +++ b/internal/config/render_test.go @@ -736,7 +736,7 @@ extensions = [".cc", ".cpp", ".hpp"] func BenchmarkRenderTOMLWithLSPServers(b *testing.B) { cfg := Default() cfg.LSP.Servers = make(map[string]LSPServer, 64) - for i := 0; i < 64; i++ { + for i := range 64 { lang := "lang" + strconv.Itoa(i) cfg.LSP.Servers[lang] = LSPServer{ Command: "server-" + strconv.Itoa(i), @@ -749,7 +749,7 @@ func BenchmarkRenderTOMLWithLSPServers(b *testing.B) { } b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { rendered := RenderTOML(cfg) if len(rendered) == 0 { b.Fatal("empty render") @@ -1304,7 +1304,7 @@ func TestRenderTOMLWindowsSandboxDefaultAndExplicitEnforceDisabled(t *testing.T) func extractSectionLines(toml, section string) []string { var lines []string inSection := false - for _, line := range strings.Split(toml, "\n") { + for line := range strings.SplitSeq(toml, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, section) { inSection = true diff --git a/internal/config/system_prompt_test.go b/internal/config/system_prompt_test.go index 207d083daf..780bd58dfa 100644 --- a/internal/config/system_prompt_test.go +++ b/internal/config/system_prompt_test.go @@ -246,7 +246,7 @@ func TestResolveSystemPromptRejectsProjectPathEscapes(t *testing.T) { t.Run(test.name, func(t *testing.T) { root := t.TempDir() outside := test.path(root) - if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(fmt.Sprintf("[agent]\nsystem_prompt_file = %q\n", outside)), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), fmt.Appendf(nil, "[agent]\nsystem_prompt_file = %q\n", outside), 0o600); err != nil { t.Fatal(err) } cfg, err := LoadForRootReadOnly(root) diff --git a/internal/config/vision.go b/internal/config/vision.go index 5a32fdad92..438b7e6424 100644 --- a/internal/config/vision.go +++ b/internal/config/vision.go @@ -2,6 +2,7 @@ package config import ( "net/url" + "slices" "strings" "reasonix/internal/provider/openai" @@ -39,10 +40,8 @@ func IsLikelyVisionModel(model string) bool { return true } tokens := strings.FieldsFunc(lower, modelTokenSeparator) - for _, token := range tokens { - if token == "audio" { - return false - } + if slices.Contains(tokens, "audio") { + return false } if strings.HasPrefix(lower, "gpt-4o") { return true diff --git a/internal/control/admission_test.go b/internal/control/admission_test.go index 37f0f275f9..4146522a39 100644 --- a/internal/control/admission_test.go +++ b/internal/control/admission_test.go @@ -16,9 +16,9 @@ import ( // deterministically open so tests can place submits inside it. Later // TurnDones pass through unblocked. func holdFinishingWindow(release <-chan struct{}, entered chan<- struct{}, events chan<- event.Event) event.Sink { - var first int32 + var first atomic.Int32 return event.FuncSink(func(e event.Event) { - if e.Kind == event.TurnDone && atomic.AddInt32(&first, 1) == 1 { + if e.Kind == event.TurnDone && first.Add(1) == 1 { entered <- struct{}{} <-release } @@ -44,7 +44,7 @@ func TestParkedTurnsRunFIFO(t *testing.T) { var order []int ran := make(chan int, 3) for i := 1; i <= 3; i++ { - i := i + if got := c.runGuarded(func(context.Context) error { ran <- i return nil @@ -117,10 +117,10 @@ func TestSubmitDuringRotationEmitsNotice(t *testing.T) { // opportunistic callers rely on the quiet no-op. func TestSubmitWhileRunningStaysSilentNoOp(t *testing.T) { block := make(chan struct{}) - var notices int32 + var notices atomic.Int32 c := New(Options{Sink: event.FuncSink(func(e event.Event) { if e.Kind == event.Notice { - atomic.AddInt32(¬ices, 1) + notices.Add(1) } })}) @@ -135,7 +135,7 @@ func TestSubmitWhileRunningStaysSilentNoOp(t *testing.T) { if got := c.runGuarded(func(context.Context) error { return nil }); got != turnDroppedRunning { t.Fatalf("admission while running = %v, want turnDroppedRunning", got) } - if n := atomic.LoadInt32(¬ices); n != 0 { + if n := notices.Load(); n != 0 { t.Fatalf("running drop should stay silent, got %d notices", n) } close(block) diff --git a/internal/control/attachments.go b/internal/control/attachments.go index 4fbea1eff0..718036b37f 100644 --- a/internal/control/attachments.go +++ b/internal/control/attachments.go @@ -33,11 +33,11 @@ var safeAttachmentExt = regexp.MustCompile(`^\.[a-z0-9]{1,12}$`) // origName supplies only the extension; the stored name is generated. func SaveAttachmentDataURL(origName, dataURL string) (string, error) { const marker = ";base64," - i := strings.Index(dataURL, marker) - if !strings.HasPrefix(dataURL, "data:") || i < 0 { + _, after, ok := strings.Cut(dataURL, marker) + if !strings.HasPrefix(dataURL, "data:") || !ok { return "", fmt.Errorf("unsupported pasted file") } - raw, err := base64.StdEncoding.DecodeString(dataURL[i+len(marker):]) + raw, err := base64.StdEncoding.DecodeString(after) if err != nil { return "", fmt.Errorf("decode pasted file: %w", err) } @@ -373,7 +373,7 @@ func rejectSymlinkComponents(path, root string) error { return fmt.Errorf("attachment path is outside .reasonix/attachments") } cur := root - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "" || part == "." { continue } diff --git a/internal/control/autoresearch_manager.go b/internal/control/autoresearch_manager.go index 6ea5d10427..fd84afbc1a 100644 --- a/internal/control/autoresearch_manager.go +++ b/internal/control/autoresearch_manager.go @@ -323,7 +323,7 @@ func parseAutoResearchEvidenceBlocks(text string) []autoResearchEvidenceBlock { func autoResearchDirectionSummary(text string) string { text = agent.StripAutoResearchEvidenceBlocks(text) - for _, line := range strings.Split(text, "\n") { + for line := range strings.SplitSeq(text, "\n") { line = strings.TrimSpace(line) lower := strings.ToLower(line) if line == "" || strings.HasPrefix(lower, "[goal:") { diff --git a/internal/control/branches.go b/internal/control/branches.go index 2eb8d25660..7f9cd65a88 100644 --- a/internal/control/branches.go +++ b/internal/control/branches.go @@ -99,10 +99,7 @@ func branchTitle(b agent.BranchInfo, depth int) string { if label, ok := structuredBranchLabel(title); ok { return label } - maxRunes := 32 - depth*4 - if maxRunes < 18 { - maxRunes = 18 - } + maxRunes := max(32-depth*4, 18) title = oneLineBranch(title, maxRunes) if title == "" { return "(untitled)" diff --git a/internal/control/controller.go b/internal/control/controller.go index 5836c8228c..46510b1fb7 100644 --- a/internal/control/controller.go +++ b/internal/control/controller.go @@ -19,6 +19,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -924,11 +925,9 @@ func (c *Controller) admitGuardedTurn(body func(ctx context.Context) error, park // spawnGuardedTurn launches an admitted turn body plus its autosave companion. // The caller must already have claimed admission (running=true) under c.mu. func (c *Controller) spawnGuardedTurn(ctx context.Context, cancel context.CancelFunc, body func(ctx context.Context) error) { - c.autosaveWG.Add(1) - go func() { - defer c.autosaveWG.Done() + c.autosaveWG.Go(func() { c.autosaveWhileRunning(ctx) - }() + }) go func() { defer cancel() defer func() { @@ -1149,9 +1148,9 @@ func (c *Controller) stopGoal(status string) { // lastAssistantText returns the content of the most recent assistant message with // non-empty text — the model's final answer for the turn (its plan, in plan mode). func lastAssistantText(msgs []provider.Message) string { - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleAssistant && strings.TrimSpace(msgs[i].Content) != "" { - return msgs[i].Content + for _, msg := range slices.Backward(msgs) { + if msg.Role == provider.RoleAssistant && strings.TrimSpace(msg.Content) != "" { + return msg.Content } } return "" @@ -1626,14 +1625,7 @@ func (c *Controller) applyPlanExec(input, display string) { } // Parse --strict flag. - strict := false - fields := strings.Fields(input) - for _, f := range fields { - if f == "--strict" { - strict = true - break - } - } + strict := slices.Contains(strings.Fields(input), "--strict") // Count completion status. total := len(todos) @@ -4424,8 +4416,8 @@ func resolveInterruptedTurnStart(msgs []provider.Message, idx int, preserveUser // graceful fallback still distinguishes the current visible turn; search // backward so a repeated prompt selects the newest occurrence. if fallbackContent != "" { - for i := len(msgs) - 1; i >= 0; i-- { - if matchesKind(msgs[i]) { + for i, msg := range slices.Backward(msgs) { + if matchesKind(msg) { return i, true } } @@ -4498,10 +4490,8 @@ func appendUniqueString(dst []string, value string) []string { if value == "" { return dst } - for _, existing := range dst { - if existing == value { - return dst - } + if slices.Contains(dst, value) { + return dst } return append(dst, value) } @@ -4515,10 +4505,8 @@ func interruptedToolSummary(call provider.ToolCall) provider.InterruptedToolSumm if path == "" || path == "/dev/null" || len(summary.Files) >= 8 { return } - for _, existing := range summary.Files { - if existing == path { - return - } + if slices.Contains(summary.Files, path) { + return } summary.Files = append(summary.Files, path) } @@ -4530,7 +4518,7 @@ func interruptedToolSummary(call provider.ToolCall) provider.InterruptedToolSumm } } } - for _, line := range strings.Split(call.Diff, "\n") { + for line := range strings.SplitSeq(call.Diff, "\n") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "+++ b/"): @@ -4780,14 +4768,14 @@ func (c *Controller) ToolResult(toolID string) *ToolResultData { msgs := c.executor.Session().Snapshot() // Search backwards: tool result first (most recent), then find the args // from the preceding assistant turn. - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role != provider.RoleTool || msgs[i].ToolCallID != toolID { + for i, msg := range slices.Backward(msgs) { + if msg.Role != provider.RoleTool || msg.ToolCallID != toolID { continue } out := &ToolResultData{ Args: "", - Output: msgs[i].Content, - Execution: msgs[i].ToolExecution, + Output: msg.Content, + Execution: msg.ToolExecution, } // Walk back to find the assistant turn that issued this call. for j := i; j >= 0; j-- { @@ -4843,7 +4831,7 @@ func (c *Controller) ReloadCommands(ctx context.Context) error { entries := make([]command.SlashEntry, 0, len(cmdSkills)+len(cmds)) for _, sk := range cmdSkills { - sk := sk + entries = append(entries, command.SlashEntry{ Name: sk.SlashName(), Description: sk.Description, @@ -4854,7 +4842,7 @@ func (c *Controller) ReloadCommands(ctx context.Context) error { if cmd.Hidden { continue } - cmd := cmd + entries = append(entries, command.SlashEntry{ Name: cmd.Name, Description: cmd.Description, diff --git a/internal/control/controller_lock_test.go b/internal/control/controller_lock_test.go index 2b47fe4658..8e233d191b 100644 --- a/internal/control/controller_lock_test.go +++ b/internal/control/controller_lock_test.go @@ -52,10 +52,8 @@ func TestRewindConcurrentWithHistoryReads(t *testing.T) { stop := make(chan struct{}) var wg sync.WaitGroup - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 4 { + wg.Go(func() { for { select { case <-stop: @@ -65,7 +63,7 @@ func TestRewindConcurrentWithHistoryReads(t *testing.T) { _ = c.CheckpointHasBoundary(lastTurn) } } - }() + }) } err := c.Rewind(lastTurn, RewindConversation) diff --git a/internal/control/controller_test.go b/internal/control/controller_test.go index 59d8a75188..8782312f15 100644 --- a/internal/control/controller_test.go +++ b/internal/control/controller_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "sync" "sync/atomic" @@ -273,9 +274,9 @@ func requestMessagesText(messages []provider.Message) string { } func lastUserMessage(messages []provider.Message) string { - for i := len(messages) - 1; i >= 0; i-- { - if messages[i].Role == provider.RoleUser { - return messages[i].Content + for _, v := range slices.Backward(messages) { + if v.Role == provider.RoleUser { + return v.Content } } return "" @@ -1022,7 +1023,7 @@ func TestRecoverInterruptedTurnAfterCompactionRelocatesVisibleTurn(t *testing.T) path := filepath.Join(dir, "compacted-crash.jsonl") orig := agent.NewSession("sys") - for i := 0; i < 3; i++ { + for range 3 { orig.Add(provider.Message{Role: provider.RoleUser, Content: "old task"}) orig.Add(provider.Message{Role: provider.RoleAssistant, Content: "old answer"}) } @@ -1347,12 +1348,10 @@ func TestConcurrentSnapshotsShareSingleRecoveryHandoff(t *testing.T) { const racingSnapshots = 8 var wg sync.WaitGroup errs := make(chan error, racingSnapshots) - for i := 0; i < racingSnapshots; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range racingSnapshots { + wg.Go(func() { errs <- c.Snapshot() - }() + }) } select { @@ -1664,7 +1663,7 @@ func TestSnapshotConflictAdoptionResetsRewriteBaseline(t *testing.T) { c := New(Options{Executor: exec, SessionDir: dir, SessionPath: path, Label: "test"}) // Rewrites the stale controller never persisted before it noticed the // newer transcript; adoption must discard this counter with the session. - for i := 0; i < 3; i++ { + for range 3 { stale.IncrementRewrite() } @@ -1723,9 +1722,7 @@ func TestConcurrentCompactionAndAutosaveNeverBranch(t *testing.T) { stop := make(chan struct{}) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for { select { case <-stop: @@ -1735,7 +1732,7 @@ func TestConcurrentCompactionAndAutosaveNeverBranch(t *testing.T) { _ = c.SnapshotActivity() } } - }() + }) for i := 1; i <= 40; i++ { sess.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn-%d", i)}) if i%4 == 0 { @@ -2077,9 +2074,9 @@ func (s *noticeSink) notices() []string { func (s *noticeSink) lastNotice() (event.Event, bool) { s.mu.Lock() defer s.mu.Unlock() - for i := len(s.events) - 1; i >= 0; i-- { - if s.events[i].Kind == event.Notice { - return s.events[i], true + for _, v := range slices.Backward(s.events) { + if v.Kind == event.Notice { + return v, true } } return event.Event{}, false @@ -2981,12 +2978,12 @@ func TestConnectConfiguredProjectMCPIsTrustedByDefault(t *testing.T) { }) })) defer server.Close() - if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"), []byte(fmt.Sprintf(` + if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"), fmt.Appendf(nil, ` [[plugins]] name = "project-docs" type = "http" url = %q -`, server.URL)), 0o644); err != nil { +`, server.URL), 0o644); err != nil { t.Fatal(err) } @@ -4096,7 +4093,7 @@ func TestApprovalPersistenceFailureKeepsSessionGrant(t *testing.T) { c.Approve(<-ids, true, true, true) }() - for i := 0; i < 2; i++ { + for i := range 2 { allow, remember, err := gateApprover{c}.Approve(context.Background(), "bash", "go test ./...", nil) if err != nil || !allow || remember { t.Fatalf("Approve call %d = (%v,%v,%v), want session-allowed despite persistence failure", i, allow, remember, err) @@ -4446,10 +4443,10 @@ func TestRunGuardedParksReplacementUntilTurnDoneReturns(t *testing.T) { releaseTurnDone := make(chan struct{}) firstBodyDone := make(chan struct{}) secondBodyRan := make(chan struct{}, 2) - var turnDones int32 + var turnDones atomic.Int32 c := New(Options{Sink: event.FuncSink(func(e event.Event) { if e.Kind == event.TurnDone { - if atomic.AddInt32(&turnDones, 1) == 1 { + if turnDones.Add(1) == 1 { close(firstTurnDone) <-releaseTurnDone } @@ -4496,7 +4493,7 @@ func TestRunGuardedParksReplacementUntilTurnDoneReturns(t *testing.T) { if c.Running() { t.Fatal("controller remained busy after the parked turn completed") } - if got := atomic.LoadInt32(&turnDones); got != 2 { + if got := turnDones.Load(); got != 2 { t.Fatalf("TurnDone emitted %d times, want 2 (one per turn)", got) } select { @@ -4508,13 +4505,13 @@ func TestRunGuardedParksReplacementUntilTurnDoneReturns(t *testing.T) { func TestRunGuardedPanicDoesNotDoubleEmitTurnDone(t *testing.T) { sess := agent.NewSession("sys") - var count int32 + var count atomic.Int32 events := make(chan event.Event, 8) c := New(Options{ Runner: appendingRunner{session: sess}, Sink: event.FuncSink(func(e event.Event) { if e.Kind == event.TurnDone { - atomic.AddInt32(&count, 1) + count.Add(1) } events <- e }), @@ -4530,10 +4527,10 @@ func TestRunGuardedPanicDoesNotDoubleEmitTurnDone(t *testing.T) { for { select { case <-events: - n := atomic.LoadInt32(&count) + n := count.Load() if n >= 1 { time.Sleep(50 * time.Millisecond) - n2 := atomic.LoadInt32(&count) + n2 := count.Load() if n2 > 1 { t.Fatalf("TurnDone emitted %d times, expected 1", n2) } diff --git a/internal/control/goal_concurrency_test.go b/internal/control/goal_concurrency_test.go index f867b862c6..2cbf96ea0b 100644 --- a/internal/control/goal_concurrency_test.go +++ b/internal/control/goal_concurrency_test.go @@ -26,9 +26,7 @@ func TestGoalStateWritesAreConcurrencySafe(t *testing.T) { stop := make(chan struct{}) var readers sync.WaitGroup - readers.Add(1) - go func() { - defer readers.Done() + readers.Go(func() { for { select { case <-stop: @@ -40,7 +38,7 @@ func TestGoalStateWritesAreConcurrencySafe(t *testing.T) { _ = c.GoalStatus() // takes c.mu } } - }() + }) var writers sync.WaitGroup for w := range 8 { diff --git a/internal/control/goal_runtime_test.go b/internal/control/goal_runtime_test.go index e595530ef6..299e3d9ef8 100644 --- a/internal/control/goal_runtime_test.go +++ b/internal/control/goal_runtime_test.go @@ -211,7 +211,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("turn budget pauses", func(t *testing.T) { g := newMachine() - for i := 0; i < g.turnsLimit; i++ { + for i := range g.turnsLimit { // Progress changes every turn so only the turn budget can fire. res := g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "keep going"}, "s", "s"+fmt.Sprint(i))) if res.cont && i == g.turnsLimit-1 { @@ -238,7 +238,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("no-progress pauses", func(t *testing.T) { g := newMachine() - for i := 0; i < g.noProgressLimit; i++ { + for range g.noProgressLimit { g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "still working"}, "sig", "sig")) } if g.status != GoalStatusBlocked || g.stopCause != stopCauseNoProgress { @@ -248,7 +248,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("host-verifiable progress resets no-progress", func(t *testing.T) { g := newMachine() - for i := 0; i < g.noProgressLimit-1; i++ { + for range g.noProgressLimit - 1 { g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "still working"}, "sig", "sig")) } res := g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "progress!"}, "sig", "sig2")) @@ -262,7 +262,7 @@ func TestTurnTokenNoProgressPausesAndResumeExtendsBudget(t *testing.T) { t.Run("resume extends turn budget once", func(t *testing.T) { g := newMachine() - for i := 0; i < g.turnsLimit; i++ { + for range g.turnsLimit { g.advance(in(&goalTurnReport{status: GoalStatusRunning, reason: "keep going"}, "s", "s")) } beforeLimit := g.turnsLimit diff --git a/internal/control/goal_test.go b/internal/control/goal_test.go index 5fc3627b93..db7f38e98b 100644 --- a/internal/control/goal_test.go +++ b/internal/control/goal_test.go @@ -1213,7 +1213,7 @@ func TestRepeatedCompleteWithIncompleteTodosPausesOnBudget(t *testing.T) { // The write-class budget is 20 turns; give the scripted provider a full // pair per turn so the model keeps reporting complete each time. turns := flattenTurns() - for i := 0; i < 21; i++ { + for range 21 { turns = append(turns, goalToolTurn(GoalStatusComplete, "", "")...) } prov := &scriptedTurns{turns: turns} diff --git a/internal/control/imagecompress.go b/internal/control/imagecompress.go index f7082db520..6e69e30bb7 100644 --- a/internal/control/imagecompress.go +++ b/internal/control/imagecompress.go @@ -63,15 +63,9 @@ func compressForVision(raw []byte, mime string) ([]byte, string) { // aspect ratio (each side at least 1px). func scaledDims(w, h, m int) (int, int) { if w >= h { - nh := h * m / w - if nh < 1 { - nh = 1 - } + nh := max(h*m/w, 1) return m, nh } - nw := w * m / h - if nw < 1 { - nw = 1 - } + nw := max(w*m/h, 1) return nw, m } diff --git a/internal/control/imagecompress_test.go b/internal/control/imagecompress_test.go index 46cddfe80c..b219f0042b 100644 --- a/internal/control/imagecompress_test.go +++ b/internal/control/imagecompress_test.go @@ -12,8 +12,8 @@ import ( func makeTestPNG(t *testing.T, w, h int) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { + for y := range h { + for x := range w { img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: uint8(x ^ y), A: 255}) } } diff --git a/internal/control/mcp.go b/internal/control/mcp.go index aa2159144b..a26832cc04 100644 --- a/internal/control/mcp.go +++ b/internal/control/mcp.go @@ -3,6 +3,7 @@ package control import ( "context" "fmt" + "slices" "sync" "time" @@ -173,12 +174,7 @@ func (m *mcpManager) serverNames() []string { // hasServer reports whether a server is live. func (m *mcpManager) hasServer(name string) bool { - for _, n := range m.serverNames() { - if n == name { - return true - } - } - return false + return slices.Contains(m.serverNames(), name) } // prompts lists the live MCP prompts (nil when no host is connected). diff --git a/internal/control/memory_command.go b/internal/control/memory_command.go index 978bd8417b..53b74940d5 100644 --- a/internal/control/memory_command.go +++ b/internal/control/memory_command.go @@ -106,8 +106,8 @@ func MemoryCommandText(api MemoryControl, input string) string { func parseMemoryCommand(input string) (subcommand, rest string) { input = strings.TrimSpace(input) - if strings.HasPrefix(input, "/memory") { - input = strings.TrimSpace(strings.TrimPrefix(input, "/memory")) + if after, ok := strings.CutPrefix(input, "/memory"); ok { + input = strings.TrimSpace(after) } if input == "" { return "", "" diff --git a/internal/control/memory_test.go b/internal/control/memory_test.go index 0f377381a4..0f8ac7e4e8 100644 --- a/internal/control/memory_test.go +++ b/internal/control/memory_test.go @@ -160,9 +160,7 @@ func TestMemoryWritesConcurrencySafe(t *testing.T) { stop := make(chan struct{}) var readers sync.WaitGroup - readers.Add(1) - go func() { - defer readers.Done() + readers.Go(func() { for { select { case <-stop: @@ -173,7 +171,7 @@ func TestMemoryWritesConcurrencySafe(t *testing.T) { _ = c.Memory() // takes c.mu, returns the snapshot pointer } } - }() + }) var writersWG sync.WaitGroup for w := range writers { diff --git a/internal/control/plan_seed_test.go b/internal/control/plan_seed_test.go index cedd75751f..5ec87536dc 100644 --- a/internal/control/plan_seed_test.go +++ b/internal/control/plan_seed_test.go @@ -2,6 +2,7 @@ package control import ( "encoding/json" + "strings" "testing" "reasonix/internal/agent" @@ -140,11 +141,11 @@ func TestPlanTodosJSONAlwaysSatisfiesSerialContract(t *testing.T) { } func TestParsePlanTodosCapsAtTwenty(t *testing.T) { - plan := "" - for i := 0; i < 30; i++ { - plan += "- item\n" + var plan strings.Builder + for range 30 { + plan.WriteString("- item\n") } - if got := parsePlanTodos(plan); len(got) != 20 { + if got := parsePlanTodos(plan.String()); len(got) != 20 { t.Fatalf("got %d todos, want cap of 20", len(got)) } } diff --git a/internal/control/plan_todos.go b/internal/control/plan_todos.go index 0f9ed8d69c..2ed77d988e 100644 --- a/internal/control/plan_todos.go +++ b/internal/control/plan_todos.go @@ -5,6 +5,7 @@ package control import ( "encoding/json" + "slices" "strings" "reasonix/internal/event" @@ -134,7 +135,7 @@ func completedPlanTodosJSON(args string) string { // todo_write calls as it executes. func parsePlanTodos(plan string) []seedTodo { var todos []seedTodo - for _, raw := range strings.Split(plan, "\n") { + for raw := range strings.SplitSeq(plan, "\n") { item, level, ok := listItem(raw) if !ok { continue @@ -189,8 +190,8 @@ func (c *Controller) hasTodoUpdateSince(start int) bool { func latestTodoArgsSince(msgs []provider.Message, start int) (string, bool) { for i := len(msgs) - 1; i >= start; i-- { - for j := len(msgs[i].ToolCalls) - 1; j >= 0; j-- { - tc := msgs[i].ToolCalls[j] + for _, v := range slices.Backward(msgs[i].ToolCalls) { + tc := v if tc.Name == "todo_write" { return tc.Arguments, true } diff --git a/internal/control/planner_gate.go b/internal/control/planner_gate.go index be07f1faf0..e791045f51 100644 --- a/internal/control/planner_gate.go +++ b/internal/control/planner_gate.go @@ -3,6 +3,7 @@ package control import ( "context" "regexp" + "slices" "strings" "unicode" "unicode/utf8" @@ -247,8 +248,8 @@ func normalizePlannerText(text string) string { func hasLeadingDirective(lower string, directives []string) bool { lower = strings.TrimSpace(lower) for _, polite := range []string{"please ", "please, ", "请", "请先", "麻烦", "麻烦先"} { - if strings.HasPrefix(lower, polite) { - lower = strings.TrimSpace(strings.TrimPrefix(lower, polite)) + if after, ok := strings.CutPrefix(lower, polite); ok { + lower = strings.TrimSpace(after) break } } @@ -582,14 +583,9 @@ func containsLexicalTerm(s, term string) bool { if containsNonASCII(term) || strings.ContainsAny(term, " -_/") { return strings.Contains(s, term) } - for _, token := range strings.FieldsFunc(s, func(r rune) bool { + return slices.Contains(strings.FieldsFunc(s, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' - }) { - if token == term { - return true - } - } - return false + }), term) } func containsNonASCII(s string) bool { diff --git a/internal/control/recovery.go b/internal/control/recovery.go index ade821a589..9d72c1699d 100644 --- a/internal/control/recovery.go +++ b/internal/control/recovery.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strings" "unicode/utf8" @@ -117,9 +118,9 @@ func (c *Controller) initRecoveryGate(reviewer recovery.Reviewer, headless bool) return "" } msgs := c.executor.Session().Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - if string(msgs[i].Role) == "user" && strings.TrimSpace(msgs[i].Content) != "" { - text := agent.UserMessageText(msgs[i]) + for _, v := range slices.Backward(msgs) { + if string(v.Role) == "user" && strings.TrimSpace(v.Content) != "" { + text := agent.UserMessageText(v) if len(text) > 800 { return text[:800] + "…" } diff --git a/internal/control/refs.go b/internal/control/refs.go index 2cff158a5f..8e07d9b7c9 100644 --- a/internal/control/refs.go +++ b/internal/control/refs.go @@ -137,7 +137,7 @@ func EscapeRefPath(path string) string { } var b strings.Builder b.Grow(len(path) + 8) - for i := 0; i < len(path); i++ { + for i := range len(path) { if path[i] == ' ' || path[i] == '\t' { b.WriteByte('\\') } @@ -154,7 +154,7 @@ func UnescapeRefPath(path string) string { } var b strings.Builder b.Grow(len(path)) - for i := 0; i < len(path); i++ { + for i := range len(path) { if path[i] == '\\' && i+1 < len(path) && (path[i+1] == ' ' || path[i+1] == '\t') { continue } diff --git a/internal/control/turn_orchestrator_test.go b/internal/control/turn_orchestrator_test.go index 98ebcc523c..520050e933 100644 --- a/internal/control/turn_orchestrator_test.go +++ b/internal/control/turn_orchestrator_test.go @@ -921,7 +921,7 @@ func TestTurnOrchestratorInterruptedAfterCompactionRelocatesVisibleTurn(t *testi } { t.Run(tc.name, func(t *testing.T) { sess := agent.NewSession("system") - for i := 0; i < 3; i++ { + for range 3 { sess.Add(provider.Message{Role: provider.RoleUser, Content: "old task"}) sess.Add(provider.Message{Role: provider.RoleAssistant, Content: "old answer"}) } diff --git a/internal/crashreport/cli.go b/internal/crashreport/cli.go index 8a36cb0635..aeaee17d6b 100644 --- a/internal/crashreport/cli.go +++ b/internal/crashreport/cli.go @@ -321,7 +321,7 @@ func sanitizeStack(stack string) string { func topFrame(stack string) string { fallback := "" functionName := "" - for _, line := range strings.Split(stack, "\n") { + for line := range strings.SplitSeq(stack, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "/") && strings.Contains(line, ".go:") { frame := line @@ -337,8 +337,8 @@ func topFrame(stack string) string { functionName = "" continue } - if strings.HasSuffix(line, "(...)") { - functionName = strings.TrimSpace(strings.TrimSuffix(line, "(...)")) + if before, ok := strings.CutSuffix(line, "(...)"); ok { + functionName = strings.TrimSpace(before) } } return clip(fallback, 300) diff --git a/internal/crashreport/cli_test.go b/internal/crashreport/cli_test.go index 1d6a54b6f5..bca4ded010 100644 --- a/internal/crashreport/cli_test.go +++ b/internal/crashreport/cli_test.go @@ -74,7 +74,7 @@ func TestCapturePanicWritesBoundedSanitizedReport(t *testing.T) { t.Fatalf("report mode=%v", info.Mode().Perm()) } - for i := 0; i < maxReports+5; i++ { + for i := range maxReports + 5 { if err := CapturePanic(home, "v1.20.0", i, []byte(stack)); err != nil { t.Fatal(err) } @@ -139,7 +139,7 @@ func TestConcurrentCaptureKeepsQueueBounded(t *testing.T) { const writers = 32 var wg sync.WaitGroup start := make(chan struct{}) - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(value int) { defer wg.Done() @@ -169,7 +169,7 @@ func TestCapturePanicPrunesOnlyCurrentReportFormat(t *testing.T) { t.Fatal(err) } - for i := 0; i < maxReports+1; i++ { + for i := range maxReports + 1 { if err := CapturePanic(home, "v1.20.0", i, []byte("reasonix.run()\n\t/home/alice/main.go:12")); err != nil { t.Fatal(err) } diff --git a/internal/desktoplauncher/launcher.go b/internal/desktoplauncher/launcher.go index 660ff097d1..48d4905799 100644 --- a/internal/desktoplauncher/launcher.go +++ b/internal/desktoplauncher/launcher.go @@ -152,7 +152,7 @@ func siblingDesktop(installRoot string) string { func StripLegacyLaunchArgs(args []string) []string { out := make([]string, 0, len(args)) skipNext := false - for i := 0; i < len(args); i++ { + for i := range args { if skipNext { skipNext = false continue diff --git a/internal/diff/diff_extra_test.go b/internal/diff/diff_extra_test.go index a5e3be09b4..7053ac6fb1 100644 --- a/internal/diff/diff_extra_test.go +++ b/internal/diff/diff_extra_test.go @@ -134,7 +134,7 @@ func TestBuildWhitespaceOnly(t *testing.T) { func TestBuildLargeFile(t *testing.T) { // Build a large file with one changed line in the middle. var oldB, newB strings.Builder - for i := 0; i < 1000; i++ { + for i := range 1000 { oldB.WriteString("line\n") if i == 500 { newB.WriteString("CHANGED\n") diff --git a/internal/diff/largediff_test.go b/internal/diff/largediff_test.go index c94f734dd2..601d923996 100644 --- a/internal/diff/largediff_test.go +++ b/internal/diff/largediff_test.go @@ -15,7 +15,7 @@ import ( func TestLargeRewriteBoundedCost(t *testing.T) { var oldB, newB strings.Builder const n = 6000 - for i := 0; i < n; i++ { + for i := range n { fmt.Fprintf(&oldB, "old line %d\n", i) fmt.Fprintf(&newB, "totally different new line %d\n", i) } @@ -49,7 +49,7 @@ func TestLargeRewriteBoundedCost(t *testing.T) { func TestSmallEditOnLargeFileStillDiffs(t *testing.T) { var oldB strings.Builder const n = 8000 - for i := 0; i < n; i++ { + for i := range n { fmt.Fprintf(&oldB, "line %d\n", i) } old := oldB.String() diff --git a/internal/doctor/session_redact.go b/internal/doctor/session_redact.go index ed32a46d02..54fbabb1ea 100644 --- a/internal/doctor/session_redact.go +++ b/internal/doctor/session_redact.go @@ -157,8 +157,8 @@ func sessionRedactionLeaseHeld(sessionPath string) bool { if agent.SessionLeaseHeld(sessionPath) { return true } - if strings.HasSuffix(sessionPath, ".guardian.jsonl") { - parent := strings.TrimSuffix(sessionPath, ".guardian.jsonl") + ".jsonl" + if before, ok := strings.CutSuffix(sessionPath, ".guardian.jsonl"); ok { + parent := before + ".jsonl" return agent.SessionLeaseHeld(parent) } return false diff --git a/internal/doctor/skill_health.go b/internal/doctor/skill_health.go index 82ffa97f0f..cb2a40dd8c 100644 --- a/internal/doctor/skill_health.go +++ b/internal/doctor/skill_health.go @@ -58,8 +58,8 @@ func CollectSkillHealthWarnings(opts SkillHealthOptions) []string { if dep == "" { continue } - if strings.HasPrefix(dep, "mcp-server:") { - srv := strings.TrimPrefix(dep, "mcp-server:") + if after, ok := strings.CutPrefix(dep, "mcp-server:"); ok { + srv := after if !pluginNames[srv] { out = append(out, fmt.Sprintf("skill %q requires %s but that MCP server is not configured", name, dep)) } else if reason, ok := opts.FailedServers[srv]; ok && reason != "" { diff --git a/internal/environment/probe.go b/internal/environment/probe.go index 011e1d8a7c..6186c97e2e 100644 --- a/internal/environment/probe.go +++ b/internal/environment/probe.go @@ -292,8 +292,8 @@ func sortResults(results []ProbeResult) { func firstLine(s string) string { s = strings.TrimSpace(s) - if i := strings.IndexByte(s, '\n'); i >= 0 { - return strings.TrimRight(s[:i], "\r") + if before, _, ok := strings.Cut(s, "\n"); ok { + return strings.TrimRight(before, "\r") } return s } diff --git a/internal/environment/probe_test.go b/internal/environment/probe_test.go index efc4ca5e62..c04054fc8f 100644 --- a/internal/environment/probe_test.go +++ b/internal/environment/probe_test.go @@ -251,7 +251,7 @@ func TestRunProbesCacheSeparatesOverrides(t *testing.T) { func TestFormatSectionLimitsToolOutput(t *testing.T) { overrides := map[string]string{} var results []ProbeResult - for i := 0; i < maxRenderedTools+2; i++ { + for i := range maxRenderedTools + 2 { name := fmt.Sprintf("tool%02d", i) overrides[name] = "/bin/" + name results = append(results, ProbeResult{Binary: name, Found: true, Output: "ok"}) diff --git a/internal/event/event_test.go b/internal/event/event_test.go index cecaa3a8d2..70e7534453 100644 --- a/internal/event/event_test.go +++ b/internal/event/event_test.go @@ -280,12 +280,10 @@ func TestFuncSinkForwardsEachConcurrentEmit(t *testing.T) { mu.Unlock() }) var wg sync.WaitGroup - for i := 0; i < 100; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 100 { + wg.Go(func() { sink.Emit(Event{Kind: Text}) - }() + }) } wg.Wait() mu.Lock() diff --git a/internal/eventwire/wire.go b/internal/eventwire/wire.go index 1be6795a04..b039405b54 100644 --- a/internal/eventwire/wire.go +++ b/internal/eventwire/wire.go @@ -451,7 +451,7 @@ func ToWireCacheDiagnostics(d *event.CacheDiagnostics) *CacheDiagnostics { // generator; callers receive a copy and may sort it without mutating eventwire. func KindNames() []string { names := make([]string, 0, int(event.KindCount)) - for kind := event.Kind(0); kind < event.KindCount; kind++ { + for kind := range event.KindCount { if name, ok := kindNames[kind]; ok { names = append(names, name) } diff --git a/internal/eventwire/wire_test.go b/internal/eventwire/wire_test.go index 7f414f3885..b5ebd3a9fe 100644 --- a/internal/eventwire/wire_test.go +++ b/internal/eventwire/wire_test.go @@ -96,7 +96,7 @@ func TestToWireNoticeCarriesDecisionReceipt(t *testing.T) { } func TestKindNamesComplete(t *testing.T) { - for k := event.Kind(0); k < event.KindCount; k++ { + for k := range event.KindCount { if ToWire(event.Event{Kind: k}).Kind == "" { t.Fatalf("kind %d has no wire name", k) } @@ -105,7 +105,7 @@ func TestKindNamesComplete(t *testing.T) { func TestDesktopWireEventKindTypeCoversSharedKinds(t *testing.T) { ts := readDesktopTypes(t) - for k := event.Kind(0); k < event.KindCount; k++ { + for k := range event.KindCount { kind := ToWire(event.Event{Kind: k}).Kind if !strings.Contains(ts, `"`+kind+`"`) { t.Fatalf("desktop WireEvent EventKind is missing %q", kind) diff --git a/internal/evidence/evidence.go b/internal/evidence/evidence.go index fd38d40555..7767ec50c4 100644 --- a/internal/evidence/evidence.go +++ b/internal/evidence/evidence.go @@ -7,6 +7,7 @@ import ( "fmt" "path/filepath" "runtime" + "slices" "strconv" "strings" "sync" @@ -765,10 +766,7 @@ func (l *Ledger) HasSuccessfulCommandAfter(command string, after int) bool { if l == nil || command == "" { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() @@ -785,10 +783,7 @@ func (l *Ledger) HasSuccessfulCompleteStepAfter(after int) bool { if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() @@ -810,10 +805,7 @@ func (l *Ledger) HasSuccessfulDeliverySignoffAfter(after int) bool { if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() receipts := append([]Receipt(nil), l.receipts...) @@ -851,10 +843,7 @@ func (l *Ledger) HasSuccessfulReviewAfter(after int) bool { if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() receipts := append([]Receipt(nil), l.receipts...) @@ -875,10 +864,7 @@ func (l *Ledger) HasHostReviewCoverageAfter(after int, requiredPaths []string) b if l == nil { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() receipts := append([]Receipt(nil), l.receipts...) l.mu.Unlock() @@ -1036,8 +1022,8 @@ func (l *Ledger) IncompleteLatestTodos() ([]TodoStepMatch, bool) { } l.mu.Lock() defer l.mu.Unlock() - for i := len(l.receipts) - 1; i >= 0; i-- { - r := l.receipts[i] + for _, v := range slices.Backward(l.receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } @@ -1251,10 +1237,7 @@ func (l *Ledger) HasSuccessfulAnchorRefreshReadAfter(paths []string, after int) if l == nil || len(wanted) == 0 { return false } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() @@ -1331,8 +1314,8 @@ func (l *Ledger) MatchLatestTodoStep(step string) (TodoStepMatch, bool) { } l.mu.Lock() defer l.mu.Unlock() - for i := len(l.receipts) - 1; i >= 0; i-- { - r := l.receipts[i] + for _, v := range slices.Backward(l.receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } @@ -1348,8 +1331,8 @@ func (l *Ledger) LatestTodos() ([]TodoItem, bool) { } l.mu.Lock() defer l.mu.Unlock() - for i := len(l.receipts) - 1; i >= 0; i-- { - r := l.receipts[i] + for _, v := range slices.Backward(l.receipts) { + r := v if r.Success && r.ToolName == "todo_write" { return append([]TodoItem(nil), r.Todos...), true } @@ -1374,8 +1357,8 @@ func (l *Ledger) UnverifiedCompletedTodos(current []TodoItem) (missing []TodoSte var previous []TodoItem baseline := -1 - for i := len(receipts) - 1; i >= 0; i-- { - r := receipts[i] + for i, v := range slices.Backward(receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } @@ -1444,10 +1427,7 @@ func hasFailedCompleteStepRecoveryForTodo(receipts []Receipt, baseline int, inde // Recovery only trusts progress that happened before the failed sign-off. // Later unrelated work must not retroactively authorize an earlier completion. func hasSuccessfulProgressBeforeReceipt(receipts []Receipt, baseline int, before int) bool { - start := baseline + 1 - if start < 0 { - start = 0 - } + start := max(baseline+1, 0) for i := start; i < before && i < len(receipts); i++ { r := receipts[i] if !r.Success || r.ToolName == "todo_write" || r.ToolName == "complete_step" || r.Read { @@ -1811,12 +1791,7 @@ func bashCommandUsesOpaqueInlineInterpreter(command string) bool { if !ok { return false } - for _, segment := range segments { - if bashSegmentUsesOpaqueInlineInterpreter(segment) { - return true - } - } - return false + return slices.ContainsFunc(segments, bashSegmentUsesOpaqueInlineInterpreter) } func bashSegmentUsesOpaqueInlineInterpreter(segment string) bool { @@ -2018,12 +1993,7 @@ func bashSegmentIsVerification(fields []string) bool { return true } if args[0] == "test" { - for _, arg := range args[1:] { - if goTestFlagWritesFile(arg) { - return false - } - } - return true + return !slices.ContainsFunc(args[1:], goTestFlagWritesFile) } // A package pattern can expand to one main package, so even `go build // ./...` may write a workspace binary. Package expansion and inherited @@ -2037,12 +2007,7 @@ func bashSegmentIsVerification(fields []string) bool { case "tsc": return tscSegmentIsVerification(args) case "mypy": - for _, arg := range args { - if mypyFlagWritesReport(arg) { - return false - } - } - return true + return !slices.ContainsFunc(args, mypyFlagWritesReport) case "npm", "pnpm", "yarn", "bun", "cargo": if len(args) > 0 && hasCommandArg(args[:1], "test", "check", "lint", "clippy") { return true @@ -2241,12 +2206,7 @@ func nodeSegmentIsVerification(args []string) bool { case "--test": // Match the repository's treatment of other conventional test runners, // but fail closed on test-runner and Node runtime flags that write files. - for _, arg := range args[1:] { - if nodeTestFlagWritesFile(arg) { - return false - } - } - return true + return !slices.ContainsFunc(args[1:], nodeTestFlagWritesFile) default: return false } @@ -2541,8 +2501,8 @@ func argNamesPath(arg, needle string) bool { if tok == needle || strings.HasSuffix(tok, "/"+needle) { return true } - if i := strings.Index(tok, ":"); i >= 0 { - rest := tok[i+1:] + if _, after, ok := strings.Cut(tok, ":"); ok { + rest := after if rest == needle || strings.HasSuffix(rest, "/"+needle) { return true } @@ -2788,8 +2748,8 @@ func hasSuccessfulCompleteStepForTodo(receipts []Receipt, index int, current []T } func latestTodoStep(step string, receipts []Receipt) TodoStepMatch { - for i := len(receipts) - 1; i >= 0; i-- { - r := receipts[i] + for _, v := range slices.Backward(receipts) { + r := v if !r.Success || r.ToolName != "todo_write" { continue } diff --git a/internal/evidence/review_report.go b/internal/evidence/review_report.go index 2e2276406f..df0638e831 100644 --- a/internal/evidence/review_report.go +++ b/internal/evidence/review_report.go @@ -161,10 +161,7 @@ func (l *Ledger) HasStructuredReviewAfter(kind ReviewKind, after int, requiredPa if l == nil { return false, false, nil } - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) l.mu.Lock() defer l.mu.Unlock() for i := start; i < len(l.receipts); i++ { diff --git a/internal/evidence/risk.go b/internal/evidence/risk.go index 2118695f53..7fa72a5cc7 100644 --- a/internal/evidence/risk.go +++ b/internal/evidence/risk.go @@ -36,10 +36,7 @@ var highRiskToolHints = []string{ // Medium: ordinary production code or limited multi-file edits. // High: security-sensitive surfaces, opaque mutations, or 10+ paths. func ClassifyMutationRisk(receipts []Receipt, after int) RiskLevel { - start := after + 1 - if start < 0 { - start = 0 - } + start := max(after+1, 0) var paths []string seen := map[string]bool{} opaque := false @@ -123,10 +120,7 @@ func (l *Ledger) PathsSince(after int) []string { if l == nil { return nil } - start := after - if start < 0 { - start = 0 - } + start := max(after, 0) l.mu.Lock() defer l.mu.Unlock() seen := map[string]bool{} diff --git a/internal/extension/benchmark_test.go b/internal/extension/benchmark_test.go index cba4e5d39e..48996fdb30 100644 --- a/internal/extension/benchmark_test.go +++ b/internal/extension/benchmark_test.go @@ -2,7 +2,7 @@ package extension import ( "context" - "sort" + "slices" "testing" "time" ) @@ -17,7 +17,7 @@ func BenchmarkExtensionKernelStartup(b *testing.B) { }) contributions := make([]Contribution, 0, 64) - for i := 0; i < 64; i++ { + for i := range 64 { contributions = append(contributions, Contribution{ Kind: KindInterceptor, ID: string(PointToolBefore), @@ -61,7 +61,7 @@ func benchmarkBuildLatency(b *testing.B, builder *Builder) { } } b.StopTimer() - sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) + slices.Sort(samples) if len(samples) == 0 { return } diff --git a/internal/extension/builder.go b/internal/extension/builder.go index 4a6674ccae..6e163675a3 100644 --- a/internal/extension/builder.go +++ b/internal/extension/builder.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "strings" "reasonix/internal/provider" @@ -408,9 +409,7 @@ func (b *Builder) assemble(resolved []Contribution, replacements map[Slot]Contri } repl := make(map[Slot]ContributionSource, len(replacements)) - for slot, owner := range replacements { - repl[slot] = owner - } + maps.Copy(repl, replacements) diagnostics := make([]string, 0, len(conflicts)) for i := range conflicts { diff --git a/internal/extension/builder_test.go b/internal/extension/builder_test.go index 27be3bcc9b..38f9ac7921 100644 --- a/internal/extension/builder_test.go +++ b/internal/extension/builder_test.go @@ -71,7 +71,7 @@ func TestBuildDeterminism(t *testing.T) { hash string } var reference *fingerprint - for seed := int64(0); seed < 100; seed++ { + for seed := range int64(100) { r := rand.New(rand.NewSource(seed)) perm := r.Perm(len(contributors)) b := NewBuilder().WithSystemPrompt("system prompt v1").WithGeneration(7) diff --git a/internal/extension/dispatch/benchmark_test.go b/internal/extension/dispatch/benchmark_test.go index 1c17045629..7901d4e4c5 100644 --- a/internal/extension/dispatch/benchmark_test.go +++ b/internal/extension/dispatch/benchmark_test.go @@ -3,7 +3,7 @@ package dispatch import ( "context" "encoding/json" - "sort" + "slices" "strconv" "testing" "time" @@ -37,7 +37,6 @@ func BenchmarkDispatchLatency(b *testing.B) { }) }) for _, count := range []int{1, 4} { - count := count b.Run("Turn/NoopInterceptors"+strconv.Itoa(count), func(b *testing.B) { d := benchmarkDispatcher(extension.PointInputReceive, count) payload := InputPayload{Text: "hello"} @@ -60,7 +59,7 @@ func BenchmarkDispatchLatency(b *testing.B) { func benchmarkDispatcher(point extension.InterceptorPoint, count int) *Dispatcher { chain := make([]extension.Contribution, 0, count) clients := make(map[string]Client, count) - for i := 0; i < count; i++ { + for i := range count { pluginID := string(rune('a' + i)) chain = append(chain, extension.Contribution{ Kind: extension.KindInterceptor, ID: string(point), @@ -87,7 +86,7 @@ func benchmarkLatency(b *testing.B, fn func() error) { } } b.StopTimer() - sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) + slices.Sort(samples) if len(samples) == 0 { return } diff --git a/internal/extension/dispatch/dispatch_test.go b/internal/extension/dispatch/dispatch_test.go index 622d8185c6..e3afc7b743 100644 --- a/internal/extension/dispatch/dispatch_test.go +++ b/internal/extension/dispatch/dispatch_test.go @@ -240,7 +240,6 @@ func pointCases() []pointCase { }, } for _, phase := range []string{PhaseStart, PhaseEnd, PhaseLoad, PhaseSave, PhaseRotate} { - phase := phase point := extension.InterceptorPoint("session." + phase) cases = append(cases, pointCase{ point: point, @@ -791,7 +790,7 @@ func TestOptionalTimeoutWarnsOnce(t *testing.T) { d := buildDispatcher( map[extension.InterceptorPoint][]extension.Contribution{point: {interceptor("opt", point, 0)}}, nil, map[string]*fakeClient{"opt": fake}, nil, warns) - for i := 0; i < 2; i++ { + for i := range 2 { payload := &ToolBeforePayload{Name: "bash", Arguments: `{"cmd":"ls"}`} result, err := d.Intercept(context.Background(), point, payload) if err != nil { @@ -990,7 +989,7 @@ func TestConcurrentDispatch(t *testing.T) { var wg sync.WaitGroup errs := make(chan error, 32) - for i := 0; i < 32; i++ { + for i := range 32 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/extension/protocol/dto_test.go b/internal/extension/protocol/dto_test.go index ea9905c567..d03e8b51f8 100644 --- a/internal/extension/protocol/dto_test.go +++ b/internal/extension/protocol/dto_test.go @@ -319,11 +319,11 @@ func TestExternalizableFieldsAcceptNullPlaceholder(t *testing.T) { []byte(`{"surfaceId":"s","sessionId":"s","generation":0,"kind":"card","payload":null}`)); err == nil { t.Fatal("non-externalizable payload accepted null") } - pointers := ExternalizablePointers(reflect.TypeOf(InterceptParams{})) + pointers := ExternalizablePointers(reflect.TypeFor[InterceptParams]()) if !reflect.DeepEqual(pointers, []string{"/payload"}) { t.Fatalf("ExternalizablePointers(InterceptParams) = %v", pointers) } - pointers = ExternalizablePointers(reflect.TypeOf(ProviderRequest{})) + pointers = ExternalizablePointers(reflect.TypeFor[ProviderRequest]()) if !reflect.DeepEqual(pointers, []string{"/messages/*/content"}) { t.Fatalf("ExternalizablePointers(ProviderRequest) = %v", pointers) } diff --git a/internal/extension/protocol/dto_ui.go b/internal/extension/protocol/dto_ui.go index 4d586b5676..b123991c7c 100644 --- a/internal/extension/protocol/dto_ui.go +++ b/internal/extension/protocol/dto_ui.go @@ -147,13 +147,13 @@ func DecodeUIPublishPayload(kind UISurfaceKind, raw json.RawMessage) (any, error var typ reflect.Type switch kind { case UISurfaceStatus: - typ = reflect.TypeOf(UIStatusPayload{}) + typ = reflect.TypeFor[UIStatusPayload]() case UISurfaceCard: - typ = reflect.TypeOf(UICardPayload{}) + typ = reflect.TypeFor[UICardPayload]() case UISurfaceForm: - typ = reflect.TypeOf(UIFormPayload{}) + typ = reflect.TypeFor[UIFormPayload]() case UISurfaceNotification: - typ = reflect.TypeOf(UINotificationPayload{}) + typ = reflect.TypeFor[UINotificationPayload]() default: return nil, fmt.Errorf("protocol: unknown UI surface kind %q", kind) } @@ -167,7 +167,7 @@ func DecodeUIPublishPayload(kind UISurfaceKind, raw json.RawMessage) (any, error func DecodeUIRequestPayload(kind UIRequestKind, raw json.RawMessage) (any, error) { switch kind { case UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect: - return decodeAndValidate(raw, reflect.TypeOf(UIFormPayload{})) + return decodeAndValidate(raw, reflect.TypeFor[UIFormPayload]()) default: return nil, fmt.Errorf("protocol: unknown UI request kind %q", kind) } diff --git a/internal/extension/protocol/registry.go b/internal/extension/protocol/registry.go index f3288e2372..73a870dcc8 100644 --- a/internal/extension/protocol/registry.go +++ b/internal/extension/protocol/registry.go @@ -66,7 +66,7 @@ func extensionNotification[P any](name Method, class OperationClass) MethodSpec return MethodSpec{name, DirectionExtensionToHostNotification, class, typeOf[P](), typeOf[NoResult]()} } -func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() } +func typeOf[T any]() reflect.Type { return reflect.TypeFor[T]() } // frozenRegistry is the Extension Protocol v1 method set. Adding, renaming, // or redirecting a method is a conscious protocol change: ValidateRegistry diff --git a/internal/extension/protocol/schema.go b/internal/extension/protocol/schema.go index 99c39aba66..0c258038a9 100644 --- a/internal/extension/protocol/schema.go +++ b/internal/extension/protocol/schema.go @@ -16,7 +16,7 @@ const SchemaDraft202012 = "https://json-schema.org/draft/2020-12/schema" // SchemaTitle is the generated document's human title. const SchemaTitle = "Reasonix Extension Protocol v1" -var rawMessageType = reflect.TypeOf(json.RawMessage{}) +var rawMessageType = reflect.TypeFor[json.RawMessage]() // BuildSchemaDocument reflection-walks the frozen registry and produces the // canonical JSON Schema (draft 2020-12) document: one methods object keyed by @@ -162,7 +162,7 @@ func buildJSONSchema(defs map[string]any, typ reflect.Type) (any, error) { func buildObjectSchema(defs map[string]any, typ reflect.Type) (map[string]any, error) { properties := map[string]any{} var required []string - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -215,7 +215,7 @@ func applyFieldTags(schema any, field reflect.StructField) any { } return schema } - for _, tag := range strings.Split(field.Tag.Get("validate"), ",") { + for tag := range strings.SplitSeq(field.Tag.Get("validate"), ",") { switch { case tag == "nonempty": if object["type"] == "string" { diff --git a/internal/extension/protocol/validate.go b/internal/extension/protocol/validate.go index 94ccb5bb94..f93db770b9 100644 --- a/internal/extension/protocol/validate.go +++ b/internal/extension/protocol/validate.go @@ -8,6 +8,7 @@ import ( "io" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -29,19 +30,19 @@ var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) // The strict decoder rejects anything outside these sets; the schema // generator emits them as JSON Schema enums. var enumTypes = map[reflect.Type][]string{ - reflect.TypeOf(Direction("")): values(DirectionHostToExtensionRequest, DirectionExtensionToHostRequest, DirectionHostToExtensionNotification, DirectionExtensionToHostNotification), - reflect.TypeOf(OperationClass("")): values(ClassLifecycle, ClassIntercept, ClassObservation, ClassProvider, ClassUI, ClassContent), - reflect.TypeOf(InterceptEvent("")): interceptEventValues(), - reflect.TypeOf(InterceptDecision("")): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny), - reflect.TypeOf(UIHostKind("")): values(UIHostTUI, UIHostDesktop, UIHostACP, UIHostHeadless), - reflect.TypeOf(UISurfaceKind("")): values(UISurfaceStatus, UISurfaceCard, UISurfaceForm, UISurfaceNotification), - reflect.TypeOf(UIRequestKind("")): values(UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect), - reflect.TypeOf(UIFieldKind("")): values(UIFieldConfirm, UIFieldInput, UIFieldSelect, UIFieldMultiselect), - reflect.TypeOf(UISeverity("")): values(UISeverityInfo, UISeverityWarn, UISeverityError), - reflect.TypeOf(ProviderRole("")): values(ProviderRoleSystem, ProviderRoleUser, ProviderRoleAssistant, ProviderRoleTool), - reflect.TypeOf(ProviderChunkType("")): values(ChunkText, ChunkReasoning, ChunkToolCallStart, ChunkToolCallDelta, ChunkToolCall, ChunkUsage, ChunkDone, ChunkError), - reflect.TypeOf(ProviderErrorCode("")): values(ProviderFailed, ProviderInterrupted), - reflect.TypeOf(ContentEncoding("")): values(ContentUTF8), + reflect.TypeFor[Direction](): values(DirectionHostToExtensionRequest, DirectionExtensionToHostRequest, DirectionHostToExtensionNotification, DirectionExtensionToHostNotification), + reflect.TypeFor[OperationClass](): values(ClassLifecycle, ClassIntercept, ClassObservation, ClassProvider, ClassUI, ClassContent), + reflect.TypeFor[InterceptEvent](): interceptEventValues(), + reflect.TypeFor[InterceptDecision](): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny), + reflect.TypeFor[UIHostKind](): values(UIHostTUI, UIHostDesktop, UIHostACP, UIHostHeadless), + reflect.TypeFor[UISurfaceKind](): values(UISurfaceStatus, UISurfaceCard, UISurfaceForm, UISurfaceNotification), + reflect.TypeFor[UIRequestKind](): values(UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect), + reflect.TypeFor[UIFieldKind](): values(UIFieldConfirm, UIFieldInput, UIFieldSelect, UIFieldMultiselect), + reflect.TypeFor[UISeverity](): values(UISeverityInfo, UISeverityWarn, UISeverityError), + reflect.TypeFor[ProviderRole](): values(ProviderRoleSystem, ProviderRoleUser, ProviderRoleAssistant, ProviderRoleTool), + reflect.TypeFor[ProviderChunkType](): values(ChunkText, ChunkReasoning, ChunkToolCallStart, ChunkToolCallDelta, ChunkToolCall, ChunkUsage, ChunkDone, ChunkError), + reflect.TypeFor[ProviderErrorCode](): values(ProviderFailed, ProviderInterrupted), + reflect.TypeFor[ContentEncoding](): values(ContentUTF8), } func init() { @@ -50,7 +51,7 @@ func init() { for i := range contracts { reasons[i] = string(contracts[i].Reason) } - enumTypes[reflect.TypeOf(ErrorReason(""))] = reasons + enumTypes[reflect.TypeFor[ErrorReason]()] = reasons } // EnumValues returns the frozen wire values of every string enum DTO type, @@ -133,7 +134,7 @@ func validateRequiredJSON(raw json.RawMessage, typ reflect.Type, at string) erro } func validateRequiredObject(object map[string]json.RawMessage, typ reflect.Type, at string) error { - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -172,7 +173,7 @@ func validateNestedRequired(raw json.RawMessage, typ reflect.Type, at string) er for typ.Kind() == reflect.Pointer { typ = typ.Elem() } - if typ == reflect.TypeOf(json.RawMessage{}) { + if typ == reflect.TypeFor[json.RawMessage]() { if len(bytes.TrimSpace(raw)) == 0 || !json.Valid(raw) { return validationError(at + " must contain valid JSON") } @@ -219,7 +220,7 @@ func validateValue(value reflect.Value, at string, omitEmpty bool) error { return validateValue(value.Elem(), at, false) } typ := value.Type() - if typ == reflect.TypeOf(json.RawMessage{}) { + if typ == reflect.TypeFor[json.RawMessage]() { raw := value.Interface().(json.RawMessage) if len(bytes.TrimSpace(raw)) == 0 { // An empty RawMessage is the zero value of an omitempty field and @@ -242,7 +243,7 @@ func validateValue(value reflect.Value, at string, omitEmpty bool) error { } switch value.Kind() { case reflect.Struct: - for i := 0; i < value.NumField(); i++ { + for i := range value.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -277,7 +278,7 @@ func validateValue(value reflect.Value, at string, omitEmpty bool) error { } } case reflect.Slice, reflect.Array: - for i := 0; i < value.Len(); i++ { + for i := range value.Len() { if err := validateValue(value.Index(i), fmt.Sprintf("%s[%d]", at, i), false); err != nil { return err } @@ -309,7 +310,7 @@ func validateTag(value reflect.Value, tags, at string, omitEmpty bool) error { } value = value.Elem() } - for _, tag := range strings.Split(tags, ",") { + for tag := range strings.SplitSeq(tags, ",") { switch { case tag == "nonempty": if value.Kind() == reflect.String && strings.TrimSpace(value.String()) == "" { @@ -347,12 +348,7 @@ func numericValue(value reflect.Value) float64 { } func contains(items []string, value string) bool { - for _, item := range items { - if item == value { - return true - } - } - return false + return slices.Contains(items, value) } func jsonField(field reflect.StructField) (name string, omitEmpty, skip bool) { @@ -393,7 +389,7 @@ func collectExternalizablePointers(typ reflect.Type, prefix string, out *[]strin } switch typ.Kind() { case reflect.Struct: - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue diff --git a/internal/extension/protocolgen/sdkgo.go b/internal/extension/protocolgen/sdkgo.go index d1bc89609e..ed1d64993b 100644 --- a/internal/extension/protocolgen/sdkgo.go +++ b/internal/extension/protocolgen/sdkgo.go @@ -47,7 +47,7 @@ var sdkInitialisms = map[string]string{ "ui": "UI", "tui": "TUI", "acp": "ACP", "utf8": "UTF8", } -var rawMessageType = reflect.TypeOf(json.RawMessage{}) +var rawMessageType = reflect.TypeFor[json.RawMessage]() // sdkTypeWalk is the deterministic first-visit record of every named wire // type reachable from the frozen registry (plus the extra roots below). @@ -77,11 +77,11 @@ func walkSDKTypes() (*sdkTypeWalk, error) { } } roots = append(roots, - reflect.TypeOf(protocol.UIStatusPayload{}), - reflect.TypeOf(protocol.UICardPayload{}), - reflect.TypeOf(protocol.UIFormPayload{}), - reflect.TypeOf(protocol.UINotificationPayload{}), - reflect.TypeOf(protocol.ProtocolErrorData{}), + reflect.TypeFor[protocol.UIStatusPayload](), + reflect.TypeFor[protocol.UICardPayload](), + reflect.TypeFor[protocol.UIFormPayload](), + reflect.TypeFor[protocol.UINotificationPayload](), + reflect.TypeFor[protocol.ProtocolErrorData](), ) for _, root := range roots { if err := w.visit(root); err != nil { @@ -109,7 +109,7 @@ func (w *sdkTypeWalk) visit(typ reflect.Type) error { w.seen[typ] = true w.order = append(w.order, typ) w.kinds[typ] = "struct" - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue @@ -298,7 +298,7 @@ func emitSDKStruct(out *strings.Builder, typ reflect.Type) error { fmt.Fprintf(out, "// %s is a generated Extension Protocol v1 wire DTO.\n", typ.Name()) fields := 0 var body strings.Builder - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) if field.PkgPath != "" { continue diff --git a/internal/extension/providerext/providerext_test.go b/internal/extension/providerext/providerext_test.go index 949df17889..7488f09c96 100644 --- a/internal/extension/providerext/providerext_test.go +++ b/internal/extension/providerext/providerext_test.go @@ -3,6 +3,7 @@ package providerext import ( "context" "errors" + "slices" "strings" "sync" "sync/atomic" @@ -107,11 +108,9 @@ func (f *fakeClient) waitCancel(t *testing.T, streamID string) { deadline := time.Now().Add(testBudget) for time.Now().Before(deadline) { f.mu.Lock() - for _, id := range f.cancels { - if id == streamID { - f.mu.Unlock() - return - } + if slices.Contains(f.cancels, streamID) { + f.mu.Unlock() + return } f.mu.Unlock() select { diff --git a/internal/extension/providerext/stream_test.go b/internal/extension/providerext/stream_test.go index 8b14b37eae..f862c47c4e 100644 --- a/internal/extension/providerext/stream_test.go +++ b/internal/extension/providerext/stream_test.go @@ -669,7 +669,7 @@ func TestConcurrentStreamsOnOneSidecar(t *testing.T) { id string } handles := make([]handle, 0, streamCount) - for i := 0; i < streamCount; i++ { + for i := range streamCount { p, err := r.Resolve(provider.Selection{Ref: "plugin/demo/fake/x"}) if err != nil { t.Fatalf("Resolve: %v", err) diff --git a/internal/extension/replace.go b/internal/extension/replace.go index a423c0360c..78492fd318 100644 --- a/internal/extension/replace.go +++ b/internal/extension/replace.go @@ -2,6 +2,7 @@ package extension import ( "fmt" + "maps" "strings" ) @@ -174,8 +175,6 @@ func (c *ReplaceClaims) Owner(slot Slot) (ContributionSource, bool) { // build state through the result. func (c *ReplaceClaims) Claims() map[Slot]ContributionSource { out := make(map[Slot]ContributionSource, len(c.owners)) - for slot, owner := range c.owners { - out[slot] = owner - } + maps.Copy(out, c.owners) return out } diff --git a/internal/extension/rpcwire/conn_strict_regression_test.go b/internal/extension/rpcwire/conn_strict_regression_test.go index 861c99a51c..3ed593f640 100644 --- a/internal/extension/rpcwire/conn_strict_regression_test.go +++ b/internal/extension/rpcwire/conn_strict_regression_test.go @@ -132,14 +132,14 @@ func TestInboundHandlerConcurrencyIsBoundedWithoutBlockingResponses(t *testing.T }) done := make(chan error, 1) go func() { done <- conn.Serve(context.Background()) }() - for i := 0; i < 2; i++ { + for range 2 { select { case <-started: case <-time.After(time.Second): t.Fatal("bounded handlers did not start") } } - for i := 0; i < 3; i++ { + for range 3 { select { case <-out.busy: case <-time.After(time.Second): @@ -169,7 +169,7 @@ func TestInboundHandlerConcurrencyIsBoundedWithoutBlockingResponses(t *testing.T func TestQueuedNotificationsPreserveBurstOrder(t *testing.T) { const count = 500 var input strings.Builder - for i := 0; i < count; i++ { + for i := range count { fmt.Fprintf(&input, "{\"jsonrpc\":\"2.0\",\"method\":\"note\",\"params\":{\"index\":%d}}\n", i) } conn := NewConn(strings.NewReader(input.String()), io.Discard, Options{ @@ -201,7 +201,7 @@ func TestQueuedNotificationsPreserveBurstOrder(t *testing.T) { func TestQueuedNotificationOverflowFailsConnection(t *testing.T) { var input strings.Builder - for i := 0; i < 100; i++ { + for i := range 100 { fmt.Fprintf(&input, "{\"jsonrpc\":\"2.0\",\"method\":\"note\",\"params\":{\"index\":%d}}\n", i) } conn := NewConn(strings.NewReader(input.String()), io.Discard, Options{ @@ -360,7 +360,7 @@ func TestConcurrentWritesDoNotInterleaveFrames(t *testing.T) { conn := NewConn(strings.NewReader(""), w, Options{}) var wg sync.WaitGroup errs := make(chan error, count) - for i := 0; i < count; i++ { + for i := range count { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/extension/rpcwire/conn_test.go b/internal/extension/rpcwire/conn_test.go index 6580a1df91..acc5f40160 100644 --- a/internal/extension/rpcwire/conn_test.go +++ b/internal/extension/rpcwire/conn_test.go @@ -88,7 +88,7 @@ func TestHandlerResponseAfterWriteReceivesTransportFailure(t *testing.T) { } func TestRequestKeepsDeliveredResponseWhenPeerClosesAfterWrite(t *testing.T) { - for attempt := 0; attempt < 100; attempt++ { + for attempt := range 100 { serverToClientR, serverToClientW := io.Pipe() clientToServerR, clientToServerW := io.Pipe() client := NewConn(serverToClientR, clientToServerW, Options{Name: "response-close-client"}) @@ -209,7 +209,7 @@ func TestTryNotifyDropsImmediatelyWhenQueueIsFull(t *testing.T) { } func TestWriterExitsAfterGracefulServeClose(t *testing.T) { - for attempt := 0; attempt < 100; attempt++ { + for attempt := range 100 { conn := NewConn(strings.NewReader(""), io.Discard, Options{Name: "writer-exit"}) if err := conn.Serve(context.Background()); err != nil { t.Fatalf("attempt %d Serve: %v", attempt, err) @@ -230,8 +230,7 @@ func TestRequestReturnsStructuredPeerError(t *testing.T) { server.Handle("fail", func(context.Context, json.RawMessage) (any, error) { return nil, &RPCError{Code: -32000, Message: "busy", Data: map[string]any{"reasonixCode": "HOST_BUSY"}} }) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() go func() { _ = client.Serve(ctx) }() go func() { _ = server.Serve(ctx) }() _, err := client.Request(ctx, "fail", struct{}{}) @@ -267,7 +266,7 @@ func TestStrictJSONRPCRejectsMissingVersionAndInvalidShape(t *testing.T) { } dec := json.NewDecoder(&out) wantIDs := []string{"1", "2", "3", "null", "5", "null"} - for i := 0; i < 6; i++ { + for i := range 6 { var frame struct { ID json.RawMessage `json:"id"` Error *ErrorObject `json:"error"` diff --git a/internal/extension/rpcwire/stall_test.go b/internal/extension/rpcwire/stall_test.go index b1b5b38999..5324d54bff 100644 --- a/internal/extension/rpcwire/stall_test.go +++ b/internal/extension/rpcwire/stall_test.go @@ -81,7 +81,7 @@ func TestWriteCallerContextAbortLeavesConnectionAlive(t *testing.T) { wire := <-drained var ping bool - for _, line := range strings.Split(strings.TrimSpace(string(wire)), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(string(wire)), "\n") { if strings.TrimSpace(line) == "" { continue } @@ -162,8 +162,7 @@ func TestQueuedFrameCancelledBeforeStartNeverLands(t *testing.T) { func TestNotifyAfterGracefulCloseFails(t *testing.T) { serverToClientR, serverToClientW := io.Pipe() conn := NewConn(serverToClientR, io.Discard, Options{Name: "close-notify-test"}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() serveDone := make(chan error, 1) go func() { serveDone <- conn.Serve(ctx) }() @@ -176,7 +175,7 @@ func TestNotifyAfterGracefulCloseFails(t *testing.T) { t.Fatalf("Serve on graceful EOF: %v", err) } - for i := 0; i < 20; i++ { + for i := range 20 { if err := conn.Notify("after/close", nil); err == nil { t.Fatalf("attempt %d: post-close Notify reported success for a dropped frame", i) } diff --git a/internal/extension/runtimeset.go b/internal/extension/runtimeset.go index 9bb626d26b..34609f3ff7 100644 --- a/internal/extension/runtimeset.go +++ b/internal/extension/runtimeset.go @@ -3,6 +3,7 @@ package extension import ( "errors" "io" + "slices" "sync" ) @@ -73,8 +74,8 @@ func (s *RuntimeSet) Close() error { s.mu.Unlock() var errs []error - for i := len(closers) - 1; i >= 0; i-- { - if err := closers[i].Close(); err != nil { + for _, v := range slices.Backward(closers) { + if err := v.Close(); err != nil { errs = append(errs, err) } } diff --git a/internal/extension/sidecar/benchmark_test.go b/internal/extension/sidecar/benchmark_test.go index 7c93cf2ff7..8f8128ea5b 100644 --- a/internal/extension/sidecar/benchmark_test.go +++ b/internal/extension/sidecar/benchmark_test.go @@ -2,7 +2,7 @@ package sidecar import ( "context" - "sort" + "slices" "strconv" "testing" "time" @@ -47,7 +47,6 @@ func BenchmarkExtensionSidecarStartup(b *testing.B) { // latency instead of only the dispatcher's in-process overhead. func BenchmarkExtensionSidecarDispatchLatency(b *testing.B) { for _, count := range []int{1, 4} { - count := count b.Run("Turn/NoopSidecars"+strconv.Itoa(count), func(b *testing.B) { d := benchmarkSidecarDispatcher(b, extension.PointInputReceive, count) payload := dispatch.InputPayload{Text: "hello"} @@ -71,7 +70,7 @@ func benchmarkSidecarDispatcher(b *testing.B, point extension.InterceptorPoint, b.Helper() chain := make([]extension.Contribution, 0, count) clients := make(map[string]*Client, count) - for i := 0; i < count; i++ { + for i := range count { pluginID := "benchmark-" + strconv.Itoa(i) client := startFakeClient(b, func(rt *pluginpkg.RuntimeSpec) { rt.Intercepts = []string{string(point)} @@ -110,7 +109,7 @@ func benchmarkSidecarLatency(b *testing.B, fn func() error) { func reportSidecarPercentiles(b *testing.B, samples []int64) { b.Helper() - sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] }) + slices.Sort(samples) if len(samples) == 0 { return } diff --git a/internal/extension/sidecar/client.go b/internal/extension/sidecar/client.go index 24f5814439..db0aec1547 100644 --- a/internal/extension/sidecar/client.go +++ b/internal/extension/sidecar/client.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strings" "sync" "sync/atomic" @@ -509,10 +510,7 @@ func (c *Client) Exited() bool { // context, and compaction family). func (c *Client) TimeoutFor(point extension.InterceptorPoint) time.Duration { if c.rt.TimeoutMillis > 0 { - timeout := time.Duration(c.rt.TimeoutMillis) * time.Millisecond - if timeout > maxInterceptTimeout { - timeout = maxInterceptTimeout - } + timeout := min(time.Duration(c.rt.TimeoutMillis)*time.Millisecond, maxInterceptTimeout) return timeout } switch point { @@ -790,10 +788,5 @@ func mapRequestError(err error) error { } func containsString(items []string, value string) bool { - for _, item := range items { - if item == value { - return true - } - } - return false + return slices.Contains(items, value) } diff --git a/internal/extension/sidecar/content.go b/internal/extension/sidecar/content.go index 8b39bfbbd4..7efc595b81 100644 --- a/internal/extension/sidecar/content.go +++ b/internal/extension/sidecar/content.go @@ -94,10 +94,7 @@ func (s *Store) Read(ref string, offset int64) (chunk []byte, next *int64, total if offset < 0 || offset > int64(len(object.data)) { return nil, nil, 0, "", protocol.MustProtocolError(protocol.ErrContentRefExpired) } - end := offset + protocol.ContentRefChunkBytes - if end > int64(len(object.data)) { - end = int64(len(object.data)) - } + end := min(offset+protocol.ContentRefChunkBytes, int64(len(object.data))) if end < int64(len(object.data)) { value := end next = &value diff --git a/internal/extension/sidecar/content_test.go b/internal/extension/sidecar/content_test.go index dc521dc865..1128f7ad16 100644 --- a/internal/extension/sidecar/content_test.go +++ b/internal/extension/sidecar/content_test.go @@ -147,8 +147,8 @@ func TestStoreEnforcesObjectCap(t *testing.T) { func TestStoreEvictsOldestBeyondCapacity(t *testing.T) { store := NewStore() refs := make([]string, 0, storeMaxEntries+1) - for i := 0; i < storeMaxEntries+1; i++ { - ref, _, _, err := store.Put([]byte(fmt.Sprintf("object-%03d", i))) + for i := range storeMaxEntries + 1 { + ref, _, _, err := store.Put(fmt.Appendf(nil, "object-%03d", i)) if err != nil { t.Fatalf("Put %d: %v", i, err) } diff --git a/internal/extension/sidecar/externalize.go b/internal/extension/sidecar/externalize.go index cfeab9915d..1b7d9be0b1 100644 --- a/internal/extension/sidecar/externalize.go +++ b/internal/extension/sidecar/externalize.go @@ -66,7 +66,7 @@ func (c *Client) externalizeInterceptParams(params *protocol.InterceptParams) er // externalizeEventParams applies the outbound content-ref rule to one event // notification's payload. func (c *Client) externalizeEventParams(params *protocol.EventParams) error { - pointer, err := externalizablePointer(reflect.TypeOf(*params), "/payload") + pointer, err := externalizablePointer(reflect.TypeFor[protocol.EventParams](), "/payload") if err != nil { return err } diff --git a/internal/extension/sidecar/fake_test.go b/internal/extension/sidecar/fake_test.go index 84ec568c16..7b1ae5f9f2 100644 --- a/internal/extension/sidecar/fake_test.go +++ b/internal/extension/sidecar/fake_test.go @@ -67,7 +67,7 @@ type fakeExternalizedField struct { func fakeModes() map[string]bool { out := map[string]bool{} - for _, mode := range strings.Split(os.Getenv(fakeEnvMode), ",") { + for mode := range strings.SplitSeq(os.Getenv(fakeEnvMode), ",") { if mode = strings.TrimSpace(mode); mode != "" { out[mode] = true } @@ -93,7 +93,7 @@ func runFakeSidecar(stdin io.Reader, stdout io.Writer) { // Push well past the 16 KiB tail so only the end survives, then leave // a credential-looking line at the very end — it must be retained by // the ring and redacted before surfacing. - for i := 0; i < 32*1024; i++ { + for i := range 32 * 1024 { fmt.Fprintf(os.Stderr, "flood line %d padding padding padding\n", i) } fmt.Fprintln(os.Stderr, "boot failed: api_key=sk-abcdef1234567890SECRETKEY is invalid") diff --git a/internal/extension/sidecar/manager_test.go b/internal/extension/sidecar/manager_test.go index a5ea18f21c..894e9ade67 100644 --- a/internal/extension/sidecar/manager_test.go +++ b/internal/extension/sidecar/manager_test.go @@ -211,7 +211,7 @@ func TestStartLoadedPackagesBoundsParallelismAndSharesCancellation(t *testing.T) done <- startResult{manager: manager, warnings: warnings, err: err} }() - for i := 0; i < maxConcurrentPackageStarts; i++ { + for i := range maxConcurrentPackageStarts { select { case <-entered: case <-time.After(5 * time.Second): diff --git a/internal/extension/sidecar/process.go b/internal/extension/sidecar/process.go index 694d2e9ba6..7379121eb0 100644 --- a/internal/extension/sidecar/process.go +++ b/internal/extension/sidecar/process.go @@ -85,10 +85,7 @@ func newStartupFailure(stage string, started time.Time, stderr string, err error if errors.As(err, &existing) { return err } - elapsed := time.Since(started) - if elapsed < 0 { - elapsed = 0 - } + elapsed := max(time.Since(started), 0) return &startupFailure{ Stage: strings.TrimSpace(stage), Elapsed: elapsed, diff --git a/internal/extension/snapshot.go b/internal/extension/snapshot.go index ba0be9ed4e..bb9d960976 100644 --- a/internal/extension/snapshot.go +++ b/internal/extension/snapshot.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "maps" "sort" "reasonix/internal/provider" @@ -64,9 +65,7 @@ func (s *RuntimeSnapshot) InterceptorChain() map[InterceptorPoint][]Contribution // Replacements returns the winning owner per replacement slot (a copy). func (s *RuntimeSnapshot) Replacements() map[Slot]ContributionSource { out := make(map[Slot]ContributionSource, len(s.replacements)) - for slot, owner := range s.replacements { - out[slot] = owner - } + maps.Copy(out, s.replacements) return out } diff --git a/internal/extension/uihub/hub_test.go b/internal/extension/uihub/hub_test.go index 49510f44ed..24d8af8b21 100644 --- a/internal/extension/uihub/hub_test.go +++ b/internal/extension/uihub/hub_test.go @@ -660,7 +660,7 @@ func TestHubConcurrentUse(t *testing.T) { t.Fatal(err) } var wg sync.WaitGroup - for i := 0; i < 8; i++ { + for i := range 8 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/fileref/search.go b/internal/fileref/search.go index ab7d88e6b5..8adf246a19 100644 --- a/internal/fileref/search.go +++ b/internal/fileref/search.go @@ -149,10 +149,7 @@ func Search(root, query string, limit int) []SearchResult { // out by a large number of file matches. const dirQuota = 5 out := make([]SearchResult, 0, limit) - nDirs := len(dirHits) - if nDirs > dirQuota { - nDirs = dirQuota - } + nDirs := min(len(dirHits), dirQuota) out = append(out, dirHits[:nDirs]...) remaining := limit - len(out) if remaining > 0 { @@ -177,7 +174,7 @@ func Search(root, query string, limit int) []SearchResult { // directories above the file (e.g. "src/planind/index.tsx" with query // "planind" matches the "planind" segment). func pathSegmentContains(relSlash, queryLower string) bool { - for _, seg := range strings.Split(relSlash, "/") { + for seg := range strings.SplitSeq(relSlash, "/") { if strings.Contains(strings.ToLower(seg), queryLower) { return true } diff --git a/internal/fileutil/encoding/encoding.go b/internal/fileutil/encoding/encoding.go index 919ccfa35c..1e68df5a82 100644 --- a/internal/fileutil/encoding/encoding.go +++ b/internal/fileutil/encoding/encoding.go @@ -99,7 +99,7 @@ func DetectUTF16NoBOM(b []byte) (Kind, bool) { } n &^= 1 // examine an even-length window so parity counts are comparable var evenNUL, oddNUL int - for i := 0; i < n; i++ { + for i := range n { if b[i] != 0 { continue } diff --git a/internal/fileutil/globset.go b/internal/fileutil/globset.go index ddb002d7b9..1fdc6a4028 100644 --- a/internal/fileutil/globset.go +++ b/internal/fileutil/globset.go @@ -54,8 +54,8 @@ func MatchSlashGlob(path, pattern string) bool { if matched, _ := doublestar.Match(pattern, path); matched { return true } - if strings.HasPrefix(pattern, "**/") { - matched, _ := doublestar.Match(strings.TrimPrefix(pattern, "**/"), path) + if after, ok := strings.CutPrefix(pattern, "**/"); ok { + matched, _ := doublestar.Match(after, path) return matched } return false diff --git a/internal/guardian/guardian.go b/internal/guardian/guardian.go index fef31113bf..0ebff756d1 100644 --- a/internal/guardian/guardian.go +++ b/internal/guardian/guardian.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "slices" "strings" "sync" "time" @@ -557,9 +558,9 @@ func firstRunesStr(s string, n int) string { func lastAssistantText(sess *agent.Session) string { msgs := sess.Snapshot() - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleAssistant && strings.TrimSpace(msgs[i].Content) != "" { - return msgs[i].Content + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleAssistant && strings.TrimSpace(v.Content) != "" { + return v.Content } } return "" diff --git a/internal/guardian/guardian_test.go b/internal/guardian/guardian_test.go index 35d9f655a5..32bc086df6 100644 --- a/internal/guardian/guardian_test.go +++ b/internal/guardian/guardian_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" "testing" @@ -108,7 +109,7 @@ func TestParseAssessmentRejectsUnknownEnum(t *testing.T) { func TestTranscriptRenderKeepsFirstAndLastUserAnchors(t *testing.T) { entries := []TranscriptEntry{{Kind: "user", Text: "first task"}} - for i := 0; i < maxRecentEntries+5; i++ { + for range maxRecentEntries + 5 { entries = append(entries, TranscriptEntry{Kind: "assistant", Text: "assistant detail"}) } entries = append(entries, TranscriptEntry{Kind: "user", Text: "latest instruction"}) @@ -249,7 +250,7 @@ func TestGuardianReviewTurnsAlternateRoles(t *testing.T) { parent := agent.NewSession("sys") parent.Add(provider.Message{Role: provider.RoleUser, Content: "do the thing"}) - for i := 0; i < 2; i++ { + for i := range 2 { if allow, _, err := gs.Review(context.Background(), "write_file", json.RawMessage(`{"file_path":"a.txt"}`), parent); err != nil || !allow { t.Fatalf("review %d = allow %v err %v, want allow nil", i+1, allow, err) } @@ -270,9 +271,9 @@ func TestGuardianReviewTurnsAlternateRoles(t *testing.T) { // The combined message must still carry the evidence boundary and the action. msgs := reqs[len(reqs)-1].Messages var review string - for i := len(msgs) - 1; i >= 0; i-- { - if msgs[i].Role == provider.RoleUser { - review = msgs[i].Content + for _, v := range slices.Backward(msgs) { + if v.Role == provider.RoleUser { + review = v.Content break } } @@ -355,7 +356,7 @@ func TestGuardianSessionAlternatesAfterCompaction(t *testing.T) { parent := agent.NewSession("sys") filler := strings.Repeat("parent transcript filler. ", 160) - for i := 0; i < compactEvery; i++ { + for i := range compactEvery { parent.Add(provider.Message{Role: provider.RoleUser, Content: fmt.Sprintf("turn %d: %s", i, filler)}) if allow, _, err := gs.Review(context.Background(), "write_file", json.RawMessage(`{"file_path":"a.txt"}`), parent); err != nil || !allow { t.Fatalf("review %d = allow %v err %v, want allow nil", i+1, allow, err) diff --git a/internal/guardian/transcript.go b/internal/guardian/transcript.go index 7e7184bc01..c83bdc0ac4 100644 --- a/internal/guardian/transcript.go +++ b/internal/guardian/transcript.go @@ -2,6 +2,7 @@ package guardian import ( "fmt" + "slices" "strings" "unicode/utf8" @@ -127,8 +128,8 @@ func renderTranscript(entries []TranscriptEntry) ([]string, string) { } // Fill remaining message budget with user entries from newest to oldest. - for i := len(userIdx) - 1; i >= 0; i-- { - idx := userIdx[i] + for _, v := range slices.Backward(userIdx) { + idx := v if idx >= len(all) || included[idx] { continue } @@ -226,17 +227,8 @@ func truncateText(content string, tokCap int) (string, bool) { runes := []rune(content) // Estimate how many runes fit in head/tail bytes (conservative: assume // max 4 bytes per rune). - headRunes := head / 4 - if headRunes > len(runes) { - headRunes = len(runes) - } - tailRunes := tail / 4 - if tailRunes > len(runes)-headRunes { - tailRunes = len(runes) - headRunes - } - if tailRunes < 0 { - tailRunes = 0 - } + headRunes := min(head/4, len(runes)) + tailRunes := max(min(tail/4, len(runes)-headRunes), 0) return string(runes[:headRunes]) + marker + string(runes[len(runes)-tailRunes:]), true } diff --git a/internal/history/search.go b/internal/history/search.go index da41debf96..31dbb39ec4 100644 --- a/internal/history/search.go +++ b/internal/history/search.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "os" "path/filepath" "sort" @@ -247,10 +248,7 @@ func (s *Searcher) Around(ctx context.Context, req AroundRequest) ([]MessageCont } before := clamp(req.Before, defaultAround, maxAround) after := clamp(req.After, defaultAround, maxAround) - start := req.MessageIndex - before - if start < 0 { - start = 0 - } + start := max(req.MessageIndex-before, 0) remainingAfter := len(msgs) - req.MessageIndex - 1 end := len(msgs) if after < remainingAfter { @@ -277,9 +275,7 @@ func normalizeScope(scope string) (string, error) { func normalizeKinds(kinds []Kind) (map[Kind]bool, error) { if len(kinds) == 0 { out := make(map[Kind]bool, len(defaultKinds)) - for k, v := range defaultKinds { - out[k] = v - } + maps.Copy(out, defaultKinds) return out, nil } out := map[Kind]bool{} diff --git a/internal/history/search_test.go b/internal/history/search_test.go index 1211e36007..30c47f0295 100644 --- a/internal/history/search_test.go +++ b/internal/history/search_test.go @@ -131,7 +131,7 @@ func TestSearchDropsCommonWordNoise(t *testing.T) { writeSession(t, filepath.Join(sessionDir, "rare.jsonl"), []provider.Message{ {Role: provider.RoleUser, Content: "rareterm common common common"}, }) - for i := 0; i < 12; i++ { + for i := range 12 { writeSession(t, filepath.Join(sessionDir, "common-"+string(rune('a'+i))+".jsonl"), []provider.Message{ {Role: provider.RoleUser, Content: "common"}, }) diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 25d573c264..c04ac22cf5 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -23,6 +23,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "sort" "strings" "time" @@ -483,12 +484,7 @@ func isShellVariableNameByte(c byte) bool { } func validEvent(event Event) bool { - for _, e := range Events { - if e == event { - return true - } - } - return false + return slices.Contains(Events, event) } func cloneEnv(in map[string]string) map[string]string { @@ -519,12 +515,7 @@ func MatchesTool(h ResolvedHook, toolName string) bool { if h.PayloadFormat != "claude" { return re.MatchString(toolName) } - for _, candidate := range claudeMatchNames(toolName) { - if re.MatchString(candidate) { - return true - } - } - return false + return slices.ContainsFunc(claudeMatchNames(toolName), re.MatchString) } // claudeAgentSpawningTools are every Reasonix tool that spawns a subagent and diff --git a/internal/hook/inspect.go b/internal/hook/inspect.go index 7aba66546b..dfbe26b9d3 100644 --- a/internal/hook/inspect.go +++ b/internal/hook/inspect.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" @@ -162,7 +163,7 @@ func appendInspectEntries(out *Inspection, s *Settings, scope Scope, source stri unknown = append(unknown, event) } } - sort.Slice(unknown, func(i, j int) bool { return unknown[i] < unknown[j] }) + slices.Sort(unknown) for _, event := range unknown { for _, cfg := range s.Hooks[event] { out.Entries = append(out.Entries, Entry{ diff --git a/internal/hook/windows_compat.go b/internal/hook/windows_compat.go index b319902c54..3b6562fe1f 100644 --- a/internal/hook/windows_compat.go +++ b/internal/hook/windows_compat.go @@ -123,7 +123,7 @@ func isWindowsBatchExecutable(executable string) bool { func isSimpleWindowsBatchTail(tail string) bool { quoted := false - for i := 0; i < len(tail); i++ { + for i := range len(tail) { switch tail[i] { case '\r', '\n': return false @@ -166,8 +166,8 @@ func hasCommandStringFlag(args []string) bool { if arg == "-" || arg == "--" || !strings.HasPrefix(arg, "-") { return false } - if strings.HasPrefix(arg, "--") { - name, _, hasInlineValue := strings.Cut(strings.TrimPrefix(arg, "--"), "=") + if after, ok := strings.CutPrefix(arg, "--"); ok { + name, _, hasInlineValue := strings.Cut(after, "=") if !hasInlineValue && bashLongOptionNeedsOperand(name) { if i+1 >= len(args) { return false diff --git a/internal/i18n/catalog_parity_test.go b/internal/i18n/catalog_parity_test.go index 765051a244..5ddd524bf6 100644 --- a/internal/i18n/catalog_parity_test.go +++ b/internal/i18n/catalog_parity_test.go @@ -35,7 +35,7 @@ func TestCatalogsAgreeOnCodeTokens(t *testing.T) { en := reflect.ValueOf(English) typ := en.Type() - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { name := typ.Field(i).Name if excluded[name] { continue diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go index 3dced00bf2..57363f0f59 100644 --- a/internal/i18n/i18n_test.go +++ b/internal/i18n/i18n_test.go @@ -15,7 +15,7 @@ func TestCatalogsComplete(t *testing.T) { typ := en.Type() catalogs := map[string]reflect.Value{"zh": reflect.ValueOf(Chinese), "zh-TW": reflect.ValueOf(ChineseTraditional)} for tag, cat := range catalogs { - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { name := typ.Field(i).Name if strings.TrimSpace(cat.Field(i).String()) == "" { t.Errorf("%s catalogue: field %q is empty", tag, name) @@ -31,7 +31,7 @@ func TestCatalogsComplete(t *testing.T) { func TestCatalogsAgreeOnPlaceholders(t *testing.T) { en := reflect.ValueOf(English) typ := en.Type() - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { name := typ.Field(i).Name if !strings.HasSuffix(name, "Fmt") { continue @@ -61,7 +61,7 @@ func TestPlanApprovalChoicesExposeThreeExplicitActions(t *testing.T) { for _, tt := range tests { t.Run(tt.tag, func(t *testing.T) { numbered := 0 - for _, line := range strings.Split(tt.value, "\n") { + for line := range strings.SplitSeq(tt.value, "\n") { line = strings.TrimSpace(line) if len(line) >= 3 && line[0] >= '1' && line[0] <= '9' && line[1] == '.' { numbered++ diff --git a/internal/installlayout/activate.go b/internal/installlayout/activate.go index 85089f1be0..af07995484 100644 --- a/internal/installlayout/activate.go +++ b/internal/installlayout/activate.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "time" @@ -239,8 +240,8 @@ func publishRootEntries(installRoot, stagingRoot string, members []Member) (roll replacements := make([]replacement, 0, len(members)) rollbackFn := func() error { var rollbackErr error - for i := len(replacements) - 1; i >= 0; i-- { - r := replacements[i] + for _, v := range slices.Backward(replacements) { + r := v if err := os.Remove(r.destination); err != nil && !os.IsNotExist(err) { rollbackErr = errors.Join(rollbackErr, err) } diff --git a/internal/installlayout/current.go b/internal/installlayout/current.go index 09e1f5feb3..a02240a694 100644 --- a/internal/installlayout/current.go +++ b/internal/installlayout/current.go @@ -209,7 +209,7 @@ func ValidateActiveDir(installRoot, version, activeDir string) error { func rejectSymlinkPathComponents(root, rel string) error { cur := root - for _, part := range strings.Split(rel, string(os.PathSeparator)) { + for part := range strings.SplitSeq(rel, string(os.PathSeparator)) { if part == "" || part == "." { continue } diff --git a/internal/installsource/apply.go b/internal/installsource/apply.go index 30b08ef307..8d1c816ad6 100644 --- a/internal/installsource/apply.go +++ b/internal/installsource/apply.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "reasonix/internal/config" @@ -84,11 +85,8 @@ func (t *installSourceTool) applySkillRoot(req request, act *action) error { } } for _, listed := range store.List() { - for _, name := range act.Skills { - if listed.Name == name { - act.Indexed = true - break - } + if slices.Contains(act.Skills, listed.Name) { + act.Indexed = true } } act.Target = act.Source diff --git a/internal/installsource/plan.go b/internal/installsource/plan.go index 8523924308..6f3170a7c2 100644 --- a/internal/installsource/plan.go +++ b/internal/installsource/plan.go @@ -19,8 +19,8 @@ var githubAPIBaseURL = "https://api.github.com" // plan turns a request into a list of actions plus a warnings slice. It // does not touch the disk; the apply phase is responsible for side effects. func (t *installSourceTool) plan(ctx context.Context, req request) ([]action, []string, error) { - if strings.HasPrefix(req.Source, "git:github.com/") { - req.Source = "https://github.com/" + strings.TrimPrefix(req.Source, "git:github.com/") + if after, ok := strings.CutPrefix(req.Source, "git:github.com/"); ok { + req.Source = "https://github.com/" + after } if isURL(req.Source) { return t.planURL(ctx, req) diff --git a/internal/installsource/plugin_package.go b/internal/installsource/plugin_package.go index 369300bf79..6310bb1fbe 100644 --- a/internal/installsource/plugin_package.go +++ b/internal/installsource/plugin_package.go @@ -500,8 +500,8 @@ func (t *installSourceTool) applyInstallPluginPackage(ctx context.Context, req r func (t *installSourceTool) preparePluginSource(ctx context.Context, source, mode string) (string, string, func(), error) { source = strings.TrimSpace(source) - if strings.HasPrefix(source, "git:github.com/") { - source = "https://github.com/" + strings.TrimPrefix(source, "git:github.com/") + if after, ok := strings.CutPrefix(source, "git:github.com/"); ok { + source = "https://github.com/" + after } if isURL(source) { src, ok := parseGitHubRepoSource(source) diff --git a/internal/instruction/resolver.go b/internal/instruction/resolver.go index 9368872631..4696a830f9 100644 --- a/internal/instruction/resolver.go +++ b/internal/instruction/resolver.go @@ -347,7 +347,7 @@ func directoryChain(root, target string) []string { } chain := []string{root} current := root - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "" || part == "." { continue } diff --git a/internal/jobs/concurrency_stress_test.go b/internal/jobs/concurrency_stress_test.go index b8dde58c9c..151cebf795 100644 --- a/internal/jobs/concurrency_stress_test.go +++ b/internal/jobs/concurrency_stress_test.go @@ -19,10 +19,10 @@ func TestManagerConcurrentAccess(t *testing.T) { const workers = 24 var wg sync.WaitGroup wg.Add(workers) - for w := 0; w < workers; w++ { + for w := range workers { go func(w int) { defer wg.Done() - for i := 0; i < 200; i++ { + for i := range 200 { switch (w + i) % 6 { case 0: j := m.Start("bash", "x", func(ctx context.Context, out io.Writer) (string, error) { diff --git a/internal/jobs/jobs.go b/internal/jobs/jobs.go index 5921704725..bfe75fbfd1 100644 --- a/internal/jobs/jobs.go +++ b/internal/jobs/jobs.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "sort" "strings" "sync" @@ -1452,9 +1453,9 @@ func rebaseArtifactMigrationJobs(jobs []artifactMigrationJob, dir string) { } func unlockArtifactMigrationJobs(jobs []artifactMigrationJob) { - for i := len(jobs) - 1; i >= 0; i-- { - if jobs[i].job != nil { - jobs[i].job.mu.Unlock() + for _, v := range slices.Backward(jobs) { + if v.job != nil { + v.job.mu.Unlock() } } } diff --git a/internal/jobs/jobs_test.go b/internal/jobs/jobs_test.go index 8b25a0e376..fc1d8c15ba 100644 --- a/internal/jobs/jobs_test.go +++ b/internal/jobs/jobs_test.go @@ -120,7 +120,7 @@ func TestReserveStartForSessionIsAtomic(t *testing.T) { start := make(chan struct{}) releases := make(chan func(), callers) results := make(chan bool, callers) - for i := 0; i < callers; i++ { + for range callers { go func() { <-start release, _, ok := m.ReserveStartForSession("session-a", "task", 3) @@ -132,7 +132,7 @@ func TestReserveStartForSessionIsAtomic(t *testing.T) { } close(start) reserved := 0 - for i := 0; i < callers; i++ { + for range callers { if <-results { reserved++ } @@ -140,7 +140,7 @@ func TestReserveStartForSessionIsAtomic(t *testing.T) { if reserved != 3 { t.Fatalf("concurrent reservations = %d, want exactly 3", reserved) } - for i := 0; i < reserved; i++ { + for range reserved { (<-releases)() } if release, running, ok := m.ReserveStartForSession("session-a", "task", 3); !ok || running != 0 { diff --git a/internal/lsp/position.go b/internal/lsp/position.go index a480cc45bf..a4c6401157 100644 --- a/internal/lsp/position.go +++ b/internal/lsp/position.go @@ -58,11 +58,11 @@ func locate(content string, line1 int, symbol, enc string) (Position, error) { return Position{}, fmt.Errorf("line %d out of range (file has %d lines)", line1, len(lines)) } text := strings.TrimSuffix(lines[line1-1], "\r") - col := strings.Index(text, symbol) - if col < 0 { + before, _, ok := strings.Cut(text, symbol) + if !ok { return Position{}, fmt.Errorf("symbol %q not found on line %d", symbol, line1) } - return Position{Line: line1 - 1, Character: encodeChar(text[:col], enc)}, nil + return Position{Line: line1 - 1, Character: encodeChar(before, enc)}, nil } func encodeChar(prefix, enc string) int { diff --git a/internal/mcplaunch/launch.go b/internal/mcplaunch/launch.go index 88e8a44804..c3277177ef 100644 --- a/internal/mcplaunch/launch.go +++ b/internal/mcplaunch/launch.go @@ -424,7 +424,7 @@ func acquireFileLock(path string, wait time.Duration) (func(), error) { if _, err := rand.Read(token); err != nil { return nil, fmt.Errorf("generate MCP authorization lock owner: %w", err) } - owner := []byte(fmt.Sprintf("%d %s\n", os.Getpid(), hex.EncodeToString(token))) + owner := fmt.Appendf(nil, "%d %s\n", os.Getpid(), hex.EncodeToString(token)) deadline := time.Now().Add(wait) for { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { diff --git a/internal/memory/auto_recall.go b/internal/memory/auto_recall.go index 76dcb8639b..a13a8de238 100644 --- a/internal/memory/auto_recall.go +++ b/internal/memory/auto_recall.go @@ -296,7 +296,7 @@ func autoRecallSearchText(memory Memory) string { } func distinctiveQueryTerm(query, normalizedTerm string) bool { - for _, field := range strings.Fields(query) { + for field := range strings.FieldsSeq(query) { trimmed := strings.Trim(field, "#()[]{}<>,;:'\"`!?=+*/\\|") if !strings.EqualFold(trimmed, normalizedTerm) { continue @@ -464,10 +464,7 @@ func clippedRecallEntry(hit RecallHit, maxRunes int) string { if utf8.RuneCountInString(entry) <= maxRunes { return entry } - cut := len(runes) / 4 - if cut < 1 { - cut = 1 - } + cut := max(len(runes)/4, 1) runes = runes[:len(runes)-cut] } return "" diff --git a/internal/memory/recall_test.go b/internal/memory/recall_test.go index 49f4ce74f6..2951d7939d 100644 --- a/internal/memory/recall_test.go +++ b/internal/memory/recall_test.go @@ -73,7 +73,7 @@ func TestRecallToolDropsCommonWordNoise(t *testing.T) { Type: TypeProject, Body: "rareterm common common common", }) - for i := 0; i < 12; i++ { + for i := range 12 { saveMemory(t, store, Memory{ Name: "common-note-" + string(rune('a'+i)), Description: "Common note", diff --git a/internal/memory/store.go b/internal/memory/store.go index f2e1db9228..5c6ee08392 100644 --- a/internal/memory/store.go +++ b/internal/memory/store.go @@ -462,7 +462,7 @@ var indexLineRe = regexp.MustCompile(`(?m)^\s*-\s\[.+?\]\(([^)]+)\.md\)\s*—\s. func indexLinesExceptIn(dir, name string) map[string]string { existing, _ := fileencoding.ReadFileUTF8(filepath.Join(dir, indexFile)) keep := map[string]string{} - for _, line := range strings.Split(string(existing), "\n") { + for line := range strings.SplitSeq(string(existing), "\n") { if mt := indexLineRe.FindStringSubmatch(line); mt != nil && mt[1] != name { keep[mt[1]] = strings.TrimRight(line, "\r") } @@ -475,7 +475,7 @@ func indexContainsIn(dir, name string) bool { if err != nil { return false } - for _, line := range strings.Split(string(existing), "\n") { + for line := range strings.SplitSeq(string(existing), "\n") { if mt := indexLineRe.FindStringSubmatch(line); mt != nil && mt[1] == name { return true } @@ -492,7 +492,7 @@ func flushIndexIn(dir string, lines map[string]string) error { processed := map[string]bool{} var preserved strings.Builder preservedEmpty := true - for _, line := range strings.Split(string(existing), "\n") { + for line := range strings.SplitSeq(string(existing), "\n") { trimmed := strings.TrimRight(line, "\r") if mt := indexLineRe.FindStringSubmatch(trimmed); mt != nil { name := mt[1] diff --git a/internal/memory/store_v2.go b/internal/memory/store_v2.go index 3981e7f2f7..7d2dd114b4 100644 --- a/internal/memory/store_v2.go +++ b/internal/memory/store_v2.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -240,8 +241,8 @@ func (s Store) findActive(ref string) (Memory, string, bool) { if parsed.qualified { return s.findActiveInDir(s.DirFor(parsed.scope), parsed.raw) } - for i := len(s.dirs()) - 1; i >= 0; i-- { - dir := s.dirs()[i] + for _, v := range slices.Backward(s.dirs()) { + dir := v if memory, path, ok := s.findActiveInDir(dir, parsed.raw); ok { return memory, path, true } diff --git a/internal/memory/store_v2_test.go b/internal/memory/store_v2_test.go index 237d1a29a6..3b0004d052 100644 --- a/internal/memory/store_v2_test.go +++ b/internal/memory/store_v2_test.go @@ -339,12 +339,10 @@ func TestStoreV2RestoreArchivedIsConcurrencySafe(t *testing.T) { var wg sync.WaitGroup errs := make(chan error, 2) for range 2 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { _, restoreErr := store.RestoreArchived(archivePath) errs <- restoreErr - }() + }) } wg.Wait() close(errs) diff --git a/internal/netclient/netclient_test.go b/internal/netclient/netclient_test.go index a9c5732177..9850803e38 100644 --- a/internal/netclient/netclient_test.go +++ b/internal/netclient/netclient_test.go @@ -125,17 +125,17 @@ func TestSummaryRedactsPassword(t *testing.T) { } func TestHTTPClientProxyModesAffectRequests(t *testing.T) { - var targetHits int32 + var targetHits atomic.Int32 target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&targetHits, 1) + targetHits.Add(1) _, _ = io.WriteString(w, "target") })) t.Cleanup(target.Close) targetAddr := strings.TrimPrefix(target.URL, "http://") - var envProxyHits int32 + var envProxyHits atomic.Int32 envProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&envProxyHits, 1) + envProxyHits.Add(1) if got, want := r.URL.String(), "http://service.test/resource"; got != want { t.Errorf("env proxy request URL = %q, want %q", got, want) } @@ -143,9 +143,9 @@ func TestHTTPClientProxyModesAffectRequests(t *testing.T) { })) t.Cleanup(envProxy.Close) - var customProxyHits int32 + var customProxyHits atomic.Int32 customProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&customProxyHits, 1) + customProxyHits.Add(1) if got, want := r.URL.String(), "http://service.test/resource"; got != want { t.Errorf("custom proxy request URL = %q, want %q", got, want) } @@ -204,9 +204,9 @@ func TestHTTPClientProxyModesAffectRequests(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - atomic.StoreInt32(&targetHits, 0) - atomic.StoreInt32(&envProxyHits, 0) - atomic.StoreInt32(&customProxyHits, 0) + targetHits.Store(0) + envProxyHits.Store(0) + customProxyHits.Store(0) client := mappedClient(t, tt.spec, "service.test:80", targetAddr) resp, err := client.Get("http://service.test/resource") @@ -221,13 +221,13 @@ func TestHTTPClientProxyModesAffectRequests(t *testing.T) { if string(body) != tt.wantBody { t.Fatalf("body = %q, want %q", body, tt.wantBody) } - if got := atomic.LoadInt32(&targetHits); got != tt.wantTargetHits { + if got := targetHits.Load(); got != tt.wantTargetHits { t.Fatalf("target hits = %d, want %d", got, tt.wantTargetHits) } - if got := atomic.LoadInt32(&envProxyHits); got != tt.wantEnvHits { + if got := envProxyHits.Load(); got != tt.wantEnvHits { t.Fatalf("env proxy hits = %d, want %d", got, tt.wantEnvHits) } - if got := atomic.LoadInt32(&customProxyHits); got != tt.wantCustomHits { + if got := customProxyHits.Load(); got != tt.wantCustomHits { t.Fatalf("custom proxy hits = %d, want %d", got, tt.wantCustomHits) } }) @@ -306,18 +306,18 @@ func TestStructuredProxyTypesAffectRequests(t *testing.T) { }) } - if got := atomic.LoadInt32(&socks5Proxy.hits); got != 1 { + if got := socks5Proxy.hits.Load(); got != 1 { t.Fatalf("socks5 proxy hits = %d, want 1", got) } - if got := atomic.LoadInt32(&socks5hProxy.hits); got != 1 { + if got := socks5hProxy.hits.Load(); got != 1 { t.Fatalf("socks5h proxy hits = %d, want 1", got) } } func TestHTTPSRequestsRespectProxyModes(t *testing.T) { - var targetHits int32 + var targetHits atomic.Int32 target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&targetHits, 1) + targetHits.Add(1) w.Header().Set("Connection", "close") _, _ = io.WriteString(w, "https-target") })) @@ -363,7 +363,7 @@ func TestHTTPSRequestsRespectProxyModes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - atomic.StoreInt32(&targetHits, 0) + targetHits.Store(0) atomic.StoreInt32(&proxyHits, 0) tr := mappedTransport(t, tt.spec, "service.test:443", targetAddr) @@ -381,7 +381,7 @@ func TestHTTPSRequestsRespectProxyModes(t *testing.T) { if string(body) != "https-target" { t.Fatalf("body = %q, want https-target", body) } - if got := atomic.LoadInt32(&targetHits); got != 1 { + if got := targetHits.Load(); got != 1 { t.Fatalf("target hits = %d, want 1", got) } if got := atomic.LoadInt32(&proxyHits); got != tt.wantProxyHits { @@ -433,7 +433,7 @@ func mappedTransport(t *testing.T, spec ProxySpec, fromAddr, toAddr string) *htt type socksHTTPProxy struct { addr string - hits int32 + hits atomic.Int32 } func newSocksHTTPProxy(t *testing.T) *socksHTTPProxy { @@ -480,7 +480,7 @@ func (p *socksHTTPProxy) handle(conn net.Conn) { if !p.readSocksAddr(r, req[3]) { return } - atomic.AddInt32(&p.hits, 1) + p.hits.Add(1) if _, err := conn.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}); err != nil { return } diff --git a/internal/outputstyle/outputstyle.go b/internal/outputstyle/outputstyle.go index dbeca04711..73fbbf9550 100644 --- a/internal/outputstyle/outputstyle.go +++ b/internal/outputstyle/outputstyle.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" @@ -67,13 +68,13 @@ func Dirs() []string { var dirs []string if os.Getenv("REASONIX_HOME") == "" { if home, err := os.UserHomeDir(); err == nil { - for i := len(conventionDirs) - 1; i >= 0; i-- { - dirs = append(dirs, filepath.Join(home, conventionDirs[i], "output-styles")) + for _, v := range slices.Backward(conventionDirs) { + dirs = append(dirs, filepath.Join(home, v, "output-styles")) } } } - for i := len(conventionDirs) - 1; i >= 0; i-- { - dirs = append(dirs, filepath.Join(".", conventionDirs[i], "output-styles")) + for _, v := range slices.Backward(conventionDirs) { + dirs = append(dirs, filepath.Join(".", v, "output-styles")) } return dirs } diff --git a/internal/permission/bash_readonly.go b/internal/permission/bash_readonly.go index 4d9ebb80ad..69a9ece38d 100644 --- a/internal/permission/bash_readonly.go +++ b/internal/permission/bash_readonly.go @@ -2,6 +2,7 @@ package permission import ( "encoding/json" + "slices" "strings" "reasonix/internal/shellsafe" @@ -123,10 +124,8 @@ func hasArgWithPrefix(args []string, prefix string) bool { func hasAnyArg(args []string, unsafe ...string) bool { for _, arg := range args { - for _, candidate := range unsafe { - if arg == candidate { - return true - } + if slices.Contains(unsafe, arg) { + return true } } return false diff --git a/internal/plugin/cache_test.go b/internal/plugin/cache_test.go index 776e213b46..5820796dfd 100644 --- a/internal/plugin/cache_test.go +++ b/internal/plugin/cache_test.go @@ -266,7 +266,7 @@ func TestSchemaCacheKeyStable(t *testing.T) { // but a fresh map can incidentally iterate the same way, so we run a few // iterations to give the runtime a chance to shuffle). reordered := spec - for i := 0; i < 32; i++ { + for range 32 { reordered.Env = map[string]string{"BAR": "2", "FOO": "1"} if got := SchemaCacheKey(reordered); got != h1 { t.Fatalf("SchemaCacheKey changed when env was rebuilt: %q vs %q", got, h1) diff --git a/internal/plugin/chrome_devtools_live_test.go b/internal/plugin/chrome_devtools_live_test.go index 39d0ea5e69..6e2d49462b 100644 --- a/internal/plugin/chrome_devtools_live_test.go +++ b/internal/plugin/chrome_devtools_live_test.go @@ -35,8 +35,7 @@ func TestChromeDevtoolsMCPLive(t *testing.T) { stdioShellPATH = func(context.Context) string { return os.Getenv("PATH") } t.Cleanup(func() { stdioShellPATH = oldShellPATH }) - lifeCtx, lifeCancel := context.WithCancel(context.Background()) - defer lifeCancel() + lifeCtx := t.Context() callCtx, callCancel := context.WithTimeout(lifeCtx, 2*time.Minute) defer callCancel() diff --git a/internal/plugin/codegraph_limit_test.go b/internal/plugin/codegraph_limit_test.go index ad6fdbb325..30b3dec82a 100644 --- a/internal/plugin/codegraph_limit_test.go +++ b/internal/plugin/codegraph_limit_test.go @@ -9,7 +9,7 @@ func TestAcquireCodeGraphSlotCapsInstances(t *testing.T) { } releases := make([]func(), 0, maxCodeGraphInstances) - for i := 0; i < maxCodeGraphInstances; i++ { + for i := range maxCodeGraphInstances { release, err := acquireCodeGraphSlot() if err != nil { t.Fatalf("acquire %d within cap should succeed: %v", i, err) diff --git a/internal/plugin/host_concurrency_test.go b/internal/plugin/host_concurrency_test.go index e6e5a44223..141cc0d1a9 100644 --- a/internal/plugin/host_concurrency_test.go +++ b/internal/plugin/host_concurrency_test.go @@ -15,7 +15,7 @@ func TestHostConcurrentAccess(t *testing.T) { h := &Host{} // Seed a few "connected" servers so the read paths have data to walk. These // methods only read name/transport/toolCount, never the (nil) transport. - for i := 0; i < 4; i++ { + for i := range 4 { h.clients = append(h.clients, &Client{name: fmt.Sprintf("srv-%d", i), transport: "stdio", toolCount: i}) h.prompts = append(h.prompts, Prompt{Server: fmt.Sprintf("srv-%d", i), Name: "p"}) } @@ -23,10 +23,10 @@ func TestHostConcurrentAccess(t *testing.T) { const workers = 24 var wg sync.WaitGroup wg.Add(workers) - for w := 0; w < workers; w++ { + for w := range workers { go func(w int) { defer wg.Done() - for i := 0; i < 500; i++ { + for i := range 500 { switch (w + i) % 6 { case 0: h.RecordFailure(Spec{Name: fmt.Sprintf("bad-%d", i%8), Type: "stdio"}, errors.New("boom")) diff --git a/internal/plugin/hotadd_test.go b/internal/plugin/hotadd_test.go index a4d91c8cfa..491884f67d 100644 --- a/internal/plugin/hotadd_test.go +++ b/internal/plugin/hotadd_test.go @@ -84,7 +84,7 @@ func TestHostAddConcurrentSameServerReusesSingleClient(t *testing.T) { errs := make([]error, callers) counts := make([]int, callers) wg.Add(callers) - for i := 0; i < callers; i++ { + for i := range callers { go func(i int) { defer wg.Done() tools, err := h.Add(ctx, spec) diff --git a/internal/plugin/known_overrides.go b/internal/plugin/known_overrides.go index 7b5daa3b18..f020d7d6f0 100644 --- a/internal/plugin/known_overrides.go +++ b/internal/plugin/known_overrides.go @@ -1,7 +1,9 @@ package plugin import ( + "maps" "path/filepath" + "slices" "strings" ) @@ -50,12 +52,7 @@ func isCodebaseMemorySpec(s Spec) bool { if isCodebaseMemoryID(s.Name) || isCodebaseMemoryCommand(s.Command) { return true } - for _, arg := range s.Args { - if isCodebaseMemoryID(arg) { - return true - } - } - return false + return slices.ContainsFunc(s.Args, isCodebaseMemoryID) } func isCodebaseMemoryCommand(command string) bool { @@ -89,9 +86,7 @@ func isStdioSpecType(typ string) bool { func mergeDefaultEnv(existing map[string]string, key, value string) map[string]string { out := make(map[string]string, len(existing)+1) - for name, v := range existing { - out[name] = v - } + maps.Copy(out, existing) if _, ok := out[key]; !ok { out[key] = value } diff --git a/internal/plugin/launcher_lock.go b/internal/plugin/launcher_lock.go index 6b1a16d231..a32d39e919 100644 --- a/internal/plugin/launcher_lock.go +++ b/internal/plugin/launcher_lock.go @@ -71,8 +71,8 @@ func launcherLocatorForSpec(spec Spec) (launcherLocator, bool) { if arg == "--from" && i+1 < len(args) { return launcherLocator{kind: kind, value: args[i+1], arg: i + 1, command: command}, true } - if strings.HasPrefix(arg, "--from=") { - return launcherLocator{kind: kind, value: strings.TrimPrefix(arg, "--from="), arg: i, prefix: "--from=", command: command}, true + if after, ok := strings.CutPrefix(arg, "--from="); ok { + return launcherLocator{kind: kind, value: after, arg: i, prefix: "--from=", command: command}, true } } } diff --git a/internal/plugin/launcher_lock_test.go b/internal/plugin/launcher_lock_test.go index e9f7d927b3..43252d3815 100644 --- a/internal/plugin/launcher_lock_test.go +++ b/internal/plugin/launcher_lock_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "strings" "testing" @@ -149,12 +150,7 @@ func TestStoredUVXFromLauncherLockKeepsFromValueAdjacent(t *testing.T) { } func stringSliceContains(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false + return slices.Contains(values, want) } func TestMutableLauncherRejectsAmbiguousFlagValue(t *testing.T) { diff --git a/internal/plugin/lazy_test.go b/internal/plugin/lazy_test.go index 525ce8bdb3..0292028f72 100644 --- a/internal/plugin/lazy_test.go +++ b/internal/plugin/lazy_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" "testing" @@ -93,10 +94,8 @@ func waitForServer(t *testing.T, host *Host, name string, timeout time.Duration) t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { - for _, n := range host.ServerNames() { - if n == name { - return - } + if slices.Contains(host.ServerNames(), name) { + return } time.Sleep(10 * time.Millisecond) } @@ -541,9 +540,7 @@ func TestLazySwapDoesNotRaceRegistrySchemas(t *testing.T) { done := make(chan struct{}) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for { select { case <-done: @@ -552,7 +549,7 @@ func TestLazySwapDoesNotRaceRegistrySchemas(t *testing.T) { _ = reg.Schemas() } } - }() + }) out, err := echo.Execute(ctx, json.RawMessage(`{"msg":"race"}`)) close(done) @@ -723,7 +720,7 @@ func TestLazyConcurrentExecuteOnlyOneSpawn(t *testing.T) { results := make([]string, goroutines) errs := make([]error, goroutines) wg.Add(goroutines) - for i := 0; i < goroutines; i++ { + for i := range goroutines { go func(i int) { defer wg.Done() out, err := echo.Execute(ctx, json.RawMessage(fmt.Sprintf(`{"msg":"r%d"}`, i))) @@ -1052,8 +1049,7 @@ func TestAddWithLifecycleSurvivesHandshakeCtxCancel(t *testing.T) { host := NewHost() defer host.Close() - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() handshakeCtx, cancelHandshake := context.WithTimeout(context.Background(), 5*time.Second) tools, err := host.AddWithLifecycle(lifeCtx, handshakeCtx, spec) cancelHandshake() // the proxy's deferred cancel fires right after connect diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 5eb1adc248..45c46eddca 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -15,6 +15,7 @@ import ( "hash/fnv" "io" "log/slog" + "maps" "reflect" "regexp" "sort" @@ -390,8 +391,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool phaseADur := recordedPhaseADur() cancelStartup() if !p.SkipPersistence { - h.bgWrites.Add(1) - go func() { defer h.bgWrites.Done(); _ = RecordStartup(spec.Name, phaseADur) }() + h.bgWrites.Go(func() { ; _ = RecordStartup(spec.Name, phaseADur) }) } ch <- result{idx: idx, spec: spec, err: fmt.Errorf("start plugin %q: %w", spec.Name, err)} return @@ -402,8 +402,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool phaseADur := recordedPhaseADur() cancelStartup() if !p.SkipPersistence { - h.bgWrites.Add(1) - go func() { defer h.bgWrites.Done(); _ = RecordStartup(spec.Name, phaseADur) }() + h.bgWrites.Go(func() { ; _ = RecordStartup(spec.Name, phaseADur) }) } c.close() err = newStartupFailure("tools/list", phaseAStart, c.startupStderr(), err) @@ -418,9 +417,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool phaseADur := recordedPhaseADur() cancelStartup() if !p.SkipPersistence { - h.bgWrites.Add(1) - go func() { - defer h.bgWrites.Done() + h.bgWrites.Go(func() { _ = RecordStartup(spec.Name, phaseADur) _ = SaveCachedSchema(spec.Name, CachedSchema{ CacheKey: SchemaCacheKey(spec), @@ -431,7 +428,7 @@ func Start(ctx context.Context, specs []Spec, p StartPolicy) (*Host, []tool.Tool }, Tools: cacheableToolsOf(ts), }) - }() + }) } // Prompts and resources are deferred to StartPhaseB so the boot path @@ -506,11 +503,9 @@ func (h *Host) Close() { // Callers must enqueue before their Close-drained startup owner completes, so // Close cannot begin waiting before the WaitGroup increment is visible. func (h *Host) queueBackgroundWrite(write func()) { - h.bgWrites.Add(1) - go func() { - defer h.bgWrites.Done() + h.bgWrites.Go(func() { write() - }() + }) } // StartPhaseB asynchronously fetches the auxiliary surfaces (prompts and @@ -1544,14 +1539,10 @@ func (c *Client) withProgress(ctx context.Context, method string, params any) (a token := fmt.Sprintf("reasonix-%d", c.progressID.Add(1)) copyParams := make(map[string]any, len(callParams)) - for key, value := range callParams { - copyParams[key] = value - } + maps.Copy(copyParams, callParams) meta := map[string]any{} if existing, ok := callParams["_meta"].(map[string]any); ok { - for key, value := range existing { - meta[key] = value - } + maps.Copy(meta, existing) } meta["progressToken"] = token copyParams["_meta"] = meta diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index 84687bcd65..b0084a57eb 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -779,8 +779,7 @@ func TestStdioFailureCapturesStderr(t *testing.T) { } func TestStartupFailureReportsStageElapsedAndRedactedStderr(t *testing.T) { - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() startupCtx, cancelStartup := context.WithTimeout(lifeCtx, 40*time.Millisecond) defer cancelStartup() @@ -828,8 +827,7 @@ func TestFailureSummaryRedactsCredentials(t *testing.T) { } func TestEnsureConnectedInBackgroundSurvivesShortCallerWait(t *testing.T) { - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() host := NewHost() defer host.Close() spec := Spec{ @@ -863,8 +861,7 @@ func TestEnsureConnectedInBackgroundSurvivesShortCallerWait(t *testing.T) { } func TestEnsureConnectedInBackgroundRemoveDoesNotResurrectServer(t *testing.T) { - lifeCtx, cancelLife := context.WithCancel(context.Background()) - defer cancelLife() + lifeCtx := t.Context() host := NewHost() defer host.Close() spec := Spec{ @@ -1129,7 +1126,7 @@ func TestStartRecordsTimeoutStats(t *testing.T) { "GO_WANT_HELPER_INIT_MS": "300", }, } - for i := 0; i < 3; i++ { + for i := range 3 { host, _, err := Start(ctx, []Spec{slow}, StartPolicy{ PerPluginTimeout: 50 * time.Millisecond, Concurrency: 1, diff --git a/internal/plugin/process_mode_test.go b/internal/plugin/process_mode_test.go index 3ca09cf753..d9e32cbb2e 100644 --- a/internal/plugin/process_mode_test.go +++ b/internal/plugin/process_mode_test.go @@ -52,13 +52,13 @@ func TestHostModeEnsureConnectedSkipsCommandSandboxAndSharesProcess(t *testing.T err error } ch := make(chan result, 3) - for i := 0; i < 3; i++ { + for range 3 { go func() { tools, err := host.EnsureConnected(ctx, spec) ch <- result{len(tools), err} }() } - for i := 0; i < 3; i++ { + for range 3 { r := <-ch if r.err != nil { t.Fatalf("EnsureConnected: %v", r.err) diff --git a/internal/plugin/protocol_inbound.go b/internal/plugin/protocol_inbound.go index 8d8d0bb45c..02445b75ba 100644 --- a/internal/plugin/protocol_inbound.go +++ b/internal/plugin/protocol_inbound.go @@ -114,8 +114,8 @@ func mcpRoots(workspaceRoot string) []mcpRoot { clean := filepath.Clean(abs) path := filepath.ToSlash(clean) fileURL := &url.URL{Scheme: "file"} - if strings.HasPrefix(path, "//") { - parts := strings.SplitN(strings.TrimPrefix(path, "//"), "/", 2) + if after, ok := strings.CutPrefix(path, "//"); ok { + parts := strings.SplitN(after, "/", 2) fileURL.Host = parts[0] if len(parts) == 2 { fileURL.Path = "/" + parts[1] diff --git a/internal/plugin/proxy_e2e_test.go b/internal/plugin/proxy_e2e_test.go index 5bb83b10bc..0e0ed8e574 100644 --- a/internal/plugin/proxy_e2e_test.go +++ b/internal/plugin/proxy_e2e_test.go @@ -33,8 +33,7 @@ func mockSpec() plugin.Spec { func TestUseCapabilityFirstDiscoveryConnectListCall(t *testing.T) { host := plugin.NewHost() defer host.Close() - lifeCtx, cancel := context.WithCancel(context.Background()) - defer cancel() + lifeCtx := t.Context() ledger := capability.NewLedger() audit := &capability.Audit{} proxy := agent.NewUseCapabilityTool(lifeCtx, host, []plugin.Spec{mockSpec()}, tool.NewRegistry(), ledger, audit, nil) @@ -110,8 +109,7 @@ func TestUseCapabilityFirstDiscoveryConnectListCall(t *testing.T) { func TestUseCapabilitySharedHostSnapshot(t *testing.T) { host := plugin.NewHost() defer host.Close() - lifeCtx, cancel := context.WithCancel(context.Background()) - defer cancel() + lifeCtx := t.Context() callCtx, cancelCall := context.WithTimeout(context.Background(), 5*time.Second) defer cancelCall() // "Tab A" connects directly (not through any proxy). diff --git a/internal/plugin/startup.go b/internal/plugin/startup.go index 35c37dbaed..cd0297bf74 100644 --- a/internal/plugin/startup.go +++ b/internal/plugin/startup.go @@ -73,10 +73,7 @@ func newStartupFailure(stage string, started time.Time, stderr string, err error if errors.As(err, &existing) { return err } - elapsed := time.Since(started) - if elapsed < 0 { - elapsed = 0 - } + elapsed := max(time.Since(started), 0) return &startupFailure{ Stage: strings.TrimSpace(stage), Elapsed: elapsed, diff --git a/internal/plugin/stats.go b/internal/plugin/stats.go index 7879c0a0d8..a0e17de2f3 100644 --- a/internal/plugin/stats.go +++ b/internal/plugin/stats.go @@ -16,7 +16,7 @@ import ( "log/slog" "os" "path/filepath" - "sort" + "slices" "time" "reasonix/internal/config" @@ -93,10 +93,7 @@ func RecordStartup(name string, dur time.Duration) error { } stats.Name = name - ms := dur.Milliseconds() - if ms < 0 { - ms = 0 - } + ms := max(dur.Milliseconds(), 0) stats.SamplesMs = append(stats.SamplesMs, ms) if len(stats.SamplesMs) > maxSamples { // Trim from the front: oldest samples leave first. @@ -278,13 +275,10 @@ func p99(samples []int64) time.Duration { return 0 } sorted := append([]int64(nil), samples...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + slices.Sort(sorted) // Use ceil(0.99 * n) - 1 so that for n=1..100 we always pick the last // element; for larger n it's the index at the 99% boundary. - idx := int(float64(len(sorted))*0.99+0.9999999) - 1 - if idx < 0 { - idx = 0 - } + idx := max(int(float64(len(sorted))*0.99+0.9999999)-1, 0) if idx >= len(sorted) { idx = len(sorted) - 1 } diff --git a/internal/plugin/stats_test.go b/internal/plugin/stats_test.go index f4093d5b68..87ed162520 100644 --- a/internal/plugin/stats_test.go +++ b/internal/plugin/stats_test.go @@ -73,7 +73,7 @@ func TestRecordStartupCapsAtWindow(t *testing.T) { withTempCache(t) const total = 25 - for i := 0; i < total; i++ { + for i := range total { // Use distinct values so we can verify the *oldest* ones were dropped. if err := RecordStartup("bar", time.Duration(i+1)*time.Millisecond); err != nil { t.Fatalf("RecordStartup #%d: %v", i, err) @@ -140,7 +140,7 @@ func TestRecommendAtBudgetDemotes(t *testing.T) { withTempCache(t) budget := 5 * time.Second - for i := 0; i < 3; i++ { + for i := range 3 { if err := RecordStartup("timeout-plugin", budget); err != nil { t.Fatalf("RecordStartup #%d: %v", i, err) } @@ -231,7 +231,7 @@ func TestStatsSlugCollisionDoesNotCrossDemote(t *testing.T) { withTempCache(t) // Interleave a chronically slow server with a healthy one sharing the slug. - for i := 0; i < defaultDemoteAfter; i++ { + for range defaultDemoteAfter { if err := RecordStartup("foo.bar", 5*time.Second); err != nil { t.Fatalf("RecordStartup foo.bar: %v", err) } @@ -264,7 +264,7 @@ func TestStatsLegacyNamedFileMigrated(t *testing.T) { withTempCache(t) legacy := StartupStats{Version: statsVersion, Name: "legacy", LastSeen: time.Now()} - for i := 0; i < 3; i++ { + for range 3 { legacy.SamplesMs = append(legacy.SamplesMs, 100) } if err := writeStatsAtomic(legacyStatsPath("legacy"), legacy); err != nil { @@ -291,7 +291,7 @@ func TestStatsLegacyNamelessCollisionNotTrusted(t *testing.T) { withTempCache(t) nameless := StartupStats{Version: statsVersion, LastSeen: time.Now()} - for i := 0; i < defaultDemoteAfter; i++ { + for range defaultDemoteAfter { nameless.SamplesMs = append(nameless.SamplesMs, 5000) // over budget } if err := writeStatsAtomic(legacyStatsPath("foo.bar"), nameless); err != nil { @@ -322,7 +322,7 @@ func TestStatsLegacyNamelessCollisionNotTrusted(t *testing.T) { func TestStatsLegacyFileWithoutNameAdopted(t *testing.T) { withTempCache(t) - for i := 0; i < 3; i++ { + for range 3 { if err := RecordStartup("legacy", 100*time.Millisecond); err != nil { t.Fatalf("RecordStartup: %v", err) } diff --git a/internal/plugin/stdio_cancel_test.go b/internal/plugin/stdio_cancel_test.go index 1150d4b04c..4e10a82c7d 100644 --- a/internal/plugin/stdio_cancel_test.go +++ b/internal/plugin/stdio_cancel_test.go @@ -343,7 +343,7 @@ func TestStdioReadLoopStaysLiveWhenReplyWriterIsBlocked(t *testing.T) { // run off the test goroutine so a deadlocked readLoop fails the timeout // below instead of hanging the whole package; Cleanup unblocks the writer. go func() { - for i := 0; i < 2*stdioReplyQueueBound; i++ { + for i := range 2 * stdioReplyQueueBound { line := fmt.Sprintf(`{"jsonrpc":"2.0","id":"srv-%d","method":"ping"}`+"\n", i) if _, err := io.WriteString(stdoutWrites, line); err != nil { return diff --git a/internal/plugin/toolresult_image_test.go b/internal/plugin/toolresult_image_test.go index 8806936050..f2637bf95e 100644 --- a/internal/plugin/toolresult_image_test.go +++ b/internal/plugin/toolresult_image_test.go @@ -103,7 +103,7 @@ func TestParseToolResultImageOversizedOmitted(t *testing.T) { func TestParseToolResultImageCountCapped(t *testing.T) { payload := base64.StdEncoding.EncodeToString([]byte("x")) items := make([]string, 0, maxToolResultImages+1) - for i := 0; i < maxToolResultImages+1; i++ { + for range maxToolResultImages + 1 { items = append(items, imageItem("image/png", payload)) } res := `{"content":[` + strings.Join(items, ",") + `]}` diff --git a/internal/plugin/transport_http.go b/internal/plugin/transport_http.go index 0e31c08484..4fbcdc3c64 100644 --- a/internal/plugin/transport_http.go +++ b/internal/plugin/transport_http.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "maps" "net/http" "net/url" "strings" @@ -47,9 +48,7 @@ func newHTTPTransport(s Spec) (*httpTransport, error) { return nil, fmt.Errorf("http plugin %q: url is required", s.Name) } headers := make(map[string]string, len(s.Headers)) - for key, value := range s.Headers { - headers[key] = value - } + maps.Copy(headers, s.Headers) return &httpTransport{ name: s.Name, url: s.URL, diff --git a/internal/plugin/transport_sse.go b/internal/plugin/transport_sse.go index 4a330541c5..b1cd9abffd 100644 --- a/internal/plugin/transport_sse.go +++ b/internal/plugin/transport_sse.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "net/url" "strings" @@ -60,9 +61,7 @@ func newSSETransport(ctx context.Context, s Spec) (*sseTransport, error) { return nil, fmt.Errorf("sse plugin %q: invalid url %q", s.Name, s.URL) } headers := make(map[string]string, len(s.Headers)) - for key, value := range s.Headers { - headers[key] = value - } + maps.Copy(headers, s.Headers) lifeCtx, cancel := context.WithCancel(ctx) t := &sseTransport{ name: s.Name, diff --git a/internal/plugin/transport_sse_test.go b/internal/plugin/transport_sse_test.go index 710f0f8fdc..5368a81714 100644 --- a/internal/plugin/transport_sse_test.go +++ b/internal/plugin/transport_sse_test.go @@ -258,7 +258,7 @@ func TestLegacySSEBoundsConcurrentServerRequestReplies(t *testing.T) { transport.mu.Lock() transport.pending[7] = waiting transport.mu.Unlock() - for i := 0; i < 2*sseReplyQueueBound; i++ { + for i := range 2 * sseReplyQueueBound { events <- fmt.Sprintf("event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":\"srv-%d\",\"method\":\"ping\"}\n\n", i) } events <- "event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":7,\"result\":{}}\n\n" diff --git a/internal/plugin/transport_stdio.go b/internal/plugin/transport_stdio.go index 6be11c220c..48df31a85a 100644 --- a/internal/plugin/transport_stdio.go +++ b/internal/plugin/transport_stdio.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "sync" "time" @@ -339,7 +340,7 @@ func executableNames(command, pathext string) []string { } names := []string{command} seen := map[string]bool{strings.ToLower(command): true} - for _, ext := range strings.Split(pathext, ";") { + for ext := range strings.SplitSeq(pathext, ";") { ext = strings.TrimSpace(ext) if ext == "" { continue @@ -475,9 +476,9 @@ func prepareStdioShellPATHProbe(cmd *exec.Cmd) { func parseShellPATH(out []byte, marker string) string { lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") - for i := len(lines) - 1; i >= 0; i-- { - if strings.HasPrefix(lines[i], marker) { - return strings.TrimSpace(strings.TrimPrefix(lines[i], marker)) + for _, line := range slices.Backward(lines) { + if rest, ok := strings.CutPrefix(line, marker); ok { + return strings.TrimSpace(rest) } } return "" @@ -512,8 +513,8 @@ func setEnvValue(env []string, key, value string) []string { } func envValue(env []string, key string) (string, bool) { - for i := len(env) - 1; i >= 0; i-- { - k, v, ok := strings.Cut(env[i], "=") + for _, entry := range slices.Backward(env) { + k, v, ok := strings.Cut(entry, "=") if ok && envKeyEqual(k, key) { return v, true } diff --git a/internal/pluginpkg/claude_compat.go b/internal/pluginpkg/claude_compat.go index e4cc51bd67..56e2ca8dd9 100644 --- a/internal/pluginpkg/claude_compat.go +++ b/internal/pluginpkg/claude_compat.go @@ -93,7 +93,7 @@ func claudeMatcherNeverFires(matcher string) bool { if matcher == "" || matcher == "*" { return false } - for _, part := range strings.Split(matcher, "|") { + for part := range strings.SplitSeq(matcher, "|") { part = strings.TrimSpace(part) if !bareClaudeToolNamePattern.MatchString(part) || !claudeUnsupportedToolMatchers[part] { return false @@ -524,7 +524,7 @@ func splitCSV(raw string) []string { raw = strings.TrimSpace(raw[1 : len(raw)-1]) } var out []string - for _, item := range strings.Split(raw, ",") { + for item := range strings.SplitSeq(raw, ",") { if item = strings.Trim(strings.TrimSpace(item), `"'`); item != "" { out = append(out, item) } diff --git a/internal/pluginpkg/manifest_v1.go b/internal/pluginpkg/manifest_v1.go index 7a8e2e05a0..fe1543e758 100644 --- a/internal/pluginpkg/manifest_v1.go +++ b/internal/pluginpkg/manifest_v1.go @@ -5,11 +5,13 @@ import ( "encoding/json" "errors" "fmt" + "maps" "os" "path" "path/filepath" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -426,9 +428,7 @@ func mergeV1MCPServers(legacy, contrib map[string]MCPServer) (map[string]MCPServ return legacy, nil } out := make(map[string]MCPServer, len(legacy)) - for name, server := range legacy { - out[name] = server - } + maps.Copy(out, legacy) for _, name := range sortedKeys(contrib) { server := contrib[name] if existing, ok := out[name]; ok { @@ -573,13 +573,7 @@ func parseV1Runtime(raw json.RawMessage) (*RuntimeSpec, error) { } } for _, capability := range rt.Capabilities { - known := false - for _, k := range runtimeCapabilities { - if capability == k { - known = true - break - } - } + known := slices.Contains(runtimeCapabilities, capability) if !known { return nil, fmt.Errorf("runtime.capabilities: unknown capability %q (want one of: %s)", capability, strings.Join(runtimeCapabilities, ", ")) } diff --git a/internal/pluginpkg/state_lock_test.go b/internal/pluginpkg/state_lock_test.go index 9334e355f9..022169f2b2 100644 --- a/internal/pluginpkg/state_lock_test.go +++ b/internal/pluginpkg/state_lock_test.go @@ -19,9 +19,7 @@ func TestStateConcurrentUpsertAndSetEnabled(t *testing.T) { var wg sync.WaitGroup for i := range n { name := fmt.Sprintf("plugin-%02d", i) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := Upsert(home, InstalledPlugin{Name: name, Root: "plugins/" + name}); err != nil { t.Errorf("Upsert(%s): %v", name, err) return @@ -29,7 +27,7 @@ func TestStateConcurrentUpsertAndSetEnabled(t *testing.T) { if err := SetEnabled(home, name, true); err != nil { t.Errorf("SetEnabled(%s): %v", name, err) } - }() + }) } wg.Wait() @@ -62,9 +60,7 @@ func TestStateConcurrentRemove(t *testing.T) { var wg sync.WaitGroup for i := range n { name := fmt.Sprintf("plugin-%02d", i) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { removed, ok, err := Remove(home, name) if err != nil { t.Errorf("Remove(%s): %v", name, err) @@ -73,7 +69,7 @@ func TestStateConcurrentRemove(t *testing.T) { if !ok || removed.Name != name { t.Errorf("Remove(%s) = %+v, ok=%v", name, removed, ok) } - }() + }) } wg.Wait() diff --git a/internal/productdocs/docs_test.go b/internal/productdocs/docs_test.go index 48824c68c0..fde2403c76 100644 --- a/internal/productdocs/docs_test.go +++ b/internal/productdocs/docs_test.go @@ -450,7 +450,7 @@ func TestDocsToolSupportsConcurrentReadOnlyCalls(t *testing.T) { const workers = 12 var wg sync.WaitGroup errs := make(chan error, workers) - for i := 0; i < workers; i++ { + for i := range workers { wg.Add(1) go func(index int) { defer wg.Done() diff --git a/internal/provider/anthropic/anthropic_test.go b/internal/provider/anthropic/anthropic_test.go index 839f8438cd..5f05729185 100644 --- a/internal/provider/anthropic/anthropic_test.go +++ b/internal/provider/anthropic/anthropic_test.go @@ -106,8 +106,7 @@ func TestBuildRequestKeepsDefaultCacheControlBytesStable(t *testing.T) { if err != nil { t.Fatalf("marshal first request: %v", err) } - requestCtx, cancel := context.WithCancel(context.Background()) - defer cancel() + requestCtx := t.Context() got, err := json.Marshal(c.buildRequest(requestCtx, req)) if err != nil { t.Fatalf("marshal second request: %v", err) diff --git a/internal/provider/openai/host.go b/internal/provider/openai/host.go index 90b23924f9..ec7eb1e6eb 100644 --- a/internal/provider/openai/host.go +++ b/internal/provider/openai/host.go @@ -2,6 +2,7 @@ package openai import ( "net/url" + "slices" "strings" "reasonix/internal/provider" @@ -24,10 +25,8 @@ func matchesVendorHost(baseURL, apex string, canonical ...string) bool { return false } host := strings.ToLower(u.Hostname()) - for _, c := range canonical { - if host == c { - return true - } + if slices.Contains(canonical, host) { + return true } return strings.HasSuffix(host, "."+apex) } diff --git a/internal/provider/openai/openai.go b/internal/provider/openai/openai.go index 14bbaaca75..4f0bbd8f7a 100644 --- a/internal/provider/openai/openai.go +++ b/internal/provider/openai/openai.go @@ -29,6 +29,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "sort" "strings" @@ -1225,9 +1226,7 @@ func (r chatRequest) MarshalJSON() ([]byte, error) { if err := json.Unmarshal(raw, &body); err != nil { return nil, err } - for key, value := range cleanExtraBody(r.ExtraBody) { - body[key] = value - } + maps.Copy(body, cleanExtraBody(r.ExtraBody)) return json.Marshal(body) } diff --git a/internal/provider/openai/openai_test.go b/internal/provider/openai/openai_test.go index 6381048afa..4e1c7f78b5 100644 --- a/internal/provider/openai/openai_test.go +++ b/internal/provider/openai/openai_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "maps" "net/http" "net/http/httptest" "strings" @@ -1677,9 +1678,7 @@ func withEffort(c provider.Config, effort string) provider.Config { extra = map[string]any{} } else { cp := make(map[string]any, len(extra)+1) - for k, v := range extra { - cp[k] = v - } + maps.Copy(cp, extra) extra = cp } extra["effort"] = effort diff --git a/internal/provider/openai/think.go b/internal/provider/openai/think.go index 8873052572..1592c46f21 100644 --- a/internal/provider/openai/think.go +++ b/internal/provider/openai/think.go @@ -88,10 +88,7 @@ func (t *thinkSplitter) drainPassthrough() string { // prefix of marker — the tail to hold back in case the rest of the tag arrives // in the next delta. func markerSuffixLen(s, marker string) int { - max := len(marker) - 1 - if max > len(s) { - max = len(s) - } + max := min(len(marker)-1, len(s)) for n := max; n > 0; n-- { if strings.HasPrefix(marker, s[len(s)-n:]) { return n diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 99cc55e548..4da6fb204f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "net" + "slices" "sort" "strings" "syscall" @@ -515,7 +516,7 @@ func repairToolCallArgs(m Message) Message { func closeTruncatedJSON(s string) string { var stack []byte inStr, esc := false, false - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] if inStr { switch { @@ -555,8 +556,8 @@ func closeTruncatedJSON(s string) string { case strings.HasSuffix(trimmed, ":"): out = trimmed + "null" } - for i := len(stack) - 1; i >= 0; i-- { - out += string(stack[i]) + for _, v := range slices.Backward(stack) { + out += string(v) } if !json.Valid([]byte(out)) { return "{}" @@ -789,13 +790,7 @@ func (p *Pricing) Cost(u *Usage) float64 { // writes or 2x 1-hour writes). Older providers leave both fields at zero and // keep the legacy one-input-rate behavior. A write count without billed // units also falls back to 1x for backward compatibility. - write := u.CacheWriteTokens - if write < 0 { - write = 0 - } - if write > miss { - write = miss - } + write := min(max(u.CacheWriteTokens, 0), miss) billedWrite := 0.0 if write > 0 { billedWrite = u.CacheWriteBilledTokens diff --git a/internal/provider/responses/responses.go b/internal/provider/responses/responses.go index d110c280e9..6144470506 100644 --- a/internal/provider/responses/responses.go +++ b/internal/provider/responses/responses.go @@ -796,10 +796,7 @@ func usageFromResponse(response *sseResponse) *provider.Usage { if u.OutputTokensDetails != nil { reasoning = u.OutputTokensDetails.ReasoningTokens } - miss := u.InputTokens - cached - if miss < 0 { - miss = 0 - } + miss := max(u.InputTokens-cached, 0) total := u.TotalTokens if total == 0 { total = u.InputTokens + u.OutputTokens diff --git a/internal/provider/responses/responses_test.go b/internal/provider/responses/responses_test.go index ac205cbe3d..47f09e1eaf 100644 --- a/internal/provider/responses/responses_test.go +++ b/internal/provider/responses/responses_test.go @@ -251,18 +251,18 @@ func TestStreamDoesNotDuplicateDoneText(t *testing.T) { defer server.Close() chunks := collect(t, New(Config{Name: "test", APIKey: "key", BaseURL: server.URL, Model: "m", Mode: "stateless"}), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}}) - var text string + var text strings.Builder var usage *provider.Usage for _, chunk := range chunks { if chunk.Type == provider.ChunkText { - text += chunk.Text + text.WriteString(chunk.Text) } if chunk.Type == provider.ChunkUsage { usage = chunk.Usage } } - if text != "hello" { - t.Fatalf("streamed text = %q, want one copy", text) + if text.String() != "hello" { + t.Fatalf("streamed text = %q, want one copy", text.String()) } if usage == nil || usage.CacheHitTokens != 2 || usage.CacheMissTokens != 1 || usage.ReasoningTokens != 1 || usage.RequestCount != 1 { t.Fatalf("usage = %+v", usage) @@ -287,16 +287,16 @@ func TestStreamToleratesWebSearchLifecycleEvents(t *testing.T) { chunks := collect(t, New(Config{Name: "deepseek", APIKey: "key", BaseURL: server.URL, Model: "deepseek-v4-flash", Mode: "stateless", WebSearch: true}), provider.Request{ Messages: []provider.Message{{Role: provider.RoleUser, Content: "search"}}, }) - var text string + var text strings.Builder for _, chunk := range chunks { if chunk.Type == provider.ChunkText { - text += chunk.Text + text.WriteString(chunk.Text) } if chunk.Type == provider.ChunkError { t.Fatalf("unexpected stream error: %v", chunk.Err) } } - if text != "found it" || chunks[len(chunks)-1].Type != provider.ChunkDone { + if text.String() != "found it" || chunks[len(chunks)-1].Type != provider.ChunkDone { t.Fatalf("chunks = %#v, want searched answer followed by done", chunks) } } diff --git a/internal/provider/retry.go b/internal/provider/retry.go index 627c3edc70..a7e9e82e6d 100644 --- a/internal/provider/retry.go +++ b/internal/provider/retry.go @@ -225,10 +225,7 @@ func backoffDelay(attempt int, retryAfter time.Duration) time.Duration { } return retryAfter } - d := time.Duration(1<<(attempt-1)) * 500 * time.Millisecond - if d > maxBackoff { - d = maxBackoff - } + d := min(time.Duration(1<<(attempt-1))*500*time.Millisecond, maxBackoff) return d + time.Duration(rand.Intn(250))*time.Millisecond } diff --git a/internal/provider/retry_test.go b/internal/provider/retry_test.go index 74857d683e..7c9023ef8f 100644 --- a/internal/provider/retry_test.go +++ b/internal/provider/retry_test.go @@ -172,7 +172,7 @@ func TestSendWithRetryRetriesTransientAuthForKnownKey(t *testing.T) { if err != nil { t.Fatalf("a previously-good key should recover from a transient 401: %v", err) } - if resp.StatusCode != 200 || calls != 3 { + if resp.StatusCode != http.StatusOK || calls != 3 { t.Fatalf("status=%d calls=%d, want 200 after 3 calls", resp.StatusCode, calls) } } @@ -228,7 +228,7 @@ func TestSendWithRetryUnblocksStalledErrorBody(t *testing.T) { cl := &http.Client{Transport: rtFunc(func(r *http.Request) (*http.Response, error) { calls++ if calls == 1 { - return &http.Response{StatusCode: 502, Body: newStallingBody(), Header: http.Header{}}, nil + return &http.Response{StatusCode: http.StatusBadGateway, Body: newStallingBody(), Header: http.Header{}}, nil } return statusResp(200, nil), nil })} @@ -248,7 +248,7 @@ func TestSendWithRetryUnblocksStalledErrorBody(t *testing.T) { if r.err != nil { t.Fatalf("should recover after the stalled 502: %v", r.err) } - if r.resp.StatusCode != 200 || calls != 2 { + if r.resp.StatusCode != http.StatusOK || calls != 2 { t.Fatalf("status=%d calls=%d, want 200 after 2 calls", r.resp.StatusCode, calls) } case <-time.After(5 * time.Second): @@ -273,7 +273,7 @@ func TestSendWithRetryRecoversAndNotifies(t *testing.T) { if err != nil { t.Fatalf("should recover after one retry: %v", err) } - if resp.StatusCode != 200 || calls != 2 { + if resp.StatusCode != http.StatusOK || calls != 2 { t.Fatalf("status=%d calls=%d, want 200 after 2 calls", resp.StatusCode, calls) } if len(infos) != 1 || infos[0].Attempt != 1 || infos[0].Max != MaxRetries { diff --git a/internal/recovery/decision_test.go b/internal/recovery/decision_test.go index 419500477b..fdcc66f76e 100644 --- a/internal/recovery/decision_test.go +++ b/internal/recovery/decision_test.go @@ -314,7 +314,7 @@ func TestBehaviorMatrixGolden(t *testing.T) { }) t.Run("third repeated operation stops without prompting", func(t *testing.T) { got := run(t, func(g *Gate) { - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ Tool: "write_file", Mutates: true, Subject: "a.go", ErrSummary: "fail", Args: json.RawMessage(`{"path":"a.go","content":"x"}`), @@ -330,7 +330,7 @@ func TestBehaviorMatrixGolden(t *testing.T) { }) t.Run("different edit after three verification failures stays automatic", func(t *testing.T) { got := run(t, func(g *Gate) { - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ Tool: "bash", Verification: true, Subject: "go test ./...", ErrSummary: "fail", Args: json.RawMessage(`{"command":"go test ./..."}`), diff --git a/internal/recovery/gate_test.go b/internal/recovery/gate_test.go index 5ee91161af..526e2a17ec 100644 --- a/internal/recovery/gate_test.go +++ b/internal/recovery/gate_test.go @@ -468,7 +468,7 @@ func TestRepeatedFailureStopsOnlyTheSameOperation(t *testing.T) { Preview: "mvn test", Verification: true, Args: failedArgs, } g.ObserveResult(context.Background(), failed) - for attempt := 0; attempt < 2; attempt++ { + for attempt := range 2 { dec, err := g.BeforeMutation(context.Background(), retry) if err != nil || !dec.Allow { t.Fatalf("retry %d = %+v, %v", attempt+1, dec, err) @@ -492,7 +492,7 @@ func TestRepeatedFailureStopsOnlyTheSameOperation(t *testing.T) { func TestDifferentFailureStartsFreshRecoveryEpisode(t *testing.T) { g := NewGate(Options{}) - for i := 0; i < 2; i++ { + for range 2 { g.ObserveResult(context.Background(), Observation{ TaskScopeID: "turn:1", Tool: "bash", Subject: "go test ./...", Verification: true, Args: json.RawMessage(`{"command":"go test ./..."}`), ErrSummary: "go failed", @@ -511,7 +511,7 @@ func TestDifferentFailureStartsFreshRecoveryEpisode(t *testing.T) { func TestNewOrdinaryTurnRetiresTechnicalFailureLatch(t *testing.T) { g := NewGate(Options{}) args := json.RawMessage(`{"command":"go test ./..."}`) - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ TaskScopeID: "turn:1", Tool: "bash", Subject: "go test ./...", Verification: true, Args: args, ErrSummary: "fail", @@ -535,7 +535,7 @@ func TestLeavingAutoRetiresTechnicalFailureLatch(t *testing.T) { mode := "auto" g := NewGate(Options{Mode: func() string { return mode }}) args := json.RawMessage(`{"command":"go test ./..."}`) - for i := 0; i < 3; i++ { + for range 3 { g.ObserveResult(context.Background(), Observation{ TaskScopeID: "goal:ship", Tool: "bash", Subject: "go test ./...", Verification: true, Args: args, ErrSummary: "fail", @@ -608,7 +608,7 @@ func TestReviewerRejectBudgetIsEpisodeCumulative(t *testing.T) { Args: argsA, ErrSummary: "fail", }) proposalA := Proposal{TaskScopeID: "turn:1", Tool: "write_file", Subject: "a.go", Mutates: true, Args: argsA} - for i := 0; i < 3; i++ { + for i := range 3 { dec, err := g.BeforeMutation(context.Background(), proposalA) if err != nil || dec.Allow || !dec.Blocked { t.Fatalf("proposal A attempt %d = %+v, %v", i+1, dec, err) @@ -738,9 +738,9 @@ func TestPlanContinueAppliesOnlyToWaitingTransition(t *testing.T) { } // Same fingerprint without new approval must re-prompt (grant consumed). - var prompts int32 + var prompts atomic.Int32 g.opts.EmitPrompt = func(ctx context.Context, taskID string, pending PendingProposal, failure *FailureEvent) (string, error) { - atomic.AddInt32(&prompts, 1) + prompts.Add(1) go func() { time.Sleep(5 * time.Millisecond) _ = g.Resolve("c2", ActionContinue, "") @@ -751,7 +751,7 @@ func TestPlanContinueAppliesOnlyToWaitingTransition(t *testing.T) { if err != nil || !dec.Allow || !dec.AuthorizePlanReplacement { t.Fatalf("second continue = %+v %v", dec, err) } - if atomic.LoadInt32(&prompts) != 1 { + if prompts.Load() != 1 { t.Fatalf("expected re-prompt after fingerprint consumption") } } @@ -1061,7 +1061,7 @@ func TestRestoreDropsStalePendingAuthorization(t *testing.T) { func TestRestoreNeverRearmsActiveLocks(t *testing.T) { args := json.RawMessage(`{"path":"a.go","content":"x"}`) goal := NewGate(Options{}) - for i := 0; i < 3; i++ { + for range 3 { goal.ObserveResult(context.Background(), Observation{ TaskScopeID: "goal:ship", Tool: "write_file", Subject: "a.go", Mutates: true, Args: args, ErrSummary: "fail", @@ -1088,7 +1088,7 @@ func TestRestoreNeverRearmsActiveLocks(t *testing.T) { } turn := NewGate(Options{}) - for i := 0; i < 3; i++ { + for range 3 { turn.ObserveResult(context.Background(), Observation{ TaskScopeID: "turn:1", Tool: "write_file", Subject: "a.go", Mutates: true, Args: args, ErrSummary: "fail", @@ -1194,7 +1194,7 @@ func TestEpisodeTotalFailuresHardStopAcrossFingerprints(t *testing.T) { g := NewGate(Options{Reviewer: staticReviewer{ReviewVerdict{ Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) - for i := 0; i < MaxEpisodeFailures; i++ { + for i := range MaxEpisodeFailures { cmd := fmt.Sprintf("go test ./pkg%d", i) g.ObserveResult(context.Background(), Observation{ Tool: "bash", Subject: cmd, Verification: true, @@ -1224,19 +1224,19 @@ func TestEpisodeBudgetIsSharedAcrossSubagentTaskIDs(t *testing.T) { Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) // Split the Episode failure budget across root and two sub-agents. - for i := 0; i < 2; i++ { + for i := range 2 { g.ObserveResult(context.Background(), Observation{ TaskID: "root", Tool: "bash", Subject: fmt.Sprintf("root-%d", i), Verification: true, Args: json.RawMessage(fmt.Sprintf(`{"command":"root %d"}`, i)), ErrSummary: "fail", }) } - for i := 0; i < 2; i++ { + for i := range 2 { g.ObserveResult(context.Background(), Observation{ TaskID: "subagent:a", Tool: "bash", Subject: fmt.Sprintf("a-%d", i), Verification: true, Args: json.RawMessage(fmt.Sprintf(`{"command":"a %d"}`, i)), ErrSummary: "fail", }) } - for i := 0; i < 2; i++ { + for i := range 2 { g.ObserveResult(context.Background(), Observation{ TaskID: "subagent:b", Tool: "bash", Subject: fmt.Sprintf("b-%d", i), Verification: true, Args: json.RawMessage(fmt.Sprintf(`{"command":"b %d"}`, i)), ErrSummary: "fail", @@ -1300,7 +1300,7 @@ func TestStoppedOperationRetriesEscalateToEpisodeStop(t *testing.T) { Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) args := json.RawMessage(`{"command":"mvn test"}`) - for i := 0; i < MaxOperationFailures; i++ { + for range MaxOperationFailures { g.ObserveResult(context.Background(), Observation{ Tool: "bash", Subject: "mvn test", Verification: true, Args: args, ErrSummary: "fail", }) @@ -1323,7 +1323,7 @@ func TestSuccessfulMutationResetsEpisodeBudgets(t *testing.T) { g := NewGate(Options{Reviewer: staticReviewer{ReviewVerdict{ Outcome: ReviewContinue, ChangeKind: ChangeSameStrategy, }}}) - for i := 0; i < 4; i++ { + for range 4 { g.ObserveResult(context.Background(), Observation{ Tool: "bash", Subject: "go test", Verification: true, Args: json.RawMessage(`{"command":"go test"}`), ErrSummary: "fail", diff --git a/internal/recovery/persist_test.go b/internal/recovery/persist_test.go index b269b3b0fb..a27abe59c2 100644 --- a/internal/recovery/persist_test.go +++ b/internal/recovery/persist_test.go @@ -89,7 +89,7 @@ func TestSaveSnapshotIsAtomicAndOwnerOnly(t *testing.T) { sessionPath := filepath.Join(t.TempDir(), "session.jsonl") const writers = 24 var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/recovery/reviewer.go b/internal/recovery/reviewer.go index f339552a7b..6cd00dd1c3 100644 --- a/internal/recovery/reviewer.go +++ b/internal/recovery/reviewer.go @@ -237,7 +237,7 @@ func buildReviewEvidence(failure *FailureEvent, diagnosis []string, proposal Pro // Drop order prefers keeping failure identity and proposal identity over large // excerpts (task summary → diagnosis notes → output → preview → args). func marshalEvidenceWithinBudget(ev reviewEvidence) ([]byte, error) { - for attempt := 0; attempt < 12; attempt++ { + for range 12 { raw, err := json.Marshal(ev) if err != nil { return nil, fmt.Errorf("marshal recovery evidence: %w", err) diff --git a/internal/recovery/reviewer_test.go b/internal/recovery/reviewer_test.go index d07958ae77..b14fffd5cd 100644 --- a/internal/recovery/reviewer_test.go +++ b/internal/recovery/reviewer_test.go @@ -228,7 +228,7 @@ func TestReviewerOutputBudgetAborts(t *testing.T) { go func() { defer close(ch) // Stream more than 4 KiB of text. - for i := 0; i < 20; i++ { + for range 20 { select { case <-ctx.Done(): return diff --git a/internal/recovery/state.go b/internal/recovery/state.go index c7c3adabc3..2ab291bb2c 100644 --- a/internal/recovery/state.go +++ b/internal/recovery/state.go @@ -2,6 +2,7 @@ package recovery import ( "encoding/json" + "slices" "strings" "unicode/utf8" ) @@ -309,8 +310,8 @@ func trimDiagnosis(af *activeFailure) { total := 0 kept := make([]string, 0, len(notes)) // Keep the newest notes within the total budget. - for i := len(notes) - 1; i >= 0; i-- { - n := clipDiagnosisNote(notes[i]) + for _, v := range slices.Backward(notes) { + n := clipDiagnosisNote(v) if n == "" { continue } @@ -341,10 +342,8 @@ func appendDiagnosisNote(af *activeFailure, note string) bool { if note == "" { return false } - for _, existing := range af.diagnosis { - if existing == note { - return false - } + if slices.Contains(af.diagnosis, note) { + return false } af.diagnosis = append(af.diagnosis, note) trimDiagnosis(af) diff --git a/internal/releaseasset/cli.go b/internal/releaseasset/cli.go index e9993a3bd5..dca96109aa 100644 --- a/internal/releaseasset/cli.go +++ b/internal/releaseasset/cli.go @@ -108,7 +108,7 @@ func fetchBounded(ctx context.Context, client *http.Client, rawURL string, limit func verifyChecksum(data []byte, assetName string, checksums []byte) error { want := "" - for _, line := range strings.Split(string(checksums), "\n") { + for line := range strings.SplitSeq(string(checksums), "\n") { fields := strings.Fields(line) if len(fields) != 2 || strings.TrimPrefix(fields[1], "*") != assetName { continue diff --git a/internal/remote/auth.go b/internal/remote/auth.go index 239d39a988..8b6ebd0d80 100644 --- a/internal/remote/auth.go +++ b/internal/remote/auth.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "sync" @@ -135,12 +136,7 @@ func publicKeyAuthCallback(methods []ssh.AuthMethod) ssh.ClientAuthCallback { } func containsAuthMethod(methods []string, want string) bool { - for _, method := range methods { - if method == want { - return true - } - } - return false + return slices.Contains(methods, want) } func agentAuth(identityFiles []string, identitiesOnly bool) (ssh.AuthMethod, func()) { diff --git a/internal/remote/bootstrap/bootstrap.go b/internal/remote/bootstrap/bootstrap.go index 1efebb10b7..9f25aebcc9 100644 --- a/internal/remote/bootstrap/bootstrap.go +++ b/internal/remote/bootstrap/bootstrap.go @@ -387,8 +387,8 @@ func resolveWorkspace(ctx context.Context, fs *sftpfs.FS, workspace, home string if workspace == "~" { return home, nil } - if strings.HasPrefix(workspace, "~/") { - return strings.TrimRight(home, "/") + "/" + strings.TrimPrefix(workspace, "~/"), nil + if after, ok0 := strings.CutPrefix(workspace, "~/"); ok0 { + return strings.TrimRight(home, "/") + "/" + after, nil } if strings.HasPrefix(workspace, "/") { return workspace, nil diff --git a/internal/remote/bootstrap/platform.go b/internal/remote/bootstrap/platform.go index 7d858faa03..0aedf66c5e 100644 --- a/internal/remote/bootstrap/platform.go +++ b/internal/remote/bootstrap/platform.go @@ -37,7 +37,7 @@ func ParseUname(out string) (goos, goarch string, err error) { // ParseVersion extracts a semver-ish string from `reasonix --version` output // like "reasonix v1.9.0" or "1.9.0". func ParseVersion(out string) (string, error) { - for _, field := range strings.Fields(strings.TrimSpace(out)) { + for field := range strings.FieldsSeq(strings.TrimSpace(out)) { v := strings.TrimPrefix(field, "v") if looksLikeSemver(v) { return v, nil diff --git a/internal/remote/dial.go b/internal/remote/dial.go index d60daf2c75..da34b73093 100644 --- a/internal/remote/dial.go +++ b/internal/remote/dial.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "slices" "time" "golang.org/x/crypto/ssh" @@ -175,7 +176,7 @@ func handshakeTimeout(dialTimeout time.Duration) time.Duration { } func closeAll(clients []*ssh.Client) { - for i := len(clients) - 1; i >= 0; i-- { - _ = clients[i].Close() + for _, v := range slices.Backward(clients) { + _ = v.Close() } } diff --git a/internal/remote/reconnect_test.go b/internal/remote/reconnect_test.go index f98b9ae5d4..ec1108371e 100644 --- a/internal/remote/reconnect_test.go +++ b/internal/remote/reconnect_test.go @@ -3,6 +3,7 @@ package remote import ( "context" "math/rand" + "slices" "sync" "testing" "time" @@ -190,17 +191,12 @@ func snapshot(mu *sync.Mutex, s *[]Status) []Status { } func containsStatus(states []Status, want Status) bool { - for _, s := range states { - if s == want { - return true - } - } - return false + return slices.Contains(states, want) } func waitForWaiters(t *testing.T, c *fakeClock, n int) { t.Helper() - for i := 0; i < 200; i++ { + for range 200 { if c.pendingWaiters() >= n { return } diff --git a/internal/remote/remote.go b/internal/remote/remote.go index 02b755034c..bd1c779911 100644 --- a/internal/remote/remote.go +++ b/internal/remote/remote.go @@ -188,7 +188,7 @@ func (p BackoffPolicy) max() time.Duration { func (p BackoffPolicy) delay(attempt int) time.Duration { d := float64(p.initial()) f := p.factor() - for i := 0; i < attempt; i++ { + for range attempt { d *= f if d >= float64(p.max()) { return p.max() diff --git a/internal/remote/sftpfs/detect.go b/internal/remote/sftpfs/detect.go index d63d984189..a1bae9828a 100644 --- a/internal/remote/sftpfs/detect.go +++ b/internal/remote/sftpfs/detect.go @@ -1,6 +1,9 @@ package sftpfs -import "unicode/utf8" +import ( + "slices" + "unicode/utf8" +) // Kind classifies file content for preview purposes. type Kind int @@ -26,10 +29,8 @@ func DetectKind(sample []byte) Kind { if len(sample) > sniffLen { sample = sample[:sniffLen] } - for _, b := range sample { - if b == 0 { - return KindBinary - } + if slices.Contains(sample, 0) { + return KindBinary } if utf8.Valid(sample) { return KindText diff --git a/internal/remote/sshtest/sshtest.go b/internal/remote/sshtest/sshtest.go index 71912b439f..2bf3d04bc1 100644 --- a/internal/remote/sshtest/sshtest.go +++ b/internal/remote/sshtest/sshtest.go @@ -146,11 +146,9 @@ func (s *Server) serve() { s.mu.Lock() s.conns = append(s.conns, nConn) s.mu.Unlock() - s.wg.Add(1) - go func() { - defer s.wg.Done() + s.wg.Go(func() { s.handleConn(nConn) - }() + }) } } diff --git a/internal/repair/config_test.go b/internal/repair/config_test.go index f7ce5bc8de..7423fd3e66 100644 --- a/internal/repair/config_test.go +++ b/internal/repair/config_test.go @@ -270,7 +270,7 @@ func TestConfigSnapshotsRotateAndVerifyHash(t *testing.T) { home := t.TempDir() t.Setenv("REASONIX_HOME", home) path := filepath.Join(home, "config.toml") - for i := 0; i < configSnapshotRetention+2; i++ { + for i := range configSnapshotRetention + 2 { content := []byte("default_model = \"model-" + string(rune('a'+i)) + "\"\n") if err := os.WriteFile(path, content, 0o600); err != nil { t.Fatal(err) @@ -468,7 +468,7 @@ func TestPruneConfigSnapshotPreservesRecreatedMetadata(t *testing.T) { func recordConfigSnapshotSeries(t *testing.T, count int) []ConfigSnapshot { t.Helper() - for i := 0; i < count; i++ { + for i := range count { content := []byte("default_model = \"model-" + string(rune('a'+i)) + "\"\n") now := time.Date(2026, time.July, 29, 0, 0, i, 0, time.UTC) if err := recordConfigSnapshot(config.UserConfigPath(), content, "v1", now); err != nil { diff --git a/internal/repair/mutation_lock.go b/internal/repair/mutation_lock.go index dcfc8125a6..e3ca660957 100644 --- a/internal/repair/mutation_lock.go +++ b/internal/repair/mutation_lock.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" "sync" @@ -103,7 +104,7 @@ func removeRepairNodeIfMatching(path, identityPath, expectedStateID string) erro } func moveRepairNodeToUniqueCleanup(path string) (string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { cleanup := fmt.Sprintf("%s.reasonix-cleanup-%d-%d", path, time.Now().UTC().UnixNano(), attempt) err := renameRepairNodeNoReplace(path, cleanup) if err == nil { @@ -171,8 +172,8 @@ func resolveParentSymlinkPath(path string) string { if resolved, err := filepath.EvalSymlinks(dir); err == nil { parts := make([]string, 0, 1+len(missing)) parts = append(parts, resolved) - for i := len(missing) - 1; i >= 0; i-- { - parts = append(parts, missing[i]) + for _, v := range slices.Backward(missing) { + parts = append(parts, v) } return filepath.Join(parts...) } @@ -263,8 +264,8 @@ func lockRepairMutationsTimeout(timeout time.Duration, paths ...string) (func(), lockPath := filepath.Join(lockDir, fmt.Sprintf("%x.lock", digest)) release, err := filelock.Acquire(ctx, lockPath) if err != nil { - for i := len(releases) - 1; i >= 0; i-- { - releases[i]() + for _, v := range slices.Backward(releases) { + v() } return nil, fmt.Errorf("lock repair mutations: %w", err) } @@ -274,8 +275,8 @@ func lockRepairMutationsTimeout(timeout time.Duration, paths ...string) (func(), var once sync.Once return func() { once.Do(func() { - for i := len(releases) - 1; i >= 0; i-- { - releases[i]() + for _, v := range slices.Backward(releases) { + v() } }) }, nil diff --git a/internal/repair/transaction.go b/internal/repair/transaction.go index f109654cf5..c0a2feb7b3 100644 --- a/internal/repair/transaction.go +++ b/internal/repair/transaction.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "time" @@ -548,8 +549,8 @@ func UndoLastRepair() (*RepairTransaction, error) { tx.Changes[i].Undone = true return persistRepairTransaction(tx) } - for i := len(tx.Changes) - 1; i >= 0; i-- { - change := tx.Changes[i] + for i, v := range slices.Backward(tx.Changes) { + change := v if change.Undone { // Progress was persisted but the backup removal may have been cut // short by a crash; finish the cleanup the completed step owed. diff --git a/internal/repair/update.go b/internal/repair/update.go index d92256b386..b6d8020aa1 100644 --- a/internal/repair/update.go +++ b/internal/repair/update.go @@ -1457,7 +1457,7 @@ func quarantineExistingAppBundleUpdateBackup(tx *UpdateTransaction) (string, str if err != nil { return "", "", fmt.Errorf("read existing handoff backup digest: %w", err) } - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { quarantine := fmt.Sprintf( "%s.reasonix-orphaned-%d-%d", tx.BackupPath, @@ -2807,7 +2807,7 @@ func rollbackPendingUpdateMatchingLocked( } func retainUpdateRollbackNode(path, suffix string) (string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { retained := fmt.Sprintf( "%s.%s-%d-%d", path, @@ -3161,7 +3161,7 @@ func stageUpdateRollbackBackup( file UpdateTransactionFile, mode os.FileMode, ) (string, string, error) { - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { stage := fmt.Sprintf( "%s.reasonix-rollback-stage-%d-%d", file.TargetPath, diff --git a/internal/repair/update_legacy.go b/internal/repair/update_legacy.go index c900109b4a..4141365501 100644 --- a/internal/repair/update_legacy.go +++ b/internal/repair/update_legacy.go @@ -188,7 +188,7 @@ func archiveSupersededAppBundleBackup(tx *UpdateTransaction, transactionID strin shortID = shortID[:16] } base := fmt.Sprintf("%s.reasonix-retired-%s-%s", tx.BackupPath, shortID, time.Now().UTC().Format("20060102T150405.000000000Z")) - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { archive := fmt.Sprintf("%s-%d", base, attempt) if err := renameRepairNodeNoReplace(tx.BackupPath, archive); err != nil { if os.IsExist(err) { @@ -223,7 +223,7 @@ func archiveSupersededPendingMarker(tx *UpdateTransaction, transactionID, kind s shortID = shortID[:16] } base := filepath.Join(archiveDir, fmt.Sprintf("%s-%s-%s", time.Now().UTC().Format("20060102T150405.000000000Z"), shortID, kind)) - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { archivePath := fmt.Sprintf("%s-%d.json", base, attempt) if err := renameRepairNodeNoReplace(pendingPath, archivePath); err != nil { if os.IsExist(err) { @@ -306,7 +306,7 @@ func ArchiveSupersededPendingFileUpdate(runningVersion, installRoot string) (boo } archiveBase := filepath.Join(archiveDir, fmt.Sprintf("%s-%s", time.Now().UTC().Format("20060102T150405.000000000Z"), shortID)) supersededUpdateBeforeArchive(pendingPath) - for attempt := 0; attempt < 16; attempt++ { + for attempt := range 16 { archivePath := fmt.Sprintf("%s-%d.json", archiveBase, attempt) if err := renameRepairNodeNoReplace(pendingPath, archivePath); err != nil { if os.IsExist(err) { diff --git a/internal/retrieval/bm25.go b/internal/retrieval/bm25.go index 0074687019..0d70cbf35d 100644 --- a/internal/retrieval/bm25.go +++ b/internal/retrieval/bm25.go @@ -178,17 +178,11 @@ func snippetAround(text string, byteIdx, maxRunes int) string { } runes := []rune(text) pos := utf8.RuneCountInString(text[:byteIdx]) - start := pos - maxRunes/2 - if start < 0 { - start = 0 - } + start := max(pos-maxRunes/2, 0) end := start + maxRunes if end > len(runes) { end = len(runes) - start = end - maxRunes - if start < 0 { - start = 0 - } + start = max(end-maxRunes, 0) } prefix := "" suffix := "" diff --git a/internal/sandbox/seatbelt_darwin_test.go b/internal/sandbox/seatbelt_darwin_test.go index 61d97f65b3..44cf2fca12 100644 --- a/internal/sandbox/seatbelt_darwin_test.go +++ b/internal/sandbox/seatbelt_darwin_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" ) @@ -62,13 +63,7 @@ func TestWriteAllowDirsIncludesTemp(t *testing.T) { dirs := writeAllowDirs(nil) tmpDir := os.TempDir() realTmp, _ := filepath.EvalSymlinks(tmpDir) - found := false - for _, d := range dirs { - if d == realTmp { - found = true - break - } - } + found := slices.Contains(dirs, realTmp) if !found { t.Errorf("writeAllowDirs should include temp dir %s, got %v", tmpDir, dirs) } @@ -78,13 +73,7 @@ func TestWriteAllowDirsIncludesSessionTemp(t *testing.T) { private := t.TempDir() dirs := writeAllowDirsForSpec(Spec{SessionTemp: private, MinimalWrites: true}) real, _ := filepath.EvalSymlinks(private) - found := false - for _, d := range dirs { - if d == real { - found = true - break - } - } + found := slices.Contains(dirs, real) if !found { t.Fatalf("SessionTemp must be allowed under Seatbelt even with MinimalWrites: %v", dirs) } @@ -176,12 +165,7 @@ func containsDarwinPath(paths []string, want string) bool { if real, err := filepath.EvalSymlinks(abs); err == nil { abs = real } - for _, path := range paths { - if path == abs { - return true - } - } - return false + return slices.Contains(paths, abs) } func TestCommandUnwrappedWhenOff(t *testing.T) { diff --git a/internal/sandbox/seatbelt_other.go b/internal/sandbox/seatbelt_other.go index d6e7bc4ea6..e005f75684 100644 --- a/internal/sandbox/seatbelt_other.go +++ b/internal/sandbox/seatbelt_other.go @@ -207,7 +207,7 @@ func bwrapExecutableMountArgs(args []string) []string { } out := make([]string, 0, 2*strings.Count(rel, string(filepath.Separator))+4) current := "/tmp" - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "" || part == "." { continue } diff --git a/internal/sandbox/seatbelt_other_test.go b/internal/sandbox/seatbelt_other_test.go index c24a68bb68..7606541853 100644 --- a/internal/sandbox/seatbelt_other_test.go +++ b/internal/sandbox/seatbelt_other_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" ) @@ -122,10 +123,5 @@ func containsPath(paths []string, want string) bool { if err != nil { return false } - for _, p := range paths { - if p == absWant { - return true - } - } - return false + return slices.Contains(paths, absWant) } diff --git a/internal/sandbox/session_temp_env.go b/internal/sandbox/session_temp_env.go index 383ecf52d7..a36e277e7e 100644 --- a/internal/sandbox/session_temp_env.go +++ b/internal/sandbox/session_temp_env.go @@ -35,7 +35,7 @@ func SessionTempEnvMap(sessionTemp string, linuxSandboxed bool) map[string]strin } out := make(map[string]string, len(pairs)) for _, kv := range pairs { - for i := 0; i < len(kv); i++ { + for i := range len(kv) { if kv[i] == '=' { out[kv[:i]] = kv[i+1:] break diff --git a/internal/secrets/redact_test.go b/internal/secrets/redact_test.go index 7409beaf13..70eaf38924 100644 --- a/internal/secrets/redact_test.go +++ b/internal/secrets/redact_test.go @@ -39,7 +39,7 @@ func TestRedactMasksCommonSecretShapes(t *testing.T) { func TestRedactLongConcurrentTranscriptAvoidsRegexpBacktracking(t *testing.T) { const secret = "sk-real-secret-value-1234567890" var transcript strings.Builder - for i := 0; i < 2_000; i++ { + for i := range 2_000 { fmt.Fprintf(&transcript, "message %d payload=%s DEEPSEEK_API_KEY=%s Authorization: Bearer %s\n", i, strings.Repeat("x", i%31), secret, secret) } input := transcript.String() @@ -48,11 +48,9 @@ func TestRedactLongConcurrentTranscriptAvoidsRegexpBacktracking(t *testing.T) { const iterations = 20 var wg sync.WaitGroup errs := make(chan string, workers) - for worker := 0; worker < workers; worker++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < iterations; i++ { + for range workers { + wg.Go(func() { + for range iterations { got := Redact(input) if strings.Contains(got, secret) { errs <- "long concurrent redaction leaked the test secret" @@ -63,7 +61,7 @@ func TestRedactLongConcurrentTranscriptAvoidsRegexpBacktracking(t *testing.T) { return } } - }() + }) } wg.Wait() close(errs) diff --git a/internal/serve/auth.go b/internal/serve/auth.go index fbb5597ef5..1549f80745 100644 --- a/internal/serve/auth.go +++ b/internal/serve/auth.go @@ -489,11 +489,11 @@ func (ag *authGate) verifySession(token string) bool { } // Check expiry (format: "unix_timestamp|base64nonce"). - pipe := strings.IndexByte(payload, '|') - if pipe < 0 { + before, _, ok := strings.Cut(payload, "|") + if !ok { return false } - expiry, err := strconv.ParseInt(payload[:pipe], 10, 64) + expiry, err := strconv.ParseInt(before, 10, 64) if err != nil { return false } @@ -531,7 +531,7 @@ func generateToken() string { // acceptsHTML reports whether the request's Accept header prefers text/html. func acceptsHTML(r *http.Request) bool { - for _, h := range strings.Fields(r.Header.Get("Accept")) { + for h := range strings.FieldsSeq(r.Header.Get("Accept")) { if strings.HasPrefix(h, "text/html") { return true } @@ -546,8 +546,8 @@ func acceptsHTML(r *http.Request) bool { func (ag *authGate) clientIP(r *http.Request) string { if ag.behindProxy { if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { - if i := strings.IndexByte(fwd, ','); i >= 0 { - return strings.TrimSpace(fwd[:i]) + if before, _, ok := strings.Cut(fwd, ","); ok { + return strings.TrimSpace(before) } return strings.TrimSpace(fwd) } diff --git a/internal/serve/auth_test.go b/internal/serve/auth_test.go index e38143eb95..9d9d0d62f8 100644 --- a/internal/serve/auth_test.go +++ b/internal/serve/auth_test.go @@ -118,7 +118,7 @@ func TestTokenModeValidCookie(t *testing.T) { }))) defer ts.Close() - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.AddCookie(&http.Cookie{Name: cookieToken, Value: "secret"}) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -137,7 +137,7 @@ func TestTokenModeInvalidCookie(t *testing.T) { }))) defer ts.Close() - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.AddCookie(&http.Cookie{Name: cookieToken, Value: "wrong"}) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -227,7 +227,7 @@ func TestTokenModeNonLoopbackCookieAllowsPlainHTTP(t *testing.T) { return http.ErrUseLastResponse }, } - req, _ := http.NewRequest("GET", ts.URL+"/?token=secret", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/?token=secret", nil) req.Host = "192.0.2.10:8787" resp, err := client.Do(req) if err != nil { @@ -305,7 +305,7 @@ func TestPasswordModeNoSessionRedirects(t *testing.T) { return http.ErrUseLastResponse }, } - req, _ := http.NewRequest("GET", ts.URL+"/", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/", nil) req.Header.Set("Accept", "text/html,application/xhtml+xml") resp, err := client.Do(req) if err != nil { @@ -329,7 +329,7 @@ func TestPasswordModeAPIWithoutSessionReturns401(t *testing.T) { defer ts.Close() // Simulate a non-browser (fetch) request by not setting Accept: text/html. - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) @@ -376,7 +376,7 @@ func TestPasswordModeValidLogin(t *testing.T) { } // Use the session cookie to access a protected page. - req, _ := http.NewRequest("GET", ts.URL+"/status", nil) + req, _ := http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.AddCookie(sessionCookie) resp2, err := http.DefaultClient.Do(req) if err != nil { @@ -401,7 +401,7 @@ func TestPasswordModeNonLoopbackHTTPLoginCookieIsUsable(t *testing.T) { }, } form := url.Values{"password": {"correct"}} - req, _ := http.NewRequest("POST", ts.URL+"/login", strings.NewReader(form.Encode())) + req, _ := http.NewRequest(http.MethodPost, ts.URL+"/login", strings.NewReader(form.Encode())) req.Host = "192.0.2.10:8787" req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) @@ -418,7 +418,7 @@ func TestPasswordModeNonLoopbackHTTPLoginCookieIsUsable(t *testing.T) { t.Fatal("plain HTTP password session cookie should stay usable without Secure") } - req, _ = http.NewRequest("GET", ts.URL+"/status", nil) + req, _ = http.NewRequest(http.MethodGet, ts.URL+"/status", nil) req.Host = "192.0.2.10:8787" req.AddCookie(sessionCookie) resp, err = http.DefaultClient.Do(req) @@ -444,7 +444,7 @@ func TestPasswordModeSanitizesRedirectCookie(t *testing.T) { }, } form := url.Values{"password": {"correct"}} - req, _ := http.NewRequest("POST", ts.URL+"/login", strings.NewReader(form.Encode())) + req, _ := http.NewRequest(http.MethodPost, ts.URL+"/login", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(&http.Cookie{Name: cookieRedirect, Value: "//evil.example/path"}) @@ -601,7 +601,7 @@ func TestRateLimiterAllowsFiveThenBlocks(t *testing.T) { rl := newRateLimit() ip := "192.0.2.1" - for i := 0; i < rateLimitMax; i++ { + for i := range rateLimitMax { if !rl.allow(ip) { t.Fatalf("attempt %d should be allowed", i+1) } @@ -616,7 +616,7 @@ func TestRateLimiterResetsAfterWindow(t *testing.T) { ip := "192.0.2.2" // Exhaust the limit. - for i := 0; i < rateLimitMax; i++ { + for range rateLimitMax { rl.allow(ip) } if rl.allow(ip) { @@ -640,7 +640,7 @@ func TestRateLimiterResetsAfterWindow(t *testing.T) { func TestRateLimiterDifferentIPs(t *testing.T) { rl := newRateLimit() // Exhaust one IP. - for i := 0; i < rateLimitMax; i++ { + for range rateLimitMax { rl.allow("192.0.2.1") } // Another IP should still be allowed. @@ -653,7 +653,7 @@ func TestRateLimiterDifferentIPs(t *testing.T) { func TestClientIPRemoteAddr(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "192.0.2.42:12345" if got := ag.clientIP(req); got != "192.0.2.42" { t.Errorf("clientIP = %q, want 192.0.2.42", got) @@ -662,7 +662,7 @@ func TestClientIPRemoteAddr(t *testing.T) { func TestClientIPIgnoresXForwardedForWithoutProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "192.0.2.1:12345" req.Header.Set("X-Forwarded-For", "10.0.0.1, 10.0.0.2") if got := ag.clientIP(req); got != "192.0.2.1" { @@ -672,7 +672,7 @@ func TestClientIPIgnoresXForwardedForWithoutProxy(t *testing.T) { func TestClientIPTrustsXForwardedForWithProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x", BehindProxy: true}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.99:12345" req.Header.Set("X-Forwarded-For", "192.0.2.42, 10.0.0.1") if got := ag.clientIP(req); got != "192.0.2.42" { @@ -684,7 +684,7 @@ func TestClientIPTrustsXForwardedForWithProxy(t *testing.T) { func TestIsTLS(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) if ag.isTLS(req) { t.Error("plain request should not be TLS") } @@ -692,7 +692,7 @@ func TestIsTLS(t *testing.T) { func TestIsTLSIgnoresForwardedProtoWithoutProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-Forwarded-Proto", "https") if ag.isTLS(req) { t.Error("should ignore X-Forwarded-Proto without behind_proxy") @@ -701,7 +701,7 @@ func TestIsTLSIgnoresForwardedProtoWithoutProxy(t *testing.T) { func TestIsTLSTrustsForwardedProtoWithProxy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x", BehindProxy: true}) - req, _ := http.NewRequest("GET", "/", nil) + req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-Forwarded-Proto", "https") if !ag.isTLS(req) { t.Error("should trust X-Forwarded-Proto with behind_proxy") @@ -711,19 +711,19 @@ func TestIsTLSTrustsForwardedProtoWithProxy(t *testing.T) { func TestAuthCookieSecurePolicy(t *testing.T) { ag := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x"}) for _, host := range []string{"localhost:8787", "127.0.0.1:8787", "[::1]:8787"} { - req, _ := http.NewRequest("GET", "http://"+host+"/", nil) + req, _ := http.NewRequest(http.MethodGet, "http://"+host+"/", nil) if ag.authCookieSecure(req) { t.Errorf("loopback host %s should allow local HTTP cookies", host) } } - req, _ := http.NewRequest("GET", "http://192.0.2.10:8787/", nil) + req, _ := http.NewRequest(http.MethodGet, "http://192.0.2.10:8787/", nil) if ag.authCookieSecure(req) { t.Fatal("plain HTTP cookies should stay usable without Secure") } proxy := newAuthGate(config.ServeConfig{AuthMode: "token", Token: "x", BehindProxy: true}) - req, _ = http.NewRequest("GET", "http://example.test/", nil) + req, _ = http.NewRequest(http.MethodGet, "http://example.test/", nil) req.Header.Set("X-Forwarded-Proto", "https") if !proxy.authCookieSecure(req) { t.Fatal("trusted forwarded HTTPS should mark cookies Secure") diff --git a/internal/serve/broadcaster_test.go b/internal/serve/broadcaster_test.go index 58804f18d7..76c25ea3f0 100644 --- a/internal/serve/broadcaster_test.go +++ b/internal/serve/broadcaster_test.go @@ -67,7 +67,7 @@ func TestBroadcasterDropsSlowSubscriber(t *testing.T) { ch, cancel := b.Subscribe() defer cancel() // Overfill far past the 64-slot buffer without reading; Emit must not block. - for i := 0; i < 1000; i++ { + for range 1000 { b.Emit(event.Event{Kind: event.Text, Text: "x"}) } if len(ch) == 0 { diff --git a/internal/serve/modelswitch_test.go b/internal/serve/modelswitch_test.go index c0ed347249..78a85c7c60 100644 --- a/internal/serve/modelswitch_test.go +++ b/internal/serve/modelswitch_test.go @@ -15,19 +15,15 @@ func TestControllerAccessorIsRaceSafe(t *testing.T) { s := &Server{ctrl: a} var wg sync.WaitGroup - for i := 0; i < 64; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 64 { + wg.Go(func() { if got := s.ctl(); got != a && got != b { t.Errorf("ctl() returned a pointer that was never set") } - }() + }) } - for i := 0; i < 16; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 16 { + wg.Go(func() { s.mu.Lock() if s.ctrl == a { s.ctrl = b @@ -35,7 +31,7 @@ func TestControllerAccessorIsRaceSafe(t *testing.T) { s.ctrl = a } s.mu.Unlock() - }() + }) } wg.Wait() } diff --git a/internal/serve/serve_lock_test.go b/internal/serve/serve_lock_test.go index 378da25c3c..1a11129a06 100644 --- a/internal/serve/serve_lock_test.go +++ b/internal/serve/serve_lock_test.go @@ -3,6 +3,7 @@ package serve import ( "context" "io" + "net/http" "net/http/httptest" "strings" "sync/atomic" @@ -266,7 +267,7 @@ func TestSubmitWaitsForExtensionReloadAndTargetsReplacement(t *testing.T) { submitDone := make(chan int, 1) go func() { - req := httptest.NewRequest("POST", "/submit", strings.NewReader(`{"input":"hello"}`)) + req := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(`{"input":"hello"}`)) rec := httptest.NewRecorder() s.submit(rec, req) submitDone <- rec.Code diff --git a/internal/serve/serve_portfile_test.go b/internal/serve/serve_portfile_test.go index 4547f09cf2..34b1938dea 100644 --- a/internal/serve/serve_portfile_test.go +++ b/internal/serve/serve_portfile_test.go @@ -28,7 +28,7 @@ func waitForHTTP(t *testing.T, addr string) { t.Helper() client := &http.Client{Timeout: 2 * time.Second} var lastErr error - for i := 0; i < 100; i++ { + for range 100 { resp, err := client.Get("http://" + addr + "/assets/logo-wordmark.svg") if err == nil { resp.Body.Close() diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go index e9a245afcc..7cd6ef2e70 100644 --- a/internal/serve/serve_test.go +++ b/internal/serve/serve_test.go @@ -154,11 +154,11 @@ func TestServeEndpoints(t *testing.T) { srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{}).Handler()) defer srv.Close() - if resp, err := http.Get(srv.URL + "/history"); err != nil || resp.StatusCode != 200 { + if resp, err := http.Get(srv.URL + "/history"); err != nil || resp.StatusCode != http.StatusOK { t.Fatalf("history = %v / %v", resp, err) } - if resp, _ := http.Get(srv.URL + "/context"); resp.StatusCode != 200 { + if resp, _ := http.Get(srv.URL + "/context"); resp.StatusCode != http.StatusOK { t.Errorf("context status = %d", resp.StatusCode) } @@ -405,7 +405,7 @@ func TestServeIndexPage(t *testing.T) { t.Fatal(err) } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("index status = %d", resp.StatusCode) } ct := resp.Header.Get("Content-Type") @@ -985,7 +985,7 @@ func TestServeContextEndpoint(t *testing.T) { t.Fatal(err) } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("context status = %d", resp.StatusCode) } var body map[string]int diff --git a/internal/sessiontemp/manager_test.go b/internal/sessiontemp/manager_test.go index f1b4527102..c3581151e3 100644 --- a/internal/sessiontemp/manager_test.go +++ b/internal/sessiontemp/manager_test.go @@ -226,11 +226,9 @@ func TestConcurrentAcquireRotateReleaseRace(t *testing.T) { defer m.Release() var wg sync.WaitGroup - for i := 0; i < 32; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 50; j++ { + for range 32 { + wg.Go(func() { + for j := range 50 { lease, err := m.Acquire() if err != nil { t.Errorf("acquire: %v", err) @@ -242,7 +240,7 @@ func TestConcurrentAcquireRotateReleaseRace(t *testing.T) { } lease.Release() } - }() + }) } wg.Wait() } diff --git a/internal/shellparse/bash.go b/internal/shellparse/bash.go index 950519aa4c..b6893384ac 100644 --- a/internal/shellparse/bash.go +++ b/internal/shellparse/bash.go @@ -297,7 +297,7 @@ func wordHasUnescapedBrace(word *syntax.Word) bool { func hasUnescapedMeta(value, meta string) bool { escaped := false - for i := 0; i < len(value); i++ { + for i := range len(value) { if escaped { escaped = false continue @@ -547,7 +547,7 @@ func IsAssignment(word string) bool { if !ok || name == "" { return false } - for i := 0; i < len(name); i++ { + for i := range len(name) { c := name[i] if i == 0 { if c != '_' && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') { diff --git a/internal/shellrun/runner_test.go b/internal/shellrun/runner_test.go index 52ce847f56..36febe8745 100644 --- a/internal/shellrun/runner_test.go +++ b/internal/shellrun/runner_test.go @@ -224,7 +224,7 @@ func TestRunForegroundPreservesInterleaving(t *testing.T) { } argv := shellArgvWith(sh, "for i in 1 2 3 4 5 6 7 8; do echo out$i; echo err$i 1>&2; done") // Repeat: two pipes reorder probabilistically, so one run can pass by luck. - for run := 0; run < 10; run++ { + for run := range 10 { res := RunForeground(context.Background(), Request{Argv: argv, Timeout: 30 * time.Second}) if res.Combined != want.String() { t.Fatalf("run %d lost child write order:\ngot %q\nwant %q", run, res.Combined, want.String()) diff --git a/internal/shellsafe/bash_redirect.go b/internal/shellsafe/bash_redirect.go index 167cafc500..701b2478bb 100644 --- a/internal/shellsafe/bash_redirect.go +++ b/internal/shellsafe/bash_redirect.go @@ -114,7 +114,7 @@ func isSafeFDDupWord(source string, word *syntax.Word) bool { if value == "" { return false } - for i := 0; i < len(value); i++ { + for i := range len(value) { if value[i] < '0' || value[i] > '9' { return false } diff --git a/internal/skill/builtins_test.go b/internal/skill/builtins_test.go index 700697a441..071fb9bf51 100644 --- a/internal/skill/builtins_test.go +++ b/internal/skill/builtins_test.go @@ -3,6 +3,7 @@ package skill import ( "context" "encoding/json" + "slices" "strings" "testing" @@ -100,13 +101,7 @@ func TestBuiltinSkillsIncludeCodeGraphHintAndToolsWhenDiscovered(t *testing.T) { t.Fatalf("explore body missing priority hint %q:\n%s", want, explore.Body) } } - found := false - for _, name := range explore.AllowedTools { - if name == "mcp__codegraph__symbols" { - found = true - break - } - } + found := slices.Contains(explore.AllowedTools, "mcp__codegraph__symbols") if !found { t.Fatalf("explore allowed tools = %v, want codegraph tool", explore.AllowedTools) } diff --git a/internal/skill/skill.go b/internal/skill/skill.go index 1b1b69fa50..16a6aeace8 100644 --- a/internal/skill/skill.go +++ b/internal/skill/skill.go @@ -19,6 +19,7 @@ import ( "os" "path" "path/filepath" + "slices" "sort" "strings" @@ -514,12 +515,7 @@ func normalizePluginPaths(paths map[string][]string) map[string][]string { } func stringSliceContains(items []string, want string) bool { - for _, item := range items { - if item == want { - return true - } - } - return false + return slices.Contains(items, want) } // Roots exposes the discovery directories with their status for `/skill paths`. @@ -1019,12 +1015,7 @@ func frontmatterHasSkillMarkerKey(content string) bool { } func isSkillMarkerFrontmatterKey(key string) bool { - for _, marker := range skillMarkerFrontmatterKeys { - if key == marker { - return true - } - } - return false + return slices.Contains(skillMarkerFrontmatterKeys, key) } // Create scaffolds a new skill stub at the chosen scope. Refuses to overwrite. @@ -1291,7 +1282,7 @@ func parseCSVFrontmatter(raw string) []string { raw = strings.TrimSpace(raw[1 : len(raw)-1]) } var out []string - for _, p := range strings.Split(raw, ",") { + for p := range strings.SplitSeq(raw, ",") { if t := strings.Trim(strings.TrimSpace(p), `"'`); t != "" { out = append(out, t) } diff --git a/internal/skill/skill_test.go b/internal/skill/skill_test.go index 4777a8a8cf..39b3469b65 100644 --- a/internal/skill/skill_test.go +++ b/internal/skill/skill_test.go @@ -811,12 +811,7 @@ func sameStrings(a, b []string) bool { } func containsString(ss []string, want string) bool { - for _, s := range ss { - if s == want { - return true - } - } - return false + return slices.Contains(ss, want) } func TestInvalidNamesSkipped(t *testing.T) { @@ -1044,7 +1039,7 @@ func TestManualInvocationSkillExcludedFromIndex(t *testing.T) { func TestApplyIndexTruncates(t *testing.T) { var skills []Skill - for i := 0; i < 200; i++ { + for range 200 { skills = append(skills, Skill{Name: "skill" + strings.Repeat("x", 20), Description: strings.Repeat("d", 50)}) } out := ApplyIndex("BASE", skills) diff --git a/internal/skill/tools.go b/internal/skill/tools.go index d7b2e58011..46e65f96aa 100644 --- a/internal/skill/tools.go +++ b/internal/skill/tools.go @@ -659,7 +659,7 @@ func cleanSkillName(raw string) string { return "" } stripped := strings.TrimSpace(bracketTagRe.ReplaceAllString(raw, " ")) - for _, tok := range strings.Fields(stripped) { + for tok := range strings.FieldsSeq(stripped) { if c := tok[0]; (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') { return tok } diff --git a/internal/stats/query.go b/internal/stats/query.go index a82149bbb3..71444bbf82 100644 --- a/internal/stats/query.go +++ b/internal/stats/query.go @@ -1,6 +1,7 @@ package stats import ( + "maps" "sort" "strings" "time" @@ -144,9 +145,7 @@ func (w *Writer) Query(f SourceFilter) (RangeStats, error) { // timeline; inactive days carry zero totals. The frontend trims the // left side of the chart on narrow containers instead of hiding days. byModel := make(map[string]int64, len(dayTotals)) - for k, v := range dayTotals { - byModel[k] = v - } + maps.Copy(byModel, dayTotals) byProvider := map[string]int64{} for m, v := range dayTotals { byProvider[providerOf(m)] += v diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go index 8e158271bd..08ad6cd058 100644 --- a/internal/stats/stats_test.go +++ b/internal/stats/stats_test.go @@ -388,12 +388,12 @@ func TestConcurrentWritersAppendWholeRecords(t *testing.T) { const writers = 8 const perWriter = 40 var wg sync.WaitGroup - for i := 0; i < writers; i++ { + for i := range writers { wg.Add(1) go func(model int) { defer wg.Done() w := NewWriter(dir) - for j := 0; j < perWriter; j++ { + for range perWriter { if err := w.Append(record{Timestamp: now, ModelRef: fmt.Sprintf("provider/model-%d", model), Total: 1}); err != nil { t.Errorf("append: %v", err) return diff --git a/internal/sysproxy/sysproxy.go b/internal/sysproxy/sysproxy.go index 9a75b39286..87659e8a2d 100644 --- a/internal/sysproxy/sysproxy.go +++ b/internal/sysproxy/sysproxy.go @@ -19,9 +19,9 @@ func splitList(s string) []string { func parseProxyList(list, scheme string) *url.URL { var fallback string for _, f := range splitList(list) { - if i := strings.IndexByte(f, '='); i >= 0 { - if strings.EqualFold(f[:i], scheme) { - return hostProxyURL(f[i+1:]) + if before, after, ok := strings.Cut(f, "="); ok { + if strings.EqualFold(before, scheme) { + return hostProxyURL(after) } continue } diff --git a/internal/taskintent/heuristic.go b/internal/taskintent/heuristic.go index c7973779b0..5e33790048 100644 --- a/internal/taskintent/heuristic.go +++ b/internal/taskintent/heuristic.go @@ -1,6 +1,7 @@ package taskintent import ( + "slices" "strings" "unicode/utf8" ) @@ -28,10 +29,8 @@ func heuristicInputIsTask(input string) bool { words := strings.Fields(normalized) if len(words) <= 3 { - for _, greeting := range shortGreetings { - if normalized == greeting { - return false - } + if slices.Contains(shortGreetings, normalized) { + return false } } @@ -43,17 +42,17 @@ func heuristicInputIsTask(input string) bool { "谢谢你", "辛苦了", } for _, phrase := range chatPhrases { - index := strings.Index(normalized, phrase) - if index < 0 { + before, after, ok := strings.Cut(normalized, phrase) + if !ok { continue } // Acknowledgement wording only short-circuits a purely conversational // turn. Preserve a real task before it or after an explicit transition, // e.g. "thanks for fixing that; now update the tests". - if prefix := strings.TrimSpace(normalized[:index]); prefix != "" && heuristicInputHasStrongTaskSignal(prefix) { + if prefix := strings.TrimSpace(before); prefix != "" && heuristicInputHasStrongTaskSignal(prefix) { return true } - if deliveryTaskHasFollowUpAfterChat(normalized[index+len(phrase):]) { + if deliveryTaskHasFollowUpAfterChat(after) { return true } return false @@ -155,14 +154,9 @@ func containsTaskNeedle(input, needle string) bool { if containsNonASCII(needle) || strings.Contains(needle, " ") { return strings.Contains(input, needle) } - for _, word := range strings.FieldsFunc(input, func(r rune) bool { + return slices.Contains(strings.FieldsFunc(input, func(r rune) bool { return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '_' - }) { - if word == needle { - return true - } - } - return false + }), needle) } func containsNonASCII(s string) bool { diff --git a/internal/taskintent/intent.go b/internal/taskintent/intent.go index bddaf2caa8..13ad5ab14d 100644 --- a/internal/taskintent/intent.go +++ b/internal/taskintent/intent.go @@ -406,8 +406,8 @@ func deliveryTaskStartsWithMutation(input string) bool { for { stripped := false for _, prefix := range []string{"please ", "can you ", "could you ", "would you ", "you should ", "帮我", "请你", "直接", "继续", "再"} { - if strings.HasPrefix(input, prefix) { - input = strings.TrimSpace(strings.TrimPrefix(input, prefix)) + if after, ok := strings.CutPrefix(input, prefix); ok { + input = strings.TrimSpace(after) stripped = true break } diff --git a/internal/taskmonitor/control.go b/internal/taskmonitor/control.go index fbdb94ae68..5e68deda36 100644 --- a/internal/taskmonitor/control.go +++ b/internal/taskmonitor/control.go @@ -277,7 +277,7 @@ func (cs *ControlService) controlOp(ctx context.Context, projectDir, taskID stri // heartbeat. Retry those expected version advances. If RecordDone already // persisted the requested terminal state, use that snapshot as the result. const maxControlSaveAttempts = 4 - for attempt := 0; attempt < maxControlSaveAttempts; attempt++ { + for attempt := range maxControlSaveAttempts { next := *snap next.Version++ next.State = targetState diff --git a/internal/taskmonitor/control_test.go b/internal/taskmonitor/control_test.go index 42a4fe7861..146eee4c5a 100644 --- a/internal/taskmonitor/control_test.go +++ b/internal/taskmonitor/control_test.go @@ -527,10 +527,7 @@ func TestControlService_ConcurrentKillersRemainCallScoped(t *testing.T) { {taskID: "task-a", sessionID: "session-a"}, {taskID: "task-b", sessionID: "session-b"}, } { - tc := tc - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-started killer := &mockKiller{fn: func(sessionID, taskID string) bool { killed <- sessionID + "/" + taskID @@ -540,7 +537,7 @@ func TestControlService_ConcurrentKillersRemainCallScoped(t *testing.T) { if err != nil || !res.Accepted { t.Errorf("StopTaskWithKiller(%s): result=%+v err=%v", tc.taskID, res, err) } - }() + }) } close(started) wg.Wait() @@ -572,17 +569,15 @@ func TestControlService_ConcurrentAccess(t *testing.T) { success := 0 var mu sync.Mutex - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 10 { + wg.Go(func() { res, _ := cs.StopTaskWithKiller(context.Background(), "/p", "t1", 1, "", "", killer) if res.Accepted { mu.Lock() success++ mu.Unlock() } - }() + }) } wg.Wait() // Exactly one caller should succeed due to mutex + version CAS diff --git a/internal/taskmonitor/jsonstore.go b/internal/taskmonitor/jsonstore.go index 49e3bbf8fb..6afb91e2a3 100644 --- a/internal/taskmonitor/jsonstore.go +++ b/internal/taskmonitor/jsonstore.go @@ -91,7 +91,7 @@ func rejectSymlinkChain(root, target string) error { if rel == "." { return nil } - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { cur = filepath.Join(cur, part) if err := rejectSymlink(cur); err != nil { return err @@ -106,7 +106,7 @@ func rejectStoreParents(projectDir, root string) error { return err } cur := projectDir - for _, part := range strings.Split(rel, string(filepath.Separator)) { + for part := range strings.SplitSeq(rel, string(filepath.Separator)) { if part == "." || part == "" { continue } @@ -224,7 +224,7 @@ func (s *FileStore) RenewRuntimeLease(ctx context.Context, projectDir, taskID, o return false, nil } const maxAttempts = 4 - for attempt := 0; attempt < maxAttempts; attempt++ { + for range maxAttempts { snap, err := s.getTaskRaw(ctx, projectDir, taskID) if err != nil || snap == nil { return false, err @@ -476,7 +476,7 @@ func (s *FileStore) AppendAuditEvent(ctx context.Context, projectDir string, ev return err } max := 0 - for _, line := range strings.Split(string(raw), "\n") { + for line := range strings.SplitSeq(string(raw), "\n") { line = strings.TrimSpace(line) if line == "" { continue diff --git a/internal/taskmonitor/recorder.go b/internal/taskmonitor/recorder.go index a8573fe6a6..a5914e06bf 100644 --- a/internal/taskmonitor/recorder.go +++ b/internal/taskmonitor/recorder.go @@ -49,7 +49,7 @@ func newRuntimeOwnerID() string { if _, err := crand.Read(nonce[:]); err == nil { return hex.EncodeToString(nonce[:]) } - h := sha256.Sum256([]byte(fmt.Sprintf("%d:%d", timeNow().UnixNano(), runtimeOwnerSequence.Add(1)))) + h := sha256.Sum256(fmt.Appendf(nil, "%d:%d", timeNow().UnixNano(), runtimeOwnerSequence.Add(1))) return hex.EncodeToString(h[:16]) } @@ -74,7 +74,7 @@ func sessionlessMonitorTaskID(jobID string) string { if _, err := crand.Read(nonce[:]); err != nil { // crypto/rand failure is exceptional; retain a bounded, non-path-like // identity rather than falling back to the colliding raw job ID. - h := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", jobID, timeNow().UnixNano()))) + h := sha256.Sum256(fmt.Appendf(nil, "%s:%d", jobID, timeNow().UnixNano())) return hex.EncodeToString(h[:8]) + "--" + jobID } return hex.EncodeToString(nonce[:]) + "--" + jobID @@ -181,7 +181,7 @@ func (r *TaskRecorder) RecordDone(id string, st jobs.Status, jobErr error) { } r.stopHeartbeat(monitorID) const maxSaveAttempts = 4 - for attempt := 0; attempt < maxSaveAttempts; attempt++ { + for range maxSaveAttempts { cur, gerr := r.store.GetTask(ctx, r.projectDir, monitorID) if gerr != nil || cur == nil { return // never recorded (recorder attached after the job started) diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 87f481843b..0ef8cf5535 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -227,7 +227,7 @@ func TestPendingQueueCountsActiveAndRecoversStaleClaims(t *testing.T) { if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } - for i := 0; i < maxPending; i++ { + for i := range maxPending { path := filepath.Join(dir, strings.Repeat("a", 16)+"-"+time.Unix(int64(i), 0).Format("150405")+".json.uploading") if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil { t.Fatal(err) diff --git a/internal/telemetry/sink.go b/internal/telemetry/sink.go index 93316077a5..d24a969605 100644 --- a/internal/telemetry/sink.go +++ b/internal/telemetry/sink.go @@ -6,6 +6,7 @@ import ( "net" "regexp" "runtime" + "slices" "strings" "time" @@ -217,10 +218,8 @@ func safeBucket(value, fallback string) string { func enumBucket(value string, allowed ...string) string { value = strings.ToLower(strings.TrimSpace(value)) - for _, item := range allowed { - if value == item { - return value - } + if slices.Contains(allowed, value) { + return value } return "other" } diff --git a/internal/tool/builtin/bash.go b/internal/tool/builtin/bash.go index 7fb3c23a14..d766be1f66 100644 --- a/internal/tool/builtin/bash.go +++ b/internal/tool/builtin/bash.go @@ -10,6 +10,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "sync" "time" @@ -629,7 +630,7 @@ func reapShellProcess(cmd *exec.Cmd, tracked *proc.TrackedCommand) { // PowerShell chaining guard. func hasUnquotedSeq(s, seq string) bool { var quote byte - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] if quote != 0 { if c == quote { @@ -748,9 +749,9 @@ func runShellPATHCommand(parent context.Context, shell string, args []string) [] func parseShellPATH(out []byte, marker string) string { lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") - for i := len(lines) - 1; i >= 0; i-- { - if strings.HasPrefix(lines[i], marker) { - return strings.TrimSpace(strings.TrimPrefix(lines[i], marker)) + for _, line := range slices.Backward(lines) { + if rest, ok := strings.CutPrefix(line, marker); ok { + return strings.TrimSpace(rest) } } return "" @@ -789,8 +790,8 @@ func setEnvValue(env []string, key, value string) []string { } func envValue(env []string, key string) (string, bool) { - for i := len(env) - 1; i >= 0; i-- { - k, v, ok := strings.Cut(env[i], "=") + for _, entry := range slices.Backward(env) { + k, v, ok := strings.Cut(entry, "=") if ok && envKeyEqual(k, key) { return v, true } diff --git a/internal/tool/builtin/bash_cancel_windows_test.go b/internal/tool/builtin/bash_cancel_windows_test.go index 379c8376c8..1bd0a7f6dc 100644 --- a/internal/tool/builtin/bash_cancel_windows_test.go +++ b/internal/tool/builtin/bash_cancel_windows_test.go @@ -54,7 +54,7 @@ func TestBashCancelKillsWindowsChildProcessTree(t *testing.T) { if err == nil { t.Fatal("expected cancel to return an error") } - for i := 0; i < 50; i++ { + for range 50 { if !windowsProcessAlive(childPID) { return } @@ -87,7 +87,7 @@ func TestBashWindowsReapsChildAfterForegroundShellExit(t *testing.T) { killWindowsPID(childPID) t.Fatalf("foreground command failed: %v (out=%q)", err, out) } - for i := 0; i < 50; i++ { + for range 50 { if !windowsProcessAlive(childPID) { return } @@ -129,7 +129,7 @@ func TestBashCancelKillsGitBashHereDocPython(t *testing.T) { killWindowsPID(childPID) t.Fatal("cancel did not interrupt Git Bash here-doc python within 20s") } - for i := 0; i < 50; i++ { + for range 50 { if !windowsProcessAlive(childPID) { return } diff --git a/internal/tool/builtin/bash_reap_test.go b/internal/tool/builtin/bash_reap_test.go index 25149dddc3..d824673b9e 100644 --- a/internal/tool/builtin/bash_reap_test.go +++ b/internal/tool/builtin/bash_reap_test.go @@ -46,7 +46,7 @@ func TestReapTreeKillsGroupStragglers(t *testing.T) { proc.KillTree(cmd) dead := false - for i := 0; i < 50; i++ { + for range 50 { if syscall.Kill(pid, 0) != nil { dead = true break diff --git a/internal/tool/builtin/bash_sandbox_escape_test.go b/internal/tool/builtin/bash_sandbox_escape_test.go index d66c2aba6c..147459baa6 100644 --- a/internal/tool/builtin/bash_sandbox_escape_test.go +++ b/internal/tool/builtin/bash_sandbox_escape_test.go @@ -213,11 +213,11 @@ func windowsSandboxFailureForShell(sh sandbox.Shell) string { func backgroundJobIDFromStartOutput(t *testing.T, out string) string { t.Helper() const prefix = `Started background job "` - start := strings.Index(out, prefix) - if start < 0 { + _, after, ok := strings.Cut(out, prefix) + if !ok { t.Fatalf("start output = %q, want background job id", out) } - rest := out[start+len(prefix):] + rest := after end := strings.IndexByte(rest, '"') if end < 0 { t.Fatalf("start output = %q, want closing quote for background job id", out) diff --git a/internal/tool/builtin/bgjobs.go b/internal/tool/builtin/bgjobs.go index 742677dde7..226bd75e2c 100644 --- a/internal/tool/builtin/bgjobs.go +++ b/internal/tool/builtin/bgjobs.go @@ -85,7 +85,7 @@ func filterLines(s, re string) (string, error) { return "", fmt.Errorf("invalid filter regexp: %w", err) } var keep []string - for _, line := range strings.Split(s, "\n") { + for line := range strings.SplitSeq(s, "\n") { if rx.MatchString(line) { keep = append(keep, line) } diff --git a/internal/tool/builtin/encoding_helpers.go b/internal/tool/builtin/encoding_helpers.go index b4d26ccbeb..941b5fd917 100644 --- a/internal/tool/builtin/encoding_helpers.go +++ b/internal/tool/builtin/encoding_helpers.go @@ -3,6 +3,7 @@ package builtin import ( "fmt" "os" + "slices" "strings" fileenc "reasonix/internal/fileutil/encoding" @@ -353,8 +354,8 @@ func stripReadFileLinePrefix(line string) (string, bool) { func replaceEditRanges(content string, ranges []editRange, replacement string) string { updated := content - for i := len(ranges) - 1; i >= 0; i-- { - r := ranges[i] + for _, v := range slices.Backward(ranges) { + r := v updated = updated[:r.start] + replacement + updated[r.end:] } return updated @@ -433,11 +434,8 @@ func firstNonEmptyLine(s string) string { } func commonPrefixLen(a, b string) int { - n := len(a) - if len(b) < n { - n = len(b) - } - for i := 0; i < n; i++ { + n := min(len(b), len(a)) + for i := range n { if a[i] != b[i] { return i } diff --git a/internal/tool/builtin/gitignore_e2e_test.go b/internal/tool/builtin/gitignore_e2e_test.go index 60eac3707c..3c1984626f 100644 --- a/internal/tool/builtin/gitignore_e2e_test.go +++ b/internal/tool/builtin/gitignore_e2e_test.go @@ -87,7 +87,7 @@ func TestGrepWorkspaceE2E(t *testing.T) { // files, relative to repo with forward slashes. func matchedRelFiles(repo, out string) map[string]bool { found := map[string]bool{} - for _, line := range strings.Split(out, "\n") { + for line := range strings.SplitSeq(out, "\n") { i := strings.LastIndex(line, ":") if i < 0 { continue diff --git a/internal/tool/builtin/managed_config.go b/internal/tool/builtin/managed_config.go index fe5eb979e8..c1f049d1db 100644 --- a/internal/tool/builtin/managed_config.go +++ b/internal/tool/builtin/managed_config.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "reasonix/internal/tool" @@ -48,12 +49,7 @@ func (m ManagedConfigPaths) Match(target string) bool { if err != nil { return false } - for _, p := range m.paths { - if abs == p { - return true - } - } - return false + return slices.Contains(m.paths, abs) } // approve asks the user whether this managed-config write may proceed, via the diff --git a/internal/tool/builtin/post_write_receipt.go b/internal/tool/builtin/post_write_receipt.go index a652b9b02a..71a78786ce 100644 --- a/internal/tool/builtin/post_write_receipt.go +++ b/internal/tool/builtin/post_write_receipt.go @@ -37,13 +37,7 @@ func renderPostWriteReceipts(receipts []editReplacementReceipt) string { // Share the bounded body between the selected first/last receipts and their // matched/replacement fields. The final clip below remains a defensive cap // for unusually large counts or labels. - fieldBudget := (maxPostWriteReceiptBytes - 256) / (2 * len(indexes)) - if fieldBudget < 128 { - fieldBudget = 128 - } - if fieldBudget > maxCapturedReceiptSpanBytes { - fieldBudget = maxCapturedReceiptSpanBytes - } + fieldBudget := min(max((maxPostWriteReceiptBytes-256)/(2*len(indexes)), 128), maxCapturedReceiptSpanBytes) var b strings.Builder b.Grow(maxPostWriteReceiptBytes) diff --git a/internal/tool/builtin/post_write_receipt_test.go b/internal/tool/builtin/post_write_receipt_test.go index f03ed96d8d..26d5f2c7a0 100644 --- a/internal/tool/builtin/post_write_receipt_test.go +++ b/internal/tool/builtin/post_write_receipt_test.go @@ -52,7 +52,7 @@ func TestPostWriteReceiptLargeSpanAllocationIsBounded(t *testing.T) { receipts := []editReplacementReceipt{{matched: large, replacement: large, occurrences: 1}} var got string result := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { + for range b.N { got = withActualPostWriteReceipts("edited large.txt", receipts) } }) diff --git a/internal/tool/builtin/readfile_stream_test.go b/internal/tool/builtin/readfile_stream_test.go index 296f2b68f5..4fc95e1b13 100644 --- a/internal/tool/builtin/readfile_stream_test.go +++ b/internal/tool/builtin/readfile_stream_test.go @@ -18,7 +18,7 @@ func TestReadFileStreamsLargeGB18030(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "big.gbk") var sb strings.Builder - for i := 0; i < 20000; i++ { + for range 20000 { sb.WriteString("第一行中文 line one 你好世界\n") } sb.WriteString("终点标记 THE-END\n") @@ -45,7 +45,7 @@ func TestReadFileLargeBoundedMemory(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "big.txt") var sb strings.Builder - for i := 0; i < 130000; i++ { // ~8 MB, no NUL + for range 130000 { // ~8 MB, no NUL sb.WriteString("a line of perfectly ordinary text in a large utf-8 file\n") } if err := os.WriteFile(path, []byte(sb.String()), 0o644); err != nil { diff --git a/internal/tool/builtin/tool_extra_test.go b/internal/tool/builtin/tool_extra_test.go index 13a5e7e38d..4d22f670f9 100644 --- a/internal/tool/builtin/tool_extra_test.go +++ b/internal/tool/builtin/tool_extra_test.go @@ -172,7 +172,7 @@ func TestGrepTruncation(t *testing.T) { dir := t.TempDir() // Create a file with many matching lines. var b strings.Builder - for i := 0; i < 300; i++ { + for i := range 300 { fmt.Fprintf(&b, "match %d\n", i) } os.WriteFile(filepath.Join(dir, "many.txt"), []byte(b.String()), 0o644) diff --git a/internal/tool/registry_canon_test.go b/internal/tool/registry_canon_test.go index 97e396ea5c..58d858033e 100644 --- a/internal/tool/registry_canon_test.go +++ b/internal/tool/registry_canon_test.go @@ -35,7 +35,7 @@ func TestSchemasCanonicalizesOncePerTool(t *testing.T) { t.Fatalf("Schema() called %d times at Add, want 1", calls) } - for i := 0; i < 50; i++ { + for range 50 { schemas := r.Schemas() if len(schemas) != 1 { t.Fatalf("Schemas() returned %d entries, want 1", len(schemas)) diff --git a/internal/tool/sessiontool/sessiontool.go b/internal/tool/sessiontool/sessiontool.go index 8cd02ceede..6f44623e7d 100644 --- a/internal/tool/sessiontool/sessiontool.go +++ b/internal/tool/sessiontool/sessiontool.go @@ -212,14 +212,14 @@ func truncateRunes(s string, max int) string { func modelFromPath(path string) string { name := filepath.Base(path) name = strings.TrimSuffix(name, ".jsonl") - firstDash := strings.Index(name, "-") - if firstDash < 0 { + _, after, ok := strings.Cut(name, "-") + if !ok { return "(unknown)" } - rest := name[firstDash+1:] - secondDash := strings.Index(rest, "-") - if secondDash < 0 { + rest := after + _, after0, ok0 := strings.Cut(rest, "-") + if !ok0 { return rest } - return rest[secondDash+1:] + return after0 } diff --git a/internal/tool/sessiontool/sessiontool_test.go b/internal/tool/sessiontool/sessiontool_test.go index 1d2bafda17..a9dcfdca8d 100644 --- a/internal/tool/sessiontool/sessiontool_test.go +++ b/internal/tool/sessiontool/sessiontool_test.go @@ -187,7 +187,7 @@ func TestReadSession_RespectsMaxTurns(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.jsonl") var msgs []provider.Message - for i := 0; i < 10; i++ { + for range 10 { msgs = append(msgs, provider.Message{Role: provider.RoleUser, Content: "turn"}, provider.Message{Role: provider.RoleAssistant, Content: "answer"}, @@ -210,7 +210,7 @@ func TestReadSession_MaxTurnsZeroNoLimit(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.jsonl") var msgs []provider.Message - for i := 0; i < 60; i++ { + for range 60 { msgs = append(msgs, provider.Message{Role: provider.RoleUser, Content: "turn"}, provider.Message{Role: provider.RoleAssistant, Content: "answer"}, diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 2d72d7adc1..5cbfeb0d0c 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -7,6 +7,7 @@ package tool import ( "context" "encoding/json" + "slices" "sort" "strings" "sync" @@ -414,11 +415,8 @@ func (r *Registry) ResolveCall(name string) (resolved Tool, canonical string, ca if !ok { continue } - for _, alias := range mcpBindingAliases(b) { - if name == alias { - matches[canonicalName] = t - break - } + if slices.Contains(mcpBindingAliases(b), name) { + matches[canonicalName] = t } } if len(matches) == 1 { diff --git a/internal/worktree/worktree.go b/internal/worktree/worktree.go index 68a4640682..e3245cc9dd 100644 --- a/internal/worktree/worktree.go +++ b/internal/worktree/worktree.go @@ -85,7 +85,7 @@ func Create(ctx context.Context, workspaceRoot, managedRoot string) (Result, err repoBase = "repository" } - for attempt := 0; attempt < 5; attempt++ { + for range 5 { id, randomErr := randomID() if randomErr != nil { return Result{}, randomErr diff --git a/tools/repolint/baseline.json b/tools/repolint/baseline.json index a7711b3a40..d6766300d1 100644 --- a/tools/repolint/baseline.json +++ b/tools/repolint/baseline.json @@ -3,11 +3,11 @@ "banner": 0, "commented-code": 0, "essay": 4111, - "file-size": 73754, + "file-size": 73519, "layering": 1, "marker": 0, "narrative": 66, - "test-file-size": 64807 + "test-file-size": 64753 }, "files": { "cmd/e2ebench/main.go": { @@ -27,14 +27,14 @@ }, "desktop/app.go": { "essay": 90, - "file-size": 11545 + "file-size": 11538 }, "desktop/app_autosave_test.go": { "essay": 1 }, "desktop/app_test.go": { "essay": 2, - "test-file-size": 10208 + "test-file-size": 10200 }, "desktop/bot_bridge.go": { "essay": 11 @@ -131,7 +131,7 @@ }, "desktop/remote_window_test.go": { "essay": 1, - "test-file-size": 129 + "test-file-size": 128 }, "desktop/runtime_rebuilt_event_test.go": { "essay": 1 @@ -166,7 +166,7 @@ }, "desktop/settings_app.go": { "essay": 28, - "file-size": 3014, + "file-size": 3013, "narrative": 1 }, "desktop/settings_app_test.go": { @@ -190,17 +190,17 @@ }, "desktop/tabs.go": { "essay": 123, - "file-size": 8822 + "file-size": 8804 }, "desktop/tabs_order_test.go": { - "test-file-size": 397 + "test-file-size": 395 }, "desktop/tabs_sink_context_test.go": { "essay": 2 }, "desktop/tabs_topic_test.go": { "essay": 1, - "test-file-size": 3425 + "test-file-size": 3421 }, "desktop/temphelper_test.go": { "essay": 4 @@ -291,7 +291,7 @@ }, "internal/acp/service.go": { "essay": 66, - "file-size": 2173 + "file-size": 2172 }, "internal/acp/service_lock_test.go": { "essay": 11, @@ -302,7 +302,7 @@ }, "internal/agent/agent.go": { "essay": 128, - "file-size": 3273 + "file-size": 3270 }, "internal/agent/ask.go": { "essay": 4 @@ -335,11 +335,11 @@ }, "internal/agent/coordinator.go": { "essay": 20, - "file-size": 291 + "file-size": 292 }, "internal/agent/coordinator_test.go": { "essay": 5, - "test-file-size": 1248 + "test-file-size": 1244 }, "internal/agent/delivery_hardening_test.go": { "essay": 5 @@ -429,11 +429,11 @@ }, "internal/agent/run_loop.go": { "essay": 45, - "file-size": 320 + "file-size": 311 }, "internal/agent/save.go": { "essay": 105, - "file-size": 1320 + "file-size": 1319 }, "internal/agent/save_test.go": { "essay": 9, @@ -489,7 +489,7 @@ }, "internal/agent/task.go": { "essay": 65, - "file-size": 1409 + "file-size": 1407 }, "internal/agent/task_test.go": { "essay": 4, @@ -503,7 +503,7 @@ }, "internal/agent/usecapability.go": { "essay": 9, - "file-size": 780 + "file-size": 777 }, "internal/agent/usecapability_test.go": { "test-file-size": 998 @@ -513,16 +513,16 @@ }, "internal/autoresearch/store.go": { "essay": 3, - "file-size": 223 + "file-size": 216 }, "internal/boot/boot.go": { "essay": 110, - "file-size": 2203, + "file-size": 2194, "narrative": 4 }, "internal/boot/boot_test.go": { "essay": 10, - "test-file-size": 4337 + "test-file-size": 4331 }, "internal/boot/extension_dispatch_test.go": { "essay": 3, @@ -577,20 +577,20 @@ }, "internal/bot/feishu/feishu.go": { "essay": 1, - "file-size": 211 + "file-size": 210 }, "internal/bot/gateway.go": { "essay": 28, - "file-size": 2113 + "file-size": 2109 }, "internal/bot/gateway_lock_test.go": { "essay": 8 }, "internal/bot/gateway_test.go": { - "test-file-size": 1700 + "test-file-size": 1698 }, "internal/bot/project_index.go": { - "file-size": 32 + "file-size": 31 }, "internal/bot/render.go": { "essay": 5 @@ -613,14 +613,14 @@ }, "internal/checkpoint/checkpoint.go": { "essay": 8, - "file-size": 314 + "file-size": 305 }, "internal/checkpoint/observer.go": { "essay": 1 }, "internal/checkpoint/transaction.go": { "essay": 2, - "file-size": 972 + "file-size": 973 }, "internal/cli/acp.go": { "essay": 7 @@ -636,18 +636,18 @@ }, "internal/cli/chat_tui.go": { "essay": 111, - "file-size": 4590 + "file-size": 4558 }, "internal/cli/chat_tui_paste.go": { "essay": 9 }, "internal/cli/chat_tui_test.go": { "essay": 8, - "test-file-size": 3590 + "test-file-size": 3582 }, "internal/cli/cli.go": { "essay": 60, - "file-size": 2008 + "file-size": 2004 }, "internal/cli/cli_test.go": { "essay": 9, @@ -655,7 +655,7 @@ }, "internal/cli/complete.go": { "essay": 13, - "file-size": 74 + "file-size": 49 }, "internal/cli/complete_test.go": { "test-file-size": 15 @@ -759,8 +759,7 @@ "essay": 2 }, "internal/cli/transcript.go": { - "essay": 1, - "file-size": 2 + "essay": 1 }, "internal/cli/upgrade.go": { "essay": 2 @@ -775,7 +774,7 @@ "essay": 6 }, "internal/config/backfill_test.go": { - "test-file-size": 945 + "test-file-size": 944 }, "internal/config/cache_policy.go": { "essay": 9 @@ -785,7 +784,7 @@ }, "internal/config/config.go": { "essay": 56, - "file-size": 1493, + "file-size": 1489, "narrative": 2 }, "internal/config/credentials.go": { @@ -806,7 +805,7 @@ }, "internal/config/edit.go": { "essay": 15, - "file-size": 1594 + "file-size": 1592 }, "internal/config/edit_test.go": { "test-file-size": 2194 @@ -819,7 +818,7 @@ }, "internal/config/load.go": { "essay": 24, - "file-size": 1200 + "file-size": 1196 }, "internal/config/main_test.go": { "essay": 1 @@ -831,7 +830,7 @@ "test-file-size": 499 }, "internal/config/migrate.go": { - "file-size": 10 + "file-size": 7 }, "internal/config/migrate_test.go": { "test-file-size": 331 @@ -875,12 +874,12 @@ }, "internal/control/controller.go": { "essay": 180, - "file-size": 5379, + "file-size": 5367, "narrative": 4 }, "internal/control/controller_test.go": { "essay": 10, - "test-file-size": 4516 + "test-file-size": 4513 }, "internal/control/errmsg.go": { "essay": 2 @@ -1003,7 +1002,7 @@ }, "internal/evidence/evidence.go": { "essay": 38, - "file-size": 2120 + "file-size": 2080 }, "internal/evidence/evidence_test.go": { "essay": 1, @@ -1033,7 +1032,7 @@ "narrative": 1 }, "internal/extension/dispatch/dispatch_test.go": { - "test-file-size": 311 + "test-file-size": 310 }, "internal/extension/dispatch/payloads.go": { "essay": 2 @@ -1147,7 +1146,7 @@ }, "internal/hook/hook.go": { "essay": 44, - "file-size": 838 + "file-size": 829 }, "internal/hook/hook_test.go": { "essay": 8, @@ -1199,7 +1198,7 @@ }, "internal/jobs/jobs.go": { "essay": 42, - "file-size": 1263 + "file-size": 1264 }, "internal/jobs/jobs_extra_test.go": { "essay": 1 @@ -1250,15 +1249,15 @@ }, "internal/plugin/lazy_test.go": { "essay": 19, - "test-file-size": 279 + "test-file-size": 275 }, "internal/plugin/plugin.go": { "essay": 30, - "file-size": 1324 + "file-size": 1315 }, "internal/plugin/plugin_test.go": { "essay": 10, - "test-file-size": 921 + "test-file-size": 918 }, "internal/plugin/security.go": { "essay": 3 @@ -1281,7 +1280,7 @@ }, "internal/plugin/transport_stdio.go": { "essay": 12, - "file-size": 3 + "file-size": 4 }, "internal/pluginpkg/claude_compat.go": { "essay": 19 @@ -1318,29 +1317,29 @@ }, "internal/provider/anthropic/anthropic_test.go": { "essay": 1, - "test-file-size": 222 + "test-file-size": 221 }, "internal/provider/openai/host.go": { "essay": 8 }, "internal/provider/openai/openai.go": { "essay": 42, - "file-size": 555 + "file-size": 554 }, "internal/provider/openai/openai_test.go": { "essay": 5, - "test-file-size": 1558 + "test-file-size": 1557 }, "internal/provider/openai/realcache_test.go": { "essay": 7 }, "internal/provider/provider.go": { "essay": 50, - "file-size": 350 + "file-size": 345 }, "internal/provider/responses/responses.go": { "essay": 26, - "file-size": 81 + "file-size": 78 }, "internal/provider/responses/responses_test.go": { "essay": 6, @@ -1518,10 +1517,10 @@ }, "internal/skill/skill.go": { "essay": 7, - "file-size": 633 + "file-size": 624 }, "internal/skill/skill_test.go": { - "test-file-size": 283 + "test-file-size": 278 }, "internal/skill/tools.go": { "essay": 2 @@ -1564,7 +1563,7 @@ }, "internal/tool/builtin/bash.go": { "essay": 14, - "file-size": 24 + "file-size": 25 }, "internal/tool/builtin/bash_env_test.go": { "essay": 4 diff --git a/tools/repolint/comments_test.go b/tools/repolint/comments_test.go index 23e750300b..00b2463fc2 100644 --- a/tools/repolint/comments_test.go +++ b/tools/repolint/comments_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func rules(t *testing.T, rel, src string) []string { t.Helper() @@ -57,14 +60,15 @@ func TestEssayWeightIsTheExcessOverTheLimit(t *testing.T) { func TestDocGoCarriesTheLongPackageExplanation(t *testing.T) { long := "package p\n" - doc := "// Package p explains itself.\n" + var doc strings.Builder + doc.WriteString("// Package p explains itself.\n") for range capPackageDoc + 2 { - doc += "//\n// more prose\n" + doc.WriteString("//\n// more prose\n") } - if got := rules(t, "sub/doc.go", doc+long); len(got) != 0 { + if got := rules(t, "sub/doc.go", doc.String()+long); len(got) != 0 { t.Fatalf("doc.go package comment flagged: %v", got) } - if got := rules(t, "sub/other.go", doc+long); len(got) == 0 { + if got := rules(t, "sub/other.go", doc.String()+long); len(got) == 0 { t.Fatal("same package comment outside doc.go should be flagged") } }