Skip to content

Commit 8df074b

Browse files
AgentWrapperclaude
authored andcommitted
chore(backend): add golangci-lint with a strong ruleset and clear the tree
Introduces backend/.golangci.yml (27 linters across correctness, dead-code/ boilerplate, style, and security), wires it into CI as a blocking job, and fixes every finding so the tree starts at zero. Config: - 27 linters: errcheck, govet, staticcheck, errorlint, bodyclose, sqlclosecheck, rowserrcheck, nilerr, makezero, unused, unparam, unconvert, wastedassign, copyloopvar, prealloc, dupl, revive (incl. exported-symbol doc comments), gocritic, misspell, usestdlibvars, predeclared, nakedret, gosec, … - Tuned for signal over noise: govet/shadow and gocritic hugeParam/rangeValCopy/ unnamedResult disabled (idiomatic-Go false positives); sqlc-generated code and tests get scoped exclusions; gosec G304 excluded (paths are config/run-file/ worktree-derived, not user input); nilerr excluded in cli/status.go (probe failures are the reported status, not a command error). CI: - New blocking lint job (golangci-lint-action, latest binary for Go-version compatibility). - go-version now read from go.mod (was pinned 1.22 while go.mod declares 1.25). Cleanup to reach zero (no behavior change): - errcheck: wrap deferred/inline Close()/Remove()/Rollback() with `_ =`. - gosec: tighten dir/file perms (0755->0750, 0644->0600). - unparam: drop always-nil error return from startLifecycle; drop unused shellPath param (zellij PowerShell) and always-500 fallbackStatus param (writeProjectError). - gocritic: regexp \d, s != "", switch->if, combined appends. - revive: doc comments on all exported symbols; rename project.ProjectRow -> project.Row (stutter); rename `max` locals shadowing the builtin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c4bbbf7 commit 8df074b

43 files changed

Lines changed: 378 additions & 83 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/go.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ jobs:
2222

2323
- uses: actions/setup-go@v5
2424
with:
25-
go-version: "1.22"
25+
# Read the version from go.mod so CI can't drift from the module
26+
# (it previously pinned 1.22 while go.mod declared 1.25).
27+
go-version-file: backend/go.mod
2628
cache: false
2729

2830
- name: Check formatting
@@ -42,3 +44,27 @@ jobs:
4244

4345
- name: Test
4446
run: go test -race ./...
47+
48+
lint:
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v4
52+
53+
- uses: actions/setup-go@v5
54+
with:
55+
go-version-file: backend/go.mod
56+
cache: false
57+
58+
- name: golangci-lint
59+
# v8 of the action drives golangci-lint v2 (the schema this config uses);
60+
# the v6 action speaks v1 CLI flags and errors against a v2 binary.
61+
uses: golangci/golangci-lint-action@v8
62+
with:
63+
# Pinned for reproducibility: bump intentionally rather than letting an
64+
# upstream release change CI. Must be built with Go >= the module's
65+
# (go.mod is 1.25); v2.12.2 is built with go1.25 — older v2 tags
66+
# (e.g. v2.1.x) are built with go1.24 and refuse to analyze 1.25 code.
67+
version: v2.12.2
68+
working-directory: backend
69+
# Blocking on the full ruleset: the tree is clean at zero findings, so
70+
# any new issue fails CI rather than being grandfathered.

