Phase 1 of mathias/skills extraction (infra#62 Track D — homelab next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill dirs + SKILLS_INDEX.md) and adds the installation surface: - Taskfile.yml — install / update / list / release / check targets - install.sh — bootstrap installer for hosts without Task. Idempotent symlink wirer; default checkout at ~/.local/share/skills/ on every host; SKILLS_REF env var pins a tag (default: main). - .gitea/workflows/release.yml — auto-tag every push to main by Bump-Type footer (major/minor/patch, default patch). Skipped when commit contains [skip-release]. - README — usage, versioning, contribution flow, secret-hygiene rule. Phase 1 wires Claude Code only (~/.claude/skills/<name> global + <repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode, antigravity, and gitea-resident agents (cobalt-dingo, agentsquad) once their skill conventions are researched. Public repo, markdown-only — no secrets, no client names. Verified via pre-push grep before initial push. [skip-release]
11 KiB
name, description
| name | description |
|---|---|
| code-review | Systematic code smell detection and Go-specific review checklist. Load before any pre-merge review. |
Code Review
Overview
Code review is smell detection and principle verification, not style policing. The goal is to identify patterns that make code hard to understand, change, test, or safely operate.
This skill is read-only analysis. When you find issues, document them with specific file:line references and prioritize by architectural impact. For fixes, load the refactoring or clean-code skill.
When to Use
- Pre-merge review of any pull request
- Before committing a significant change
- When asked to "review this code"
- After the GREEN phase of TDD (use with
clean-codeskill)
Review Process
Phase 0: Iron Laws (block on any violation)
Before reading the diff, scan for these. Any violation is a blocking finding regardless of how clean the rest of the change looks.
- No security vulnerabilities — command injection, SQL injection, credential exposure, path traversal, unchecked input at system boundaries
- No silently swallowed errors —
err != nilwithout wrapping, logging, or surfacing is always wrong;_ = f()on a fallible call is wrong - No missing validation at system boundaries — user input, external API responses, and file reads must be validated before use
If any iron law is violated, flag it as Blocking before any other phase. The other phases still run, but the iron-law findings come first in the report. See Phase 3 for detailed patterns and code examples covering iron laws 2 and 3.
Phase 1: Context
Before examining code:
- Understand what the change is trying to accomplish
- Identify the affected packages and their purpose
- Check test coverage — is the behavior tested?
- Read the commit message and PR description
Phase 2: Architectural Analysis (Highest Priority)
Check for structural problems first. These are expensive to fix later.
Dependency violations:
- Does domain logic import infrastructure packages?
- Are there circular imports between packages?
- Does a type do things that multiple different stakeholders would request changes to?
Abstraction problems:
- Is there a large switch/if-else on a type string that should be an interface?
- Are there God types that know too much about too many things?
- Is there Shotgun Surgery — one logical change requires editing many files?
Phase 3: Go-Specific Checklist
Error Handling
// Required: wrap with context
return fmt.Errorf("parse invoice: %w", err)
// Forbidden: naked return
return err
// Forbidden: log and return (pick one)
log.Error("failed", "err", err)
return err
// Forbidden: silently ignore
_ = doSomething()
// Suspicious: sentinel errors without documentation
var ErrFoo = errors.New("foo") // Is this exported? Is it documented?
Goroutine Safety
- Is shared state protected by a mutex or accessed only via channels?
- Are goroutines guaranteed to terminate (no goroutine leaks)?
- Is
sync.WaitGroupused correctly (Add before go, Done via defer)? - Is there a
go test -racepassing?
// Check: is this safe for concurrent use?
type Cache struct {
mu sync.RWMutex // Good: explicit lock
items map[string]Item
}
// Red flag: map access without lock in concurrent context
type BadCache struct {
items map[string]Item // Unsafe if accessed from multiple goroutines
}
Exported API Minimalism
- Only export what callers outside the package actually need
- Internal implementation details should be unexported
- Does the exported API make sense as a standalone contract?
// Good: minimal exported surface
type UserService struct { /* unexported fields */ }
func NewUserService(...) *UserService { ... }
func (s *UserService) GetUser(ctx context.Context, id UserID) (User, error) { ... }
// Bad: over-exported internals
type UserService struct {
Store UserStore // Exported field — callers can replace it, breaks encapsulation
Logger *slog.Logger // Exported — callers can mutate it
}
Dependency Safety
Before adding any new dependency:
- Has
govulncheckbeen run? (govulncheck ./...) - Is there a justification in the commit message for non-stdlib additions?
- Pre-approved without justification:
testify,slog,templ,sqlc - Everything else needs a reason
Generic/interface{} Usage
// Bad: any/interface{} when a concrete type or generic works
func Process(data interface{}) error { ... }
// Good: typed
func Process[T Processor](data T) error { ... }
// Good: concrete type when only one type is expected
func ProcessOrder(o *Order) error { ... }
Context Propagation
- Every function that does I/O (DB, HTTP, file) should accept
ctx context.Contextas the first parameter - Context should be passed down, not stored in structs
context.Background()should only appear at the top-level (main, test setup)
// Good
func (s *Store) GetUser(ctx context.Context, id UserID) (User, error) { ... }
// Bad: no context — can't cancel, can't trace, can't timeout
func (s *Store) GetUser(id UserID) (User, error) { ... }
// Bad: context stored in struct
type Handler struct {
ctx context.Context // Don't do this
}
Logging with slog
// Good: structured, leveled
slog.InfoContext(ctx, "user created", "user_id", u.ID, "email", u.Email)
// Bad: unstructured
log.Printf("user created: %s", u.Email)
// Bad: logging errors that are also returned
slog.Error("failed to save", "err", err)
return fmt.Errorf("save user: %w", err) // caller is double-counting
Phase 4: Code Smell Detection
Apply the catalog from clean-code/references/code-smells.md. Key smells to prioritize:
Architectural impact (fix immediately):
- Global Data — package-level variables modified at runtime
- Shotgun Surgery — one change touches many unrelated files
- Feature Envy — a method uses another package's internals more than its own
- God Object — a type that does everything
Design impact (fix before merge if possible):
- Long Method — functions > ~50 lines in Go (Go can be longer than OOP, but be honest)
- Long Parameter List — > 4-5 parameters; consider a config struct
- Duplicate Code — same logic copy-pasted (wait for Rule of Three)
- Message Chain —
a.B().C().D()— violates Law of Demeter - Primitive Obsession — using
stringfor email, user ID, currency
Readability (note in review, fix if cheap):
- Magic Numbers — bare literals without named constants
- Uncommunicative Names —
data,info,x,tmp - Fallacious Comments — comments that don't match the code
Phase 5: Test Quality
- Are new behaviors covered by tests?
- Are tests table-driven where appropriate?
- Do tests test behavior, not implementation?
- Are there mock-only tests that test nothing real?
For comprehensive test review, load the test-design skill.
Severity Classification
| Severity | Description | Action |
|---|---|---|
| Blocking | Security issue, data corruption risk, broken behavior | Must fix before merge |
| High | Architectural violation, goroutine safety, missing context | Fix before merge |
| Medium | Design smell, missing tests for changed behavior | Fix in this PR or create issue |
| Low | Naming, style, minor duplication | Note in review, optional |
Output Format
When documenting findings:
## Code Review: [package/file]
### Blocking
- `internal/store/user.go:47`: Error swallowed with `_ =` — callers can't know if Save failed
### High
- `internal/handler/order.go:12`: Domain logic in HTTP handler — moves tax calculation out of service layer
- `cmd/server/main.go:89`: Missing context propagation — `GetUser` call has no timeout
### Medium
- `internal/service/invoice.go:34`: Long method (87 lines) — extract `calculateLineItems` and `applyTaxRules`
### Low
- `internal/domain/user.go:5`: Field `Data string` — name reveals nothing about what data
Brain MCP Integration
The brain MCP holds prior review decisions across sessions. Use it to avoid re-litigating the same tradeoffs.
At review start:
- Run
brain_querywith the package or feature name + "review" to surface prior review findings, recurring smells, or architectural decisions for this area. This prevents flagging the same smell that was already accepted as a tradeoff.
After review completes:
- For findings the author accepts as a tradeoff (with rationale), run
brain_writeto record the decision. Future reviews will not re-flag it.
Never:
- Embed brain queries in the review output. They are context for you, not findings for the author.
Logging
Call session_log once at the end of every phase to record the outcome.
Pass-rate is computed downstream by the /pass-rate HTTP endpoint, which
treats pass as success, fail as failure, skip as neither.
At end of each phase:
session_logwith{skill: "code-review", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}
Phases for this skill: phase-0-iron-laws, phase-1-context, phase-2-architectural-analysis, phase-3-go-checklist, phase-4-code-smell-detection, phase-5-test-quality
Status semantics:
pass— the phase's intended outcome was reached.fail— the phase's intended outcome was NOT reached.skip— phase was skipped intentionally.
Why this matters: the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
Common Go Anti-Patterns
| Anti-Pattern | What to Look For |
|---|---|
| Goroutine leak | go func() without a done channel or WaitGroup |
| Error ignored | _ = f() or missing if err != nil after a fallible call |
| Nil map write | m[k] = v without m = make(map[...]{}) initialization |
| Race on map | Concurrent map read/write without mutex |
| ctx in struct | type T struct { ctx context.Context } |
| Naked panic | panic("something went wrong") in non-initialization code |
| fmt.Println in library | Use slog or accept a logger; never print to stdout from lib code |
| init() abuse | init() with side effects that make testing hard |
References
clean-code/references/code-smells.md— comprehensive smell catalog- Load
refactoringskill when recommending fixes - Load
test-designskill for reviewing test quality - Load
solidskill for SOLID principle violations
Mode 2 Routing Note
This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes.