/** * Go Review — Code review powered by "100 Go Mistakes and How to Avoid Them" * * Registers a go_review tool that reads Go code changes (git diff) and returns * them alongside the complete 100 Go Mistakes checklist from https://100go.co/ * The LLM reviews the diff against all 100 mistakes, producing categorized suggestions. * * Features: * - Reviews staged, unstaged, commit, or range diffs filtered to .go files * - Embeds ALL 100 Go mistakes as the review rubric (11 categories) * - Categorizes findings: Bug/Critical, Suggestion, Nit, Good pattern * - Custom TUI rendering for call + result * - System prompt injection so the agent auto-invokes when reviewing Go code * * Based on: "100 Go Mistakes and How to Avoid Them" — https://100go.co/ * Usage: pi -e extensions/go-review.ts */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { StringEnum } from "@mariozechner/pi-ai"; import { Text } from "@mariozechner/pi-tui"; import { applyExtensionDefaults } from "./themeMap.ts"; const GO_MISTAKES = [ "## 100 Go Mistakes Review Checklist (https://100go.co/)", "### Code and Project Organization", "#1 Unintended variable shadowing — Redeclaring in inner block silently references wrong var. Use govet -shadow.", "#2 Unnecessary nested code — Happy path left-aligned. If returns, omit else. Return early.", "#3 Misusing init functions — Limits error handling, complicates testing. Use explicit init functions.", "#4 Overusing getters/setters — Not idiomatic Go. Use only when adding value.", "#5 Interface pollution — Discover abstractions, do not create them prematurely.", "#6 Interface on producer side — Keep interfaces on consumer side.", "#7 Returning interfaces — Return concrete types. Accept interfaces, return structs.", "#8 any says nothing — Only use any when genuinely needed. Kills type safety.", "#9 Confused about generics — Use only for concrete needs. Premature generics add complexity.", "#10 Type embedding problems — Promotes fields/methods. Can expose hidden behaviors.", "#11 Not using functional options — type Option func(*options) error for optional config.", "#12 Project misorganization — Follow go.dev/doc/modules/layout. Be consistent.", "#13 Creating utility packages — Name packages after what they provide, not contain.", "#14 Package name collisions — Avoid variable-package name collisions. Use import aliases.", "#15 Missing code documentation — Document every exported element starting with its name.", "#16 Not using linters — govet, errcheck, golangci-lint, gofmt. Automate in CI.", "### Data Types", "#17 Octal literal confusion — Prefix with 0o. Bare 0-prefix is octal.", "#18 Neglecting integer overflows — Silent at runtime. Implement overflow checks.", "#19 Not understanding floating-points — Approximate. Compare within delta. Order operations.", "#20 Slice length vs capacity — Length = elements; Capacity = backing array.", "#21 Inefficient slice initialization — make([]T, len) or make([]T, 0, cap) when known.", "#22 nil vs empty slice — Different things. encoding/json and reflect distinguish them.", "#23 Checking slice emptiness — len(s)==0, not s==nil. Works for nil and empty.", "#24 Slice copy mistakes — copy() uses min(len(dst),len(src)). Dst needs length.", "#25 Slice append side effects — Sub-slice with capacity: append mutates original. Use s[l:h:m].", "#26 Slices and memory leaks — Slicing keeps backing array. copy() to release.", "#27 Inefficient map initialization — make(map[K]V, size) when count known.", "#28 Maps and memory leaks — Maps never shrink. Recreate or use pointers.", "#29 Comparing values incorrectly — == only for comparable. reflect.DeepEqual for complex.", "### Control Structures", "#30 Range copies elements — Value is copy. Access via index to mutate.", "#31 Range expression evaluated once — Copied before loop. Changes not reflected.", "#32 Range loop pointer issue (Go<1.22) — Loop var reuse. Fixed in Go 1.22+.", "#33 Map iteration assumptions — No order, non-deterministic, additions may not appear.", "#34 Break terminates wrong statement — Hits innermost for/switch/select. Use labeled break.", "#35 Defer inside loop — Runs on function return. Extract to function for per-iteration.", "### Strings", "#36 Rune concept — Unicode code point 1-4 bytes. len()=bytes not runes.", "#37 Inaccurate string iteration — range by byte pos. s[i]=byte. Use range value.", "#38 Misusing trim — TrimRight/Left=rune set. TrimSuffix/Prefix=exact string.", "#39 Under-optimized concatenation — strings.Builder in loops, not +=. Grow(n).", "#40 Useless string conversions — bytes pkg mirrors strings. Use []byte for I/O.", "#41 Substring memory leaks — Share backing array. strings.Clone() or copy.", "### Functions and Methods", "#42 Wrong receiver type — Pointer: mutating/large. Value: immutable/small. Default pointer.", "#43 Named result parameters — Readability for same-type. Zero-initialized.", "#44 Named result side effects — Zero-init error returned as nil. Subtle bug.", "#45 Returning nil receiver — Typed nil ptr is non-nil interface. Return explicit nil.", "#46 Filename as function input — Accept io.Reader for reusability/testability.", "#47 Defer argument evaluation — Evaluated immediately. Use pointer/closure.", "### Error Management", "#48 Panicking — Only unrecoverable. Return errors for everything else.", "#49 When to wrap errors — %w for context+unwrap. %v to prevent dependency.", "#50 Error type comparison — errors.As not type assertion with wrapped errors.", "#51 Error value comparison — errors.Is not == with wrapped sentinel errors.", "#52 Handling error twice — Log OR return, not both. Use %w wrapping.", "#53 Not handling errors — _ = fn() to explicitly ignore.", "#54 Defer errors — Do not ignore. Propagate or _ = with comment.", "### Concurrency: Foundations", "#55 Concurrency vs parallelism — Structure vs execution. Concurrency enables parallelism.", "#56 Not always faster — Overhead. Benchmark to validate.", "#57 Channels vs mutexes — Parallel=mutexes. Concurrent=channels.", "#58 Race problems — Data race: simultaneous write. Race condition: timing. Different.", "#59 Workload type — CPU-bound: ~GOMAXPROCS. I/O-bound: external dependent.", "#60 Go contexts — Deadlines, cancellation, key-values. Accept in blocking funcs.", "### Concurrency: Practice", "#61 Inappropriate context — HTTP ctx cancels on response. Use WithoutCancel for bg.", "#62 Goroutine without stop plan — Every goroutine needs shutdown. context/close/defer.", "#63 Goroutines+loop vars (Go<1.22) — Vars reused. Pass as args. Fixed 1.22+.", "#64 Non-deterministic select — Multiple ready: random pick. No order guarantee.", "#65 Notification channels — chan struct{} for signals. Zero-size.", "#66 Nil channels — Block forever. Remove select cases dynamically.", "#67 Channel size — Unbuffered=sync. Default to 1. Queues rarely balanced.", "#68 String formatting deadlocks — fmt calls String() may acquire locks.", "#69 Data races with append — Shared slice+capacity: race. Copy or separate.", "#70 Mutex with slices/maps — Assignment copies header not data. Deep-copy.", "#71 WaitGroup misuse — Add() before spawn, not inside goroutine.", "#72 Forgetting sync.Cond — Broadcast signaling to multiple goroutines.", "#73 Not using errgroup — Goroutine groups with error handling.", "#74 Copying sync types — Never copy after use. Pointer. go vet.", "### Standard Library", "#75 Wrong time duration — time.NewTicker(1000)=1000ns. Use time.Second.", "#76 time.After leaks — New timer each call. Use NewTimer+Reset in loops.", "#77 JSON mistakes — Embedded fields, time format, float64 numbers.", "#78 SQL mistakes — Placeholders, close Rows, pool config, context.", "#79 Not closing resources — HTTP bodies, sql.Rows, os.File. defer.", "#80 Missing return after HTTP reply — Handler continues. Return after write.", "#81 Default HTTP client — No timeout=hangs. Configure all timeouts.", "### Testing", "#82 Not categorizing tests — Build tags, env vars, testing.Short().", "#83 Not enabling -race — Always in CI. Catches data races.", "#84 Not using parallel/shuffle — t.Parallel(), -shuffle. Quality+speed.", "#85 Not using table-driven tests — Struct with inputs/expected. Easy to add.", "#86 Sleeping in tests — Slow/flaky. Use channels, sync, polling.", "#87 Time API in tests — Abstract behind interface. Inject clock.", "#88 Not using httptest/iotest — httptest.NewServer, iotest.ErrReader.", "#89 Inaccurate benchmarks — ResetTimer, prevent elimination, RunParallel.", "#90 Not exploring testing features — Helper, Cleanup, testdata, TempDir.", "#91 Not using fuzzing — Go 1.18+ native. Discovers edge cases.", "### Optimizations", "#92 CPU cache ignorance — Data locality. Structs-of-arrays.", "#93 False sharing — Adjacent writes same cache line. Pad 64 bytes.", "#94 Instruction-level parallelism — Enable ILP. Avoid data dependencies.", "#95 Data alignment — Largest fields first. Minimize padding.", "#96 Stack vs heap — Stack fast. Escape analysis. Minimize pointer indirection.", "#97 Reducing allocations — sync.Pool, preallocate, value types.", "#98 Inlining — Small funcs inlined. Keep hot functions small.", "#99 Diagnostics tooling — pprof, tracer, benchmarks. Profile first.", "#100 GC understanding — Concurrent mark-sweep. GOGC. Reduce allocations.", "#101 Go in Docker/K8s — GOMAXPROCS=host CPUs. Use automaxprocs.", ].join("\n"); interface GoReviewDetails { mode: string; ref?: string; path?: string; filesChanged: number; insertions: number; deletions: number; goFilesFound: number; truncated: boolean; } export default function (pi: ExtensionAPI) { pi.registerTool({ name: "go_review", label: "Go Review", description: "Review Go code changes against the 100 Go Mistakes checklist by Teivah. " + "Reads git diffs (staged, unstaged, commits, or ranges), filters to .go files, " + "and returns the diff alongside the complete 100-mistake rubric for analysis. " + "Use this whenever reviewing Go code, PRs, or changes before committing.", promptSnippet: "Review Go code changes using the 100 Go Mistakes checklist", promptGuidelines: [ "Use go_review when the user asks to review Go code, check Go changes, or audit a Go PR.", "After receiving the diff and checklist, analyze every changed file against relevant mistakes.", "Categorize each finding: Bug/Critical, Suggestion, Nit, Good pattern.", "Always cite the mistake number (e.g. #39) and the specific file and line/code fragment.", "End with a verdict: Approve, Request Changes, or Needs Discussion.", ], parameters: Type.Object({ mode: StringEnum(["working", "staged", "commit", "range", "all"] as const, { description: "working=unstaged, staged=cached, commit=specific SHA, range=two refs, all=HEAD diff", }), ref: Type.Optional(Type.String({ description: "Commit SHA, branch, or range (e.g. main..HEAD). Required for commit/range." })), path: Type.Optional(Type.String({ description: "Limit to file or directory (e.g. internal/auth)" })), }), async execute(_toolCallId, params, signal, _onUpdate, _ctx) { const { mode, ref, path: fp } = params; const args: string[] = []; switch (mode) { case "working": args.push("diff", "--stat", "--patch", "--", "*.go"); break; case "staged": args.push("diff", "--cached", "--stat", "--patch", "--", "*.go"); break; case "all": args.push("diff", "HEAD", "--stat", "--patch", "--", "*.go"); break; case "commit": if (!ref) throw new Error("ref required for commit mode"); args.push("show", "--stat", "--patch", ref, "--", "*.go"); break; case "range": if (!ref) throw new Error("ref required for range mode"); args.push("diff", "--stat", "--patch", ref, "--", "*.go"); break; } if (fp) { args[args.length - 1] = fp.endsWith(".go") ? fp : fp + "/**/*.go"; if (!fp.endsWith(".go")) args.push(fp + "/*.go"); } const r = await pi.exec("git", args, { signal, timeout: 30000 }); if (r.code !== 0) throw new Error("git failed (" + r.code + "): " + r.stderr); if (!r.stdout.trim()) { return { content: [{ type: "text" as const, text: "No Go file changes found. Try: staged, working, all, commit, or range." }], details: { mode, ref, path: fp, filesChanged: 0, insertions: 0, deletions: 0, goFilesFound: 0, truncated: false } as GoReviewDetails, }; } const sl = r.stdout.split("\n").find(function(l) { return /\d+ files? changed/.test(l); }) || ""; const fc = parseInt((sl.match(/(\d+) files? changed/) || [])[1] || "0"); const ins = parseInt((sl.match(/(\d+) insertions?/) || [])[1] || "0"); const del = parseInt((sl.match(/(\d+) deletions?/) || [])[1] || "0"); const gm = r.stdout.match(/^diff --git a\/.*\.go b\/.*\.go$/gm); const gf = gm ? gm.length : 0; const ML = 1500; const lns = r.stdout.split("\n"); let trunc = false; let dt = r.stdout; if (lns.length > ML) { dt = lns.slice(0, ML).join("\n"); trunc = true; } let t = "## Go Code Review: " + mode + (ref ? " " + ref : "") + (fp ? " (" + fp + ")" : "") + "\n\n"; t += "**" + gf + "** Go files, **+" + ins + "** / **-" + del + "**\n\n"; t += "### Diff\n\n```diff\n" + dt + "\n```\n\n"; if (trunc) t += "> Truncated to " + ML + " lines. Use path param to focus.\n\n"; t += "---\n\n### Review Instructions\n\n"; t += "Analyze the diff against the 100 Go Mistakes checklist below.\n"; t += "For each issue: cite **mistake #**, **file:line/fragment**, categorize (Bug/Suggestion/Nit).\n"; t += "Note Good patterns. End with **Verdict**: Approve / Request Changes / Needs Discussion.\n"; t += "Only flag mistakes **actually present**. Most impactful first.\n\n"; t += GO_MISTAKES; return { content: [{ type: "text" as const, text: t }], details: { mode, ref, path: fp, filesChanged: fc, insertions: ins, deletions: del, goFilesFound: gf, truncated: trunc } as GoReviewDetails, }; }, renderCall(args, theme, _ctx) { const cc: Record = { working: "warning", staged: "accent", commit: "success", range: "success", all: "warning" }; let t = theme.fg("toolTitle", theme.bold("go_review ")); t += theme.fg(cc[args.mode] || "accent", args.mode); if (args.ref) t += theme.fg("muted", " " + args.ref); if (args.path) t += theme.fg("dim", " — " + args.path); t += theme.fg("dim", " (100 Go Mistakes)"); return new Text(t, 0, 0); }, renderResult(result, { expanded, isPartial }, theme, _ctx) { if (isPartial) return new Text(theme.fg("warning", "Scanning Go changes..."), 0, 0); const d = result.details as GoReviewDetails | undefined; if (!d || d.goFilesFound === 0) return new Text(theme.fg("dim", "No Go changes found"), 0, 0); let t = theme.fg("accent", d.goFilesFound + " Go files"); t += theme.fg("dim", " | ") + theme.fg("success", "+" + d.insertions) + theme.fg("dim", "/") + theme.fg("error", "-" + d.deletions); t += theme.fg("dim", " | ") + theme.fg("muted", "100 Go Mistakes checklist"); if (d.truncated) t += theme.fg("warning", " (truncated)"); if (expanded) { t += "\n" + theme.fg("dim", "─".repeat(50)); const c = result.content[0]; if (c && c.type === "text") { const sl = c.text.split("\n").filter(function(l: string) { return l.includes("|") && l.includes("+"); }).slice(0, 8); for (const line of sl) t += "\n" + theme.fg("dim", " " + line.trim()); if (sl.length === 0) t += "\n" + theme.fg("dim", " (expand for diff + checklist)"); } } return new Text(t, 0, 0); }, }); pi.on("session_start", async (_event, ctx) => { applyExtensionDefaults(import.meta.url, ctx); ctx.addSystemPrompt("go-review", function() { return "You have the go_review tool: reviews Go code against 100 Go Mistakes (https://100go.co/). " + "Invoke it when user asks to review Go code/PRs/changes. Produce structured review: " + "Bug/Critical, Suggestion, Nit, Good pattern. Cite mistake # and line. End with Verdict."; }); }); }