backend/.golangci.yml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# golangci-lint v2 config for the AO backend.
2+
# Run: golangci-lint run ./... (from backend/)
3+
version: "2"
4+
5+
run:
6+
timeout: 5m
7+
8+
issues:
9+
# Report every finding, not the default first-50-per-linter / 3-same.
10+
max-issues-per-linter: 0
11+
max-same-issues: 0
12+
13+
linters:
14+
default: none
15+
enable:
16+
# --- correctness ---
17+
- errcheck # unchecked errors
18+
- govet # suspicious constructs
19+
- ineffassign # ineffectual assignments
20+
- staticcheck # the big static analyzer
21+
- unused # dead code (funcs/vars/types/fields)
22+
- errorlint # error wrapping / comparison bugs
23+
- bodyclose # unclosed HTTP response bodies
24+
- sqlclosecheck # unclosed sql.Rows/Stmt
25+
- rowserrcheck # missing rows.Err()
26+
- nilerr # `return nil` after a non-nil err check
27+
- makezero # append to a non-zero-len make() slice
28+
- gocheckcompilerdirectives # malformed //go: directives
29+
- reassign # reassigning package-level vars from other pkgs
30+
# --- dead code / boilerplate (the "nuke" linters) ---
31+
- unparam # unused function params / always-same returns
32+
- unconvert # unnecessary type conversions
33+
- wastedassign # assignments never read
34+
- copyloopvar # redundant loop-var copies (Go 1.22+)
35+
- prealloc # slices that could be preallocated
36+
- dupl # copy-pasted code blocks
37+
# --- style / quality ---
38+
- revive # configurable golint successor
39+
- gocritic # opinionated diagnostics + style
40+
- misspell # typos in comments/strings
41+
- usestdlibvars # use stdlib consts (http.MethodGet, etc.)
42+
- predeclared # shadowing predeclared identifiers
43+
- nakedret # naked returns in long funcs
44+
# --- security ---
45+
- gosec
46+
47+
settings:
48+
errcheck:
49+
check-type-assertions: true
50+
govet:
51+
enable-all: true
52+
disable:
53+
- fieldalignment # struct field ordering is not worth the churn
54+
- shadow # shadowing `err` in nested scopes is idiomatic Go
55+
revive:
56+
rules:
57+
- { name: exported } # doc comments on every exported symbol
58+
- { name: blank-imports }
59+
- { name: context-as-argument }
60+
- { name: context-keys-type }
61+
- { name: dot-imports }
62+
- { name: error-return }
63+
- { name: error-strings }
64+
- { name: error-naming }
65+
- { name: indent-error-flow }
66+
- { name: errorf }
67+
- { name: empty-block }
68+
- { name: superfluous-else }
69+
- { name: unreachable-code }
70+
- { name: redefines-builtin-id }
71+
- { name: range }
72+
- { name: time-naming }
73+
- { name: var-declaration }
74+
gocritic:
75+
enabled-tags: [diagnostic, performance, style]
76+
disabled-checks:
77+
- ifElseChain # overlaps revive/superfluous-else
78+
- commentedOutCode
79+
- hugeParam # pass-by-pointer micro-opt; hurts clarity, risks nil/aliasing
80+
- rangeValCopy # same — copying a struct in range is usually fine
81+
- unnamedResult # named returns are a style choice, not a defect
82+
dupl:
83+
threshold: 140
84+
gosec:
85+
excludes:
86+
- G104 # unchecked errors — errcheck owns this
87+
- G304 # file inclusion via variable — paths are config/run-file/worktree-derived, not user input
88+
89+
exclusions:
90+
generated: lax # skip sqlc/codegen ("Code generated ... DO NOT EDIT")
91+
rules:
92+
# Tests: relax the noisiest checks (deliberate error-drops, repeated setup,
93+
# preallocation, and upgrade-response bodies that don't need closing).
94+
- path: _test\.go
95+
linters: [errcheck, dupl, gosec, unparam, gocritic, prealloc, bodyclose]
96+
# status.go deliberately reports probe failures in the result struct
97+
# (st.State/st.Error) and returns nil — a down daemon is the status being
98+
# reported, not a failure of the status command itself.
99+
- path: internal/cli/status\.go
100+
linters: [nilerr]
101+
# The reflect/unsafe field-inspection test is intentional.
102+
- path: wiring_test\.go
103+
linters: [gosec]
104+
# Spawning git/agent subprocesses with computed args is the point.
105+
- linters: [gosec]
106+
text: "G204"
107+
108+
formatters:
109+
enable:
110+
- gofmt
111+
- goimports
112+
settings:
113+
goimports:
114+
local-prefixes:
115+
- github.com/aoagents/agent-orchestrator

backend/internal/adapters/runtime/tmux/tmux.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ var sessionIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
2525

2626
var getenv = os.Getenv
2727

28+
// Options configures a tmux Runtime; every field has a default (see New).
2829
type Options struct {
2930
Binary string
3031
Timeout time.Duration
3132
Shell string
3233
}
3334

