[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6E356MZ2EDAYSGVXTKGP6Y6":3,"skill-same:01M6E356MZ2EDAYSGVXTKGP6Y6":89},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":61,"linkOnly":62,"markdownContent":63,"markdownBody":64,"frontMatter":65,"refName":66,"commitSha":67,"skillDirectory":68,"forksCount":69,"repositoryHtmlUrl":70,"repositoryHomepageUrl":71,"ownerHtmlUrl":72,"ownerBlogUrl":6,"ownerDescription":73,"updatedAt":74,"install":75,"resources":88},"01M6E356MZ2EDAYSGVXTKGP6Y6","code-guidelines-go",null,"Go 1.24–1.27 coding guidelines for the dimetron\u002Fpi-go AI agent runtime. Use this skill whenever writing, reviewing, or refactoring ANY Go code in pi-go. This covers idiomatic style, error handling, concurrency, project layout, testing (table-driven, fuzz, benchmarks, synctest), new stdlib usage, golangci-lint v2 configuration, slog logging, and manual dependency injection with functional options. Always consult this skill before generating Go code for pi-go, even for small snippets, to ensure every output is idiomatic and modern.","dimetron\u002Fpi-go","dimetron","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F877433?v=4","01M6E34QX9HKYKMWJVZQEPR2Q3","Dmytro Rashko","01M6E34R2A6563XM0PTZE8KPGG",224,"MIT","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002F.pi-go\u002Fskills\u002Fcode-guidelines-go\u002FSKILL.md",[18,23,26],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"go","guidelines","code","code guidelines go","code guidelines","go ai","idiomatic","for","usage","functional","error","style","layout","project","snippets","options","dependency","use","testing","pi","coding","table","refactoring","consult","concurrency","injection","lint","ai agent","writing","2026-11-10 10:12:30",false,"---\nname: code-guidelines-go\ndescription: >\n  Go 1.24–1.27 coding guidelines for the dimetron\u002Fpi-go AI agent runtime.\n  Use this skill whenever writing, reviewing, or refactoring ANY Go code in pi-go.\n  This covers idiomatic style, error handling, concurrency, project layout, testing\n  (table-driven, fuzz, benchmarks, synctest), new stdlib usage, golangci-lint v2\n  configuration, slog logging, and manual dependency injection with functional\n  options. Always consult this skill before generating Go code for pi-go, even for\n  small snippets, to ensure every output is idiomatic and modern.\n---\n\n# Go 1.24–1.27 Coding Guidelines — pi-go\n\n**Project**: `github.com\u002Fdimetron\u002Fpi-go` — Go-native AI coding agent runtime  \n**Layout**: `cmd\u002Fpi\u002F` · `internal\u002F` · `.pi-go\u002F` · `.vibe\u002Fcompiled\u002F` · `scripts\u002F`  \n**Min version**: Go 1.24 (tool directive support) | Target: Go 1.27\n\n> For what Go 1.27 changed — generic methods, struct-literal field selectors,\n> `encoding\u002Fjson\u002Fv2`, the stdlib `uuid` package, removals that break a build, and the\n> `GOEXPERIMENT=none` trap on this machine — see the **`go-127`** skill.\n\n---\n\n## 1. Style & Naming\n\n### Packages\n- Short, lowercase, singular, no underscores: `config` ✓ `agentUtils` ✗\n- No stutter: package `agent` → `type Runner` (not `AgentRunner`)\n- Never `util`, `common`, `types`, `helpers`\n\n### Identifiers\n- Receivers: 1–2 chars from type name, consistent across all methods, never `self`\u002F`this`\n- Acronyms full caps: `ID`, `URL`, `HTTP`, `LLM` — enforced by staticcheck\n- Short names in small scopes (`i`, `ctx`, `err`), descriptive in large scopes\n\n```go\n\u002F\u002F ✓ Correct\nfunc (r *Runner) Execute(ctx context.Context) error { ... }\nfunc (r *Runner) Stop()                             { ... }\n\n\u002F\u002F ✗ Wrong\nfunc (self *Runner) Execute(ctx context.Context) error { ... }\nfunc (runner *Runner) Stop()                           { ... }\n```\n\n### Interfaces\n- Define at the **consumer**, not producer\n- 1–3 methods; single-method uses `-er` suffix (`Reader`, `Handler`, `Closer`)\n- Accept interfaces, return concrete structs\n\n```go\n\u002F\u002F consumer defines what it needs (internal\u002Fagent\u002F)\ntype LLMClient interface {\n    Complete(ctx context.Context, prompt string) (string, error)\n}\n\n\u002F\u002F producer returns concrete struct (internal\u002Fllm\u002F)\nfunc NewOpenAIClient(apiKey string) *OpenAIClient { ... }\n```\n\n### Godoc\nEvery exported name starts its comment with the name itself:\n```go\n\u002F\u002F Package agent provides the core AI agent runtime for pi-go.\npackage agent\n\n\u002F\u002F Runner orchestrates tool calls and LLM interactions.\ntype Runner struct { ... }\n\n\u002F\u002F Execute runs the agent loop until completion or context cancellation.\nfunc (r *Runner) Execute(ctx context.Context, task string) error { ... }\n```\n\n---\n\n## 2. Idiomatic & Functional Style\n\n### Guard clauses — early return, flat code\nAvoid deep nesting. Return errors early, keep the happy path at the left edge:\n```go\n\u002F\u002F ✓ Guard clauses\nfunc (r *Runner) Start(ctx context.Context) error {\n    if r.running {\n        return ErrAlreadyRunning\n    }\n    if r.client == nil {\n        return errors.New(\"nil client\")\n    }\n    return r.loop(ctx)\n}\n\n\u002F\u002F ✗ Nested conditionals\nfunc (r *Runner) Start(ctx context.Context) error {\n    if !r.running {\n        if r.client != nil {\n            return r.loop(ctx)\n        }\n        return errors.New(\"nil client\")\n    }\n    return ErrAlreadyRunning\n}\n```\n\n### Pure functions — isolate logic from I\u002FO\nExtract testable logic into pure functions that take inputs and return outputs.\nKeep side effects (network, disk, logging) at the edges:\n```go\n\u002F\u002F ✓ Pure — easy to test, no mocks needed\nfunc buildPrompt(system string, history []Message, tools []Tool) string { ... }\nfunc mergeToolResults(existing, incoming []ToolResult) []ToolResult { ... }\nfunc selectModel(budget Budget, task Task) string { ... }\n\n\u002F\u002F Wire I\u002FO at the caller\nfunc (r *Runner) Execute(ctx context.Context, task string) error {\n    prompt := buildPrompt(r.system, r.history, r.tools)  \u002F\u002F pure\n    resp, err := r.client.Complete(ctx, prompt)           \u002F\u002F side effect\n    ...\n}\n```\n\n### Avoid init() — explicit initialization\n`init()` hides execution order and makes testing harder. Wire everything in `main` or constructors:\n```go\n\u002F\u002F ✗ Hidden global state\nfunc init() { registry.Register(\"search\", searchTool) }\n\n\u002F\u002F ✓ Explicit wiring\nfunc NewRegistry(tools ...Tool) *Registry { ... }\n```\n\n### Value semantics — return values, don't mutate pointers\nPrefer returning new values over mutating inputs. Makes data flow visible:\n```go\n\u002F\u002F ✓ Value in, value out\nfunc withDefaults(cfg Config) Config {\n    if cfg.Timeout == 0 { cfg.Timeout = 30 * time.Second }\n    if cfg.Model == \"\" { cfg.Model = \"claude-sonnet-4-5-20250514\" }\n    return cfg\n}\n\n\u002F\u002F ✗ Mutate in place — caller can't see what changed\nfunc applyDefaults(cfg *Config) { ... }\n```\n\n### Function types as first-class values\nUse named function types to simplify callback and middleware patterns:\n```go\n\u002F\u002F Named function type — implements Handler implicitly\ntype ToolFunc func(ctx context.Context, input json.RawMessage) (string, error)\n\n\u002F\u002F Higher-order function — returns a decorated version\nfunc WithTimeout(d time.Duration, fn ToolFunc) ToolFunc {\n    return func(ctx context.Context, input json.RawMessage) (string, error) {\n        ctx, cancel := context.WithTimeout(ctx, d)\n        defer cancel()\n        return fn(ctx, input)\n    }\n}\n```\n\n### Composition over flags — small composable pieces\nPrefer combining simple functions over adding boolean parameters:\n```go\n\u002F\u002F ✗ Boolean flags multiply code paths\nfunc Send(msg Message, retry bool, validate bool) error { ... }\n\n\u002F\u002F ✓ Compose behaviors\nfunc Send(msg Message) error { ... }\nfunc WithRetry(n int, fn func(Message) error) func(Message) error { ... }\nfunc WithValidation(fn func(Message) error) func(Message) error { ... }\n\n\u002F\u002F Usage: send := WithRetry(3, WithValidation(Send))\n```\n\n### No naked returns — always name what you return\n```go\n\u002F\u002F ✗ Naked return — unclear what's being returned\nfunc parse(s string) (result int, err error) {\n    result, err = strconv.Atoi(s)\n    return\n}\n\n\u002F\u002F ✓ Explicit return values\nfunc parse(s string) (int, error) {\n    return strconv.Atoi(s)\n}\n```\n\n---\n\n## 3. Error Handling\n\n**Rule**: Add context at every boundary. Return OR log an error — never both.\n\n```go\nfunc LoadConfig(path string) (*Config, error) {\n    data, err := os.ReadFile(path)\n    if err != nil {\n        return nil, fmt.Errorf(\"read config %s: %w\", path, err)\n    }\n    var cfg Config\n    if err := json.Unmarshal(data, &cfg); err != nil {\n        return nil, fmt.Errorf(\"parse config %s: %w\", path, err)\n    }\n    return &cfg, nil\n}\n```\n\n### errors.Is \u002F errors.As \u002F errors.AsType (Go 1.26)\n```go\nvar ErrNotFound = errors.New(\"not found\")   \u002F\u002F sentinel — package-level var\n\n\u002F\u002F errors.Is — value matching through chain\nif errors.Is(err, ErrNotFound) { ... }\n\n\u002F\u002F errors.AsType — generic, no pre-declaration needed (Go 1.26)\nif pathErr, ok := errors.AsType[*os.PathError](err); ok {\n    slog.Error(\"path error\", \"path\", pathErr.Path)\n}\n```\n\n### Concurrent errors\nUse `errgroup` for first-error semantics; use `errors.Join` to collect all:\n```go\ng, ctx := errgroup.WithContext(ctx)\ng.SetLimit(10)\nfor _, r := range records {\n    g.Go(func() error { return process(ctx, r) })\n}\nreturn g.Wait()\n```\n\n---\n\n## 4. Concurrency\n\n### context.Context — always first parameter, never stored in struct\n```go\n\u002F\u002F ✓ Correct\nfunc (s *Service) Process(ctx context.Context, req Request) error {\n    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n    defer cancel()\n    return s.client.Call(ctx, req)\n}\n```\n\n### Top-level context (main.go)\n```go\nctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\ndefer cancel()\nif err := run(ctx); err != nil && !errors.Is(err, context.Canceled) {\n    slog.Error(\"fatal\", \"error\", err)\n    os.Exit(1)\n}\n```\n\n### WaitGroup.Go replaces Add\u002FDone (Go 1.25)\n```go\nvar wg sync.WaitGroup\nfor _, item := range items {\n    wg.Go(func() { processItem(item) }) \u002F\u002F loop var capture safe since 1.22\n}\nwg.Wait()\n```\n\n### sync.OnceValue for lazy singletons (Go 1.21+)\n```go\nvar getConfig = sync.OnceValue(func() *Config {\n    cfg, err := loadConfig()\n    if err != nil { panic(fmt.Sprintf(\"load config: %v\", err)) }\n    return cfg\n})\n\u002F\u002F Usage: cfg := getConfig()\n```\n\n### Goroutine leak prevention\n- Every goroutine must have an exit path (context cancel, channel close, done signal)\n- In tests, use `goleak.VerifyTestMain(m)` in `TestMain`\n- `goroutineleak` pprof profile — GA in Go 1.26 → 1.27, no GOEXPERIMENT needed.\n  Scrape `\u002Fdebug\u002Fpprof\u002Fgoroutineleak`, or `pprof.Lookup(\"goroutineleak\")`.\n  It misses leaks whose primitive is reachable via a global or a runnable goroutine's\n  locals, so keep `goleak` in tests. See `go-127` §3.2\n\n---\n\n## 5. Project Layout\n\n```\npi-go\u002F\n├── cmd\u002Fpi\u002Fmain.go           # Thin wiring only — no business logic\n├── internal\u002F\n│   ├── agent\u002F               # Core runner, tool dispatch\n│   ├── config\u002F              # Config loading\n│   ├── llm\u002F                 # LLM client implementations\n│   └── tools\u002F               # Tool registry\n├── .pi-go\u002F                  # Templates & runtime config\n├── .vibe\u002Fcompiled\u002F          # Compiled vibe assets\n├── scripts\u002F                 # Build \u002F dev scripts\n├── go.mod\n├── go.sum\n├── Makefile\n└── .golangci.yml\n```\n\n- No `pkg\u002F` directory — this is a CLI app, not a library\n- `main.go` only: parse config, wire deps, call `run(ctx)`\n\n### Tool directives (Go 1.24) — replaces tools.go\n```\n\u002F\u002F go.mod\ntool (\n    github.com\u002Fgolangci\u002Fgolangci-lint\u002Fcmd\u002Fgolangci-lint\n)\n```\n```bash\ngo tool golangci-lint run   # run tracked tool\ngo get -u tool              # upgrade all tools\n```\n\n### Embedding dot-prefixed directories\n```go\n\u002F\u002Fgo:embed all:.pi-go          \u002F\u002F \"all:\" required — dot-files excluded otherwise\nvar PiGoConfig embed.FS\n\n\u002F\u002Fgo:embed all:.vibe\u002Fcompiled\nvar VibeCompiled embed.FS\n```\n\n### Makefile targets\n```makefile\nbuild:\n\tgo build -ldflags \"-X main.version=$(VERSION)\" -o bin\u002Fpi .\u002Fcmd\u002Fpi\u002F\ntest:\n\tgo test -v -race -buildvcs .\u002F...\nlint:\n\tgo tool golangci-lint run\naudit: test lint\n\tgo mod tidy -diff && go mod verify\n```\n\n---\n\n## 6. Testing\n\n### Table-driven tests with t.Run\n```go\nfunc TestParseDuration(t *testing.T) {\n    tests := []struct {\n        name    string\n        input   string\n        want    time.Duration\n        wantErr bool\n    }{\n        {\"valid seconds\", \"30s\", 30 * time.Second, false},\n        {\"empty string\", \"\", 0, true},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            t.Parallel()                                     \u002F\u002F always parallelise subtests\n            got, err := ParseDuration(tt.input)\n            if (err != nil) != tt.wantErr {\n                t.Fatalf(\"error = %v, wantErr %v\", err, tt.wantErr)\n            }\n            if diff := cmp.Diff(tt.want, got); diff != \"\" {\n                t.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n            }\n        })\n    }\n}\n```\n- Use `t.Helper()` as first line of every test helper\n- `t.Context()` (Go 1.24) — auto-cancelled context, no manual setup needed\n- Use `google\u002Fgo-cmp` for struct comparison over `testify\u002Fassert`\n- Package convention: `package foo_test` for API tests, `package foo` for white-box\n\n### Fuzz testing (input parsing must be fuzzed)\n```go\nfunc FuzzParseCommand(f *testing.F) {\n    f.Add(\"run task --model gpt-4\")\n    f.Add(\"\")\n    f.Fuzz(func(t *testing.T, input string) {\n        cmd, err := ParseCommand(input)\n        if err != nil { t.Skip() }\n        if got := cmd.String(); got != input {\n            if _, err2 := ParseCommand(got); err2 != nil {\n                t.Errorf(\"round-trip failed: %v\", err2)\n            }\n        }\n    })\n}\n\u002F\u002F Run: go test -fuzz=FuzzParseCommand -fuzztime=30s\n\u002F\u002F Failures auto-saved to testdata\u002Ffuzz\u002F as regression tests\n```\n\n### b.Loop() for benchmarks (Go 1.24) — replaces b.N\n```go\nfunc BenchmarkProcess(b *testing.B) {\n    data := prepareData()          \u002F\u002F setup excluded from timing automatically\n    b.ReportAllocs()\n    for b.Loop() {                 \u002F\u002F no b.ResetTimer(), no sink var needed\n        process(data)\n    }\n}\n\u002F\u002F Run: go test -bench=. -benchmem\n\u002F\u002F Compare: benchstat old.txt new.txt\n```\n\n### testing\u002Fsynctest — deterministic concurrency tests (Go 1.24→1.25 stable)\nUse `synctest.Test` for ANY test involving timers, tickers, timeouts, or context cancellation. Eliminates `time.Sleep` flakiness entirely:\n```go\nfunc TestAgentTimeout(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)\n        defer cancel()\n\n        synctest.Sleep(5*time.Second - time.Nanosecond)  \u002F\u002F sleep + Wait (Go 1.27)\n        if ctx.Err() != nil {\n            t.Fatal(\"should not have timed out yet\")\n        }\n        synctest.Sleep(time.Nanosecond)\n        if !errors.Is(ctx.Err(), context.DeadlineExceeded) {\n            t.Fatal(\"should have timed out\")\n        }\n    })\n}\n```\n> **Note**: `synctest.Run` was deprecated — use `synctest.Test` (Go 1.25+).  \n> `synctest.Sleep(d)` (Go 1.27) replaces the `time.Sleep(d); synctest.Wait()` pair —\n> prefer it inside a bubble, since a bare `time.Sleep` leaves the wake order between the\n> test and the code under test unpredictable when both sleep the same duration.  \n> `httptest.NewTestServer(t, h)` (Go 1.27) gives an in-memory HTTP server that runs in\n> synthetic time inside a bubble and self-cleans — no `defer srv.Close()`.  \n> `t.Attr(key, value)` (Go 1.25) emits structured metadata in test output.\n\n---\n\n## 7. Modern Stdlib — Use These Instead of External Libs\n\n| Task | Old \u002F External | ✓ Use Instead |\n|------|---------------|--------------|\n| Random numbers | `math\u002Frand` | `math\u002Frand\u002Fv2` |\n| Slice sort\u002Fsearch | hand-written | `slices.Sort`, `slices.Contains` |\n| Map utilities | hand-written | `maps.Keys`, `maps.Clone` |\n| Default value | ternary logic | `cmp.Or(a, b, \"default\")` |\n| Custom iteration | returning slices | `iter.Seq[V]`, `iter.Seq2[K,V]` |\n| Lazy singleton | `sync.Once` + var | `sync.OnceValue` |\n| Directory FS safety | path.Join | `os.Root` (prevents traversal) |\n| Zero-value JSON omit | `omitempty` | `omitzero` tag |\n| Split on last separator | `strings.LastIndex` + slicing | `strings.CutLast` \u002F `bytes.CutLast` (1.27) |\n| UUIDs | `github.com\u002Fgoogle\u002Fuuid` | stdlib `uuid` (1.27); `uuid.NewV7()` for keys |\n\n```go\n\u002F\u002F iter.Seq2 for lazy DB rows\nfunc (db *DB) Rows(ctx context.Context) iter.Seq2[Row, error] {\n    return func(yield func(Row, error) bool) {\n        rows, _ := db.Query(ctx)\n        defer rows.Close()\n        for rows.Next() {\n            var r Row; rows.Scan(&r)\n            if !yield(r, nil) { return }\n        }\n    }\n}\n\n\u002F\u002F slices + cmp replacing manual loops\nslices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Name, b.Name) })\nsortedKeys := slices.Sorted(maps.Keys(m))\nport := cmp.Or(envPort, flagPort, \"8080\")\n```\n\n### Go 1.24 new packages (use where relevant)\n- `weak.Make(obj)` — weak pointer for memory-efficient caches\n- `unique.Make(v)` — value interning for fast equality\n- `crypto\u002Fmlkem`, `crypto\u002Fhkdf`, `crypto\u002Fpbkdf2`, `crypto\u002Fsha3` — prefer over external\n- `os.Root` — scoped filesystem access, prevents path traversal\n\n### Go 1.26 syntax\n```go\n\u002F\u002F new(expr) — eliminates ptr() helper boilerplate\ntimeout := new(30 * time.Second)   \u002F\u002F *time.Duration pointing to 30s\nenabled := new(true)               \u002F\u002F *bool\n\n\u002F\u002F errors.AsType — already shown in §2\n```\n\n### Go 1.27 syntax\n```go\n\u002F\u002F Generic methods — a generic operation can now live on the type it belongs to.\n\u002F\u002F Restriction: interfaces cannot declare them, and they cannot satisfy an interface\n\u002F\u002F method — so keep the package-level generic func wherever the call site is an\n\u002F\u002F interface (which, per §1, is most of pi-go).\nfunc (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{v: f(b.v)} }\n\n\u002F\u002F Struct literal keys may be any valid field selector — promoted fields included\nu := User{ID: 7, Name: \"Mittens\"}          \u002F\u002F was User{Base: Base{ID: 7}, Name: ...}\n\n\u002F\u002F Function type inference now applies in conversions and composite literals too\nops := []func([]int) int{first, last}      \u002F\u002F was {first[int], last[int]}\n```\nFull detail, including removals that break a build: **`go-127`** skill.\n\n---\n\n## 8. golangci-lint v2 — Mandatory Gate\n\n**All Go code MUST pass `golangci-lint run` before commit.** Run it after every change:\n```bash\ngolangci-lint run .\u002F...          # check all packages\ngolangci-lint run .\u002Finternal\u002F... # check specific subtree\n```\n\nIf a linter fires, fix the code — do not add `\u002F\u002Fnolint` without a comment explaining why the suppression is necessary.\n\n### Active linters (from `.golangci.yml`)\n\n| Category | Linters |\n|----------|---------|\n| **Correctness** | `errcheck`, `govet`, `staticcheck`, `unused`, `ineffassign` |\n| **Style & bugs** | `bodyclose`, `copyloopvar`, `durationcheck`, `errname`, `errorlint`, `fatcontext`, `misspell`, `nilerr`, `revive`, `unconvert`, `wastedassign` |\n| **Formatters** | `gofmt`, `goimports` (local prefix: `github.com\u002Fdimetron\u002Fpi-go`) |\n\n### Key settings to be aware of\n\n- **errcheck**: `check-type-assertions: true` — always handle type assertion ok values\n- **govet**: all analyzers enabled except `fieldalignment` and `shadow`\n- **revive**: enforces `indent-error-flow` (guard clauses), `receiver-naming`, `error-strings`, `superfluous-else`, `empty-block`\n- **staticcheck**: all checks enabled (ST1000 package comments excluded for internal)\n- **misspell**: US locale\n\n### Exclusions\n\n- **Test files** (`_test.go`): `errcheck`, `bodyclose`, `nilerr` relaxed\n- **`internal\u002Ftools\u002F`**: `nilerr` relaxed (errors returned inside result structs)\n- **`internal\u002F(lsp|cli|memory|tui)\u002F`**: `nilerr` relaxed (callback wrappers)\n- **`research\u002F`**: most linters disabled (experimental code)\n\n### Import ordering (enforced by goimports)\n```go\nimport (\n    \u002F\u002F 1. stdlib\n    \"context\"\n    \"fmt\"\n\n    \u002F\u002F 2. third-party\n    \"github.com\u002Fcharmbracelet\u002Fbubbletea\"\n\n    \u002F\u002F 3. local (auto-grouped by goimports local-prefixes)\n    \"github.com\u002Fdimetron\u002Fpi-go\u002Finternal\u002Fconfig\"\n)\n```\n\n### CI (GitHub Actions)\n```yaml\nlint:\n  runs-on: ubuntu-latest\n  steps:\n    - uses: actions\u002Fcheckout@v5\n    - uses: actions\u002Fsetup-go@v6\n      with: { go-version: '1.27' }\n    - uses: golangci\u002Fgolangci-lint-action@v9\n      with: { version: v2 }\n\ntest:\n  runs-on: ubuntu-latest\n  steps:\n    - uses: actions\u002Fcheckout@v5\n    - uses: actions\u002Fsetup-go@v6\n      with: { go-version: '1.27' }\n    - run: go test -v -race -coverprofile=coverage.out .\u002F...\n    - run: go mod tidy -diff && go mod verify\n```\n\n---\n\n## 9. Logging — log\u002Fslog (stdlib only)\n\n**Decision**: `log\u002Fslog` for pi-go. Zero deps, swappable handler, context-aware.  \nIf throughput ever matters: swap backend to `zapslog.NewHandler()` — no call-site changes.\n\n```go\n\u002F\u002F internal\u002Flogging\u002Flogging.go\nfunc NewLogger(level slog.Level, jsonOutput bool) *slog.Logger {\n    opts := &slog.HandlerOptions{Level: level, AddSource: level \u003C= slog.LevelDebug}\n    var h slog.Handler\n    if jsonOutput {\n        h = slog.NewJSONHandler(os.Stderr, opts)  \u002F\u002F production\n    } else {\n        h = slog.NewTextHandler(os.Stderr, opts)  \u002F\u002F development\n    }\n    return slog.New(h)\n}\n```\n\n- **Never** use `log` (stdlib old), `fmt.Println`, or init a global `zap.Logger` — `depguard` will catch it\n- Pass logger via constructor, not context (context logging = middleware only)\n- Use `.With()` for component-scoped fields\n- **Go 1.26**: `slog.NewMultiHandler(h1, h2)` for fan-out\n\n```go\ntype Runner struct { logger *slog.Logger }\n\nfunc NewRunner(client LLMClient, logger *slog.Logger) *Runner {\n    return &Runner{logger: logger.With(\"component\", \"runner\")}\n}\n\n\u002F\u002F In methods\nr.logger.Info(\"executing task\", slog.String(\"task\", task), slog.Int(\"iter\", n))\nr.logger.Error(\"tool failed\", slog.Any(\"error\", err), slog.String(\"tool\", name))\n```\n\n---\n\n## 10. Dependency Injection — Manual + Functional Options\n\nNo Wire, no fx. Manual constructor injection wired in `cmd\u002Fpi\u002Fmain.go`.\n\n### Functional options for configurable constructors\n```go\ntype clientConfig struct {\n    model   string\n    timeout time.Duration\n}\n\ntype Option func(*clientConfig)\n\nfunc WithModel(m string) Option        { return func(c *clientConfig) { c.model = m } }\nfunc WithTimeout(d time.Duration) Option { return func(c *clientConfig) { c.timeout = d } }\n\nfunc NewOpenAIClient(apiKey string, opts ...Option) *OpenAIClient {\n    cfg := &clientConfig{model: \"gpt-4o\", timeout: 30 * time.Second}\n    for _, o := range opts { o(cfg) }\n    return &OpenAIClient{apiKey: apiKey, cfg: cfg}\n}\n```\n\n### Composition root (cmd\u002Fpi\u002Fmain.go)\n```go\nfunc run(ctx context.Context) error {\n    cfg := config.Load()\n    logger := logging.NewLogger(cfg.LogLevel, cfg.JSONOutput)\n    slog.SetDefault(logger)\n\n    llmClient := llm.NewOpenAIClient(cfg.APIKey,\n        llm.WithModel(cfg.Model),\n        llm.WithTimeout(cfg.Timeout),\n    )\n    registry := tools.NewRegistry(logger, tools.WithBuiltins())\n    runner := agent.NewRunner(llmClient, registry, logger,\n        agent.WithMaxIterations(cfg.MaxIterations),\n    )\n    return runner.Run(ctx, os.Args[1:])\n}\n```\n\n### Interface-based test mocks — no mock framework needed\n```go\ntype mockLLM struct{ response string; err error }\nfunc (m *mockLLM) Complete(_ context.Context, _ string) (string, error) {\n    return m.response, m.err\n}\n\nfunc TestRunner_Execute(t *testing.T) {\n    r := agent.NewRunner(&mockLLM{response: \"ok\"}, nil, slog.Default())\n    if err := r.Execute(t.Context(), \"task\"); err != nil {\n        t.Fatalf(\"unexpected: %v\", err)\n    }\n}\n```\n\n---\n\n## Quick Reference\n\n| Concern | Use | Avoid |\n|---------|-----|-------|\n| Control flow | Guard clauses, early return | Deep nesting |\n| Logic | Pure functions, value in\u002Fout | Mutating pointer args |\n| Composition | Small functions + higher-order combinators | Boolean flag parameters |\n| Init | Explicit constructors | `init()` functions |\n| Returns | Explicit return values | Naked returns |\n| Lint gate | `golangci-lint run` before commit | `\u002F\u002Fnolint` without justification |\n| Imports | stdlib \u002F third-party \u002F local (3 groups) | Mixed or unsorted imports |\n| Logging | `log\u002Fslog` | `log`, `zap`, `zerolog` |\n| DI | manual constructors + functional options | Wire, fx |\n| Error wrapping | `fmt.Errorf(\"...: %w\", err)` | `%v` when caller needs to unwrap |\n| Error type check | `errors.AsType[T]` (1.26) \u002F `errors.As` | type assertions |\n| Benchmarks | `b.Loop()` | `for i := 0; i \u003C b.N; i++` |\n| Concurrent tests | `synctest.Test` + `synctest.Sleep` (1.27) | `time.Sleep` in tests |\n| HTTP test server | `httptest.NewTestServer(t, h)` (1.27) | `httptest.NewServer` + `defer Close()` |\n| Split on last separator | `strings.CutLast` (1.27) | `strings.LastIndex` + slicing |\n| UUIDs | stdlib `uuid` (1.27) | `github.com\u002Fgoogle\u002Fuuid` |\n| WaitGroup | `wg.Go(func(){...})` (1.25) | `wg.Add(1); defer wg.Done()` |\n| Slice ops | `slices.*`, `maps.*` | hand-written loops |\n| Randomness | `math\u002Frand\u002Fv2` | `math\u002Frand` |\n| Tool deps | `go.mod tool directive` (1.24) | `tools.go` blank imports |\n","# Go 1.24–1.27 Coding Guidelines — pi-go\n\n**Project**: `github.com\u002Fdimetron\u002Fpi-go` — Go-native AI coding agent runtime  \n**Layout**: `cmd\u002Fpi\u002F` · `internal\u002F` · `.pi-go\u002F` · `.vibe\u002Fcompiled\u002F` · `scripts\u002F`  \n**Min version**: Go 1.24 (tool directive support) | Target: Go 1.27\n\n> For what Go 1.27 changed — generic methods, struct-literal field selectors,\n> `encoding\u002Fjson\u002Fv2`, the stdlib `uuid` package, removals that break a build, and the\n> `GOEXPERIMENT=none` trap on this machine — see the **`go-127`** skill.\n\n---\n\n## 1. Style & Naming\n\n### Packages\n- Short, lowercase, singular, no underscores: `config` ✓ `agentUtils` ✗\n- No stutter: package `agent` → `type Runner` (not `AgentRunner`)\n- Never `util`, `common`, `types`, `helpers`\n\n### Identifiers\n- Receivers: 1–2 chars from type name, consistent across all methods, never `self`\u002F`this`\n- Acronyms full caps: `ID`, `URL`, `HTTP`, `LLM` — enforced by staticcheck\n- Short names in small scopes (`i`, `ctx`, `err`), descriptive in large scopes\n\n```go\n\u002F\u002F ✓ Correct\nfunc (r *Runner) Execute(ctx context.Context) error { ... }\nfunc (r *Runner) Stop()                             { ... }\n\n\u002F\u002F ✗ Wrong\nfunc (self *Runner) Execute(ctx context.Context) error { ... }\nfunc (runner *Runner) Stop()                           { ... }\n```\n\n### Interfaces\n- Define at the **consumer**, not producer\n- 1–3 methods; single-method uses `-er` suffix (`Reader`, `Handler`, `Closer`)\n- Accept interfaces, return concrete structs\n\n```go\n\u002F\u002F consumer defines what it needs (internal\u002Fagent\u002F)\ntype LLMClient interface {\n    Complete(ctx context.Context, prompt string) (string, error)\n}\n\n\u002F\u002F producer returns concrete struct (internal\u002Fllm\u002F)\nfunc NewOpenAIClient(apiKey string) *OpenAIClient { ... }\n```\n\n### Godoc\nEvery exported name starts its comment with the name itself:\n```go\n\u002F\u002F Package agent provides the core AI agent runtime for pi-go.\npackage agent\n\n\u002F\u002F Runner orchestrates tool calls and LLM interactions.\ntype Runner struct { ... }\n\n\u002F\u002F Execute runs the agent loop until completion or context cancellation.\nfunc (r *Runner) Execute(ctx context.Context, task string) error { ... }\n```\n\n---\n\n## 2. Idiomatic & Functional Style\n\n### Guard clauses — early return, flat code\nAvoid deep nesting. Return errors early, keep the happy path at the left edge:\n```go\n\u002F\u002F ✓ Guard clauses\nfunc (r *Runner) Start(ctx context.Context) error {\n    if r.running {\n        return ErrAlreadyRunning\n    }\n    if r.client == nil {\n        return errors.New(\"nil client\")\n    }\n    return r.loop(ctx)\n}\n\n\u002F\u002F ✗ Nested conditionals\nfunc (r *Runner) Start(ctx context.Context) error {\n    if !r.running {\n        if r.client != nil {\n            return r.loop(ctx)\n        }\n        return errors.New(\"nil client\")\n    }\n    return ErrAlreadyRunning\n}\n```\n\n### Pure functions — isolate logic from I\u002FO\nExtract testable logic into pure functions that take inputs and return outputs.\nKeep side effects (network, disk, logging) at the edges:\n```go\n\u002F\u002F ✓ Pure — easy to test, no mocks needed\nfunc buildPrompt(system string, history []Message, tools []Tool) string { ... }\nfunc mergeToolResults(existing, incoming []ToolResult) []ToolResult { ... }\nfunc selectModel(budget Budget, task Task) string { ... }\n\n\u002F\u002F Wire I\u002FO at the caller\nfunc (r *Runner) Execute(ctx context.Context, task string) error {\n    prompt := buildPrompt(r.system, r.history, r.tools)  \u002F\u002F pure\n    resp, err := r.client.Complete(ctx, prompt)           \u002F\u002F side effect\n    ...\n}\n```\n\n### Avoid init() — explicit initialization\n`init()` hides execution order and makes testing harder. Wire everything in `main` or constructors:\n```go\n\u002F\u002F ✗ Hidden global state\nfunc init() { registry.Register(\"search\", searchTool) }\n\n\u002F\u002F ✓ Explicit wiring\nfunc NewRegistry(tools ...Tool) *Registry { ... }\n```\n\n### Value semantics — return values, don't mutate pointers\nPrefer returning new values over mutating inputs. Makes data flow visible:\n```go\n\u002F\u002F ✓ Value in, value out\nfunc withDefaults(cfg Config) Config {\n    if cfg.Timeout == 0 { cfg.Timeout = 30 * time.Second }\n    if cfg.Model == \"\" { cfg.Model = \"claude-sonnet-4-5-20250514\" }\n    return cfg\n}\n\n\u002F\u002F ✗ Mutate in place — caller can't see what changed\nfunc applyDefaults(cfg *Config) { ... }\n```\n\n### Function types as first-class values\nUse named function types to simplify callback and middleware patterns:\n```go\n\u002F\u002F Named function type — implements Handler implicitly\ntype ToolFunc func(ctx context.Context, input json.RawMessage) (string, error)\n\n\u002F\u002F Higher-order function — returns a decorated version\nfunc WithTimeout(d time.Duration, fn ToolFunc) ToolFunc {\n    return func(ctx context.Context, input json.RawMessage) (string, error) {\n        ctx, cancel := context.WithTimeout(ctx, d)\n        defer cancel()\n        return fn(ctx, input)\n    }\n}\n```\n\n### Composition over flags — small composable pieces\nPrefer combining simple functions over adding boolean parameters:\n```go\n\u002F\u002F ✗ Boolean flags multiply code paths\nfunc Send(msg Message, retry bool, validate bool) error { ... }\n\n\u002F\u002F ✓ Compose behaviors\nfunc Send(msg Message) error { ... }\nfunc WithRetry(n int, fn func(Message) error) func(Message) error { ... }\nfunc WithValidation(fn func(Message) error) func(Message) error { ... }\n\n\u002F\u002F Usage: send := WithRetry(3, WithValidation(Send))\n```\n\n### No naked returns — always name what you return\n```go\n\u002F\u002F ✗ Naked return — unclear what's being returned\nfunc parse(s string) (result int, err error) {\n    result, err = strconv.Atoi(s)\n    return\n}\n\n\u002F\u002F ✓ Explicit return values\nfunc parse(s string) (int, error) {\n    return strconv.Atoi(s)\n}\n```\n\n---\n\n## 3. Error Handling\n\n**Rule**: Add context at every boundary. Return OR log an error — never both.\n\n```go\nfunc LoadConfig(path string) (*Config, error) {\n    data, err := os.ReadFile(path)\n    if err != nil {\n        return nil, fmt.Errorf(\"read config %s: %w\", path, err)\n    }\n    var cfg Config\n    if err := json.Unmarshal(data, &cfg); err != nil {\n        return nil, fmt.Errorf(\"parse config %s: %w\", path, err)\n    }\n    return &cfg, nil\n}\n```\n\n### errors.Is \u002F errors.As \u002F errors.AsType (Go 1.26)\n```go\nvar ErrNotFound = errors.New(\"not found\")   \u002F\u002F sentinel — package-level var\n\n\u002F\u002F errors.Is — value matching through chain\nif errors.Is(err, ErrNotFound) { ... }\n\n\u002F\u002F errors.AsType — generic, no pre-declaration needed (Go 1.26)\nif pathErr, ok := errors.AsType[*os.PathError](err); ok {\n    slog.Error(\"path error\", \"path\", pathErr.Path)\n}\n```\n\n### Concurrent errors\nUse `errgroup` for first-error semantics; use `errors.Join` to collect all:\n```go\ng, ctx := errgroup.WithContext(ctx)\ng.SetLimit(10)\nfor _, r := range records {\n    g.Go(func() error { return process(ctx, r) })\n}\nreturn g.Wait()\n```\n\n---\n\n## 4. Concurrency\n\n### context.Context — always first parameter, never stored in struct\n```go\n\u002F\u002F ✓ Correct\nfunc (s *Service) Process(ctx context.Context, req Request) error {\n    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n    defer cancel()\n    return s.client.Call(ctx, req)\n}\n```\n\n### Top-level context (main.go)\n```go\nctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\ndefer cancel()\nif err := run(ctx); err != nil && !errors.Is(err, context.Canceled) {\n    slog.Error(\"fatal\", \"error\", err)\n    os.Exit(1)\n}\n```\n\n### WaitGroup.Go replaces Add\u002FDone (Go 1.25)\n```go\nvar wg sync.WaitGroup\nfor _, item := range items {\n    wg.Go(func() { processItem(item) }) \u002F\u002F loop var capture safe since 1.22\n}\nwg.Wait()\n```\n\n### sync.OnceValue for lazy singletons (Go 1.21+)\n```go\nvar getConfig = sync.OnceValue(func() *Config {\n    cfg, err := loadConfig()\n    if err != nil { panic(fmt.Sprintf(\"load config: %v\", err)) }\n    return cfg\n})\n\u002F\u002F Usage: cfg := getConfig()\n```\n\n### Goroutine leak prevention\n- Every goroutine must have an exit path (context cancel, channel close, done signal)\n- In tests, use `goleak.VerifyTestMain(m)` in `TestMain`\n- `goroutineleak` pprof profile — GA in Go 1.26 → 1.27, no GOEXPERIMENT needed.\n  Scrape `\u002Fdebug\u002Fpprof\u002Fgoroutineleak`, or `pprof.Lookup(\"goroutineleak\")`.\n  It misses leaks whose primitive is reachable via a global or a runnable goroutine's\n  locals, so keep `goleak` in tests. See `go-127` §3.2\n\n---\n\n## 5. Project Layout\n\n```\npi-go\u002F\n├── cmd\u002Fpi\u002Fmain.go           # Thin wiring only — no business logic\n├── internal\u002F\n│   ├── agent\u002F               # Core runner, tool dispatch\n│   ├── config\u002F              # Config loading\n│   ├── llm\u002F                 # LLM client implementations\n│   └── tools\u002F               # Tool registry\n├── .pi-go\u002F                  # Templates & runtime config\n├── .vibe\u002Fcompiled\u002F          # Compiled vibe assets\n├── scripts\u002F                 # Build \u002F dev scripts\n├── go.mod\n├── go.sum\n├── Makefile\n└── .golangci.yml\n```\n\n- No `pkg\u002F` directory — this is a CLI app, not a library\n- `main.go` only: parse config, wire deps, call `run(ctx)`\n\n### Tool directives (Go 1.24) — replaces tools.go\n```\n\u002F\u002F go.mod\ntool (\n    github.com\u002Fgolangci\u002Fgolangci-lint\u002Fcmd\u002Fgolangci-lint\n)\n```\n```bash\ngo tool golangci-lint run   # run tracked tool\ngo get -u tool              # upgrade all tools\n```\n\n### Embedding dot-prefixed directories\n```go\n\u002F\u002Fgo:embed all:.pi-go          \u002F\u002F \"all:\" required — dot-files excluded otherwise\nvar PiGoConfig embed.FS\n\n\u002F\u002Fgo:embed all:.vibe\u002Fcompiled\nvar VibeCompiled embed.FS\n```\n\n### Makefile targets\n```makefile\nbuild:\n\tgo build -ldflags \"-X main.version=$(VERSION)\" -o bin\u002Fpi .\u002Fcmd\u002Fpi\u002F\ntest:\n\tgo test -v -race -buildvcs .\u002F...\nlint:\n\tgo tool golangci-lint run\naudit: test lint\n\tgo mod tidy -diff && go mod verify\n```\n\n---\n\n## 6. Testing\n\n### Table-driven tests with t.Run\n```go\nfunc TestParseDuration(t *testing.T) {\n    tests := []struct {\n        name    string\n        input   string\n        want    time.Duration\n        wantErr bool\n    }{\n        {\"valid seconds\", \"30s\", 30 * time.Second, false},\n        {\"empty string\", \"\", 0, true},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            t.Parallel()                                     \u002F\u002F always parallelise subtests\n            got, err := ParseDuration(tt.input)\n            if (err != nil) != tt.wantErr {\n                t.Fatalf(\"error = %v, wantErr %v\", err, tt.wantErr)\n            }\n            if diff := cmp.Diff(tt.want, got); diff != \"\" {\n                t.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n            }\n        })\n    }\n}\n```\n- Use `t.Helper()` as first line of every test helper\n- `t.Context()` (Go 1.24) — auto-cancelled context, no manual setup needed\n- Use `google\u002Fgo-cmp` for struct comparison over `testify\u002Fassert`\n- Package convention: `package foo_test` for API tests, `package foo` for white-box\n\n### Fuzz testing (input parsing must be fuzzed)\n```go\nfunc FuzzParseCommand(f *testing.F) {\n    f.Add(\"run task --model gpt-4\")\n    f.Add(\"\")\n    f.Fuzz(func(t *testing.T, input string) {\n        cmd, err := ParseCommand(input)\n        if err != nil { t.Skip() }\n        if got := cmd.String(); got != input {\n            if _, err2 := ParseCommand(got); err2 != nil {\n                t.Errorf(\"round-trip failed: %v\", err2)\n            }\n        }\n    })\n}\n\u002F\u002F Run: go test -fuzz=FuzzParseCommand -fuzztime=30s\n\u002F\u002F Failures auto-saved to testdata\u002Ffuzz\u002F as regression tests\n```\n\n### b.Loop() for benchmarks (Go 1.24) — replaces b.N\n```go\nfunc BenchmarkProcess(b *testing.B) {\n    data := prepareData()          \u002F\u002F setup excluded from timing automatically\n    b.ReportAllocs()\n    for b.Loop() {                 \u002F\u002F no b.ResetTimer(), no sink var needed\n        process(data)\n    }\n}\n\u002F\u002F Run: go test -bench=. -benchmem\n\u002F\u002F Compare: benchstat old.txt new.txt\n```\n\n### testing\u002Fsynctest — deterministic concurrency tests (Go 1.24→1.25 stable)\nUse `synctest.Test` for ANY test involving timers, tickers, timeouts, or context cancellation. Eliminates `time.Sleep` flakiness entirely:\n```go\nfunc TestAgentTimeout(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)\n        defer cancel()\n\n        synctest.Sleep(5*time.Second - time.Nanosecond)  \u002F\u002F sleep + Wait (Go 1.27)\n        if ctx.Err() != nil {\n            t.Fatal(\"should not have timed out yet\")\n        }\n        synctest.Sleep(time.Nanosecond)\n        if !errors.Is(ctx.Err(), context.DeadlineExceeded) {\n            t.Fatal(\"should have timed out\")\n        }\n    })\n}\n```\n> **Note**: `synctest.Run` was deprecated — use `synctest.Test` (Go 1.25+).  \n> `synctest.Sleep(d)` (Go 1.27) replaces the `time.Sleep(d); synctest.Wait()` pair —\n> prefer it inside a bubble, since a bare `time.Sleep` leaves the wake order between the\n> test and the code under test unpredictable when both sleep the same duration.  \n> `httptest.NewTestServer(t, h)` (Go 1.27) gives an in-memory HTTP server that runs in\n> synthetic time inside a bubble and self-cleans — no `defer srv.Close()`.  \n> `t.Attr(key, value)` (Go 1.25) emits structured metadata in test output.\n\n---\n\n## 7. Modern Stdlib — Use These Instead of External Libs\n\n| Task | Old \u002F External | ✓ Use Instead |\n|------|---------------|--------------|\n| Random numbers | `math\u002Frand` | `math\u002Frand\u002Fv2` |\n| Slice sort\u002Fsearch | hand-written | `slices.Sort`, `slices.Contains` |\n| Map utilities | hand-written | `maps.Keys`, `maps.Clone` |\n| Default value | ternary logic | `cmp.Or(a, b, \"default\")` |\n| Custom iteration | returning slices | `iter.Seq[V]`, `iter.Seq2[K,V]` |\n| Lazy singleton | `sync.Once` + var | `sync.OnceValue` |\n| Directory FS safety | path.Join | `os.Root` (prevents traversal) |\n| Zero-value JSON omit | `omitempty` | `omitzero` tag |\n| Split on last separator | `strings.LastIndex` + slicing | `strings.CutLast` \u002F `bytes.CutLast` (1.27) |\n| UUIDs | `github.com\u002Fgoogle\u002Fuuid` | stdlib `uuid` (1.27); `uuid.NewV7()` for keys |\n\n```go\n\u002F\u002F iter.Seq2 for lazy DB rows\nfunc (db *DB) Rows(ctx context.Context) iter.Seq2[Row, error] {\n    return func(yield func(Row, error) bool) {\n        rows, _ := db.Query(ctx)\n        defer rows.Close()\n        for rows.Next() {\n            var r Row; rows.Scan(&r)\n            if !yield(r, nil) { return }\n        }\n    }\n}\n\n\u002F\u002F slices + cmp replacing manual loops\nslices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Name, b.Name) })\nsortedKeys := slices.Sorted(maps.Keys(m))\nport := cmp.Or(envPort, flagPort, \"8080\")\n```\n\n### Go 1.24 new packages (use where relevant)\n- `weak.Make(obj)` — weak pointer for memory-efficient caches\n- `unique.Make(v)` — value interning for fast equality\n- `crypto\u002Fmlkem`, `crypto\u002Fhkdf`, `crypto\u002Fpbkdf2`, `crypto\u002Fsha3` — prefer over external\n- `os.Root` — scoped filesystem access, prevents path traversal\n\n### Go 1.26 syntax\n```go\n\u002F\u002F new(expr) — eliminates ptr() helper boilerplate\ntimeout := new(30 * time.Second)   \u002F\u002F *time.Duration pointing to 30s\nenabled := new(true)               \u002F\u002F *bool\n\n\u002F\u002F errors.AsType — already shown in §2\n```\n\n### Go 1.27 syntax\n```go\n\u002F\u002F Generic methods — a generic operation can now live on the type it belongs to.\n\u002F\u002F Restriction: interfaces cannot declare them, and they cannot satisfy an interface\n\u002F\u002F method — so keep the package-level generic func wherever the call site is an\n\u002F\u002F interface (which, per §1, is most of pi-go).\nfunc (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{v: f(b.v)} }\n\n\u002F\u002F Struct literal keys may be any valid field selector — promoted fields included\nu := User{ID: 7, Name: \"Mittens\"}          \u002F\u002F was User{Base: Base{ID: 7}, Name: ...}\n\n\u002F\u002F Function type inference now applies in conversions and composite literals too\nops := []func([]int) int{first, last}      \u002F\u002F was {first[int], last[int]}\n```\nFull detail, including removals that break a build: **`go-127`** skill.\n\n---\n\n## 8. golangci-lint v2 — Mandatory Gate\n\n**All Go code MUST pass `golangci-lint run` before commit.** Run it after every change:\n```bash\ngolangci-lint run .\u002F...          # check all packages\ngolangci-lint run .\u002Finternal\u002F... # check specific subtree\n```\n\nIf a linter fires, fix the code — do not add `\u002F\u002Fnolint` without a comment explaining why the suppression is necessary.\n\n### Active linters (from `.golangci.yml`)\n\n| Category | Linters |\n|----------|---------|\n| **Correctness** | `errcheck`, `govet`, `staticcheck`, `unused`, `ineffassign` |\n| **Style & bugs** | `bodyclose`, `copyloopvar`, `durationcheck`, `errname`, `errorlint`, `fatcontext`, `misspell`, `nilerr`, `revive`, `unconvert`, `wastedassign` |\n| **Formatters** | `gofmt`, `goimports` (local prefix: `github.com\u002Fdimetron\u002Fpi-go`) |\n\n### Key settings to be aware of\n\n- **errcheck**: `check-type-assertions: true` — always handle type assertion ok values\n- **govet**: all analyzers enabled except `fieldalignment` and `shadow`\n- **revive**: enforces `indent-error-flow` (guard clauses), `receiver-naming`, `error-strings`, `superfluous-else`, `empty-block`\n- **staticcheck**: all checks enabled (ST1000 package comments excluded for internal)\n- **misspell**: US locale\n\n### Exclusions\n\n- **Test files** (`_test.go`): `errcheck`, `bodyclose`, `nilerr` relaxed\n- **`internal\u002Ftools\u002F`**: `nilerr` relaxed (errors returned inside result structs)\n- **`internal\u002F(lsp|cli|memory|tui)\u002F`**: `nilerr` relaxed (callback wrappers)\n- **`research\u002F`**: most linters disabled (experimental code)\n\n### Import ordering (enforced by goimports)\n```go\nimport (\n    \u002F\u002F 1. stdlib\n    \"context\"\n    \"fmt\"\n\n    \u002F\u002F 2. third-party\n    \"github.com\u002Fcharmbracelet\u002Fbubbletea\"\n\n    \u002F\u002F 3. local (auto-grouped by goimports local-prefixes)\n    \"github.com\u002Fdimetron\u002Fpi-go\u002Finternal\u002Fconfig\"\n)\n```\n\n### CI (GitHub Actions)\n```yaml\nlint:\n  runs-on: ubuntu-latest\n  steps:\n    - uses: actions\u002Fcheckout@v5\n    - uses: actions\u002Fsetup-go@v6\n      with: { go-version: '1.27' }\n    - uses: golangci\u002Fgolangci-lint-action@v9\n      with: { version: v2 }\n\ntest:\n  runs-on: ubuntu-latest\n  steps:\n    - uses: actions\u002Fcheckout@v5\n    - uses: actions\u002Fsetup-go@v6\n      with: { go-version: '1.27' }\n    - run: go test -v -race -coverprofile=coverage.out .\u002F...\n    - run: go mod tidy -diff && go mod verify\n```\n\n---\n\n## 9. Logging — log\u002Fslog (stdlib only)\n\n**Decision**: `log\u002Fslog` for pi-go. Zero deps, swappable handler, context-aware.  \nIf throughput ever matters: swap backend to `zapslog.NewHandler()` — no call-site changes.\n\n```go\n\u002F\u002F internal\u002Flogging\u002Flogging.go\nfunc NewLogger(level slog.Level, jsonOutput bool) *slog.Logger {\n    opts := &slog.HandlerOptions{Level: level, AddSource: level \u003C= slog.LevelDebug}\n    var h slog.Handler\n    if jsonOutput {\n        h = slog.NewJSONHandler(os.Stderr, opts)  \u002F\u002F production\n    } else {\n        h = slog.NewTextHandler(os.Stderr, opts)  \u002F\u002F development\n    }\n    return slog.New(h)\n}\n```\n\n- **Never** use `log` (stdlib old), `fmt.Println`, or init a global `zap.Logger` — `depguard` will catch it\n- Pass logger via constructor, not context (context logging = middleware only)\n- Use `.With()` for component-scoped fields\n- **Go 1.26**: `slog.NewMultiHandler(h1, h2)` for fan-out\n\n```go\ntype Runner struct { logger *slog.Logger }\n\nfunc NewRunner(client LLMClient, logger *slog.Logger) *Runner {\n    return &Runner{logger: logger.With(\"component\", \"runner\")}\n}\n\n\u002F\u002F In methods\nr.logger.Info(\"executing task\", slog.String(\"task\", task), slog.Int(\"iter\", n))\nr.logger.Error(\"tool failed\", slog.Any(\"error\", err), slog.String(\"tool\", name))\n```\n\n---\n\n## 10. Dependency Injection — Manual + Functional Options\n\nNo Wire, no fx. Manual constructor injection wired in `cmd\u002Fpi\u002Fmain.go`.\n\n### Functional options for configurable constructors\n```go\ntype clientConfig struct {\n    model   string\n    timeout time.Duration\n}\n\ntype Option func(*clientConfig)\n\nfunc WithModel(m string) Option        { return func(c *clientConfig) { c.model = m } }\nfunc WithTimeout(d time.Duration) Option { return func(c *clientConfig) { c.timeout = d } }\n\nfunc NewOpenAIClient(apiKey string, opts ...Option) *OpenAIClient {\n    cfg := &clientConfig{model: \"gpt-4o\", timeout: 30 * time.Second}\n    for _, o := range opts { o(cfg) }\n    return &OpenAIClient{apiKey: apiKey, cfg: cfg}\n}\n```\n\n### Composition root (cmd\u002Fpi\u002Fmain.go)\n```go\nfunc run(ctx context.Context) error {\n    cfg := config.Load()\n    logger := logging.NewLogger(cfg.LogLevel, cfg.JSONOutput)\n    slog.SetDefault(logger)\n\n    llmClient := llm.NewOpenAIClient(cfg.APIKey,\n        llm.WithModel(cfg.Model),\n        llm.WithTimeout(cfg.Timeout),\n    )\n    registry := tools.NewRegistry(logger, tools.WithBuiltins())\n    runner := agent.NewRunner(llmClient, registry, logger,\n        agent.WithMaxIterations(cfg.MaxIterations),\n    )\n    return runner.Run(ctx, os.Args[1:])\n}\n```\n\n### Interface-based test mocks — no mock framework needed\n```go\ntype mockLLM struct{ response string; err error }\nfunc (m *mockLLM) Complete(_ context.Context, _ string) (string, error) {\n    return m.response, m.err\n}\n\nfunc TestRunner_Execute(t *testing.T) {\n    r := agent.NewRunner(&mockLLM{response: \"ok\"}, nil, slog.Default())\n    if err := r.Execute(t.Context(), \"task\"); err != nil {\n        t.Fatalf(\"unexpected: %v\", err)\n    }\n}\n```\n\n---\n\n## Quick Reference\n\n| Concern | Use | Avoid |\n|---------|-----|-------|\n| Control flow | Guard clauses, early return | Deep nesting |\n| Logic | Pure functions, value in\u002Fout | Mutating pointer args |\n| Composition | Small functions + higher-order combinators | Boolean flag parameters |\n| Init | Explicit constructors | `init()` functions |\n| Returns | Explicit return values | Naked returns |\n| Lint gate | `golangci-lint run` before commit | `\u002F\u002Fnolint` without justification |\n| Imports | stdlib \u002F third-party \u002F local (3 groups) | Mixed or unsorted imports |\n| Logging | `log\u002Fslog` | `log`, `zap`, `zerolog` |\n| DI | manual constructors + functional options | Wire, fx |\n| Error wrapping | `fmt.Errorf(\"...: %w\", err)` | `%v` when caller needs to unwrap |\n| Error type check | `errors.AsType[T]` (1.26) \u002F `errors.As` | type assertions |\n| Benchmarks | `b.Loop()` | `for i := 0; i \u003C b.N; i++` |\n| Concurrent tests | `synctest.Test` + `synctest.Sleep` (1.27) | `time.Sleep` in tests |\n| HTTP test server | `httptest.NewTestServer(t, h)` (1.27) | `httptest.NewServer` + `defer Close()` |\n| Split on last separator | `strings.CutLast` (1.27) | `strings.LastIndex` + slicing |\n| UUIDs | stdlib `uuid` (1.27) | `github.com\u002Fgoogle\u002Fuuid` |\n| WaitGroup | `wg.Go(func(){...})` (1.25) | `wg.Add(1); defer wg.Done()` |\n| Slice ops | `slices.*`, `maps.*` | hand-written loops |\n| Randomness | `math\u002Frand\u002Fv2` | `math\u002Frand` |\n| Tool deps | `go.mod tool directive` (1.24) | `tools.go` blank imports |\n",{"name":5,"description":7},"main","bcfcd42255878c3d40e545abcdeb2520b6d31a52",".pi-go\u002Fskills\u002Fcode-guidelines-go",34,"https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go","https:\u002F\u002Fpi-go.sh","https:\u002F\u002Fgithub.com\u002Fdimetron","🎸💻 🚧🦞","2026-11-12 06:53:11",{"source":8,"skillName":5,"commands":76,"prompt":86,"zipAvailable":87},[77,80,83],{"tool":78,"command":79},"npx","npx skills add dimetron\u002Fpi-go --skill code-guidelines-go",{"tool":81,"command":82},"bunx","bunx skills add dimetron\u002Fpi-go --skill code-guidelines-go",{"tool":84,"command":85},"pnpm","pnpm dlx skills add dimetron\u002Fpi-go --skill code-guidelines-go","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002F.pi-go\u002Fskills\u002Fcode-guidelines-go\u002FSKILL.md",true,[],[90,116,139,158,174,195],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":97,"publishedAt":61},"01M70SJ3216A0Y2NXYJJ2MVER0","verify-session-flow","Formally verify the interactive session flow before or after changing it. Use when editing internal\u002Ftui\u002Ftui.go's handleKey, handleInterruptKey, handleInputSubmit, ctrlCCount or quitting, internal\u002Ftui\u002Fagent_loop.go's enqueuePrompt, submit, steerPrompt, cancelAgent or handleAgentDone, or the deferred-init splash — or when a user reports that Ctrl+C quit on the first press, that Esc did not stop a turn or a goal loop, that a queued prompt was lost when they canceled, or that the TUI hung on a spinner nothing could clear.","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002Finternal\u002Fextension\u002Fbundled_skills\u002Fverify-session-flow\u002FSKILL.md",[96],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[98,99,100,101,102,103,104,49,105,106,107,108,109,110,111,112,113,32,114,115],"session","verify session flow","verify","flow","submit","editing","init","loop","prompt","when","interactive","reports","first","goal","CTRL","clear","spinner","tui",{"skillNo":117,"name":118,"nameZh":6,"description":119,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":122,"publishedAt":61},"01M70SJ02CTB40V2VFGAF80SBG","verify-run-pipeline","Formally verify the \u002Frun phase machine before or after changing it. Use when editing internal\u002Ftui\u002Frun.go's phase transitions (handleRunAgentDone, runGates result handling, verifyRunComplete, retryRun, reportGateHang, merge), internal\u002Fsop\u002Fverdict.go's Passed, or the checklist refresh — or when a run merged work it should not have, retried a hung gate, lost a cycle at the retry budget, or merged on a stale PASS.","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002Finternal\u002Fextension\u002Fbundled_skills\u002Fverify-run-pipeline\u002FSKILL.md",[],[123,124,100,125,126,127,103,128,129,130,49,107,131,132,133,134,135,136,137,138],"pipeline","verify run pipeline","run","run pipeline","retry","cycle","run phase","machine","phase","refresh","work","verdict","transitions","result","merge","sop",{"skillNo":140,"name":141,"nameZh":6,"description":142,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":143,"categories":144,"keywords":146,"publishedAt":61},"01M70SHWJV9N3PQWRAMCQE9C2E","verify-history-rewrite","Formally verify the session-history rewrite path before or after changing it. Use when editing internal\u002Fsession\u002Fautocompact.go, internal\u002Fsession\u002Fcompaction*.go, internal\u002Fsession\u002Fstore.go's Compact\u002FClearEvents\u002FrewriteEvents, internal\u002Fagent\u002Fagent.go's Run\u002FRunStreaming\u002FrunPreTurn, or internal\u002Ftui\u002Fcommands.go's clearConversation\u002FclearSessionContext and the context gauge — or when a turn is canceled mid-flight, a tool call and its result get separated, a session's history disappears while the agent is still working, or the model is sent a tool result whose call was compacted away.","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002Finternal\u002Fextension\u002Fbundled_skills\u002Fverify-history-rewrite\u002FSKILL.md",[145],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[147,148,149,100,150,151,103,98,152,153,49,154,107,155,156,136,125,157,32,115],"history","rewrite","verify history rewrite","get","model","tool","call","compact","store","gauge","commands",{"skillNo":159,"name":160,"nameZh":6,"description":161,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":162,"categories":163,"keywords":165,"publishedAt":61},"01M70SHSJ5A2XV5SXBXM96RH7P","verify-goal-loop","Formally verify the \u002Fgoal automatic loop before or after changing it. Use when editing internal\u002Ftui\u002Fgoal.go, internal\u002Ftui\u002Fagent_loop.go's handleAgentDone\u002FcancelAgent\u002FsteerPrompt, or internal\u002Ftools\u002Fgoal.go's GoalController — or when a goal loop will not stop, stops one cycle early, closes a goal that is not met, or restarts a goal you finished.","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002Finternal\u002Fextension\u002Fbundled_skills\u002Fverify-goal-loop\u002FSKILL.md",[164],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[105,166,167,100,111,168,103,128,49,107,169,170,32,115,171,172,173],"verify goal loop","verify goal","goal loop","you","automatic","Tools","it","stop",{"skillNo":175,"name":176,"nameZh":6,"description":177,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":178,"categories":179,"keywords":181,"publishedAt":61},"01M70SHPYN1AS6D6RJ8HN270RZ","verify-agent-lifecycle","Formally verify the subagent lifecycle before or after changing it. Use when editing internal\u002Fsubagent\u002Forchestrator.go's Spawn, trackAgent, forwardAgentEvents, terminalStatus, Cancel, ShutdownWithTimeout or evictCompletedAgentsLocked, or internal\u002Fsubagent\u002Fpool.go — or when a canceled agent is reported as failed, an agent vanishes from the list with no verdict, the orchestrator panics on a pool release, or an agent can be canceled twice.","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002Finternal\u002Fextension\u002Fbundled_skills\u002Fverify-agent-lifecycle\u002FSKILL.md",[180],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[182,183,184,100,185,103,49,107,186,187,188,189,134,190,191,192,193,32,172,194],"lifecycle","verify agent lifecycle","agent lifecycle","as","cancel","subagent","can","with","orchestrator","spawn","release","list","pool",{"skillNo":196,"name":197,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":198,"categories":199,"keywords":202,"publishedAt":61},"01M6E37WZM8ZF36DRYQGZDGP7M","test-new-skill","https:\u002F\u002Fgithub.com\u002Fdimetron\u002Fpi-go\u002Fblob\u002Fmain\u002Finternal\u002Ftui\u002F.pi-go\u002Fskills\u002Ftest-new-skill\u002FSKILL.md",[200,201],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[203,204,205,206],"test","new skill","test new skill","new"]