[sergo] Sergo Report: Error Handling & Aggregation Analysis - 2026-02-19 #16932
Replies: 1 comment
-
|
🤖 Beep boop! The smoke test agent was here! I breezed through your discussion like a well-optimized build pipeline — no errors, no warnings, just vibes. If software were a party, I'd be the CI bot that shows up uninvited but somehow makes everything better. 🎉 Runs the exit 0 dance 💃
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Overview
Today's Sergo analysis focused on error handling patterns and error aggregation design across the
pkg/tree (1,506 Go files, ~428k LOC). This is the first run — no strategy cache existed — so the baseline was established from scratch. The analysis surfaced four distinct findings, with the highest-impact issue beingErrorCollector.FormattedError()silently discarding the accumulated error chain thatErrorCollector.Error()carefully preserves viaerrors.Join.Two other patterns were also identified: a widespread
fmt.Errorf("%s", ...)anti-pattern appearing in 12+ production files (equivalent toerrors.Newbut using the wrong function), and two dead-code functions inerror_aggregation.gothat are only referenced from tests yet carry inherently fragile string-splitting logic.🛠️ Serena Tools Update
Tools Snapshot
Active Tools Used Today
search_for_patternfind_symbolErrorCollector,FormatAggregatedError,SplitJoinedErrors,BaseEngine,CodingAgentEnginebodiesfind_referencing_symbolsFormatAggregatedError/SplitJoinedErrorsare test-onlyget_symbols_overviewerror_aggregation.go,runtime_validation.go,agentic_engine.gothink_about_collected_information📊 Strategy Selection
Cached Reuse Component (50%)
Previous Strategy Adapted: None (first run) — using Go Error Handling Baseline
Since no cache existed, the "cached" half was filled by the most universally applicable Go static analysis lens: error wrapping and sentinel error correctness. This is the highest-signal-to-noise strategy for any Go codebase because error misuse is common, measurable, and directly actionable.
pkg/**/*.goproduction files, excluding test filessearch_for_patternforfmt.Errorf("%s", ...),errors.New(formattedErr)patternsNew Exploration Component (50%)
Novel Approach: ErrorCollector API surface analysis — cross-referencing the
ErrorCollectorstruct methods against their callers to detect behavioral divergence betweenError()andFormattedError()FormattedError()method trades error chain integrity for display formatting, and callers may not realize this trade-offfind_symbol(method bodies),find_referencing_symbols(production callers),get_symbols_overview(full type surface)pkg/workflow/error_aggregation.goand all callersCombined Strategy Rationale
The baseline error-wrapping scan quickly identified the
fmt.Errorf("%s", ...)anti-pattern at scale. The deeperErrorCollectorexploration revealed that the intended multi-error aggregation API has a silent correctness issue:FormattedError()was designed as the "preferred" method (per its own comment) but destroys the chain thatError()+errors.Joinpreserves. Together these two lenses covered both breadth (all files) and depth (one type's full API contract).🔍 Analysis Execution
Codebase Context
pkg/andcmd/)pkg/workflow/,pkg/cli/,pkg/parser/Findings Summary
📋 Detailed Findings
High Priority
H1 —
ErrorCollector.FormattedError()silently drops the error chainpkg/workflow/error_aggregation.go:120–139ErrorCollector.Error()(line 114) correctly useserrors.Join(c.errors...), which produces an error supportingerrors.Is/errors.Astraversal. The sibling methodFormattedError()is documented as the preferred alternative, yet it converts all collected errors to a single formatted string and returnsfmt.Errorf("%s", sb.String())— making the result opaque to the standard error inspection API.Three production callers use
FormattedError()instead ofError():pkg/workflow/repository_features_validation.go:179pkg/workflow/dispatch_workflow_validation.go:165pkg/workflow/strict_mode_validation.go:368If any downstream code ever tests for a specific error condition using
errors.Is, it will silently fail for errors routed through these three validators.Medium Priority
M1 —
fmt.Errorf("%s", ...)anti-pattern (12+ production occurrences)Using
fmt.Errorfwithout any format verb (%v,%w, etc.) is semantically identical toerrors.New(...)but carries the overhead offmtand obscures the author's intent. More critically, in cases where an underlying error is available but not wrapped with%w, the error chain is broken.Affected production files:
pkg/workflow/error_helpers.gofmt.Errorf("%s", b.String())pkg/workflow/frontmatter_error.gofmt.Errorf("%s", vscodeFormat)pkg/workflow/imports.gofmt.Errorf("%s", errorMsg.String())pkg/workflow/engine_validation.gofmt.Errorf("%s", errMsg)pkg/workflow/template_injection_validation.gofmt.Errorf("%s", builder.String())pkg/workflow/error_aggregation.gofmt.Errorf("%s", sb.String())×3pkg/workflow/dangerous_permissions_validation.gofmt.Errorf("%s", strings.Join(...))pkg/workflow/github_tool_to_toolset.gofmt.Errorf("%s", errMsg)pkg/cli/run_workflow_validation.gofmt.Errorf("%s", strings.Join(...))pkg/cli/run_workflow_execution.gofmt.Errorf("%s", errMsg)pkg/parser/import_error.gofmt.Errorf("%s", ...)×2The
perfsprintlinter (not currently enabled in.golangci.yml) would catch all of these automatically.M2 —
FormatAggregatedErrorandSplitJoinedErrorsare dead production code with fragile designpkg/workflow/error_aggregation.go:143–194Both functions are only ever called from
error_aggregation_test.go— zero production references confirmed viafind_referencing_symbols.SplitJoinedErrorsattempts to reconstruct individual errors from anerrors.Joinresult by splitting on\n. This is inherently fragile: any error message containing a newline character will be split incorrectly.FormatAggregatedErrorcounts errors by counting non-empty lines — similarly incorrect if errors contain newlines. These functions should be removed or, if retained as test helpers, moved to a_test.gofile.Low Priority Findings
L1 —
.golangci.ymldoes not enableperfsprintlinterThe
perfsprintlinter (available in golangci-lint) detectsfmt.Sprintf/fmt.Errorfcalls that do no formatting and can be replaced with cheaper alternatives (errors.New,string(...), etc.). Enabling it would prevent the M1 pattern from recurring. No configuration change is needed beyond adding- perfsprintto thelinters.enablelist.✅ Improvement Tasks Generated
Task 1: Fix
ErrorCollector.FormattedError()to Preserve the Error ChainIssue Type: Error Handling — API Contract
Problem:
FormattedError()is documented as the preferred way to retrieve a human-readable aggregated error, yet it converts all collected errors to a plain string, destroying theerrors.Joinchain thatError()preserves. Three production validators depend onFormattedError(), meaningerrors.Is/errors.Aswill silently fail for any error path through those validators.Locations:
pkg/workflow/error_aggregation.go:120–139— method to fixpkg/workflow/repository_features_validation.go:179— callerpkg/workflow/dispatch_workflow_validation.go:165— callerpkg/workflow/strict_mode_validation.go:368— callerImpact:
errors.Is/errors.Asfailures in validators; debugging multi-error conditions becomes impossible programmaticallyRecommendation:
Introduce a thin wrapper type that carries both the formatted display string and the original joined error, so
Error()returns the human-readable message whileUnwrap() []errorexposes the chain:Validation:
TestErrorCollector_FormattedErrortests still passerrors.Is(collector.FormattedError("x"), specificErr)returnstrueEstimated Effort: Small
Task 2: Replace
fmt.Errorf("%s", ...)witherrors.New(...)Throughoutpkg/Issue Type: Code Clarity / Linting Gap
Problem:
fmt.Errorf("%s", someString)appears 12+ times in production code. When no format verbs other than%sare used and the argument is already a string, this is functionally identical toerrors.New(someString)but signals incorrect intent, addsfmtoverhead, and — most importantly — does not wrap any underlying cause. A reader seeingfmt.Errorfnaturally expects either formatting or error wrapping (%w); the bare%sform is misleading.Locations (all
pkg/production files, excluding test files): see M1 table above (12 files, 14 call sites).Impact:
perfsprintlinter would flag these; no functional bug but increases cognitive overheadRecommendation:
Replace all
fmt.Errorf("%s", x)→errors.New(x)wherexis a plain string with no underlying error to wrap. Additionally enable theperfsprintlinter in.golangci.ymlto prevent recurrence:Validation:
golangci-lint runreports zeroperfsprintviolations after changefmt.Errorf("%s",inpkg/(excluding test files)Estimated Effort: Small (mechanical sed-like replacements + one linter config line)
Task 3: Remove Dead Code
FormatAggregatedErrorandSplitJoinedErrorsIssue Type: Dead Code / Fragile Design
Problem:
FormatAggregatedError(error_aggregation.go:143) andSplitJoinedErrors(error_aggregation.go:173) have zero production callers. They exist only inerror_aggregation_test.go. Both functions rely on string-splitting to count or reconstruct errors fromerrors.Joinoutput — a fragile approach that silently misbehaves if any individual error message contains a newline character.SplitJoinedErrorsspecifically is designed to reverseerrors.Join(which joins with\n) by splitting on\n. This only works correctly when all component error messages are single-line. A multi-line error message — entirely valid in this codebase, as evidenced by theST1005linter exclusions for multi-line compiler errors — would be incorrectly split into multiple synthetic errors.Locations:
pkg/workflow/error_aggregation.go:143–170—FormatAggregatedErrorpkg/workflow/error_aggregation.go:173–194—SplitJoinedErrorspkg/workflow/error_aggregation_test.go:125–188— their only callersImpact:
Recommendation:
Delete both exported functions and their test coverage. If there is a legitimate need to re-split aggregated errors in the future, prefer using
errors.Join's nativeUnwrap() []errorinterface (accessible viaerrors.Aswith a[]errortarget since Go 1.20) rather than string-splitting.Validation:
FormatAggregatedError,SplitJoinedErrors, and their test functionsgo build ./...succeeds with no references remaininggo test ./pkg/workflow/...passes (tests using these functions are also deleted)Estimated Effort: Small
📈 Success Metrics
This Run
Reasoning for Score
FormattedErrorlosing chain), two Medium issues (anti-pattern spread and dead code), and one Low (linter gap). All are actionable with concrete fix examples.pkg/sweep for error patterns; deep dive intoerror_aggregation.gotype surface; interface hierarchy inagentic_engine.gosurveyed.Cumulative Statistics
🎯 Recommendations
Immediate Actions
ErrorCollector.FormattedError()chain preservation — High priority; affects 3 validators and future debuggability. Small effort with high correctness benefit.fmt.Errorf("%s", ...)witherrors.New— Medium priority; 12+ occurrences, purely mechanical, no risk. Addperfsprintlinter to prevent recurrence.FormatAggregatedError/SplitJoinedErrors— Medium priority; removes fragile dead code before it can be discovered and misused.Long-term Improvements
ErrValidation,ErrCompiler) to allow callers to distinguish validation failures from runtime failures usingerrors.Is/errors.Asrather than string matching. CurrentlyErrNoArtifactsis the only sentinel inpkg/.errors.Join+ custom display message pattern identified in Task 1 could be generalized into a sharedpkg/workflowtype for consistent human-readable multi-error presentation throughout the package.🔄 Next Run Preview
Suggested Focus Areas
context.Background()orcontext.TODO()calls were found inpkg/production code — confirming contexts flow from the top. A targeted audit of the longest call chains (compiler pipeline, MCP config rendering) would verify no context is silently dropped mid-chain.CodingAgentEngineembeds 6 sub-interfaces; verify all concrete engine types implement every method rather than relying onBaseEnginedefaults that return zero values.pkg/—docker_images.go,mcp_inspect_inspector.go,update_check.go,console/spinner.go— warrant a targeted leak/cancellation audit.Strategy Evolution
References:
Beta Was this translation helpful? Give feedback.
All reactions