35+
// Runtime runs agent sessions inside tmux sessions, driving them via the tmux
36+
// CLI. It implements ports.Runtime.
3437
type Runtime struct {
3538
binary string
3639
timeout time.Duration
@@ -50,6 +53,8 @@ func (execRunner) Run(ctx context.Context, name string, args ...string) ([]byte,
5053
return exec.CommandContext(ctx, name, args...).CombinedOutput()
5154
}
5255

56+
// New builds a tmux Runtime, filling unset Options with defaults: binary
57+
// "tmux", shell from $SHELL (else /bin/sh), and the default timeout.
5358
func New(opts Options) *Runtime {
5459
binary := opts.Binary
5560
if binary == "" {
@@ -69,6 +74,8 @@ func New(opts Options) *Runtime {
6974
return &Runtime{binary: binary, timeout: timeout, shell: shellPath, runner: execRunner{}}
7075
}
7176

77+
// Create starts a new tmux session in the workspace, running the agent's
78+
// launch command, and returns a handle to it.
7279
func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) {
7380
id, err := tmuxSessionName(cfg.SessionID)
7481
if err != nil {
@@ -92,6 +99,8 @@ func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.Ru
9299
return ports.RuntimeHandle{ID: id, RuntimeName: runtimeName}, nil
93100
}
94101

102+
// Destroy kills the handle's tmux session. An already-gone session is treated
103+
// as success.
95104
func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error {
96105
id, err := handleID(handle)
97106
if err != nil {
@@ -107,6 +116,8 @@ func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error
107116
return nil
108117
}
109118

119+
// SendMessage types a message into the session's pane and presses Enter,
120+
// routing large messages through a tmux paste buffer.
110121
func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error {
111122
id, err := handleID(handle)
112123
if err != nil {
@@ -124,6 +135,7 @@ func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, m
124135
return nil
125136
}
126137

138+
// GetOutput captures the last `lines` lines of the session pane.
127139
func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lines int) (string, error) {
128140
id, err := handleID(handle)
129141
if err != nil {
@@ -139,6 +151,7 @@ func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lin
139151
return string(out), nil
140152
}
141153

154+
// IsAlive reports whether the handle's tmux session still exists.
142155
func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) {
143156
id, err := handleID(handle)
144157
if err != nil {
@@ -155,6 +168,8 @@ func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool
155168
return false, fmt.Errorf("tmux runtime: probe session %s: %w", id, err)
156169
}
157170

171+
// AttachCommand returns the argv a human runs to attach their terminal to the
172+
// session.
158173
func (r *Runtime) AttachCommand(handle ports.RuntimeHandle) ([]string, error) {
159174
id, err := handleID(handle)
160175
if err != nil {
@@ -170,7 +185,7 @@ func (r *Runtime) sendViaBuffer(ctx context.Context, id, message string) error {
170185
return fmt.Errorf("tmux runtime: create message temp file: %w", err)
171186
}
172187
path := file.Name()
173-
defer os.Remove(path)
188+
defer func() { _ = os.Remove(path) }()
174189
if _, err := file.WriteString(message); err != nil {
175190
_ = file.Close()
176191
return fmt.Errorf("tmux runtime: write message temp file: %w", err)

backend/internal/adapters/runtime/zellij/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func layoutString(workspacePath, shellPath string, shellArgs []string, shellComm
9999

100100
func shellLaunchCommand(cfg ports.RuntimeConfig, shellPath string, spec shellLaunchSpec) string {
101101
if len(spec.args) > 0 && spec.args[0] == "-NoLogo" {
102-
return wrapLaunchCommandPowerShell(cfg, shellPath)
102+
return wrapLaunchCommandPowerShell(cfg)
103103
}
104104
if len(spec.args) > 0 && spec.args[0] == "/D" {
105105
return wrapLaunchCommandCmd(cfg)
@@ -136,7 +136,7 @@ func wrapLaunchCommandUnix(cfg ports.RuntimeConfig, shellPath string) string {
136136
return b.String()
137137
}
138138

139-
func wrapLaunchCommandPowerShell(cfg ports.RuntimeConfig, shellPath string) string {
139+
func wrapLaunchCommandPowerShell(cfg ports.RuntimeConfig) string {
140140
path := cfg.Env["PATH"]
141141
if path == "" {
142142
path = getenv("PATH")

backend/internal/adapters/runtime/zellij/zellij.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ const (
2929
)
3030

3131
var sessionIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
32-
var paneIDPattern = regexp.MustCompile(`^terminal_[0-9]+$`)
32+
var paneIDPattern = regexp.MustCompile(`^terminal_\d+$`)
3333

3434
var getenv = os.Getenv
3535

36+
// Options configures a zellij Runtime; every field has a sensible default
37+
// (see New), so the zero value is usable.
3638
type Options struct {
3739
Binary string
3840
Timeout time.Duration
@@ -42,6 +44,8 @@ type Options struct {
4244
ChunkSize int
4345
}
4446

47+
// Runtime runs agent sessions inside zellij sessions, driving them via the
48+
// zellij CLI. It implements ports.Runtime.
4549
type Runtime struct {
4650
binary string
4751
timeout time.Duration
@@ -68,6 +72,9 @@ func (execRunner) Run(ctx context.Context, env []string, name string, args ...st
6872
return cmd.CombinedOutput()
6973
}
7074

75+
// New builds a zellij Runtime, filling unset Options with defaults: binary
76+
// "zellij", shell from $SHELL (else /bin/sh, or powershell.exe on Windows), and
77+
// the default timeout and output chunk size.
7178
func New(opts Options) *Runtime {
7279
binary := opts.Binary
7380
if binary == "" {
@@ -95,6 +102,8 @@ func New(opts Options) *Runtime {
95102
return &Runtime{binary: binary, timeout: timeout, shell: shellPath, socketDir: opts.SocketDir, configDir: opts.ConfigDir, chunkSize: chunkSize, runner: execRunner{}}
96103
}
97104

105+
// Create starts a new zellij session in the workspace, running the agent's
106+
// launch command, and returns a handle to it.
98107
func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) {
99108
id, err := zellijSessionName(cfg.SessionID)
100109
if err != nil {
@@ -114,7 +123,7 @@ func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.Ru
114123
if err != nil {
115124
return ports.RuntimeHandle{}, err
116125
}
117-
defer os.Remove(layoutPath)
126+
defer func() { _ = os.Remove(layoutPath) }()
118127

119128
if _, err := r.run(ctx, createSessionArgs(id, layoutPath)...); err != nil {
120129
return ports.RuntimeHandle{}, fmt.Errorf("zellij runtime: create session %s: %w", id, err)
@@ -127,6 +136,8 @@ func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.Ru
127136
return ports.RuntimeHandle{ID: handleIDValue(id, paneID), RuntimeName: runtimeName}, nil
128137
}
129138

139+
// Destroy kills the handle's zellij session. An already-gone session is treated
140+
// as success.
130141
func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error {
131142
id, _, err := handleID(handle)
132143
if err != nil {
@@ -142,6 +153,8 @@ func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error
142153
return nil
143154
}
144155

156+
// SendMessage pastes a message into the session's pane (chunked) and presses
157+
// Enter to submit it.
145158
func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error {
146159
id, paneID, err := handleID(handle)
147160
if err != nil {
@@ -158,6 +171,7 @@ func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, m
158171
return nil
159172
}
160173

174+
// GetOutput returns the last `lines` lines of the session pane's screen dump.
161175
func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lines int) (string, error) {
162176
id, paneID, err := handleID(handle)
163177
if err != nil {
@@ -173,6 +187,8 @@ func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lin
173187
return tailLines(string(out), lines), nil
174188
}
175189

190+
// IsAlive reports whether the handle's session still appears in `zellij
191+
// list-sessions`.
176192
func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) {
177193
id, _, err := handleID(handle)
178194
if err != nil {
@@ -189,6 +205,8 @@ func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool
189205
return sessionListedAlive(string(out), id), nil
190206
}
191207

208+
// AttachCommand returns the argv a human runs to attach their terminal to the
209+
// session.
192210
func (r *Runtime) AttachCommand(handle ports.RuntimeHandle) ([]string, error) {
193211
id, _, err := handleID(handle)
194212
if err != nil {
@@ -420,7 +438,7 @@ func chunks(s string, maxBytes int) []string {
420438
return []string{s}
421439
}
422440
parts := []string{}
423-
for len(s) > 0 {
441+
for s != "" {
424442
if len(s) <= maxBytes {
425443
parts = append(parts, s)
426444
break

backend/internal/adapters/tracker/github/auth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var ErrNoToken = errors.New("github tracker: no token configured")
2222
// StaticTokenSource is a literal token, typically used in tests.
2323
type StaticTokenSource string
2424

25+
// Token returns the literal token, or ErrNoToken if it is blank.
2526
func (s StaticTokenSource) Token(context.Context) (string, error) {
2627
t := strings.TrimSpace(string(s))
2728
if t == "" {
@@ -39,6 +40,8 @@ type EnvTokenSource struct {
3940
EnvVars []string
4041
}
4142

43+
// Token returns the first non-empty configured env var (falling back to
44+
// GITHUB_TOKEN), or ErrNoToken if none is set.
4245
func (s EnvTokenSource) Token(context.Context) (string, error) {
4346
for _, name := range s.EnvVars {
4447
if v := strings.TrimSpace(os.Getenv(name)); v != "" {

0 commit comments

Comments
 (0)