golang-check Fix Plan β 2026-08-26
Source: full run of .agents/skills/golang-check/golang-check.sh (gocognit >15, gocyclo >12, staticcheck, file-size hard 1000 / soft 500).
Raw output: /tmp/golang-check-raw.txt (transient).
Summary
| Check |
Result |
Findings |
| π΄ Build blockers (vet/compile) |
PASS |
0 |
| π Correctness (SA4xxx, SA1xxx) |
PASS |
0 |
| π‘ Dead code (U1000) |
FAIL |
1 |
| π΅ Complexity gocognit >15 |
FAIL |
20 (8 production, 12 tests) |
| π΅ Complexity gocyclo >12 |
FAIL |
39 (~7 production-only, rest tests/shared) |
| βͺ File size |
FAIL |
9 hard (>1000): 4 prod + 5 test Β· 168 soft (>500): ~91 prod, ~77 test |
The codebase is correctness-clean; all remaining work is dead code, complexity, and file organization. No //nolint, no threshold changes.
Tier π‘ P0 β Dead code (fix first, trivial)
| File |
Check |
Explanation |
Fix |
tools/tool_search_test.go:68 |
staticcheck U1000 |
func schemaNames is unused (U1000 = "unused code"). Likely orphaned after a test refactor. |
Delete the function. If its assertion logic is still valuable, wire it into the test that should use it instead of deleting blindly β check git history first. Re-run staticcheck. |
Tier π΅ P1 β Production complexity (root-cause refactors)
Each fix extracts cohesive helpers (SRP), keeps behavior identical, and adds unit tests for newly extracted pure functions. Gate per item: relevant package tests + re-run of the specific check.
P1.1 e2e/perfdrive/main.go:50 main() β cognit 33 / cyclo 22
|
|
| Why over |
One monolithic main: 13 flag defs + arg validation + PTY spawn + drain goroutine + prompt injection + sampling loop with wait-file/grace state machine + CSV write, all inline. |
| Fix |
Decompose into linear orchestration: type driveConfig (parsed flags), parseFlags() (*driveConfig, error) (flag defs + required check β exit 2 moves here), startProcess(cfg) (*drivenProc, error) (exec+pty+drain goroutine), (d *drivenProc) sendPrompt(text) error, collectSamples(d, cfg, notify func(sample)) []sample (sampling loop; extract markerDeadline(markerSeenAt time.Time, seen bool, grace, cfg) (time.Time, bool) for wait-file state), writeCSV(path string, samples []sample) error. main becomes: parse β start β prompt β sample β write, cyclo β€6 each. |
| Tests |
writeCSV and markerDeadline become unit-testable (t.TempDir). |
|
|
| Why over |
Switch over 5 ops with validation + call + formatting inlined per case; rename case alone nests 4 branches (nil-check, newName check, preview policy, apply). Stale comment says "runSymbols handles documentSymbol". |
| Fix |
Table dispatch: var refactorOps = map[string]func(context.Context, lspRefactoringManager, *LSPTool, lspParams, string) (string, error) with one small method per op: refactorPrepareRename, refactorRename (split into previewRenameEdit + applyRenameEdit), refactorCompletion, refactorCodeAction, refactorFormatting. Handler lookup + "unavailable"/"unknown op" guard only β cyclo ~4. Fix the stale comment while there. Mirror the same pattern already used by formatLocations/formatHover. |
| Tests |
Existing tools/lsp_test.go op coverage must stay green; add unknown-op case if missing. |
P1.3 tools/lsp.go:117 ExecuteContext (cyclo 14) and :268 runAdvanced (cyclo 14)
|
|
| Why over |
Same switch-with-inline-work shape as P1.2 (ExecuteContext routes query/symbols/refactoring/advanced with param validation inline; runAdvanced has 5 cases). |
| Fix |
ExecuteContext: keep a single routing table map[string]handlerFunc; move shared line/character validation to validatePosition(p) error called once before dispatch. runAdvanced: per-op methods advancedImplementation, advancedWorkspaceSymbols, β¦ sharing a generic callAndFormat where profitable; switch becomes pure dispatch. |
| Tests |
Existing LSP tool tests; add position-validation table test. |
|
|
| Why over |
Nested loops (terms Γ 5 fields) with tf/idf math, dedupe map, identifier boost, coverage/confidence post-processing all in one function. |
| Fix |
Extract pure helpers: dedupeTokens([]string) []string (reusable β likely duplicates logic in expandQuery), (s *FieldedScorer) termContribution(fd docFields, q string) float64 (inner field loop + BM25 term score), identifierBoost(query string, d DocumentMeta, score float64) float64, confidenceFor(score, coverage float64) float64. Score becomes guard clauses + one loop calling helpers β cognit β€8. |
| Tests |
Unit tests for termContribution (zero-tf short-circuit, avg<1 clamp) and confidenceFor boundaries (score=0 β coverage; saturation cap 1). |
|
|
| Why over |
Param normalization + two chunking strategies (region-based with clamping loop; sliding-window fallback) inlined in one function. |
| Fix |
Extract normalizeChunkParams(window, overlap int, analyzer CodeAnalyzer) (int, int, CodeAnalyzer), regionChunks(path string, lines []string, regions []Region) []DocumentMeta (validation/clamp loop), windowChunks(path string, lines []string, window, overlap int) []DocumentMeta (sliding-window loop incl. final-chunk break). ChunkSource = normalize β regions β early return β windows. Each helper cyclo β€6. |
| Tests |
Table test on windowChunks: window>len(lines), exact multiple, overlapβ₯window fallback. |
P1.6 internal/lsp/manager.go:259 (*Manager).spawn β cognit 17
|
|
| Why over |
Resolve + create + six protocol handler registrations (closures mutating running) + initialize handshake + initialized notification + two error-cleanup paths in one body. |
| Fix |
Extract (m *Manager) registerClientHandlers(client *Client, running *serverClient) (all OnNotification/OnRequest wiring β the workspace/diagnostic/refresh comment moves with it) and (m *Manager) handshake(ctx context.Context, client *Client, spec *ServerSpec, root string) (*serverClient, error) (Initialize + Initialized + cleanup-on-error returning capabilities-applied client). spawn = resolve β factory β register β handshake. |
| Tests |
Existing manager tests green; no behavior change. |
P1.7 core/commands/config.go:413 (*configMenu).settingRetrySettings β cognit 17
|
|
| Why over |
SelectOption callback containing a switch whose cases each open another nested ShowInput callback with accept/apply/re-enter branching. |
| Fix |
Extract per-item prompts: (m *configMenu) promptMaxRetries(cfg *config.Config) and (m *configMenu) promptProviderRetryDelay(cfg *config.Config) (each contains its own input callback; cognit β€7 each); replace the inner switch with a map[string]func(*config.Config) handler lookup. Follows the existing per-setting-method convention in this file (settingTheme etc.). |
| Tests |
Existing config menu command tests must stay green. |
|
|
| Why over |
Header block (rebuilt warning, counts, score range) + per-result loop (relpath computation, optional id suffix, match truncation) + footer, all inline. |
| Fix |
Extract relPath(abs string) string (ProjectDir-relative with .. guard), formatMatchLine(buf, m smartLineMatch) (sanitize/truncate-at-140), formatOneResult(buf, i int, r bm25.SearchResult, matches []smartLineMatch), formatHeader(buf, query, results, idx, rebuilt). formatResults = four sequential calls. |
| Tests |
Unit-test relPath (outside-project prefix guard) and formatMatchLine truncation boundary at exactly 140 columns. |
P1.9 plugins/plugin.go:334 (*JSBridge).buildCompletionWrapper β cognit 16
|
|
| Why over |
Wrapper closure contains VM lock dance, recover, export-shape unwrapping ([]interface{} β map checks β Completion filtering) in one closure. |
| Fix |
Extract pure completionsFromExport(v any) []Completion (handles array/map/nil shapes, drops empty & <nil> values). Closure keeps only enterVM/lockVM/recover/call/export. The extracted function is JS-free and directly unit-testable. |
| Tests |
New completionsFromExport table test: non-array export, non-map items, missing value, <nil> value, valid entries. |
P1.10 tools/bash.go:211 (*BashTool).ExecuteContext β cyclo 13 (cognit ok)
|
|
| Why over |
Seven sequential guard clauses (parse, empty, escalation, blocked, allowed, analyzed, confinement) plus result-handling precedence ladder (ctx-cancelled β timedOut β tooLarge β hint β out). |
| Fix |
Extract (t *BashTool) validateInput(ctx context.Context, p *bashParams) error (all pre-run guards, early returns) and (t *BashTool) reportResult(ctx, p, output, err, duration, timedOut, tooLarge, hint) (string, error) (post-run precedence ladder). ExecuteContext β 5 branches. Behavior-preserving; ordering inside each helper must match current semantics exactly (cancelled beats timeout beats tooLarge). |
| Tests |
Existing tools/bash_test.go precedence tests are the safety net; add explicit cancelled-vs-timeout precedence case if absent. |
Test-function complexity (P4 β lower priority)
12 gocognit offenders are tests (worst: TestPluginHooks_MessagePreSend_Decisions 44, TestLoadManifest_HooksYAML 28, TestQuotaResets_CompletionOffersCreditsByExpiry 23β¦). Root cause: long arrange sections + assertion ladders + repeated payload walking. Standard treatment when touched:
| Pattern |
Fix |
| Giant single test asserting many decisions |
Split into focused subtests via t.Run (each branch becomes its own function scope) or separate TestX_Case funcs |
| Repeated JSON/payload extraction ladders |
Extract mustDecode(t, β¦), assertPayloadField(t, β¦) helpers using require |
| Duplicated environment setup |
Shared newTestXxx(t) constructor |
Do not chase these opportunistically across all packages; apply when a file is already being modified (see P3).
Tier βͺ P2 β Hard file-size violations (>1000 lines, build gate)
Split along existing seams only β move code verbatim first (compile-safe), then trim. The repo already uses the <file>_<aspect>.go convention (agentmanager_lifecycle.go, config_cli_helpers.go, openai_completions_timings.go).
Production (4)
| File |
Lines |
Explanation |
Fix |
core/commands/config_cli.go |
1067 |
CLI handlers + persistence machinery + ~25 setter factories in one file. |
Move setter section (configSetter type + all set* funcs, from ~line 477 to EOF β 590 lines) to new core/commands/config_cli_setters.go. Remaining β 480. No logic changes. |
multiagent/foreground_orchestrator.go |
1084 |
Orchestrator core + output-event forwarding state machine + accessors mixed. |
Move output-forwarding cluster (agentOutputState, handleAgentOutputEvent, handleAgentContentEvent, handleAgentEndEvent, finishThinking, toolResultPreview, recordRoleEvent, forwardCacheStats, β lines 310β446) to multiagent/orchestrator_output.go; move companion-count/setter accessors (SetPromptRegistryβ¦CompanionCount) to multiagent/orchestrator_config.go. Core < 800. |
internal/agentic/provider/protocol/openai_completions.go |
1025 |
Request building + message conversion + cache-control injection under banner comments. |
Split at the banner seams: message conversion (convertMessagesβ¦convertTools, image/dataURL helpers β lines 260β420) β openai_completions_messages.go; cache-control section (cacheControl type through supportsLongCacheRetention, β lines 424βend) β openai_completions_cache.go. Keep request-building in the base file. |
core/agentmanager.go |
1017 |
Turn-record types + steering plumbing inflate the core file; siblings already exist (_lifecycle, _events, _modes, _state, _accessors). |
Move turn-record types (TurnTokenUsage/TurnRecord/TurnToolCall/TurnToolResult/TurnTiming, ~60 lines) β core/agentmanager_turns.go; move steering cluster (SendUserInput*, steeringSourceAdapter, DispatchPendingSteering, SteeringQueue/SetSteeringQueue) β core/agentmanager_steering.go. |
Tests (5)
| File |
Lines |
Explanation |
Fix |
core/commands/team_test.go |
1108 |
38 test funcs across team CRUD/lifecycle/membership. |
Split by area: team_create_test.go (create/validation), team_lifecycle_test.go (start/stop/status), remainder stays. Shared fakes β team_testhelpers_test.go. |
config/config_test.go |
1035 |
43 test funcs mixing loader, merge, provider assertions (assertProvider cognit 19 also lives here). |
Split into config_providers_test.go (provider/model assertions β home of assertProvider, refactor it per P1 patterns) and config_loadmerge_test.go. |
plugins/quota_resets_test.go |
1067 |
Reset-store tests vs completion-flow tests (contains the two worst quota tests). |
Move completion tests (TestQuotaReset_Completion, TestQuotaResets_CompletionOffersCreditsByExpiry, neighbors from ~line 862) β quota_resets_completion_test.go; simplify them per P4 patterns while moving. |
provider/manager_test.go |
1046 |
Registry, resolution, and fallback concerns interleaved. |
Split into manager_registry_test.go and manager_resolution_test.go along existing test-name prefixes. |
tools/bash_test.go |
1004 |
70 test funcs: security policy vs execution/formatting. |
Move security tests (checkBlocked/checkAllowed/checkAnalyzed/confinement/escalation) β bash_security_test.go; execution/timeout/format stay in bash_test.go. |
Verification for every split: go build ./..., affected go test -count=1 -race ./pkg/..., then re-run size check.
Tier βͺ P3 β Soft violations near hard limit (prod files β₯850 lines)
Not gating today but will trip the 1000 hard limit with modest growth. Schedule as dedicated splits (same seam technique as P2), ordered by proximity:
| File |
Lines |
Proposed split |
internal/agentic/agent_streaming.go |
991 |
stream event pump vs renderer/progress plumbing β agent_streaming_events.go |
config/loader.go |
975 |
load cascade vs save/migrate β loader_save.go (its tests already split that way) |
tools/search/bm25/index.go |
986 |
index build/persist vs query/scoring entrypoints β index_query.go |
internal/lsp/manager.go |
935 |
spawn/handshake (after P1.6) vs registry/lifecycle β manager_lifecycle.go |
internal/agentic/agent.go |
947 |
run-loop vs tool-execution glue β agent_tools_bridge.go |
config/config_merge.go |
952 |
merge primitives vs per-section merge tables |
internal/python/stdlib/re.go |
938 |
pattern compile vs match/exec engines |
app/bootstrap.go |
912 |
config/bootstrap phases β bootstrap_providers.go |
app/submithandler.go |
850 |
submit pipeline stages |
plugins/plugin.go |
847 |
bridge wrappers vs manifest lifecycle (post-P1.9) |
plugins/bridge_extended.go |
829 |
per-domain bridge extensions |
tui/chat_viewport_components.go |
887 |
component builders vs layout |
config/wizard_render.go |
881 |
render views already partially split (wizard_render_views.go) β continue |
skills/loader.go |
826 |
discovery vs parse/validate |
Remaining ~77 soft files (500β850, prod+test): boy-scout rule only β when a file is touched for other reasons, extract obvious units until it's under 500 or the natural seam is exhausted. Do not batch-refactor these.
Execution order & gates
- P0 dead code (single delete) β rerun staticcheck.
- P1.1βP1.10 complexity fixes, worst-first (P1.1βP1.10); after each: package tests + targeted recheck; add unit tests for extracted pure helpers.
- P2 hard file-size splits (prod first, then tests); after each:
go build ./... + package race tests.
- P3 near-limit splits, one PR/file.
- Final gate:
go vet ./... && go test -count=1 -race -cover ./... && .agents/skills/golang-check/golang-check.sh β expect exit 0 except accepted soft-violation residue (<500-line misses documented above).
Rules honored throughout: behavior-preserving refactors, no //nolint, no threshold edits, root causes fixed, new helpers get tests.