From 64332c40d06f89895f3068cb2440486cbd0f03ea Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 15:20:49 +0300 Subject: [PATCH 01/23] =?UTF-8?q?feat(go-coding):=20make=20the=20router=20?= =?UTF-8?q?route=20=E2=80=94=20mandatory=20hop=20table=20and=20minimum=20c?= =?UTF-8?q?hecklist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/go-coding/SKILL.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/skills/go-coding/SKILL.md b/skills/go-coding/SKILL.md index fc6715f..f82df23 100644 --- a/skills/go-coding/SKILL.md +++ b/skills/go-coding/SKILL.md @@ -1,6 +1,6 @@ --- name: go-coding -description: Go coding-standards router for idiomatic Go (Go 1.26.4+; golangci-lint v2). This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-linting, go-layout for layout/naming/API design). For a single, already-identified topic load that skill directly. Not for non-Go languages or domain/business rules. +description: Go coding-standards router for idiomatic Go (Go 1.26.4+; golangci-lint v2). This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-linting, go-layout for layout/naming/API design). For a single, already-identified topic load that skill directly. Loading this router alone does not apply the standards — it names the focused skill to load next. Not for non-Go languages or domain/business rules. --- # go-coding — Go standards router @@ -29,6 +29,36 @@ Two principles from the project research drive it: Open the focused `go-*` skill for the topic — it carries the cited rules and the judgment; run the tool in the middle column to enforce them. Don't invent rules: each skill cites its sources. +## Route, then load + +The router is an index, not the standard. Before writing or reviewing Go you MUST load the focused +skill matching the change — with the Skill tool (`go-coding:go-errors`, …), not by recalling it: + +| The diff touches… | Load | +|---|---| +| any `_test.go`, a benchmark, a fuzz target, a "verified by temporarily breaking it" claim | `go-testing` | +| `fmt.Errorf`, `errors.*`, a sentinel, a typed error, a `switch` over an enum | `go-errors` | +| a loop, map, slice, string split, `interface{}`, a struct literal that could be `new(expr)` | `go-idioms` | +| `go func`, `chan`, `sync.`, `atomic.`, `errgroup`, `context.With*`, a `Close` on a goroutine-owned resource | `go-concurrency` | +| a new package, an exported identifier, a `cmd/` or `internal/` decision, a doc comment on an API | `go-layout` | +| `.golangci.y*ml`, a linter complaint you do not understand | `go-lint-setup` | + +One load per skill per session is enough; the skill stays in context. Orchestrators dispatching +implementer or reviewer subagents carry this table into every brief — a subagent does not inherit +the parent session's skills. + +## Minimum checklist (when a second load is not affordable) + +Apply these even if you load nothing else; they are the rules the focused skills most often catch: + +- Wrap with `fmt.Errorf("…: %w", err)`; inspect with `errors.Is` / `errors.AsType` and guard the + result (`ok && v != nil` — a typed-nil pointer satisfies the match). Never swallow an error. +- Every guard has a test that fails when the guard is deleted; a "temporarily broke it by hand" + check is not evidence — commit it as a can-fail test. Table-driven `t.Run` with got/want messages. +- `range n`, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any`; `go fix ./...` before hand-edits. +- `ctx` first; no goroutine without an owner that waits for it; `t.Context()` in tests. +- Run `gofmt`/`gofumpt` and `golangci-lint run` — never reason out what a tool decides. + ## Authoritative sources (cite, don't guess) - Effective Go — From 23a6f721aeab05c12ebaff3016aa5e9a8343399a Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 16:24:51 +0300 Subject: [PATCH 02/23] feat(skills): concrete trigger words for go-layout, go-concurrency and go-testing --- skills/go-concurrency/SKILL.md | 2 +- skills/go-layout/SKILL.md | 2 +- skills/go-testing/SKILL.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/go-concurrency/SKILL.md b/skills/go-concurrency/SKILL.md index 11ff072..fc87b06 100644 --- a/skills/go-concurrency/SKILL.md +++ b/skills/go-concurrency/SKILL.md @@ -1,6 +1,6 @@ --- name: go-concurrency -description: Idiomatic, leak-free Go concurrency. This skill should be used when the user writes or reviews Go goroutines, channels, `sync`/`atomic`, `context`, `errgroup`, or worker pools — goroutine lifetimes/leaks, context propagation, cancellation causes (`context.Cause`, `WithTimeoutCause`), work that must outlive a request (`context.WithoutCancel`, `AfterFunc`), typed atomics, mutex misuse, cleanup/finalizers, or data races. Pair with `go test -race`, `go vet`, and `goleak`. Defers time/concurrency *testing* mechanics to `go-testing` (synctest). Not for non-Go languages. +description: Idiomatic, leak-free Go concurrency. This skill should be used when a diff or question contains go func, chan, select, sync.WaitGroup/Mutex/Once, atomic, errgroup, context.WithCancel/Timeout/Cause, a retry or backoff loop, a worker pool, an HTTP client with per-request cancellation, or a Close on a goroutine-owned resource — goroutine lifetimes and leaks, context propagation and cancellation causes, work that must outlive a request, typed atomics, mutex misuse, data races. Pair with go test -race, go vet and goleak. Time/concurrency testing mechanics live in go-testing (synctest). Not for non-Go languages. --- # go-concurrency — Go concurrency diff --git a/skills/go-layout/SKILL.md b/skills/go-layout/SKILL.md index 52095aa..841b107 100644 --- a/skills/go-layout/SKILL.md +++ b/skills/go-layout/SKILL.md @@ -1,6 +1,6 @@ --- name: go-layout -description: Go project layout, naming, and API-surface design. This skill should be used when the user structures a Go module, names things, or shapes an exported API — `internal/`, `cmd/`, start-flat-then-grow, package/variable/receiver naming, initialism casing (`userID`, `HTTPServer`), pointer vs value receivers, in-band errors, named results, option structs vs variadic options, returning concrete types, or writing doc comments. Counters imported Java/C# structure. Not for build tooling or non-layout idioms (→ `go-idioms`). +description: Go project layout, package design and API surface. This skill should be used when the user creates a new package or directory, adds or renames an exported identifier, decides between cmd/ and internal/, writes a doc comment on an exported API, or asks about util/common grab-bags, start-flat-then-grow, receiver naming, initialisms, or in-band error values. Also when reviewing a diff that adds a new package or changes a public type or signature. Counters imported ceremony (hexagonal/DDD layers) with the standard-library shape. Pair with golangci-lint's revive rules (var-naming, receiver-naming, exported). Not for error handling (go-errors) or test design (go-testing). --- # go-layout — layout, naming & API surface diff --git a/skills/go-testing/SKILL.md b/skills/go-testing/SKILL.md index b71bab7..8268230 100644 --- a/skills/go-testing/SKILL.md +++ b/skills/go-testing/SKILL.md @@ -1,6 +1,6 @@ --- name: go-testing -description: Idiomatic Go testing. This skill should be used when the user writes or reviews Go tests, benchmarks, or fuzz targets — table-driven tests, `t.Parallel` (and what cannot run under it), `t.Context`, `t.Chdir`/`t.Setenv`, `t.TempDir` vs `t.ArtifactDir`, `t.Output`, `testing.B.Loop`, the race detector, goroutine-leak detection, `testing/synctest` for time/concurrency, fuzzing, golden files, or writing failure messages that actually diagnose. Pair with `go test -race`. Not for non-Go test frameworks; error-wrapping belongs to `go-errors`. +description: Idiomatic Go testing. This skill should be used when the user writes or reviews Go tests, benchmarks, or fuzz targets — any _test.go file, a can-fail control that proves a guard is mutation-detectable, a refusal test that must assert the operation-specific facet rather than a shared sentinel, table-driven tests, `t.Parallel` (and what cannot run under it), `t.Context`, `t.Chdir`/`t.Setenv`, `t.TempDir` vs `t.ArtifactDir`, `t.Output`, `testing.B.Loop`, the race detector, goroutine-leak detection, `testing/synctest` for time/concurrency, fuzzing, golden files, or writing failure messages that actually diagnose. Pair with `go test -race`. Not for non-Go test frameworks; error-wrapping belongs to `go-errors`. --- # go-testing — Go testing From 31dcc2d4b7191f1668ebab84feb3006d3d3cb545 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 16:37:57 +0300 Subject: [PATCH 03/23] refactor(skills): merge go-linting into go-lint-setup; keep a deprecation stub Moves the golangci-lint v2 config-schema, modernize/errorlint-adoption, and upgrade-breakage content from go-linting into go-lint-setup under a new "Adopting or debugging an existing config" heading, since go-lint-setup is the skill that actually gets routed to. go-linting becomes a three-line deprecation stub (removed in 0.6.0) rather than being deleted outright. Sweeps every other live go-linting pointer in the repo (router routing table and description, README, AGENTS.md inventory, the Cursor rule mirror, the reference lint config's header comment, and the go-errors/ go-idioms/go-explain/go-reviewer cross-references) to go-lint-setup, so the fold isn't undermined by stale internal links. --- AGENTS.md | 6 +-- README.md | 4 +- agents/go-reviewer.md | 2 +- docs/authoring.md | 4 +- docs/testing.md | 2 +- references/golangci.v2.yml | 6 +-- rules/go-context.mdc | 2 +- skills/go-coding/SKILL.md | 4 +- skills/go-errors/SKILL.md | 2 +- skills/go-explain/SKILL.md | 2 +- skills/go-idioms/SKILL.md | 2 +- skills/go-lint-setup/SKILL.md | 65 ++++++++++++++++++++++++-- skills/go-linting/SKILL.md | 86 ++--------------------------------- 13 files changed, 84 insertions(+), 103 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f3686b0..84e6608 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to AI coding assistants (Claude Code, Cursor, and co The **Go Coding Plugin** is an AI plugin by Cadasto B.V. that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, commands, agents, hooks, and Cursor rules. It targets **both Claude Code and Cursor** from a single shared component set. -> **Current status — v0.4.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting`, `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. +> **Current status — v0.4.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. ## Domain Context @@ -31,7 +31,7 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Claude manifest**: `.claude-plugin/plugin.json` — `name`, `version`, `description`, `author` (an **object** `{name, url}` — `claude plugin validate` rejects a string), `license`, `repository`, `keywords`. Claude Code discovers components from the **default folders** (`skills/`, `commands/`, `agents/`, `hooks/`) automatically; no explicit path map is needed. - **Cursor manifest**: `.cursor-plugin/plugin.json` — same metadata **plus** explicit top-level path keys (`skills`, `rules`, `agents`, `commands`, `hooks`). No `mcpServers` — this plugin has no MCP backend. Keep `name`/`version`/`description`/`author` identical to the Claude manifest. -- **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting`, `go-layout` standards skills. +- **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout` standards skills. - **Slash commands** are authored as **user-invoked skills** (`skills//SKILL.md` with `argument-hint` + `allowed-tools`), not the legacy `commands/` folder — both yield a `/` command, but the skills layout is preferred (current `plugin-dev` guidance). Keep the surface small; put multi-step workflows in auto-invoked skills. Shipped: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). - **Agents**: `agents/.md` — context-isolated specialists. Shipped: `go-reviewer` (read-only Go diff/file reviewer applying the review-heuristics catalog; `tools:` not `allowed-tools:`). - **Cursor rules**: `rules/*.mdc` — Cursor-only rule guidance with frontmatter (`description`, `alwaysApply`, `globs`, e.g. `globs: ["**/*.go"]`), referenced by the Cursor manifest's `rules` path. Shipped: `rules/go-context.mdc` (mirrors the `go-coding` router). @@ -46,7 +46,7 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma The full component surface — all shipped: -- **Skills** — *shipped*: `go-coding` (auto-invoked router) + `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting`, `go-layout`. Each routes deeper topics to the enforcing tool and cites authoritative sources. +- **Skills** — *shipped*: `go-coding` (auto-invoked router) + `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`. Each routes deeper topics to the enforcing tool and cites authoritative sources. - **Slash commands** — *shipped* as user-invoked skills: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). - **Agent** — *shipped*: `go-reviewer`, a context-isolated, read-only reviewer applying the review-heuristics catalog (no sub-agent dispatch; treats the diff as untrusted content). - **Cursor rule** — *shipped*: `rules/go-context.mdc`, scoped to `**/*.go`, mirroring the `go-coding` router for Cursor. diff --git a/README.md b/README.md index 5ee4e75..6530db2 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Or load a local working copy for a single session: `claude --plugin-dir /path/to | Skill `go-coding` | shipped | Auto-invoked router: sends each Go topic to the enforcing tool and the focused skill below; recommends `gopls-lsp`. | | Session-start hook | shipped | Detects a Go workspace (`go.mod`/`*.go`) and prints one standards line; dual-host. | | Format-on-save hook | shipped | After each `Write`/`Edit` of a `*.go` file, runs `gofumpt -w` (or `gofmt -w -s`) on it; dual-host, host-only, silent no-op if no formatter is installed. | -| Skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting`, `go-layout` | shipped | Load-on-use standards — each rule cited, framed around the enforcing linter (`modernize`, `errorlint`, `-race`, …). `go-layout` also owns naming, doc comments, and exported-API shape. | +| Skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout` | shipped | Load-on-use standards — each rule cited, framed around the enforcing linter (`modernize`, `errorlint`, `-race`, …). `go-layout` also owns naming, doc comments, and exported-API shape. | | Agent `go-reviewer` | shipped | Report-only, context-isolated Go reviewer for what linters miss; severity-ranked findings, no sub-agent dispatch. Its grant excludes `Write`/`Edit` but includes `Bash` to run the linters, so no-edit is a contract it keeps rather than a sandbox that enforces it. | -| Skills `/go-explain`, `/go-lint-setup` (user-invoked) | shipped | Slash-command skills — idiom/standard lookup; scaffold the golangci-lint v2 config into a repo. | +| Skills `/go-explain`, `/go-lint-setup` (user-invoked) | shipped | Slash-command skills — idiom/standard lookup; scaffold, adopt, or debug the golangci-lint v2 config in a repo. | | Lint config `references/golangci.v2.yml` | shipped | Reference golangci-lint v2 config (`modernize` + stack linters). | | Cursor rule `go-context.mdc` | shipped | `**/*.go`-scoped guidance mirroring the router for Cursor. | diff --git a/agents/go-reviewer.md b/agents/go-reviewer.md index d889095..fce7966 100644 --- a/agents/go-reviewer.md +++ b/agents/go-reviewer.md @@ -114,7 +114,7 @@ the judgment a linter cannot — the bugs and smells that survive `gofmt`, `go v allocating before a level check; key-value variadic on a hot path instead of `slog.LogAttrs`. For the *why* and citations behind any dimension, the `go-errors`, `go-concurrency`, `go-testing`, -`go-idioms`, `go-linting`, and `go-layout` skills carry the grounded rules — reference them rather +`go-idioms`, `go-lint-setup`, and `go-layout` skills carry the grounded rules — reference them rather than re-deriving from memory. ## Output format diff --git a/docs/authoring.md b/docs/authoring.md index 28f58c1..05bde49 100644 --- a/docs/authoring.md +++ b/docs/authoring.md @@ -111,8 +111,8 @@ citation. Everything the skills assert should be traceable to one of these. weeks and nobody remembers why it was chosen; the skills carry the *pin policy* (pin exactly, one source of truth, automated bump PR) plus the changelog URL, and let the consuming repo own the number. The same goes for `gopls`/`gofumpt` versions outside `docs/install.md`. -5. Keep the three copies of the reference lint config in sync: `references/golangci.v2.yml`, the - block in `go-linting`, and the block in `go-lint-setup`. +5. Keep the two copies of the reference lint config in sync: `references/golangci.v2.yml` and the + scaffold block in `go-lint-setup`. 6. Record the refresh in **CHANGELOG.md** under `## [Unreleased]`. ## Dual-host parity diff --git a/docs/testing.md b/docs/testing.md index b19eac1..1a88800 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -18,7 +18,7 @@ Install from your working copy (see [install.md](install.md)), then exercise eac - **Session-start hook** — open a repo with a `go.mod`/`*.go`; one Go-standards line should print at session start (and nothing in a non-Go repo). - **`go-coding` router** — ask for a Go review or idiom help; it should route to the enforcing tool and the focused skill. -- **Standards skills** — a topic prompt should engage the matching skill (for example error wrapping → `go-errors`, a flaky time-based test → `go-testing`/`go-concurrency`, linter setup → `go-linting`). +- **Standards skills** — a topic prompt should engage the matching skill (for example error wrapping → `go-errors`, a flaky time-based test → `go-testing`/`go-concurrency`, linter setup → `go-lint-setup`). - **`go-reviewer` agent** — ask for a Go code review; it returns severity-ranked findings and does not spawn sub-agents. - **Slash commands (skills)** — `/go-explain ` and `/go-lint-setup`. - **Cursor rule** — in Cursor, open a `.go` file and confirm `go-context.mdc` attaches. diff --git a/references/golangci.v2.yml b/references/golangci.v2.yml index 015cf44..1022d02 100644 --- a/references/golangci.v2.yml +++ b/references/golangci.v2.yml @@ -4,9 +4,9 @@ # Schema is golangci-lint v2 — a v1 config will NOT parse (run `golangci-lint migrate` on a v1 # config). Needs a golangci-lint v2 release recent enough to know every linter named below; if it # rejects one, bump the pin rather than dropping the line. Pin an exact version in CI in one place -# (see `go-linting` — no version is blessed here on purpose). See the `go-linting` skill for what -# each linter does and why. Keep this in sync with BOTH inlined copies: the reference block in -# `skills/go-linting/SKILL.md` and the scaffold block in `skills/go-lint-setup/SKILL.md`. +# (see `go-lint-setup` — no version is blessed here on purpose). See the `go-lint-setup` skill for +# what each linter does and why. Keep this in sync with the scaffold block in +# `skills/go-lint-setup/SKILL.md`. version: "2" linters: diff --git a/rules/go-context.mdc b/rules/go-context.mdc index 3fa9428..2eaf90c 100644 --- a/rules/go-context.mdc +++ b/rules/go-context.mdc @@ -16,7 +16,7 @@ This Cursor rule mirrors the `go-coding` router skill — apply it when editing | Topic | Run (deterministic) | Deeper skill | |---|---|---| | Formatting | `gofmt`/`gofumpt` (+ `goimports`) — machine-enforced | — | -| Static analysis / bugs | `go vet ./...`, `golangci-lint run` | `go-linting` | +| Static analysis / bugs | `go vet ./...`, `golangci-lint run` | `go-lint-setup` | | Modern idioms | `go fix ./...` (the toolchain's modernizers), or `golangci-lint run --enable-only=modernize` | `go-idioms` | | Errors | `golangci-lint run --enable-only=errorlint,exhaustive` | `go-errors` | | Concurrency | `go test -race ./...`, `go vet ./...` | `go-concurrency` | diff --git a/skills/go-coding/SKILL.md b/skills/go-coding/SKILL.md index f82df23..1eb92ea 100644 --- a/skills/go-coding/SKILL.md +++ b/skills/go-coding/SKILL.md @@ -1,6 +1,6 @@ --- name: go-coding -description: Go coding-standards router for idiomatic Go (Go 1.26.4+; golangci-lint v2). This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-linting, go-layout for layout/naming/API design). For a single, already-identified topic load that skill directly. Loading this router alone does not apply the standards — it names the focused skill to load next. Not for non-Go languages or domain/business rules. +description: Go coding-standards router for idiomatic Go (Go 1.26.4+; golangci-lint v2). This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-lint-setup, go-layout for layout/naming/API design). For a single, already-identified topic load that skill directly. Loading this router alone does not apply the standards — it names the focused skill to load next. Not for non-Go languages or domain/business rules. --- # go-coding — Go standards router @@ -18,7 +18,7 @@ Two principles from the project research drive it: | Topic | Run now (deterministic) | Deeper skill | |---|---|---| | Formatting | `gofmt -l` / `gofumpt -l` (+ `goimports`) — machine-enforced, non-negotiable | — | -| Static analysis / likely bugs | `go vet ./...`, `golangci-lint run` | `go-linting` | +| Static analysis / likely bugs | `go vet ./...`, `golangci-lint run` | `go-lint-setup` | | Modern idioms (range-int, `min`/`max`, `slices`/`maps`, `wg.Go`, `strings.Cut`, `new(expr)`, `errors.AsType`) | `go fix ./...` (the toolchain's modernizer suite), or `golangci-lint run --enable-only=modernize` for CI reproducibility | `go-idioms` | | Errors (`%w`, `errors.Is`/`AsType`, `errors.Join`, sentinel/typed, enum dispatch) | `golangci-lint run --enable-only=errorlint,exhaustive` | `go-errors` | | Concurrency (goroutine leaks, ctx lifecycle, atomics) | `go test -race ./...`, `go vet ./...` | `go-concurrency` | diff --git a/skills/go-errors/SKILL.md b/skills/go-errors/SKILL.md index ffb90cc..d320f8a 100644 --- a/skills/go-errors/SKILL.md +++ b/skills/go-errors/SKILL.md @@ -1,6 +1,6 @@ --- name: go-errors -description: Idiomatic Go error handling. This skill should be used when the user writes, reviews, or debugs Go error code — wrapping with `%w`, inspecting via `errors.Is`/`errors.AsType`, sentinel vs typed errors, `errors.Join`, unchecked `Close` errors, when panic is legitimate, enum-switch dispatch defaults, keeping payload values out of boundary errors/logs, or chasing a silently-swallowed or context-losing error. Pair with the `errorlint` linter (set it up via `go-linting`). Not for non-Go languages. +description: Idiomatic Go error handling. This skill should be used when the user writes, reviews, or debugs Go error code — wrapping with `%w`, inspecting via `errors.Is`/`errors.AsType`, sentinel vs typed errors, `errors.Join`, unchecked `Close` errors, when panic is legitimate, enum-switch dispatch defaults, keeping payload values out of boundary errors/logs, or chasing a silently-swallowed or context-losing error. Pair with the `errorlint` linter (set it up via `go-lint-setup`). Not for non-Go languages. --- # go-errors — Go error handling diff --git a/skills/go-explain/SKILL.md b/skills/go-explain/SKILL.md index 9fdb3e5..dbd739b 100644 --- a/skills/go-explain/SKILL.md +++ b/skills/go-explain/SKILL.md @@ -21,6 +21,6 @@ Cover, in a few lines: Answer against the **Go 1.26.4+** baseline. Name the version an idiom landed in (that's step 1) — that is provenance for the reader, not a gate on the recommendation. For a fuller treatment, route to the matching skill: `go-errors`, `go-concurrency`, -`go-testing`, `go-idioms`, `go-linting`, or `go-layout`. +`go-testing`, `go-idioms`, `go-lint-setup`, or `go-layout`. Keep it tight — this is a lookup, not a lecture. If `$ARGUMENTS` is empty, ask what to explain. diff --git a/skills/go-idioms/SKILL.md b/skills/go-idioms/SKILL.md index 12eb5bb..87d7064 100644 --- a/skills/go-idioms/SKILL.md +++ b/skills/go-idioms/SKILL.md @@ -1,6 +1,6 @@ --- name: go-idioms -description: Modern idiomatic Go (the `modernize` analyzer set). This skill should be used when the user writes, reviews, or modernizes Go and wants current-version idioms — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)` and `errors.AsType` (Go 1.26), dropped loop-var copies — or asks which modernize fixer owns a rewrite. Framed so advice equals tooling (`go fix ./...`, or `golangci-lint --enable-only=modernize`). Not for golangci-lint configuration (use `go-linting`) or non-Go languages. +description: Modern idiomatic Go (the `modernize` analyzer set). This skill should be used when the user writes, reviews, or modernizes Go and wants current-version idioms — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)` and `errors.AsType` (Go 1.26), dropped loop-var copies — or asks which modernize fixer owns a rewrite. Framed so advice equals tooling (`go fix ./...`, or `golangci-lint --enable-only=modernize`). Not for golangci-lint configuration (use `go-lint-setup`) or non-Go languages. --- # go-idioms — modern Go (modernize) diff --git a/skills/go-lint-setup/SKILL.md b/skills/go-lint-setup/SKILL.md index 70c3015..c540584 100644 --- a/skills/go-lint-setup/SKILL.md +++ b/skills/go-lint-setup/SKILL.md @@ -1,6 +1,6 @@ --- name: go-lint-setup -description: Scaffold the reference golangci-lint v2 config into a Go repo. This skill should be used when the user runs `/go-lint-setup` or asks to "set up", "scaffold", "add", or "bootstrap" golangci-lint or a `.golangci.yml` for a Go project — it writes the plugin's reference v2 config (modernize + stack linters) and will not overwrite an existing config unprompted. For understanding what the linters do or migrating a v1 config, use `go-linting`. Not for non-Go projects. +description: Scaffold the reference golangci-lint v2 config into a Go repo. This skill should be used when the user runs `/go-lint-setup` or asks to "set up", "scaffold", "add", or "bootstrap" golangci-lint or a `.golangci.yml` for a Go project — it writes the plugin's reference v2 config (modernize + stack linters) and will not overwrite an existing config unprompted, or asks why golangci-lint v2 rejects a config, which linters the default set enables, how to adopt modernize/errorlint in an existing repo, or how to suppress a finding with `//nolint`. Not for non-Go projects. argument-hint: optional target path (defaults to .golangci.yml) allowed-tools: Read, Write, Glob, Bash --- @@ -21,8 +21,8 @@ Steps: 2. **Write** the config below to `.golangci.yml` (or the path given in `$ARGUMENTS`). 3. **Report how to run it:** `golangci-lint run`, and `golangci-lint run --fix` for the auto-fixable findings (`modernize` + the formatters). Suggest pinning an exact `golangci-lint` version in CI in - one place (the action's `version:` input) with an automated bump PR — see `go-linting`; don't - invent a version number here, point at the releases page. + one place (the action's `version:` input) with an automated bump PR — see *Adopting or debugging + an existing config* below; don't invent a version number here, point at the releases page. Config to write (mirrors `references/golangci.v2.yml` — keep the two in sync): @@ -50,4 +50,61 @@ formatters: - goimports ``` -For what each linter does and why, see the `go-linting` skill. +For what each linter does and why, see *Adopting or debugging an existing config* below, or the +inline comments in `references/golangci.v2.yml`. + +## Adopting or debugging an existing config + +golangci-lint **v2** (Mar 2025) changed the config schema from v1 — **a v1 config will not parse**: + +- Top-level `version: "2"` is required. +- `linters.default: standard | all | none | fast` selects the base set (no more `enable-all`). + `standard` = errcheck, govet, ineffassign, staticcheck, unused. +- **Formatters moved to their own `formatters:` section** (gofmt/gofumpt/goimports are no longer + "linters"), with their settings under `formatters.settings`. `golangci-lint fmt` runs that section. +- **Exclusions moved under `linters`**: v1's `issues.exclude-rules` → `linters.exclusions.rules`, + and `issues.exclude-dirs`/`exclude-files` → `linters.exclusions.paths`. `linters-settings` split + into `linters.settings` + `formatters.settings`. A config that still uses the old key names — + `issues:`, `linters-settings:`, `enable-all` — is v1 and needs `golangci-lint migrate` (Step 1 + above), not a hand-port; `migrate` rewrites in place, keeps a `.golangci.bck.yml` backup, and + takes `--format {yml,yaml,toml,json}` — it drops comments and unknown/deprecated keys, so re-add + comments and diff the result. + +### Discipline once adopted + +- **Pin an exact version in CI, in exactly one place — and keep the pin moving.** Upstream's own + recommendation is a specific release, not `latest`: a new release can add or retune linters and + turn every build red at once, with no code change to blame. Put the version in a single source of + truth — the `golangci/golangci-lint-action` `version:` input (it also caches, and beats a plain + binary install) or the install script's tag — never copied across several workflows and Makefiles. + Then let Renovate/Dependabot raise the bump as its own PR, so the version stays current *and* + every rule-set change arrives reviewable. This skill deliberately names no blessed version; read + the changelog for the current line. +- **Install the release binary, not from source.** Upstream states that `go install`/`go get`, the + tools pattern, and `tool` directives "aren't guaranteed to work" — they compile golangci-lint with + whatever local Go version is around. Use the binary, the action, or the Docker image, from a + release built with Go ≥ the module's toolchain (1.26+) so it can parse the language version. +- **Suppress narrowly, and say why.** `//nolint:errcheck // best-effort close on a read-only handle` + — never a bare `//nolint` (it disables every linter on that line) and never a blanket + file-level disable where a `linters.exclusions.rules` entry with a path pattern is the honest + answer. `nolintlint` enforces the specific-and-explained form. + +**Common breakage when bumping the pin:** run `--fix` first, then either land the leftover findings +or add an explicit `linters.exclusions.rules` entry with a reason. If the pinned build rejects a +linter name from the reference config, the pin is too old — bump it rather than deleting the linter. + +**Adopting `modernize`:** it is the single highest-leverage linter in the reference set — it +operationalizes most `go-idioms` rules on the same engine as gopls/`go fix`, so the plugin's advice +stays consistent with the toolchain. As of **Go 1.26** the rewritten `go fix ./...` runs that same +modernizer suite from the toolchain itself; keep `modernize` in golangci-lint so CI enforces it +reproducibly against the pinned version rather than whatever toolchain a developer happens to have. +`errorlint` pairs with it in the reference config (`%w` + `errors.Is`/`AsType` discipline — see +`go-errors`). + +## Sources +- golangci-lint docs — ; v1→v2 migration guide (`migrate`, key moves) — +- v2 announcement (`fmt`, `formatters`) — +- `modernize` — + +--- +*Decomposition inspired by [`samber/cc-skills-golang`](https://github.com/samber/cc-skills-golang) (MIT © 2026 Samuel Berthe); rules grounded in the sources above.* diff --git a/skills/go-linting/SKILL.md b/skills/go-linting/SKILL.md index 3e9e970..ccaaed5 100644 --- a/skills/go-linting/SKILL.md +++ b/skills/go-linting/SKILL.md @@ -1,86 +1,10 @@ --- name: go-linting -description: golangci-lint v2 setup and adoption for Go. This skill should be used when the user configures, upgrades, or debugs Go linting — the `.golangci.yml` file, the v2 schema (versioned config, `linters.default` set, separate formatters section, `linters.exclusions`), migrating a v1 config, `golangci-lint fmt`, suppressing a finding with `//nolint`, the `modernize` linter, or stack linters (errorlint, bodyclose, noctx, usetesting, …). Not for what individual idioms mean (use `go-idioms`) or writing rules by hand. +description: Deprecated in 0.5.0 — merged into go-lint-setup. This skill should be used only when an old prompt or document still names go-linting; it redirects there. --- -# go-linting — golangci-lint v2 +# go-linting — deprecated -> **Bundled `references/` is at the plugin root** (beside `skills/`, two levels above this file) — *not* under this skill. Read `references/golangci.v2.yml` as `${CLAUDE_PLUGIN_ROOT}/references/golangci.v2.yml` on Claude Code, or `../../references/golangci.v2.yml` from this skill's directory, or Glob for the installed `references/golangci.v2.yml` (host-agnostic). - -golangci-lint **v2** (Mar 2025) is the de-facto meta-linter and the deterministic core of this -plugin. Its schema changed from v1 — **v1 config will not parse**: - -- Top-level `version: "2"` is required. -- `linters.default: standard | all | none | fast` selects the base set (no more `enable-all`). - `standard` = errcheck, govet, ineffassign, staticcheck, unused. -- **Formatters moved to their own `formatters:` section** (gofmt/gofumpt/goimports are no longer - "linters"), with their settings under `formatters.settings`. `golangci-lint fmt` runs that section. -- **Exclusions moved under `linters`**: v1's `issues.exclude-rules` → `linters.exclusions.rules`, - and `issues.exclude-dirs`/`exclude-files` → `linters.exclusions.paths`. `linters-settings` split - into `linters.settings` + `formatters.settings`. -- **Don't hand-port a v1 config — run `golangci-lint migrate`.** It rewrites in place, keeps a - `.golangci.bck.yml` backup, and takes `--format {yml,yaml,toml,json}`. It drops comments and - unknown/deprecated keys, so re-add comments and diff the result. - -## Reference config (v2) - -```yaml -version: "2" -linters: - default: standard - enable: - - modernize # highest value: range-int, min/max, slices/maps, wg.Go, strings.Cut… - - errorlint # %w + errors.Is/As discipline - - exhaustive # a switch over an enum names every member - - bodyclose # unclosed http.Response.Body - - rowserrcheck # unchecked sql.Rows.Err - - sqlclosecheck # unclosed sql.Rows/Stmt - - noctx # HTTP/SQL without context - - contextcheck # context not propagated - - containedctx # context.Context stored in a struct - - perfsprint # fmt.Sprintf where a cheaper call exists - - usetesting # os.Setenv/os.Chdir/context.Background in tests → the t.* forms - - nolintlint # a //nolint must name a linter and carry a reason - - revive # configurable golint successor -formatters: - enable: [gofumpt, goimports] -``` - -## Adoption - -- Run: `golangci-lint run`; auto-fix what's fixable (incl. `modernize` and formatters): - `golangci-lint run --fix`. `golangci-lint fmt` applies only the `formatters:` section. -- **Pin an exact version in CI, in exactly one place — and keep the pin moving.** Upstream's own - recommendation is a specific release, not `latest`: a new release can add or retune linters and - turn every build red at once, with no code change to blame. Put the version in a single source of - truth — the `golangci/golangci-lint-action` `version:` input (it also caches, and beats a plain - binary install) or the install script's tag — never copied across several workflows and Makefiles. - Then let Renovate/Dependabot raise the bump as its own PR, so the version stays current *and* - every rule-set change arrives reviewable. This skill deliberately names no blessed version; read - the changelog for the current line. -- **Install the release binary, not from source.** Upstream states that `go install`/`go get`, the - tools pattern, and `tool` directives "aren't guaranteed to work" — they compile golangci-lint with - whatever local Go version is around. Use the binary, the action, or the Docker image, from a - release built with Go ≥ the module's toolchain (1.26+) so it can parse the language version. -- **Bumping the pin:** run `--fix` first, then either land the leftover findings or add an explicit - `linters.exclusions.rules` entry with a reason. If the pinned build rejects a linter name from the - reference config, the pin is too old — bump it rather than deleting the linter. -- **Suppress narrowly, and say why.** `//nolint:errcheck // best-effort close on a read-only handle` - — never a bare `//nolint` (it disables every linter on that line) and never a blanket - file-level disable where a `linters.exclusions.rules` entry with a path pattern is the honest - answer. `nolintlint` enforces the specific-and-explained form. -- `modernize` is the single highest-leverage linter — it operationalizes most `go-idioms` rules on - the same engine as gopls/`go fix`, so the plugin's advice stays consistent with the toolchain. As - of **Go 1.26** the rewritten `go fix ./...` runs that same modernizer suite from the toolchain - itself; keep `modernize` in golangci-lint so CI enforces it reproducibly against the pinned - version rather than whatever toolchain a developer happens to have. -- Adopt the shipped reference config `references/golangci.v2.yml`, or run `/go-lint-setup` to - scaffold it into a repo (it won't overwrite an existing config without asking). - -## Sources -- golangci-lint docs — ; v1→v2 migration guide (`migrate`, key moves) — -- v2 announcement (`fmt`, `formatters`) — -- `modernize` — - ---- -*Decomposition inspired by [`samber/cc-skills-golang`](https://github.com/samber/cc-skills-golang) (MIT © 2026 Samuel Berthe); rules grounded in the sources above.* +This skill's golangci-lint v2 config-schema, adoption, and upgrade-breakage content moved into `go-lint-setup`. +It now lives under `go-lint-setup`'s `## Adopting or debugging an existing config` heading — load that skill instead. +This stub is removed in 0.6.0. From dd6e0714f38028b48da696c6f0b3bfe71cca04ac Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 16:52:52 +0300 Subject: [PATCH 04/23] =?UTF-8?q?refactor(skills):=20go-lint-setup=20?= =?UTF-8?q?=E2=80=94=20restore=20references/=20path=20note,=20lint=20trigg?= =?UTF-8?q?er=20words,=20section=20nesting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes three review findings on the go-linting fold: the host-agnostic references/ path-resolution note was dropped along with go-linting and never replaced, so the new pointer to references/golangci.v2.yml's inline comments had no guidance on resolving that path from a user repo; the description lost golangci-lint fmt, linters.exclusions, and migrating a v1 config as live trigger words even though this skill now owns that content; and "Common breakage when bumping the pin" / "Adopting modernize" were nested under the "Discipline once adopted" sub-heading despite not being discipline bullets themselves. --- skills/go-lint-setup/SKILL.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/skills/go-lint-setup/SKILL.md b/skills/go-lint-setup/SKILL.md index c540584..c36999d 100644 --- a/skills/go-lint-setup/SKILL.md +++ b/skills/go-lint-setup/SKILL.md @@ -1,12 +1,14 @@ --- name: go-lint-setup -description: Scaffold the reference golangci-lint v2 config into a Go repo. This skill should be used when the user runs `/go-lint-setup` or asks to "set up", "scaffold", "add", or "bootstrap" golangci-lint or a `.golangci.yml` for a Go project — it writes the plugin's reference v2 config (modernize + stack linters) and will not overwrite an existing config unprompted, or asks why golangci-lint v2 rejects a config, which linters the default set enables, how to adopt modernize/errorlint in an existing repo, or how to suppress a finding with `//nolint`. Not for non-Go projects. +description: Scaffold the reference golangci-lint v2 config into a Go repo. This skill should be used when the user runs `/go-lint-setup` or asks to "set up", "scaffold", "add", or "bootstrap" golangci-lint or a `.golangci.yml` for a Go project — it writes the plugin's reference v2 config (modernize + stack linters) and will not overwrite an existing config unprompted, or asks why golangci-lint v2 rejects a config, about migrating a v1 config, which linters the default set enables, how to adopt modernize/errorlint in an existing repo, what `golangci-lint fmt` does, how to write a `linters.exclusions` rule, or how to suppress a finding with `//nolint`. Not for non-Go projects. argument-hint: optional target path (defaults to .golangci.yml) allowed-tools: Read, Write, Glob, Bash --- # go-lint-setup — scaffold golangci-lint v2 +> **Bundled `references/` is at the plugin root** (beside `skills/`, two levels above this file) — *not* under this skill. Read `references/golangci.v2.yml` as `${CLAUDE_PLUGIN_ROOT}/references/golangci.v2.yml` on Claude Code, or `../../references/golangci.v2.yml` from this skill's directory, or Glob for the installed `references/golangci.v2.yml` (host-agnostic). + Scaffold the plugin's reference **golangci-lint v2** config into the current repo so its linting matches the `go-coding` standards. Single interaction. @@ -70,6 +72,18 @@ golangci-lint **v2** (Mar 2025) changed the config schema from v1 — **a v1 con takes `--format {yml,yaml,toml,json}` — it drops comments and unknown/deprecated keys, so re-add comments and diff the result. +**Common breakage when bumping the pin:** run `--fix` first, then either land the leftover findings +or add an explicit `linters.exclusions.rules` entry with a reason. If the pinned build rejects a +linter name from the reference config, the pin is too old — bump it rather than deleting the linter. + +**Adopting `modernize`:** it is the single highest-leverage linter in the reference set — it +operationalizes most `go-idioms` rules on the same engine as gopls/`go fix`, so the plugin's advice +stays consistent with the toolchain. As of **Go 1.26** the rewritten `go fix ./...` runs that same +modernizer suite from the toolchain itself; keep `modernize` in golangci-lint so CI enforces it +reproducibly against the pinned version rather than whatever toolchain a developer happens to have. +`errorlint` pairs with it in the reference config (`%w` + `errors.Is`/`AsType` discipline — see +`go-errors`). + ### Discipline once adopted - **Pin an exact version in CI, in exactly one place — and keep the pin moving.** Upstream's own @@ -89,18 +103,6 @@ golangci-lint **v2** (Mar 2025) changed the config schema from v1 — **a v1 con file-level disable where a `linters.exclusions.rules` entry with a path pattern is the honest answer. `nolintlint` enforces the specific-and-explained form. -**Common breakage when bumping the pin:** run `--fix` first, then either land the leftover findings -or add an explicit `linters.exclusions.rules` entry with a reason. If the pinned build rejects a -linter name from the reference config, the pin is too old — bump it rather than deleting the linter. - -**Adopting `modernize`:** it is the single highest-leverage linter in the reference set — it -operationalizes most `go-idioms` rules on the same engine as gopls/`go fix`, so the plugin's advice -stays consistent with the toolchain. As of **Go 1.26** the rewritten `go fix ./...` runs that same -modernizer suite from the toolchain itself; keep `modernize` in golangci-lint so CI enforces it -reproducibly against the pinned version rather than whatever toolchain a developer happens to have. -`errorlint` pairs with it in the reference config (`%w` + `errors.Is`/`AsType` discipline — see -`go-errors`). - ## Sources - golangci-lint docs — ; v1→v2 migration guide (`migrate`, key moves) — - v2 announcement (`fmt`, `formatters`) — From df9c18e9956ad1e4325f54ec5e09d1925c4c4efe Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 17:07:54 +0300 Subject: [PATCH 05/23] =?UTF-8?q?feat(skills):=20Go=201.27=20support=20?= =?UTF-8?q?=E2=80=94=20forward=20hints=20in=20go-idioms,=20golangci-lint?= =?UTF-8?q?=20compat=20note,=20version-floor=20wording?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/validate.yml | 11 +++++++--- AGENTS.md | 2 +- README.md | 2 +- agents/go-reviewer.md | 2 +- docs/authoring.md | 7 ++++--- docs/install.md | 14 ++++++------- rules/go-context.mdc | 2 +- skills/go-coding/SKILL.md | 2 +- skills/go-explain/SKILL.md | 6 ++++-- skills/go-idioms/SKILL.md | 37 ++++++++++++++++++++++++++++------ skills/go-lint-setup/SKILL.md | 4 ++++ skills/go-testing/SKILL.md | 5 ++++- 12 files changed, 67 insertions(+), 27 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 45a79b3..1a9d0b0 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,17 +8,22 @@ on: jobs: validate: runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.26.x', '1.27.x'] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.x' - # The floor-minor Go toolchain activates the validator's Fixer-column check - # (go tool fix help is the authority for what `go fix` ships). Keep this minor + # The floor-minor (1.26.x) leg activates the validator's Fixer-column check + # (go tool fix help is the authority for what `go fix` ships); keep that minor # in step with GO_FLOOR_MINOR in scripts/validate.py and the documented baseline. + # The 1.27.x leg proves the plugin also validates on the next Go version — its + # Fixer-column check soft-skips (a note, not a failure) since it isn't the floor. - uses: actions/setup-go@v5 with: - go-version: '1.26.x' + go-version: ${{ matrix.go-version }} cache: false # no go.mod — nothing to cache # CI is strict and deterministic: Python is guaranteed here, so run the # validator directly (the scripts/validate.sh graceful skip is for local use). diff --git a/AGENTS.md b/AGENTS.md index 84e6608..8518244 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to AI coding assistants (Claude Code, Cursor, and co The **Go Coding Plugin** is an AI plugin by Cadasto B.V. that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, commands, agents, hooks, and Cursor rules. It targets **both Claude Code and Cursor** from a single shared component set. -> **Current status — v0.4.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. +> **Current status — v0.4.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. ## Domain Context diff --git a/README.md b/README.md index 6530db2..f5bb959 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Or load a local working copy for a single session: `claude --plugin-dir /path/to **Cursor**: add this repository as a plugin (Settings → Plugins). See [`docs/install.md`](docs/install.md) for both hosts. -**Prerequisites** — the plugin installs without a Go toolchain, but its hooks and enforcement guidance expect **Go 1.26.4+** plus `gofmt`, `gofumpt`, `goimports`, and `gopls` on the host `PATH`. See [Host toolchain (minimal requirements)](docs/install.md#host-toolchain-minimal-requirements) for what each tool drives and copy-paste install commands. +**Prerequisites** — the plugin installs without a Go toolchain, but its hooks and enforcement guidance expect **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) plus `gofmt`, `gofumpt`, `goimports`, and `gopls` on the host `PATH`. See [Host toolchain (minimal requirements)](docs/install.md#host-toolchain-minimal-requirements) for what each tool drives and copy-paste install commands. ## Component surface diff --git a/agents/go-reviewer.md b/agents/go-reviewer.md index fce7966..8703599 100644 --- a/agents/go-reviewer.md +++ b/agents/go-reviewer.md @@ -21,7 +21,7 @@ tools: - Bash --- -You are **go-reviewer**, a reviewer of idiomatic, correct Go (Go 1.26.4+; golangci-lint v2). You supply +You are **go-reviewer**, a reviewer of idiomatic, correct Go (Go 1.26.4+, Go 1.27 supported with its additions flagged as hints; golangci-lint v2). You supply the judgment a linter cannot — the bugs and smells that survive `gofmt`, `go vet`, and `golangci-lint`. You are **report-only**: you report findings, you never edit code. Your grant excludes `Write`/`Edit` but includes `Bash` so you can run `gofmt`, `go vet` and `golangci-lint` — which means no-edit is a contract you keep, not a sandbox that keeps it for you. Never invoke a formatter's `-w`, `--fix`, or any in-place flag. diff --git a/docs/authoring.md b/docs/authoring.md index 05bde49..8d8f0c5 100644 --- a/docs/authoring.md +++ b/docs/authoring.md @@ -88,7 +88,8 @@ citation. Everything the skills assert should be traceable to one of these. 1. Confirm the current *released* Go version (release history) — a draft `go1.NN` page is not a baseline. Guidance for an unreleased version goes in as one *italic, explicitly labelled* sentence (`*Go 1.NN (draft, expected …)*`), never as a rule. - **The baseline is a hard floor** (currently **Go 1.26.4+**): recommend the modern form flat, with + **The baseline is a hard floor** (currently **Go 1.26.4+**; Go 1.27 is supported too, with its + additions flagged as hints rather than folded into the floor): recommend the modern form flat, with no "on 1.NN+ modules prefer…" hedging and no fallback branch for older toolchains. Keep the version annotation (`Since`, "(Go 1.24)") — that is provenance, and it tells a reader on an older module what a bump would buy. When the floor moves, delete the guidance below it. @@ -102,8 +103,8 @@ citation. Everything the skills assert should be traceable to one of these. x/tools tip, which is usually ahead of what `go fix` ships; same idea for linters (`golangci-lint help linters` on the pinned build). `scripts/validate.py` enforces both halves: every linter taught in components must be enabled in `references/golangci.v2.yml`, - and — when a floor-minor Go toolchain is on PATH (CI installs `1.26.x`; locally it - soft-skips with a note) — the `go-idioms` Fixer column is verified against + and — when a floor-minor Go toolchain is on PATH (CI's matrix installs both `1.26.x` and + `1.27.x`; locally it soft-skips with a note) — the `go-idioms` Fixer column is verified against `go tool fix help`: plain names must be registered, † names must not be. The floor minor lives in `GO_FLOOR_MINOR` in the script and in the workflow's `setup-go` pin — move all three (docs baseline included) together. diff --git a/docs/install.md b/docs/install.md index 8b0925a..c65e3f8 100644 --- a/docs/install.md +++ b/docs/install.md @@ -59,18 +59,18 @@ At minimum the host should provide: ### Install / upgrade Go (official tarball, Linux) -Pick the latest **1.26.x** patch (1.26.4 or newer) from and the build matching your platform (`linux-amd64` shown): +Pick the latest **1.27.x** patch (**go1.27.1** at time of writing) from and the build matching your platform (`linux-amd64` shown) — the plugin's floor is **Go 1.26.4 or newer**, so an existing 1.26.4+ toolchain also works and nothing below requires the upgrade: ```bash -# replace the version with the current latest 1.26.x patch (1.26.4+) -curl -fLO https://go.dev/dl/go1.26.4.linux-amd64.tar.gz +# replace the version with the current latest 1.27.x patch (go1.27.1 at time of writing; 1.26.4+ also works) +curl -fLO https://go.dev/dl/go1.27.1.linux-amd64.tar.gz sudo rm -rf /usr/local/go # remove any prior install (don't overlay) -sudo tar -C /usr/local -xzf go1.26.4.linux-amd64.tar.gz +sudo tar -C /usr/local -xzf go1.27.1.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin # add to your shell profile if not already present -go version # → go version go1.26.4 linux/amd64 +go version # → go version go1.27.1 linux/amd64 ``` -> macOS/Windows or a package manager (Homebrew `go`, `winget`, distro packages) work equally well — the only requirement is that `go version` reports **1.26.x** (1.26.4+). `gofmt` is included in every Go distribution, so nothing extra is needed for the hook's fallback path. +> macOS/Windows or a package manager (Homebrew `go`, `winget`, distro packages) work equally well — the only requirement is that `go version` reports **1.26.4 or newer** (1.27.x recommended). `gofmt` is included in every Go distribution, so nothing extra is needed for the hook's fallback path. ### Install the supporting tools @@ -92,7 +92,7 @@ command -v goimports # goimports has no --version flag gopls version # → golang.org/x/tools/gopls v0.22.x ``` -These are **host-only** dev tools; the plugin still works without them (the format hook degrades to `gofmt`, then to a silent no-op). Full-tree `golangci-lint` runs separately — often in a pinned container — so it does not depend on these host binaries. +These are **host-only** dev tools; the plugin still works without them (the format hook degrades to `gofmt`, then to a silent no-op). Full-tree `golangci-lint` runs separately — often in a pinned container — so it does not depend on these host binaries. On Go 1.27, that container/pin needs **golangci-lint ≥ v2.13.0** (released 2026-08-19, the release that added Go 1.27 support) — see ; anything older predates 1.27 support. ## Hooks diff --git a/rules/go-context.mdc b/rules/go-context.mdc index 2eaf90c..2f1259b 100644 --- a/rules/go-context.mdc +++ b/rules/go-context.mdc @@ -1,5 +1,5 @@ --- -description: Go coding standards — idiomatic Go (Go 1.26.4+; golangci-lint v2) for .go files. Mirrors the go-coding router skill for Cursor. +description: Go coding standards — idiomatic Go (Go 1.26.4+, Go 1.27 supported with hints; golangci-lint v2) for .go files. Mirrors the go-coding router skill for Cursor. globs: ["**/*.go"] alwaysApply: false --- diff --git a/skills/go-coding/SKILL.md b/skills/go-coding/SKILL.md index 1eb92ea..7f7960d 100644 --- a/skills/go-coding/SKILL.md +++ b/skills/go-coding/SKILL.md @@ -1,6 +1,6 @@ --- name: go-coding -description: Go coding-standards router for idiomatic Go (Go 1.26.4+; golangci-lint v2). This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-lint-setup, go-layout for layout/naming/API design). For a single, already-identified topic load that skill directly. Loading this router alone does not apply the standards — it names the focused skill to load next. Not for non-Go languages or domain/business rules. +description: This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-lint-setup, go-layout for layout/naming/API design). Go coding-standards router for idiomatic Go — Go 1.26.4+ (Go 1.27 supported; its additions are flagged as hints), golangci-lint v2. For a single, already-identified topic load that skill directly. Loading this router alone does not apply the standards — it names the focused skill to load next. Not for non-Go languages or domain/business rules. --- # go-coding — Go standards router diff --git a/skills/go-explain/SKILL.md b/skills/go-explain/SKILL.md index dbd739b..e3ae7de 100644 --- a/skills/go-explain/SKILL.md +++ b/skills/go-explain/SKILL.md @@ -18,8 +18,10 @@ Cover, in a few lines: 4. **Source** — cite one authoritative reference: Effective Go, Go Code Review Comments, the Google or Uber Go style guide, a `go.dev/blog` post, or `pkg.go.dev`. -Answer against the **Go 1.26.4+** baseline. Name the version an idiom landed in (that's step 1) — -that is provenance for the reader, not a gate on the recommendation. For a fuller treatment, route +Answer against the **Go 1.26.4+** baseline (Go 1.27 is supported too; its additions are hints, not +floor changes). Name the version an idiom landed in (that's step 1) — that is provenance for the +reader, not a gate on the recommendation. When the user's toolchain is on Go 1.27+, the 1.27 form +may be the better answer instead — say so explicitly and cite the source. For a fuller treatment, route to the matching skill: `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-lint-setup`, or `go-layout`. diff --git a/skills/go-idioms/SKILL.md b/skills/go-idioms/SKILL.md index 87d7064..aac0f48 100644 --- a/skills/go-idioms/SKILL.md +++ b/skills/go-idioms/SKILL.md @@ -1,6 +1,6 @@ --- name: go-idioms -description: Modern idiomatic Go (the `modernize` analyzer set). This skill should be used when the user writes, reviews, or modernizes Go and wants current-version idioms — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)` and `errors.AsType` (Go 1.26), dropped loop-var copies — or asks which modernize fixer owns a rewrite. Framed so advice equals tooling (`go fix ./...`, or `golangci-lint --enable-only=modernize`). Not for golangci-lint configuration (use `go-lint-setup`) or non-Go languages. +description: Modern idiomatic Go (the `modernize` analyzer set) — Go 1.26+, with Go 1.27 additions noted. This skill should be used when the user writes, reviews, or modernizes Go and wants current-version idioms — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)` and `errors.AsType` (Go 1.26), dropped loop-var copies, or Go 1.27 additions (generic methods, json/v2-backed `encoding/json`, the `atomictypes`/`embedlit`/`slicesbackward`/`unsafefuncs` go fix modernizers) — or asks which modernize fixer owns a rewrite. Framed so advice equals tooling (`go fix ./...`, or `golangci-lint --enable-only=modernize`). Not for golangci-lint configuration (use `go-lint-setup`) or non-Go languages. --- # go-idioms — modern Go (modernize) @@ -18,7 +18,8 @@ golangci-lint run --enable-only=modernize --fix # the x/tools modernize suit Both draw on the same `golang.org/x/tools` engine as gopls, but golangci-lint pins its own (usually newer) snapshot of it — that gap is what the **†** marker below tracks. This skill explains *why* -and catches what review notices before the tool runs. The **baseline is Go 1.26.4+**, so every row +and catches what review notices before the tool runs. The **baseline is Go 1.26.4+** (Go 1.27 is +supported too; its additions are flagged as hints in **Newer in Go 1.27** below), so every row below applies as written — the `Since` column is provenance: it explains why older code looks different, and what an older module would have to bump to before adopting the idiom. @@ -27,7 +28,9 @@ different, and what an older module would have to bump to before adopting the id The **Fixer** column names the analyzer that owns each rewrite. Plain = registered in the Go 1.26.4 toolchain's `go fix` (ground truth: `go tool fix help`; per-fixer docs: `go tool fix help `). **†** = only in the newer `x/tools` suite so far — golangci-lint's `modernize` and gopls run it, the -1.26.4 toolchain's `go fix` does not. `—` = no fixer exists: review has to catch it. +1.26.4 toolchain's `go fix` does not. `—` = no fixer exists: review has to catch it. Two † rows +below (`atomictypes`, `slicesbackward`) graduate into the stock `go fix` on Go 1.27 — see +**Newer in Go 1.27** below rather than reading that as a change to this table. | Prefer | Over | Since | Fixer | |---|---|---|---| @@ -73,17 +76,39 @@ something a modernizer rewrites. - **`slices.Sorted(maps.Keys(m))`** (1.23) when iterating a map for output — map order is random, and unstable output is a flaky-test and noisy-diff source. -*Go 1.27 (draft, expected Aug 2026) graduates several † fixers into the toolchain's `go fix` +*Go 1.27 (released 2026-08-19) graduates several † fixers into the toolchain's `go fix` (`atomictypes`, `slicesbackward`, plus new `embedlit` and `unsafefuncs`), renames `waitgroup` → `waitgroupgo`, and drops `fmtappendf`; it also lands `encoding/json/v2` + `encoding/json/jsontext` -(v1 is reimplemented on v2, opt out with `GOEXPERIMENT=nojsonv2`), `strings.CutLast`/`bytes.CutLast`, -and a stdlib `uuid` package.* +(v1 is reimplemented on v2, opt out with `GOEXPERIMENT=nojsonv2`) and `strings.CutLast`/`bytes.CutLast`. +See **Newer in Go 1.27** below for the hints these enable — none of it is required on the 1.26 floor.* + +## Newer in Go 1.27 (hints, not requirements) + +Go 1.27 is additive over 1.26 — every 1.26 rule above still applies unchanged, and nothing here is +required while a module's `go` directive stays at 1.26. Once a repo's toolchain (and `go` directive) +moves to 1.27, these are worth reaching for — phrased as "available from 1.27" / "prefer … once on +1.27", never as a requirement. + +| Idiom (available from 1.27) | Supersedes / complements | Fixer / linter | Since | Source | +|---|---|---|---|---| +| Generic methods — a method may declare its own type parameters | a package-level generic helper function bound to the receiver type as a workaround for "methods can't be generic" | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| Prefer `encoding/json/v2` + `jsontext` for new/hot JSON paths once on 1.27 | v1 `encoding/json` (still works unchanged; only exact error-message text may shift) | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| `atomictypes` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | raw `sync/atomic` functions | `atomictypes` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| `slicesbackward` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | `for i := len(s)-1; i >= 0; i--` | `slicesbackward` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| `embedlit` — folds a field assignment right after a composite literal into the literal | `t := T{}; t.Field = v` builder pattern | `embedlit` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | + +- **`go test` now runs the `stdversion` vet check by default** (available from 1.27): it flags use of + stdlib symbols newer than the module's `go` directive — the guardrail that keeps a 1.26-floor + module from silently depending on a 1.27-only symbol. Trust it over manual review for this. Source: + [go.dev/doc/go1.27](https://go.dev/doc/go1.27). ## Sources - `modernize` (per-fixer docs, the Fixer column) — - `go fix` (rewritten in 1.26) — ; range-over-func — - `slog` — ; Go 1.21–1.26 release notes (`new(expr)`, self-ref generics — ) - `os.Root` / `omitzero` / `rand.Text` — ; Code Review Comments (Declaring Empty Slices, Crypto Rand) — +- Go 1.27 release notes (generic methods, `stdversion`, `encoding/json/v2`, new `go fix` modernizers) — +- `atomictypes` / `slicesbackward` / `embedlit` modernizer commits — , , --- *Decomposition inspired by [`samber/cc-skills-golang`](https://github.com/samber/cc-skills-golang) (MIT © 2026 Samuel Berthe); rules grounded in the sources above.* diff --git a/skills/go-lint-setup/SKILL.md b/skills/go-lint-setup/SKILL.md index c36999d..96e197b 100644 --- a/skills/go-lint-setup/SKILL.md +++ b/skills/go-lint-setup/SKILL.md @@ -84,6 +84,10 @@ reproducibly against the pinned version rather than whatever toolchain a develop `errorlint` pairs with it in the reference config (`%w` + `errors.Is`/`AsType` discipline — see `go-errors`). +**Go 1.27 needs golangci-lint ≥ v2.13.0** (released 2026-08-19 — the same day as Go 1.27 itself) for +Go 1.27 support; anything v2.12.x or earlier predates it. Source: +. + ### Discipline once adopted - **Pin an exact version in CI, in exactly one place — and keep the pin moving.** Upstream's own diff --git a/skills/go-testing/SKILL.md b/skills/go-testing/SKILL.md index 8268230..73f99df 100644 --- a/skills/go-testing/SKILL.md +++ b/skills/go-testing/SKILL.md @@ -48,7 +48,10 @@ Deterministic backstop: `go test -race ./...` (always, in CI), `go test -bench`, bytes. **Golden files** (an `-update` flag writing `testdata/*.golden`) for large structured output. A golden pins *shape*, not behaviour — when it records something another system executes (SQL, wire requests, rendered configs), pair it with at least one test that executes the artefact for - real; a snapshot can be stable and wrong. + real; a snapshot can be stable and wrong. (Go 1.27) Never assert on compressed bytes verbatim — + `compress/flate`'s encoder changed, so `gzip`/`zip`/`zlib`/PNG output differs byte-for-byte from + 1.26 even though decompression is unaffected; compare decompressed content or a stable digest of + it instead. Source: . - **Deterministic crypto tests (Go 1.26):** `testing/cryptotest.SetGlobalRandom(t, seed)` pins a deterministic randomness source for the test's duration — reach for it instead of hand-injecting a custom `io.Reader` when testing code that draws from `crypto/rand`. It's process-global, so it From 7b48d277e18e94326383ef7a81bb4104c22b95a7 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 17:12:08 +0300 Subject: [PATCH 06/23] fix(skills): correct pre-release Go 1.27 mentions in go-concurrency/go-testing; AGENTS CI matrix note --- AGENTS.md | 2 +- skills/go-concurrency/SKILL.md | 4 ++-- skills/go-testing/SKILL.md | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8518244..4d5a99d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Cursor hooks**: `hooks/cursor-hooks.json` — object `{ "hooks": { "sessionStart": [...], "afterFileEdit": [...] } }`; the command runs from the plugin root (a **workspace-relative** path, **not** `${CLAUDE_PLUGIN_ROOT}`). Present — wires `session-start.sh` (`sessionStart`) and `format-on-save.sh` (`afterFileEdit`). - **Shared hook scripts**: `hooks/session-start.sh` — detects `go.mod` / `*.go`, prints one Go-standards context line, exits 0 always. `hooks/format-on-save.sh` — after a `*.go` Write/Edit, runs `gofumpt -w` (or `gofmt -w -s`) on that single file; resolves the path from `$CLAUDE_FILE_PATH` or the stdin tool-payload JSON, host-only, silent no-op if no formatter is installed, exits 0 always. Both host-agnostic so either manifest can invoke them. Present. - **MCP config** *(optional, not present)*: `.mcp.json` — only if the plugin later integrates an MCP server. There is no companion MCP server today; do not reference one. -- **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI installs `1.26.x`, locally it soft-skips) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. +- **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI's matrix installs both `1.26.x` and `1.27.x`, locally it soft-skips) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. - **Contributor docs**: `docs/` for human-facing references — `install.md`, `testing.md`, `versioning.md`, `authoring.md`. `.github/` holds issue + PR templates, `copilot-instructions.md`, and the CI workflow. (Planning and research working notes are kept locally under `docs/`, **gitignored** — not part of the published plugin.) ### Component surface diff --git a/skills/go-concurrency/SKILL.md b/skills/go-concurrency/SKILL.md index fc87b06..1d1a136 100644 --- a/skills/go-concurrency/SKILL.md +++ b/skills/go-concurrency/SKILL.md @@ -11,8 +11,8 @@ The runtime also ships an experimental `goroutineleak` profile in `runtime/pprof that reports leaked goroutines — a toolchain-native complement to `goleak` for leak hunts (enable it with `GOEXPERIMENT=goroutineleakprofile` at build time). The implementation is production-ready; the experiment flag is only about API feedback, and it costs nothing unless in use. -*Go 1.27 (draft, expected Aug 2026) enables it by default — no `GOEXPERIMENT`, and -`/debug/pprof/goroutineleak` via `net/http/pprof`.* +*Go 1.27 (released 2026-08-19) deletes the `goroutineleakprofile` GOEXPERIMENT flag. Source: +.* ## Rules diff --git a/skills/go-testing/SKILL.md b/skills/go-testing/SKILL.md index 73f99df..a4c2412 100644 --- a/skills/go-testing/SKILL.md +++ b/skills/go-testing/SKILL.md @@ -42,8 +42,9 @@ Deterministic backstop: `go test -race ./...` (always, in CI), `go test -bench`, `synctest.Test(t, func(t *testing.T){ … })`; `synctest.Wait()` blocks until every goroutine in the bubble is durably blocked. Reach for it instead of `time.Sleep`-based polling. (Always `synctest.Test` — the pre-stable `synctest.Run` no longer exists.) - *Go 1.27 (draft, expected Aug 2026) adds `synctest.Sleep` (`time.Sleep` + `Wait` in one) and - `httptest.NewTestServer`, an in-memory server usable inside a bubble.* + *Go 1.27 (released 2026-08-19) adds `synctest.Sleep` (`time.Sleep` + `Wait` in one) and + `httptest.NewTestServer`, an in-memory server usable inside a bubble. Source: + .* - **Fuzzing** (`func FuzzX(f *testing.F)`) for parsers, codecs, and anything consuming untrusted bytes. **Golden files** (an `-update` flag writing `testdata/*.golden`) for large structured output. A golden pins *shape*, not behaviour — when it records something another system executes (SQL, From cead11e890fc474866c1bb68f195317e3763d109 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 17:29:18 +0300 Subject: [PATCH 07/23] =?UTF-8?q?fix(docs,skills):=20Go=201.27=20follow-up?= =?UTF-8?q?s=20=E2=80=94=20install.md=20consistency,=20fixer=20rename/drop?= =?UTF-8?q?,=20citations,=20fail-fast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/validate.yml | 1 + AGENTS.md | 2 +- docs/authoring.md | 4 ++-- docs/install.md | 6 +++--- skills/go-concurrency/SKILL.md | 11 ++++++----- skills/go-idioms/SKILL.md | 17 +++++++++-------- skills/go-lint-setup/SKILL.md | 4 ++-- skills/go-testing/SKILL.md | 4 ++-- 8 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1a9d0b0..3b83a0f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -9,6 +9,7 @@ jobs: validate: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: go-version: ['1.26.x', '1.27.x'] steps: diff --git a/AGENTS.md b/AGENTS.md index 4d5a99d..bedd070 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Cursor hooks**: `hooks/cursor-hooks.json` — object `{ "hooks": { "sessionStart": [...], "afterFileEdit": [...] } }`; the command runs from the plugin root (a **workspace-relative** path, **not** `${CLAUDE_PLUGIN_ROOT}`). Present — wires `session-start.sh` (`sessionStart`) and `format-on-save.sh` (`afterFileEdit`). - **Shared hook scripts**: `hooks/session-start.sh` — detects `go.mod` / `*.go`, prints one Go-standards context line, exits 0 always. `hooks/format-on-save.sh` — after a `*.go` Write/Edit, runs `gofumpt -w` (or `gofmt -w -s`) on that single file; resolves the path from `$CLAUDE_FILE_PATH` or the stdin tool-payload JSON, host-only, silent no-op if no formatter is installed, exits 0 always. Both host-agnostic so either manifest can invoke them. Present. - **MCP config** *(optional, not present)*: `.mcp.json` — only if the plugin later integrates an MCP server. There is no companion MCP server today; do not reference one. -- **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI's matrix installs both `1.26.x` and `1.27.x`, locally it soft-skips) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. +- **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI's matrix installs `1.26.x` and `1.27.x`; the strict check runs on the 1.26.x (floor) leg, the 1.27.x leg soft-skips it) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. - **Contributor docs**: `docs/` for human-facing references — `install.md`, `testing.md`, `versioning.md`, `authoring.md`. `.github/` holds issue + PR templates, `copilot-instructions.md`, and the CI workflow. (Planning and research working notes are kept locally under `docs/`, **gitignored** — not part of the published plugin.) ### Component surface diff --git a/docs/authoring.md b/docs/authoring.md index 8d8f0c5..099d9d2 100644 --- a/docs/authoring.md +++ b/docs/authoring.md @@ -106,8 +106,8 @@ citation. Everything the skills assert should be traceable to one of these. and — when a floor-minor Go toolchain is on PATH (CI's matrix installs both `1.26.x` and `1.27.x`; locally it soft-skips with a note) — the `go-idioms` Fixer column is verified against `go tool fix help`: plain names must be registered, † names must not be. The floor minor - lives in `GO_FLOOR_MINOR` in the script and in the workflow's `setup-go` pin — move all - three (docs baseline included) together. + lives in `GO_FLOOR_MINOR` in the script and in the workflow's matrix floor entry (`1.26.x`) — move + all three (docs baseline included) together. **Never hardcode a tool version in a component.** A named `golangci-lint` release rots within weeks and nobody remembers why it was chosen; the skills carry the *pin policy* (pin exactly, one source of truth, automated bump PR) plus the changelog URL, and let the consuming repo own the diff --git a/docs/install.md b/docs/install.md index c65e3f8..e491425 100644 --- a/docs/install.md +++ b/docs/install.md @@ -51,7 +51,7 @@ At minimum the host should provide: | Tool | Provided by | Used for | If missing | |------|-------------|----------|------------| -| **Go 1.26.x** (min 1.26.4) | [go.dev/dl](https://go.dev/dl/) / package manager | everything; satisfies `go.mod` `go 1.26.x`; `go fix ./...` runs the modernizers | no toolchain at all | +| **Go 1.26.4+ (1.27.x recommended)** | [go.dev/dl](https://go.dev/dl/) / package manager | everything; satisfies a `go.mod` `go 1.26.x` or `1.27.x` directive; `go fix ./...` runs the modernizers | no toolchain at all | | **`gofmt`** | the Go distribution | `format-on-save.sh` fallback (`gofmt -w -s`) | n/a — always ships with Go | | **`gofumpt`** | `go install` | `format-on-save.sh` primary (`gofumpt -w`), stricter gofmt superset | hook degrades to `gofmt` | | **`goimports`** | `go install` | `goimports` formatter in the golangci-lint v2 config (import grouping/pruning) | import-group formatting skipped | @@ -74,7 +74,7 @@ go version # → go version go1. ### Install the supporting tools -`go install` drops binaries in `$(go env GOPATH)/bin` (default `~/go/bin`) — make sure that directory is on your `PATH`. Run these **after** Go is in place so they compile against your 1.26 toolchain: +`go install` drops binaries in `$(go env GOPATH)/bin` (default `~/go/bin`) — make sure that directory is on your `PATH`. Run these **after** Go is in place so they compile against your installed toolchain (1.26.4+ or 1.27.x): ```bash go install mvdan.cc/gofumpt@latest # stricter gofmt superset (hook primary) @@ -85,7 +85,7 @@ go install golang.org/x/tools/gopls@v0.22.0 # language server for the Verify: ```bash -go version # → 1.26.x (1.26.4+) +go version # → 1.27.x (or 1.26.4+ on the floor) command -v gofmt # ships with Go (in GOROOT/bin) gofumpt --version command -v goimports # goimports has no --version flag diff --git a/skills/go-concurrency/SKILL.md b/skills/go-concurrency/SKILL.md index 1d1a136..f6c58a1 100644 --- a/skills/go-concurrency/SKILL.md +++ b/skills/go-concurrency/SKILL.md @@ -8,11 +8,12 @@ description: Idiomatic, leak-free Go concurrency. This skill should be used when Deterministic backstop: `go test -race ./...`, `go vet ./...` (catches copylocks, lost cancel), and `go.uber.org/goleak`. The race detector is the source of truth — run it before reasoning. The runtime also ships an experimental `goroutineleak` profile in `runtime/pprof` (Go 1.26) -that reports leaked goroutines — a toolchain-native complement to `goleak` for leak hunts (enable it -with `GOEXPERIMENT=goroutineleakprofile` at build time). The implementation is production-ready; the -experiment flag is only about API feedback, and it costs nothing unless in use. -*Go 1.27 (released 2026-08-19) deletes the `goroutineleakprofile` GOEXPERIMENT flag. Source: -.* +that reports leaked goroutines — a toolchain-native complement to `goleak` for leak hunts (on Go +1.26, enable it with `GOEXPERIMENT=goroutineleakprofile` at build time). The implementation is +production-ready; the experiment flag is only about API feedback, and it costs nothing unless in use. +*On Go 1.27 the profile is generally available with no build flag — read it via +`pprof.Lookup("goroutineleak")` or the `net/http/pprof` endpoint `/debug/pprof/goroutineleak`. Source: +; .* ## Rules diff --git a/skills/go-idioms/SKILL.md b/skills/go-idioms/SKILL.md index aac0f48..774b15d 100644 --- a/skills/go-idioms/SKILL.md +++ b/skills/go-idioms/SKILL.md @@ -29,7 +29,8 @@ The **Fixer** column names the analyzer that owns each rewrite. Plain = register toolchain's `go fix` (ground truth: `go tool fix help`; per-fixer docs: `go tool fix help `). **†** = only in the newer `x/tools` suite so far — golangci-lint's `modernize` and gopls run it, the 1.26.4 toolchain's `go fix` does not. `—` = no fixer exists: review has to catch it. Two † rows -below (`atomictypes`, `slicesbackward`) graduate into the stock `go fix` on Go 1.27 — see +below (`atomictypes`, `slicesbackward`) graduate into the stock `go fix` on 1.27, which also renames +`waitgroup` → `waitgroupgo` and drops `fmtappendf` () — see **Newer in Go 1.27** below rather than reading that as a change to this table. | Prefer | Over | Since | Fixer | @@ -76,22 +77,22 @@ something a modernizer rewrites. - **`slices.Sorted(maps.Keys(m))`** (1.23) when iterating a map for output — map order is random, and unstable output is a flaky-test and noisy-diff source. -*Go 1.27 (released 2026-08-19) graduates several † fixers into the toolchain's `go fix` -(`atomictypes`, `slicesbackward`, plus new `embedlit` and `unsafefuncs`), renames `waitgroup` → -`waitgroupgo`, and drops `fmtappendf`; it also lands `encoding/json/v2` + `encoding/json/jsontext` +*Go 1.27 (released 2026-08-19, ) graduates several † fixers into the toolchain's +`go fix` (`atomictypes`, `slicesbackward`, plus new `embedlit` and `unsafefuncs`), renames `waitgroup` +→ `waitgroupgo`, and drops `fmtappendf`; it also lands `encoding/json/v2` + `encoding/json/jsontext` (v1 is reimplemented on v2, opt out with `GOEXPERIMENT=nojsonv2`) and `strings.CutLast`/`bytes.CutLast`. -See **Newer in Go 1.27** below for the hints these enable — none of it is required on the 1.26 floor.* +Source: . See **Newer in Go 1.27** below for the hints these enable — none +of it is required on the 1.26 floor.* ## Newer in Go 1.27 (hints, not requirements) Go 1.27 is additive over 1.26 — every 1.26 rule above still applies unchanged, and nothing here is required while a module's `go` directive stays at 1.26. Once a repo's toolchain (and `go` directive) -moves to 1.27, these are worth reaching for — phrased as "available from 1.27" / "prefer … once on -1.27", never as a requirement. +moves to 1.27, these are worth reaching for. | Idiom (available from 1.27) | Supersedes / complements | Fixer / linter | Since | Source | |---|---|---|---|---| -| Generic methods — a method may declare its own type parameters | a package-level generic helper function bound to the receiver type as a workaround for "methods can't be generic" | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| Generic methods — a method may declare its own type parameters (interface methods still may not declare type parameters, nor be implemented by generic methods) | a package-level generic helper function bound to the receiver type as a workaround for "methods can't be generic" | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | | Prefer `encoding/json/v2` + `jsontext` for new/hot JSON paths once on 1.27 | v1 `encoding/json` (still works unchanged; only exact error-message text may shift) | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | | `atomictypes` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | raw `sync/atomic` functions | `atomictypes` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | | `slicesbackward` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | `for i := len(s)-1; i >= 0; i--` | `slicesbackward` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | diff --git a/skills/go-lint-setup/SKILL.md b/skills/go-lint-setup/SKILL.md index 96e197b..78cdc77 100644 --- a/skills/go-lint-setup/SKILL.md +++ b/skills/go-lint-setup/SKILL.md @@ -85,8 +85,8 @@ reproducibly against the pinned version rather than whatever toolchain a develop `go-errors`). **Go 1.27 needs golangci-lint ≥ v2.13.0** (released 2026-08-19 — the same day as Go 1.27 itself) for -Go 1.27 support; anything v2.12.x or earlier predates it. Source: -. +Go 1.27 support; anything v2.12.x or earlier predates it — a compatibility floor, not a pin (see +*Discipline once adopted*). Source: . ### Discipline once adopted diff --git a/skills/go-testing/SKILL.md b/skills/go-testing/SKILL.md index a4c2412..e6727ad 100644 --- a/skills/go-testing/SKILL.md +++ b/skills/go-testing/SKILL.md @@ -42,8 +42,8 @@ Deterministic backstop: `go test -race ./...` (always, in CI), `go test -bench`, `synctest.Test(t, func(t *testing.T){ … })`; `synctest.Wait()` blocks until every goroutine in the bubble is durably blocked. Reach for it instead of `time.Sleep`-based polling. (Always `synctest.Test` — the pre-stable `synctest.Run` no longer exists.) - *Go 1.27 (released 2026-08-19) adds `synctest.Sleep` (`time.Sleep` + `Wait` in one) and - `httptest.NewTestServer`, an in-memory server usable inside a bubble. Source: + *Go 1.27 (released 2026-08-19, ) adds `synctest.Sleep` (`time.Sleep` + `Wait` + in one) and `httptest.NewTestServer`, an in-memory server usable inside a bubble. Source: .* - **Fuzzing** (`func FuzzX(f *testing.F)`) for parsers, codecs, and anything consuming untrusted bytes. **Golden files** (an `-update` flag writing `testdata/*.golden`) for large structured output. From 81f3c1ba96bd2c73e3f6dc4bd10e0449918deec9 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 17:37:28 +0300 Subject: [PATCH 08/23] feat(hooks): session banner names the skill hop and the commands; hook tests --- hooks/session-start.sh | 6 +++++- scripts/hooks-test.sh | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100755 scripts/hooks-test.sh diff --git a/hooks/session-start.sh b/hooks/session-start.sh index d7cd68b..bdd70f5 100755 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -14,7 +14,11 @@ is_go_workspace() { } if is_go_workspace; then - echo "› Go workspace detected — go-coding standards available (ask for a Go review or idiom guidance; gofmt + golangci-lint v2 and the gopls-lsp plugin recommended)." + lint=" · /go-lint-setup scaffolds golangci-lint v2 (no config found)" + for cfg in .golangci.yml .golangci.yaml .golangci.toml .golangci.json; do + [ -e "$cfg" ] && lint="" && break + done + echo "› Go workspace — go-coding: load go-coding then the skill for your diff (go-errors · go-testing · go-idioms · go-concurrency · go-layout); /go-explain for a one-shot idiom lookup; go-reviewer for a diff review${lint}. gofmt/golangci-lint v2 + gopls-lsp recommended." fi exit 0 diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh new file mode 100755 index 0000000..918420b --- /dev/null +++ b/scripts/hooks-test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Bash tests for the plugin hooks. Run: ./scripts/hooks-test.sh (exit 0 = all pass) +set -u +here="$(cd "$(dirname "$0")/.." && pwd)"; fails=0 +run_case() { # NAME DIR EXPECT_SUBSTRING (empty = expect no output) + local name="$1" dir="$2" expect="$3" out + out="$(cd "$dir" && bash "$here/hooks/session-start.sh")" + if [ -z "$expect" ]; then [ -z "$out" ] || { echo "FAIL $name: expected silence, got: $out"; fails=$((fails+1)); return; } + else case "$out" in *"$expect"*) ;; *) echo "FAIL $name: missing '$expect' in: $out"; fails=$((fails+1)); return;; esac; fi + echo "ok $name" +} +run_case_absent() { # NAME DIR UNEXPECTED_SUBSTRING + local name="$1" dir="$2" unexpected="$3" out + out="$(cd "$dir" && bash "$here/hooks/session-start.sh")" + case "$out" in *"$unexpected"*) echo "FAIL $name: unexpected '$unexpected' in: $out"; fails=$((fails+1)); return;; esac + echo "ok $name" +} +t="$(mktemp -d)"; trap 'rm -rf "$t"' EXIT +mkdir -p "$t/go-with-lint" "$t/go-no-lint" "$t/not-go" +printf 'module x\n\ngo 1.26.0\n' > "$t/go-with-lint/go.mod"; : > "$t/go-with-lint/.golangci.yml" +printf 'module x\n\ngo 1.26.0\n' > "$t/go-no-lint/go.mod" +run_case "go repo with lint config names the hop" "$t/go-with-lint" "load go-coding then the skill for your diff" +run_case "go repo with lint config names /go-explain" "$t/go-with-lint" "/go-explain " +run_case_absent "go repo with lint config omits lint-setup" "$t/go-with-lint" "/go-lint-setup" +run_case "go repo without lint config offers setup" "$t/go-no-lint" "/go-lint-setup" +run_case "non-go repo is silent" "$t/not-go" "" +[ "$fails" -eq 0 ] || exit 1 From 67cfb86f223a14b762959698ea74ce0b63b35683 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 17:47:30 +0300 Subject: [PATCH 09/23] feat(hooks): nudge the matching go-coding skill once per session after a Go edit --- hooks/cursor-hooks.json | 3 +++ hooks/hooks.json | 5 +++++ hooks/skill-nudge.sh | 25 +++++++++++++++++++++++++ scripts/hooks-test.sh | 15 +++++++++++++++ 4 files changed, 48 insertions(+) create mode 100755 hooks/skill-nudge.sh diff --git a/hooks/cursor-hooks.json b/hooks/cursor-hooks.json index ccde61e..ba804bf 100644 --- a/hooks/cursor-hooks.json +++ b/hooks/cursor-hooks.json @@ -8,6 +8,9 @@ "afterFileEdit": [ { "command": "bash hooks/format-on-save.sh" + }, + { + "command": "bash hooks/skill-nudge.sh" } ] } diff --git a/hooks/hooks.json b/hooks/hooks.json index 42f88e9..e0e0ff2 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -18,6 +18,11 @@ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/format-on-save.sh", "timeout": 20 + }, + { + "type": "command", + "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/skill-nudge.sh", + "timeout": 5 } ] } diff --git a/hooks/skill-nudge.sh b/hooks/skill-nudge.sh new file mode 100755 index 0000000..0fd47d8 --- /dev/null +++ b/hooks/skill-nudge.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# PostToolUse / afterFileEdit hook: after a Go file is edited, print ONE line naming the go-coding +# skill the edit calls for — once per skill per session. Deterministic trigger for the focused +# skills a usage analysis showed are rarely loaded. Always exits 0; never blocks an edit. +set -u +f="${CLAUDE_FILE_PATH:-}"; sid="" +if [ ! -t 0 ]; then + payload="$(cat)" + [ -n "$f" ] || f="$(printf '%s' "$payload" | grep -oE '"file_?[Pp]ath"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 | sed -E 's/.*"([^"]+)"$/\1/')" + sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 | sed -E 's/.*"([^"]+)"$/\1/')" +fi +[ -n "$f" ] || exit 0 +case "$f" in *.go) ;; *) exit 0 ;; esac +[ -f "$f" ] || exit 0 +[ -n "$sid" ] || sid="ppid$PPID" +skill=""; topic="" +case "$f" in *_test.go) skill="go-testing"; topic="a test file";; esac +if [ -z "$skill" ] && grep -qE 'go func|chan |<-chan|chan<-|sync\.|atomic\.|errgroup\.' "$f"; then skill="go-concurrency"; topic="goroutines, channels or sync"; fi +if [ -z "$skill" ] && grep -qE 'fmt\.Errorf|errors\.(Is|As|AsType|New|Join)' "$f"; then skill="go-errors"; topic="an error path"; fi +[ -n "$skill" ] || exit 0 +marker="${TMPDIR:-/tmp}/go-coding-nudge.${sid}.${skill}" +[ -e "$marker" ] && exit 0 +: > "$marker" 2>/dev/null || true +echo "› go-coding: this edit touches ${topic} — load go-coding:${skill} before continuing." +exit 0 diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh index 918420b..a48a345 100755 --- a/scripts/hooks-test.sh +++ b/scripts/hooks-test.sh @@ -24,4 +24,19 @@ run_case "go repo with lint config names /go-explain" "$t/go-with-lint" "/go-ex run_case_absent "go repo with lint config omits lint-setup" "$t/go-with-lint" "/go-lint-setup" run_case "go repo without lint config offers setup" "$t/go-no-lint" "/go-lint-setup" run_case "non-go repo is silent" "$t/not-go" "" + +nudge() { # FILE SESSION -> stdout + printf '{"session_id":"%s","tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$2" "$1" | bash "$here/hooks/skill-nudge.sh" +} +s="test-$$"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* +: > "$t/a_test.go"; printf 'package a\nfunc f(){ go func(){}() }\n' > "$t/w.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e.go"; : > "$t/plain.go"; : > "$t/readme.md" +chk() { local name="$1" got="$2" expect="$3"; case "$got" in *"$expect"*) echo "ok $name";; *) echo "FAIL $name: got '$got'"; fails=$((fails+1));; esac; } +chk "test file nudges go-testing" "$(nudge "$t/a_test.go" "$s")" "go-coding:go-testing" +chk "second test file is silent" "$(nudge "$t/a_test.go" "$s")x" "x" +chk "goroutine nudges go-concurrency" "$(nudge "$t/w.go" "$s")" "go-coding:go-concurrency" +chk "fmt.Errorf nudges go-errors" "$(nudge "$t/e.go" "$s")" "go-coding:go-errors" +chk "plain go file is silent" "$(nudge "$t/plain.go" "$s")x" "x" +chk "non-go file is silent" "$(nudge "$t/readme.md" "$s")x" "x" +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* + [ "$fails" -eq 0 ] || exit 1 From f1dcc8d84b41f5e104fea6d3042524c079b7caa7 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 17:51:55 +0300 Subject: [PATCH 10/23] test(hooks): assert silence as exact emptiness in hooks-test; can-fail control --- scripts/hooks-test.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh index a48a345..8c8add0 100755 --- a/scripts/hooks-test.sh +++ b/scripts/hooks-test.sh @@ -31,12 +31,13 @@ nudge() { # FILE SESSION -> stdout s="test-$$"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* : > "$t/a_test.go"; printf 'package a\nfunc f(){ go func(){}() }\n' > "$t/w.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e.go"; : > "$t/plain.go"; : > "$t/readme.md" chk() { local name="$1" got="$2" expect="$3"; case "$got" in *"$expect"*) echo "ok $name";; *) echo "FAIL $name: got '$got'"; fails=$((fails+1));; esac; } +chk_silent() { local name="$1" got="$2"; if [ -z "$got" ]; then echo "ok $name"; else echo "FAIL $name: expected silence, got '$got'"; fails=$((fails+1)); fi; } chk "test file nudges go-testing" "$(nudge "$t/a_test.go" "$s")" "go-coding:go-testing" -chk "second test file is silent" "$(nudge "$t/a_test.go" "$s")x" "x" +chk_silent "second test file is silent" "$(nudge "$t/a_test.go" "$s")" chk "goroutine nudges go-concurrency" "$(nudge "$t/w.go" "$s")" "go-coding:go-concurrency" chk "fmt.Errorf nudges go-errors" "$(nudge "$t/e.go" "$s")" "go-coding:go-errors" -chk "plain go file is silent" "$(nudge "$t/plain.go" "$s")x" "x" -chk "non-go file is silent" "$(nudge "$t/readme.md" "$s")x" "x" +chk_silent "plain go file is silent" "$(nudge "$t/plain.go" "$s")" +chk_silent "non-go file is silent" "$(nudge "$t/readme.md" "$s")" rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* [ "$fails" -eq 0 ] || exit 1 From 38b6b86d3c6d0292c76a70559d55c88cddb454b9 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 18:08:16 +0300 Subject: [PATCH 11/23] docs(agent,readme): one review seat per diff; brief templates for orchestrators --- README.md | 14 ++++++++++++++ agents/go-reviewer.md | 5 ++++- skills/go-coding/SKILL.md | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f5bb959..660cf4b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,20 @@ Or load a local working copy for a single session: `claude --plugin-dir /path/to Guidance is grounded in authoritative sources — [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), the [Google](https://google.github.io/styleguide/go/) and [Uber](https://github.com/uber-go/guide) style guides — and the standard toolchain (`gofmt`/`gofumpt`, `go vet`, `staticcheck`, `golangci-lint`, `go test -race`). +## Using with subagent orchestrators + +Subagents do not inherit the parent session's skills. A plan runner that dispatches implementers +and reviewers must say so in every brief: + +- **Implementer brief:** "Before writing code, invoke the Skill tool with `go-coding:go-coding`, then + the focused skills matching your diff (see its *Route, then load* table). Run `golangci-lint run` + on every touched package before committing." +- **Reviewer brief:** "Before reading the diff, load `go-coding:go-coding` plus `go-errors`, + `go-testing` and the skills the diff calls for; cite the rule a finding rests on. Do not dispatch + `go-reviewer` — you are the review seat." + +Use `go-reviewer` directly when no such seat exists (an ad-hoc "review this file" request). + ## Development No build step — the plugin is pure Markdown + JSON. Validate locally: diff --git a/agents/go-reviewer.md b/agents/go-reviewer.md index 8703599..d2d79b4 100644 --- a/agents/go-reviewer.md +++ b/agents/go-reviewer.md @@ -9,7 +9,10 @@ description: > refactor ("review the worker pool in scheduler.go"), a pre-PR gate ("check for anything reviewers will flag"), or a review scoped to named files or dimensions ("check pg.go for resource leaks and context handling"). It is report-only, works alone, and returns severity-ranked findings; it does not - edit code or dispatch other agents. Not for non-Go languages or for problems + edit code or dispatch other agents. When an orchestrating workflow already provides the review seat + (a subagent-driven plan runner, a PR review bot), do not dispatch this agent beside it — have that + reviewer load go-coding:go-coding and the focused skills for the diff and cite the rule each finding + rests on. Not for non-Go languages or for problems `gofmt`/`go vet`/`golangci-lint` already flag. See "When to invoke" in the agent body for worked scenarios. model: inherit diff --git a/skills/go-coding/SKILL.md b/skills/go-coding/SKILL.md index 7f7960d..76b1bb6 100644 --- a/skills/go-coding/SKILL.md +++ b/skills/go-coding/SKILL.md @@ -72,6 +72,10 @@ Apply these even if you load nothing else; they are the rules the focused skills Dispatch the `go-reviewer` agent — a report-only, context-isolated reviewer that applies the review-heuristics catalog and returns severity-ranked findings on a diff or file. +If a workflow already owns the reviewer seat, that reviewer loads the focused skills itself instead — +one review seat per diff. Orchestrators: put the "Route, then load" table into every implementer and +reviewer brief. + Two user-invoked skills round out the surface: `/go-explain ` for a one-shot idiom lookup, and `/go-lint-setup` to scaffold the reference golangci-lint v2 config into a repo. From a305725e67d7c6a7f2e2431fa5c39eb93eabcf80 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 18:16:08 +0300 Subject: [PATCH 12/23] =?UTF-8?q?feat(scripts):=20usage-report.py=20?= =?UTF-8?q?=E2=80=94=20measure=20skill/agent=20adoption=20from=20local=20t?= =?UTF-8?q?ranscripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/testing.md | 29 ++++ scripts/usage-report.py | 340 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+) create mode 100755 scripts/usage-report.py diff --git a/docs/testing.md b/docs/testing.md index 1a88800..49c21ca 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -24,3 +24,32 @@ Install from your working copy (see [install.md](install.md)), then exercise eac - **Cursor rule** — in Cursor, open a `.go` file and confirm `go-context.mdc` attaches. After editing content, reinstall (or restart the session) to pick up changes. + +## Measuring adoption + +The layout and concurrency skills, and the router's dispatch behavior, were shaped by +a usage analysis of local Claude Code session transcripts. `scripts/usage-report.py` +(stdlib-only) reproduces that measurement so adoption stays checkable over time: + +``` +python3 scripts/usage-report.py --since YYYY-MM-DD --out report.md +``` + +Run with no arguments to scan `~/.claude/projects` from the beginning and print the +report to stdout; `--since` narrows to a start date, `--out` writes the report to a +file instead. `--help` repeats the counting rules. + +**Counted:** a `Skill` tool invocation whose `skill` input starts with `go-coding:`; a +`Task`/`Agent` tool invocation with `subagent_type` `go-coding:go-reviewer`; a user +`` invocation naming a go-coding skill. Events are split into main-session, +subagent, and user-invoked, per month, and deduplicated by session. + +**Not counted:** the SessionStart banner line, or skill-body text echoed back inside +tool results — only structured tool invocations and explicit slash-command text count. + +**Target:** the focused standards skills (`go-errors`, `go-testing`, `go-idioms`, +`go-linting`, `go-lint-setup`, `go-layout`, `go-concurrency`) should load on at least +50% of sessions where the `go-coding` router itself loads, and `go-layout` / +`go-concurrency` specifically should show non-zero counts in any period where the +corresponding work (project layout/API design, or goroutines/channels/context) is +actually touched. diff --git a/scripts/usage-report.py b/scripts/usage-report.py new file mode 100755 index 0000000..c27bfc0 --- /dev/null +++ b/scripts/usage-report.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +"""Measure go-coding skill/agent adoption from local Claude Code transcripts. + +Scans Claude Code session transcripts (JSONL files under a projects directory, +one subdirectory per project) for real invocations of this plugin's skills and +its `go-reviewer` agent, then renders a Markdown adoption report. + +An event is counted when a transcript line is one of: + + * an ``assistant`` message containing a ``tool_use`` block with + ``name == "Skill"`` and ``input.skill`` starting with ``go-coding:``; + * an ``assistant`` message containing a ``tool_use`` block with + ``name`` in (``Task``, ``Agent``) and + ``input.subagent_type == "go-coding:go-reviewer"``; + * a ``user`` message whose text contains a ``...`` + invocation naming one of this plugin's skills (bare, e.g. ``/go-explain``, + or namespaced, e.g. ``/go-coding:go-explain``). + +Session-start banner text and skill-body text echoed back inside tool results +are NOT scanned for matches — only structured ``tool_use`` blocks and user +```` invocations count. Each matching line contributes one event, +attributed to "main" or "subagent" via the transcript's ``isSidechain`` field +(subagent transcripts also live under a ``/subagents/`` path). + +Usage: + python3 scripts/usage-report.py [--projects-dir DIR] [--since YYYY-MM-DD] [--out FILE] + +stdlib only — no third-party imports. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path + +# Skill directory names this plugin ships (used to recognize a bare, unnamespaced +# slash-command invocation such as "/go-explain" as a go-coding command). +GO_SKILL_SHORT_NAMES = { + "go-coding", "go-concurrency", "go-errors", "go-explain", "go-idioms", + "go-layout", "go-linting", "go-lint-setup", "go-testing", +} +GO_REVIEWER_SUBAGENT_TYPE = "go-coding:go-reviewer" + +SKILL_PREFIX_RE = re.compile(r"^go-coding:") +COMMAND_NAME_RE = re.compile(r"\s*(.*?)\s*") + +TOOL_USE_AGENT_NAMES = ("Task", "Agent") + + +class Event: + """One counted invocation.""" + + __slots__ = ("name", "month", "source", "session_id") + + def __init__(self, name: str, month: str, source: str, session_id: str | None): + self.name = name + self.month = month + self.source = source # "main" | "subagent" | "user" + self.session_id = session_id + + +def month_of(ts: str | None) -> str: + if not ts or len(ts) < 7: + return "unknown" + return ts[:7] # YYYY-MM + + +def iter_transcript_files(projects_dir: Path): + """Yield (path, is_subagent_file) for every session transcript under projects_dir.""" + if not projects_dir.is_dir(): + return + for project_dir in sorted(p for p in projects_dir.iterdir() if p.is_dir()): + for f in sorted(project_dir.glob("*.jsonl")): + yield f, False + for f in sorted(project_dir.glob("*/subagents/*.jsonl")): + yield f, True + + +def canonical_command_name(raw: str) -> str | None: + """Return the canonical `go-coding:` name for a invocation, + or None if it does not name a go-coding skill.""" + cmd = raw.strip().lstrip("/") + if not cmd: + return None + base = cmd.split(":")[-1] + if cmd.startswith("go-coding:"): + return "go-coding:" + base + if base in GO_SKILL_SHORT_NAMES: + return "go-coding:" + base + return None + + +def user_texts(message: dict) -> list[str]: + content = message.get("content") + if isinstance(content, str): + return [content] + texts = [] + if isinstance(content, list): + for c in content: + if isinstance(c, dict) and c.get("type") == "text": + texts.append(c.get("text", "")) + return texts + + +def scan(projects_dir: Path, since: datetime | None): + """Scan all transcripts, returning (events, session_ids_with_event, files_scanned).""" + events: list[Event] = [] + sessions_with_event: set[str] = set() + files_scanned = 0 + + for fpath, _is_subagent_file in iter_transcript_files(projects_dir): + files_scanned += 1 + try: + with fpath.open("r", errors="replace") as fh: + for line in fh: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except (json.JSONDecodeError, ValueError): + continue + + ts = obj.get("timestamp") + if since is not None: + parsed = parse_iso(ts) + if parsed is not None and parsed < since: + continue + + sid = obj.get("sessionId") + is_sidechain = bool(obj.get("isSidechain", False)) + typ = obj.get("type") + month = month_of(ts) + + if typ == "assistant": + message = obj.get("message") or {} + content = message.get("content") + if not isinstance(content, list): + continue + for c in content: + if not isinstance(c, dict) or c.get("type") != "tool_use": + continue + tool_name = c.get("name") + inp = c.get("input") or {} + if tool_name == "Skill": + skill = inp.get("skill", "") + if isinstance(skill, str) and SKILL_PREFIX_RE.match(skill): + source = "subagent" if is_sidechain else "main" + events.append(Event(skill, month, source, sid)) + if sid: + sessions_with_event.add(sid) + elif tool_name in TOOL_USE_AGENT_NAMES: + subagent_type = inp.get("subagent_type", "") + if subagent_type == GO_REVIEWER_SUBAGENT_TYPE: + source = "subagent" if is_sidechain else "main" + events.append(Event(subagent_type, month, source, sid)) + if sid: + sessions_with_event.add(sid) + + elif typ == "user": + message = obj.get("message") or {} + for text in user_texts(message): + for m in COMMAND_NAME_RE.finditer(text): + canon = canonical_command_name(m.group(1)) + if canon: + events.append(Event(canon, month, "user", sid)) + if sid: + sessions_with_event.add(sid) + except OSError as e: + print(f"WARNING: could not read {fpath}: {e}", file=sys.stderr) + + return events, sessions_with_event, files_scanned + + +def parse_iso(ts: str | None): + if not ts: + return None + try: + # Transcript timestamps are ISO-8601 UTC, e.g. "2026-08-26T22:24:05.213Z". + return datetime.fromisoformat(ts.replace("Z", "+00:00")) + except ValueError: + return None + + +def aggregate(events: list[Event]): + """skill -> {"total": n, "main": n, "subagent": n, "user": n, "months": {month: n}}""" + agg: dict[str, dict] = defaultdict(lambda: { + "total": 0, "main": 0, "subagent": 0, "user": 0, "months": defaultdict(int), + }) + for e in events: + row = agg[e.name] + row["total"] += 1 + row[e.source] += 1 + row["months"][e.month] += 1 + + session_counts: dict[str, set] = defaultdict(set) + for e in events: + if e.session_id: + session_counts[e.name].add(e.session_id) + + return agg, session_counts + + +def render_markdown(agg, session_counts, events, sessions_with_event, files_scanned, + projects_dir: Path, since: datetime | None) -> str: + lines = [] + lines.append("# go-coding usage report") + lines.append("") + generated = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + lines.append(f"Generated: {generated}") + lines.append(f"Projects dir: `{projects_dir}`") + lines.append(f"Since: {since.date().isoformat() if since else '(all time)'}") + lines.append(f"Transcript files scanned: {files_scanned}") + lines.append(f"Total events: {len(events)}") + lines.append("") + + all_months = sorted({m for row in agg.values() for m in row["months"] if m != "unknown"}) + + lines.append("## Per skill / agent") + lines.append("") + header = ["Skill/Agent", "Total", "Main", "Subagent", "User"] + all_months + lines.append("| " + " | ".join(header) + " |") + lines.append("|" + "|".join(["---"] * len(header)) + "|") + for name in sorted(agg.keys()): + row = agg[name] + cells = [ + name, + str(row["total"]), + str(row["main"]), + str(row["subagent"]), + str(row["user"]), + ] + [str(row["months"].get(m, 0)) for m in all_months] + lines.append("| " + " | ".join(cells) + " |") + if not agg: + lines.append("| _(no events found)_ | | | | |" + "".join(" |" for _ in all_months)) + lines.append("") + + lines.append("## Sessions with >=1 event, per skill / agent") + lines.append("") + lines.append("| Skill/Agent | Sessions |") + lines.append("|---|---|") + for name in sorted(session_counts.keys()): + lines.append(f"| {name} | {len(session_counts[name])} |") + if not session_counts: + lines.append("| _(no events found)_ | |") + lines.append("") + + lines.append(f"Distinct sessions with >=1 go-coding event (any skill/agent): {len(sessions_with_event)}") + lines.append("") + + return "\n".join(lines) + "\n" + + +def build_arg_parser() -> argparse.ArgumentParser: + epilog = """\ +Counting rules: + Counted: + - assistant tool_use block, name "Skill", input.skill starting "go-coding:" + - assistant tool_use block, name "Task" or "Agent", input.subagent_type + == "go-coding:go-reviewer" + - user invocations naming a go-coding skill (bare, e.g. + "/go-explain", or namespaced, e.g. "/go-coding:go-explain") + Not counted: + - the SessionStart banner text + - skill-body text echoed back inside tool results + Each transcript session is identified by its sessionId; a session's events are + attributed to "main" or "subagent" via the transcript's isSidechain field. +""" + parser = argparse.ArgumentParser( + description="Measure go-coding skill/agent adoption from local Claude Code transcripts.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=epilog, + ) + parser.add_argument( + "--projects-dir", + type=Path, + default=Path.home() / ".claude" / "projects", + help="Directory containing Claude Code project transcript folders " + "(default: ~/.claude/projects)", + ) + parser.add_argument( + "--since", + type=str, + default=None, + metavar="YYYY-MM-DD", + help="Only count events at or after this date (default: all time)", + ) + parser.add_argument( + "--out", + type=Path, + default=None, + metavar="FILE", + help="Write the Markdown report to FILE (default: print to stdout)", + ) + return parser + + +def main(argv=None) -> int: + parser = build_arg_parser() + args = parser.parse_args(argv) + + since = None + if args.since: + try: + since = datetime.strptime(args.since, "%Y-%m-%d").replace(tzinfo=timezone.utc) + except ValueError: + print(f"error: --since must be YYYY-MM-DD, got {args.since!r}", file=sys.stderr) + return 2 + + projects_dir: Path = args.projects_dir + if not projects_dir.is_dir(): + print(f"No transcripts found: projects directory does not exist: {projects_dir}") + return 0 + + events, sessions_with_event, files_scanned = scan(projects_dir, since) + + if files_scanned == 0: + print(f"No transcripts found: no .jsonl files under {projects_dir}") + return 0 + + agg, session_counts = aggregate(events) + report = render_markdown( + agg, session_counts, events, sessions_with_event, files_scanned, projects_dir, since, + ) + + if args.out: + args.out.write_text(report) + print(f"Wrote report to {args.out}") + else: + print(report) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 8ad6af99f1355f47e6e2b39e087480be71e52d7b Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 18:28:59 +0300 Subject: [PATCH 13/23] chore(release): v0.5.0 --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- AGENTS.md | 9 ++-- CHANGELOG.md | 90 ++++++++++++++++++++++++++++++++++++++ README.md | 3 +- 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1562193..828060c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "go-coding", - "version": "0.4.1", + "version": "0.5.0", "description": "Idiomatic Go coding standards for AI assistants — formatting, errors, concurrency, testing, layout.", "author": { "name": "Cadasto B.V.", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 25d426a..7b9a729 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "go-coding", - "version": "0.4.1", + "version": "0.5.0", "description": "Idiomatic Go coding standards for AI assistants — formatting, errors, concurrency, testing, layout.", "author": { "name": "Cadasto B.V.", diff --git a/AGENTS.md b/AGENTS.md index bedd070..c3cb98d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to AI coding assistants (Claude Code, Cursor, and co The **Go Coding Plugin** is an AI plugin by Cadasto B.V. that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, commands, agents, hooks, and Cursor rules. It targets **both Claude Code and Cursor** from a single shared component set. -> **Current status — v0.4.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. +> **Current status — v0.5.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. ## Domain Context @@ -31,7 +31,7 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Claude manifest**: `.claude-plugin/plugin.json` — `name`, `version`, `description`, `author` (an **object** `{name, url}` — `claude plugin validate` rejects a string), `license`, `repository`, `keywords`. Claude Code discovers components from the **default folders** (`skills/`, `commands/`, `agents/`, `hooks/`) automatically; no explicit path map is needed. - **Cursor manifest**: `.cursor-plugin/plugin.json` — same metadata **plus** explicit top-level path keys (`skills`, `rules`, `agents`, `commands`, `hooks`). No `mcpServers` — this plugin has no MCP backend. Keep `name`/`version`/`description`/`author` identical to the Claude manifest. -- **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout` standards skills. +- **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` standards skills. `go-linting` is deprecated in 0.5.0 — a redirect stub merged into `go-lint-setup`, removed in 0.6.0. - **Slash commands** are authored as **user-invoked skills** (`skills//SKILL.md` with `argument-hint` + `allowed-tools`), not the legacy `commands/` folder — both yield a `/` command, but the skills layout is preferred (current `plugin-dev` guidance). Keep the surface small; put multi-step workflows in auto-invoked skills. Shipped: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). - **Agents**: `agents/.md` — context-isolated specialists. Shipped: `go-reviewer` (read-only Go diff/file reviewer applying the review-heuristics catalog; `tools:` not `allowed-tools:`). - **Cursor rules**: `rules/*.mdc` — Cursor-only rule guidance with frontmatter (`description`, `alwaysApply`, `globs`, e.g. `globs: ["**/*.go"]`), referenced by the Cursor manifest's `rules` path. Shipped: `rules/go-context.mdc` (mirrors the `go-coding` router). @@ -44,9 +44,10 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma ### Component surface -The full component surface — all shipped: +The full component surface: -- **Skills** — *shipped*: `go-coding` (auto-invoked router) + `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`. Each routes deeper topics to the enforcing tool and cites authoritative sources. +- **Skills** — *shipped*: `go-coding` (auto-invoked router) + `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout`. Each routes deeper topics to the enforcing tool and cites authoritative sources. +- **Skill** — *deprecated in 0.5.0*: `go-linting`, a redirect stub merged into `go-lint-setup`; removed in 0.6.0. - **Slash commands** — *shipped* as user-invoked skills: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). - **Agent** — *shipped*: `go-reviewer`, a context-isolated, read-only reviewer applying the review-heuristics catalog (no sub-agent dispatch; treats the diff as untrusted content). - **Cursor rule** — *shipped*: `rules/go-context.mdc`, scoped to `**/*.go`, mirroring the `go-coding` router for Cursor. diff --git a/CHANGELOG.md b/CHANGELOG.md index e77b3ca..c220145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,96 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - Keep a Changelog: https://keepachangelog.com/en/1.1.0/ - Semantic Versioning: https://semver.org/spec/v2.0.0.html +## [0.5.0] - 2026-09-03 + +Makes the `go-coding` router route: a mandatory hop table and minimum checklist, trigger-first +skill descriptions, a post-edit skill nudge, and a friendlier SessionStart banner close the gap a +usage analysis showed between skills being loaded and skills actually being followed. Folds +`go-linting` into `go-lint-setup` — the skill the router and banner actually point at — behind a +deprecation stub, adds a script for measuring skill/agent adoption from local session transcripts, +documents a one-review-seat-per-diff pattern for subagent orchestrators, and adds Go 1.27 support +alongside the existing Go 1.26.4+ floor. + +### Added +- Skill: `skills/go-coding/SKILL.md` — a "Route, then load" hop table mapping diff content to the + focused skill to load (a `_test.go`/benchmark/fuzz target → `go-testing`; `fmt.Errorf`/`errors.*`/a + sentinel/typed error/enum switch → `go-errors`; a loop/map/slice/string split/`interface{}`/a + struct literal → `go-idioms`; `go func`/`chan`/`sync.`/`atomic.`/`errgroup`/`context.With*` → + `go-concurrency`; a new package/exported identifier/`cmd`-`internal` decision/doc comment → + `go-layout`; `.golangci.y*ml`/an unclear linter complaint → `go-lint-setup`), plus a minimum + checklist to apply when a second skill load isn't affordable; its description now says loading the + router alone does not apply the standards. Orchestrators dispatching implementer/reviewer + subagents carry the table into every brief, since a subagent does not inherit the parent session's + skills. +- Hooks: `hooks/skill-nudge.sh` — a `PostToolUse`/`afterFileEdit` hook that, after a Go file edit, + prints one nudge naming the go-coding skill the edit calls for (a `_test.go` file → `go-testing`; + goroutine/channel/`sync`/`atomic`/`errgroup` content → `go-concurrency`; `fmt.Errorf`/`errors.*` → + `go-errors`), once per skill per session; wired dual-host in `hooks/hooks.json` and + `hooks/cursor-hooks.json`. A usage analysis showed these focused skills load far less often than + the router itself. +- Hooks: `hooks/session-start.sh` — the SessionStart banner now names the skill-loading hop, lists + the focused skills, points at `/go-explain` and `go-reviewer`, and adds a `/go-lint-setup` prompt + when no `.golangci.y*ml`/`.toml`/`.json` file is found in the workspace. +- Scripts: `scripts/hooks-test.sh` — a test harness covering `hooks/skill-nudge.sh` and the + session-start banner, with a `chk_silent` helper that asserts exact emptiness for the no-nudge + cases instead of a vacuous non-match check, and a can-fail test control. +- Docs: `README.md` — "Using with subagent orchestrators": implementer and reviewer brief templates + that carry the router's hop table into a dispatched subagent's instructions, so a plan runner's + implementers and reviewers apply the standards even though a subagent does not inherit the parent + session's skills; `go-reviewer` is dispatched directly only when no such reviewer seat exists. +- Scripts, Docs: `scripts/usage-report.py`, `docs/testing.md` — a stdlib-only script that scans local + Claude Code session transcripts for go-coding skill/agent invocations (a `Skill`-tool call prefixed + `go-coding:`, a `go-coding:go-reviewer` subagent dispatch, or a user slash-command invocation) and + renders a monthly adoption report split into main-session, subagent, and user-invoked counts, + deduplicated by session; `docs/testing.md` gains a "Measuring adoption" section naming what is and + isn't counted and the adoption target. A usage analysis of local session transcripts shaped this + release's router and skill-description changes; this script reproduces that measurement so + adoption stays checkable going forward. +- Skills: `skills/go-idioms/SKILL.md` — Go 1.27 forward hints: two † fixers (`atomictypes`, + `slicesbackward`) graduate into the stock `go fix`, alongside new `embedlit`/`unsafefuncs` fixers, + a `waitgroup` → `waitgroupgo` rename, and a dropped `fmtappendf`; a new "Newer in Go 1.27" section + covers generic methods, preferring `encoding/json/v2`/`jsontext` on new/hot paths once on 1.27, and + the default `stdversion` vet check — all framed as additive over the Go 1.26 floor, never as a + requirement. +- Skills: `skills/go-lint-setup/SKILL.md` — a golangci-lint Go-1.27 compatibility note: Go 1.27 + needs golangci-lint ≥ v2.13.0, stated as a compatibility floor rather than a version pin. +- Validation: `.github/workflows/validate.yml` — a Go version matrix (`1.26.x`, `1.27.x`) with + `fail-fast: false`; the floor-minor (`1.26.x`) leg keeps running the strict `go-idioms` + Fixer-column check against `go tool fix help`, the `1.27.x` leg soft-skips it. + +### Changed +- Skills: `skills/go-lint-setup/SKILL.md` — absorbs `go-linting`'s golangci-lint v2 config-schema, + adoption, and upgrade-breakage content under a new "Adopting or debugging an existing config" + heading, since `go-lint-setup` is the skill the router and session-start banner actually route to. +- Agent, Skill: `agents/go-reviewer.md`, `skills/go-coding/SKILL.md` — one review seat per diff: when + an orchestrating workflow already provides a reviewer seat, `go-reviewer` is not dispatched beside + it; that reviewer loads `go-coding` and the focused skills for the diff itself and cites the rule + each finding rests on. +- Skills: `skills/go-layout/SKILL.md`, `skills/go-concurrency/SKILL.md`, `skills/go-testing/SKILL.md` + — descriptions rewritten trigger-first, naming the concrete diff content that should load each + skill (a new package/exported identifier/doc comment for `go-layout`; + `go func`/`chan`/`sync.`/`atomic.`/`errgroup`/a retry loop for `go-concurrency`; any `_test.go` + file or can-fail control for `go-testing`), since a usage analysis showed the prior, more abstract + descriptions rarely triggered these skills. +- Skills, Agent, Cursor rule, Docs: `skills/go-idioms/SKILL.md`, `skills/go-testing/SKILL.md`, + `skills/go-concurrency/SKILL.md`, `skills/go-coding/SKILL.md`, `skills/go-explain/SKILL.md`, + `agents/go-reviewer.md`, `rules/go-context.mdc`, `docs/authoring.md`, `docs/install.md`, + `README.md`, `AGENTS.md` — version-floor wording now states Go 1.26.4+ as the hard floor with Go + 1.27 supported, its additions flagged as hints rather than folded into the floor; + `docs/install.md` recommends the latest 1.27.x patch while noting an existing 1.26.4+ toolchain + still satisfies the floor; earlier pre-release ("draft, expected Aug 2026") mentions of Go 1.27 in + `go-concurrency` and `go-testing` are corrected to the released feature set and dated to its actual + 2026-08-19 release; `AGENTS.md` and `docs/authoring.md` describe the CI matrix's two legs instead + of a single pinned Go version. + +### Deprecated +- Skill: `skills/go-linting/SKILL.md` — deprecated: reduced to a three-line stub redirecting to + `go-lint-setup`'s "Adopting or debugging an existing config" section, which now carries the + content that used to live here. Every other internal pointer (the router's routing table and + description, `README.md`, `AGENTS.md`'s inventory, `rules/go-context.mdc`, + `references/golangci.v2.yml`'s header comment, and cross-references from `go-errors`, `go-idioms`, + `go-explain`, and `go-reviewer`) now points at `go-lint-setup` instead. Removal in 0.6.0. + ## [0.4.1] - 2026-08-25 Corrects three component defects and the docs that described them: a `PostToolUse` hook timeout that was five hours rather than twenty seconds, a reviewer agent calling itself read-only while holding `Bash`, and `go-lint-setup` offering a migration it had no tool grant to run. diff --git a/README.md b/README.md index 660cf4b..935c5d1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ Or load a local working copy for a single session: `claude --plugin-dir /path/to | Skill `go-coding` | shipped | Auto-invoked router: sends each Go topic to the enforcing tool and the focused skill below; recommends `gopls-lsp`. | | Session-start hook | shipped | Detects a Go workspace (`go.mod`/`*.go`) and prints one standards line; dual-host. | | Format-on-save hook | shipped | After each `Write`/`Edit` of a `*.go` file, runs `gofumpt -w` (or `gofmt -w -s`) on it; dual-host, host-only, silent no-op if no formatter is installed. | -| Skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout` | shipped | Load-on-use standards — each rule cited, framed around the enforcing linter (`modernize`, `errorlint`, `-race`, …). `go-layout` also owns naming, doc comments, and exported-API shape. | +| Skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` | shipped | Load-on-use standards — each rule cited, framed around the enforcing linter (`modernize`, `errorlint`, `-race`, …). `go-layout` also owns naming, doc comments, and exported-API shape. | +| Skill `go-linting` | deprecated in 0.5.0 | Redirect stub merged into `go-lint-setup`, which now owns the golangci-lint v2 config-schema, adoption, and upgrade-breakage content; removed in 0.6.0. | | Agent `go-reviewer` | shipped | Report-only, context-isolated Go reviewer for what linters miss; severity-ranked findings, no sub-agent dispatch. Its grant excludes `Write`/`Edit` but includes `Bash` to run the linters, so no-edit is a contract it keeps rather than a sandbox that enforces it. | | Skills `/go-explain`, `/go-lint-setup` (user-invoked) | shipped | Slash-command skills — idiom/standard lookup; scaffold, adopt, or debug the golangci-lint v2 config in a repo. | | Lint config `references/golangci.v2.yml` | shipped | Reference golangci-lint v2 config (`modernize` + stack linters). | From 9ef7aa84d069d05540ccf723c9380cf9b6393347 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 19:00:31 +0300 Subject: [PATCH 14/23] fix(hooks): deliver the skill nudge as a systemMessage; committed can-fail self-tests; hooks-test in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PostToolUse nudge only ever echoed a plain line to stdout, which under Claude Code reaches the transcript, not the model's context — the nudge never actually surfaced. skill-nudge.sh now emits a hook systemMessage JSON object under Claude Code (detected via CLAUDE_PLUGIN_ROOT or a hook_event_name in the payload) and keeps the plain line for Cursor's afterFileEdit, still exiting 0 on every path with no jq/python. hooks-test.sh gains: two assertions proving each delivery path (systemMessage under Claude, plain line under Cursor); a can-fail self-test block proving chk_silent/run_case_absent actually fail on bad input, backing the CHANGELOG's existing "can-fail test control" claim; and a CI step so the suite runs on every PR instead of nowhere. 15/15 cases pass. --- .github/workflows/validate.yml | 2 ++ CHANGELOG.md | 13 ++++++++----- hooks/skill-nudge.sh | 17 +++++++++++++---- scripts/hooks-test.sh | 31 +++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3b83a0f..ac8f0f7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,3 +29,5 @@ jobs: # CI is strict and deterministic: Python is guaranteed here, so run the # validator directly (the scripts/validate.sh graceful skip is for local use). - run: python3 scripts/validate.py + # Bash-only hook test harness; runs on both matrix legs. + - run: ./scripts/hooks-test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index c220145..29d03ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,17 +29,20 @@ alongside the existing Go 1.26.4+ floor. subagents carry the table into every brief, since a subagent does not inherit the parent session's skills. - Hooks: `hooks/skill-nudge.sh` — a `PostToolUse`/`afterFileEdit` hook that, after a Go file edit, - prints one nudge naming the go-coding skill the edit calls for (a `_test.go` file → `go-testing`; + delivers one nudge naming the go-coding skill the edit calls for (a `_test.go` file → `go-testing`; goroutine/channel/`sync`/`atomic`/`errgroup` content → `go-concurrency`; `fmt.Errorf`/`errors.*` → - `go-errors`), once per skill per session; wired dual-host in `hooks/hooks.json` and - `hooks/cursor-hooks.json`. A usage analysis showed these focused skills load far less often than - the router itself. + `go-errors`), once per skill per session, via the hook `systemMessage` channel under Claude Code + (reaches the model's context on exit 0) and a plain line under Cursor; wired dual-host in + `hooks/hooks.json` and `hooks/cursor-hooks.json`. A usage analysis showed these focused skills load + far less often than the router itself. - Hooks: `hooks/session-start.sh` — the SessionStart banner now names the skill-loading hop, lists the focused skills, points at `/go-explain` and `go-reviewer`, and adds a `/go-lint-setup` prompt when no `.golangci.y*ml`/`.toml`/`.json` file is found in the workspace. - Scripts: `scripts/hooks-test.sh` — a test harness covering `hooks/skill-nudge.sh` and the session-start banner, with a `chk_silent` helper that asserts exact emptiness for the no-nudge - cases instead of a vacuous non-match check, and a can-fail test control. + cases instead of a vacuous non-match check, and a can-fail test control: self-tests proving the + negative-case helpers (`chk_silent`, `run_case_absent`) actually fail on bad input rather than + passing vacuously. Runs in CI (`.github/workflows/validate.yml`) alongside `scripts/validate.py`. - Docs: `README.md` — "Using with subagent orchestrators": implementer and reviewer brief templates that carry the router's hop table into a dispatched subagent's instructions, so a plan runner's implementers and reviewers apply the standards even though a subagent does not inherit the parent diff --git a/hooks/skill-nudge.sh b/hooks/skill-nudge.sh index 0fd47d8..fe70fb1 100755 --- a/hooks/skill-nudge.sh +++ b/hooks/skill-nudge.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# PostToolUse / afterFileEdit hook: after a Go file is edited, print ONE line naming the go-coding -# skill the edit calls for — once per skill per session. Deterministic trigger for the focused -# skills a usage analysis showed are rarely loaded. Always exits 0; never blocks an edit. +# PostToolUse / afterFileEdit hook: after a Go file is edited, name ONE go-coding skill the edit +# calls for — once per skill per session — printed as a hook systemMessage (Claude Code) or a +# plain line (Cursor). Deterministic trigger for the focused skills a usage analysis showed are +# rarely loaded. Always exits 0; never blocks an edit. set -u f="${CLAUDE_FILE_PATH:-}"; sid="" if [ ! -t 0 ]; then @@ -21,5 +22,13 @@ if [ -z "$skill" ] && grep -qE 'fmt\.Errorf|errors\.(Is|As|AsType|New|Join)' "$f marker="${TMPDIR:-/tmp}/go-coding-nudge.${sid}.${skill}" [ -e "$marker" ] && exit 0 : > "$marker" 2>/dev/null || true -echo "› go-coding: this edit touches ${topic} — load go-coding:${skill} before continuing." +# topic/skill above are fixed literals set in this script (no quotes or backslashes), so the +# message below needs no JSON escaping before it goes into printf. If either is ever built from +# variable/external text instead, escape it first — printf does no JSON escaping of its own. +msg="› go-coding: this edit touches ${topic} — load go-coding:${skill} before continuing." +if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || printf '%s' "${payload:-}" | grep -q '"hook_event_name"'; then + printf '{"systemMessage":"%s"}\n' "$msg" # Claude Code: systemMessage reaches the model's context on exit 0 +else + printf '%s\n' "$msg" # Cursor afterFileEdit: plain line +fi exit 0 diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh index 8c8add0..c761158 100755 --- a/scripts/hooks-test.sh +++ b/scripts/hooks-test.sh @@ -32,6 +32,11 @@ s="test-$$"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* : > "$t/a_test.go"; printf 'package a\nfunc f(){ go func(){}() }\n' > "$t/w.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e.go"; : > "$t/plain.go"; : > "$t/readme.md" chk() { local name="$1" got="$2" expect="$3"; case "$got" in *"$expect"*) echo "ok $name";; *) echo "FAIL $name: got '$got'"; fails=$((fails+1));; esac; } chk_silent() { local name="$1" got="$2"; if [ -z "$got" ]; then echo "ok $name"; else echo "FAIL $name: expected silence, got '$got'"; fails=$((fails+1)); fi; } +# chk matches by substring, which is delivery-channel-agnostic: the nudge text appears whether +# wrapped as {"systemMessage":"..."} (Claude Code) or printed as a plain line (Cursor). These six +# cases therefore exercise whichever path is active for the ambient CLAUDE_PLUGIN_ROOT (unset in a +# plain shell, so normally the Cursor/plain path); the two delivery-channel cases below force each +# path explicitly. chk "test file nudges go-testing" "$(nudge "$t/a_test.go" "$s")" "go-coding:go-testing" chk_silent "second test file is silent" "$(nudge "$t/a_test.go" "$s")" chk "goroutine nudges go-concurrency" "$(nudge "$t/w.go" "$s")" "go-coding:go-concurrency" @@ -40,4 +45,30 @@ chk_silent "plain go file is silent" "$(nudge "$t/plain.go" "$s")" chk_silent "non-go file is silent" "$(nudge "$t/readme.md" "$s")" rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* +# Delivery channel (F4): a fresh fixture + session per case so dedupe cannot silence it, and each +# host path forced explicitly rather than relying on the ambient CLAUDE_PLUGIN_ROOT. +: > "$t/e2.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e2.go" +sjson="$s-json"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sjson."* +chk "nudge is a systemMessage under Claude" "$(CLAUDE_PLUGIN_ROOT=/x nudge "$t/e2.go" "$sjson")" '{"systemMessage":' +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sjson."* + +scursor="$s-cursor"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$scursor."* +# Built directly (not via nudge()) per the sketch: env -u only strips CLAUDE_PLUGIN_ROOT from an +# external-command invocation, and nudge's payload already lacks hook_event_name. +out="$(printf '{"session_id":"%s","tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$scursor" "$t/e2.go" | env -u CLAUDE_PLUGIN_ROOT bash "$here/hooks/skill-nudge.sh")" +case "$out" in + '{'*) echo "FAIL nudge is a plain line under Cursor: got JSON '$out'"; fails=$((fails+1));; + *"go-coding:go-errors"*) echo "ok nudge is a plain line under Cursor";; + *) echo "FAIL nudge is a plain line under Cursor: got '$out'"; fails=$((fails+1));; +esac +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$scursor."* + +# Can-fail control (F3): prove chk_silent and run_case_absent actually fail on bad input, so a +# broken helper (e.g. always echoing "ok") can't hide a real regression above. Each probe runs in a +# `$(...)` subshell — already isolated from this shell's `fails` — with its own `fails=0` so the +# probe's internal increment never has a chance to leak into the suite's real count. +selftest() { local name="$1" out="$2"; case "$out" in FAIL*) echo "ok $name";; *) echo "FAIL $name: helper did not fail on bad input: '$out'"; fails=$((fails+1));; esac; } +selftest "can-fail: chk_silent rejects non-empty" "$( fails=0; chk_silent "probe" "not-empty" )" +selftest "can-fail: run_case_absent rejects presence" "$( fails=0; run_case_absent "probe" "$t/go-no-lint" "/go-lint-setup" )" + [ "$fails" -eq 0 ] || exit 1 From 43f5dc224087b30726eb40a531fdfef477135c31 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 19:00:43 +0300 Subject: [PATCH 15/23] docs: inventory the nudge hook and test harness; re-front go-lint-setup; drop go-linting from the adoption target A whole-branch review found the component inventories (AGENTS.md, README.md, docs/install.md) still describing a two-hook plugin, hooks-test.sh unreferenced from any doc or release step, and go-lint-setup's body opening with an unconditional "Write the config" despite now also answering adoption/debug questions. Fixes: - AGENTS.md, README.md, docs/install.md: list skill-nudge.sh as the third host-agnostic hook; README/AGENTS gain rows/bullets for scripts/hooks-test.sh and scripts/usage-report.py; docs/versioning.md's release step 2 and both Development command blocks now run ./scripts/hooks-test.sh beside validate.sh. - docs/testing.md: hooks-test.sh added to Validation; a Local triggering test for the nudge hook that requires confirming the model ACTS on the nudge, not only that a line appears; the adoption-target list drops the deprecated go-linting stub; the counted-events paragraph now names the report's two tables (every-event vs. distinct-sessions) and how the 50% target is read off the sessions one. - skills/go-lint-setup/SKILL.md: H1 and intro now name both the scaffold and adopt/debug halves, with one routing line ahead of the numbered steps so a debug question doesn't fall into "write a file"; description already covered both, left untouched. - Bundled minors: AGENTS.md's three "read-only" -> "report-only" go-reviewer mentions (0.4.1's own established wording); validate.py's error text and the golangci.v2.yml header corrected to name two config copies, not three. --- AGENTS.md | 13 +++++++------ README.md | 3 +++ docs/install.md | 3 ++- docs/testing.md | 18 ++++++++++++------ docs/versioning.md | 2 +- references/golangci.v2.yml | 6 +++--- scripts/validate.py | 2 +- skills/go-lint-setup/SKILL.md | 11 ++++++++--- 8 files changed, 37 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c3cb98d..b5bef2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to AI coding assistants (Claude Code, Cursor, and co The **Go Coding Plugin** is an AI plugin by Cadasto B.V. that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, commands, agents, hooks, and Cursor rules. It targets **both Claude Code and Cursor** from a single shared component set. -> **Current status — v0.5.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the read-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` hooks. Do not assume a file is present because it is documented here — check first. +> **Current status — v0.5.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the report-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` + `skill-nudge` hooks. Do not assume a file is present because it is documented here — check first. ## Domain Context @@ -33,11 +33,11 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Cursor manifest**: `.cursor-plugin/plugin.json` — same metadata **plus** explicit top-level path keys (`skills`, `rules`, `agents`, `commands`, `hooks`). No `mcpServers` — this plugin has no MCP backend. Keep `name`/`version`/`description`/`author` identical to the Claude manifest. - **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` standards skills. `go-linting` is deprecated in 0.5.0 — a redirect stub merged into `go-lint-setup`, removed in 0.6.0. - **Slash commands** are authored as **user-invoked skills** (`skills//SKILL.md` with `argument-hint` + `allowed-tools`), not the legacy `commands/` folder — both yield a `/` command, but the skills layout is preferred (current `plugin-dev` guidance). Keep the surface small; put multi-step workflows in auto-invoked skills. Shipped: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). -- **Agents**: `agents/.md` — context-isolated specialists. Shipped: `go-reviewer` (read-only Go diff/file reviewer applying the review-heuristics catalog; `tools:` not `allowed-tools:`). +- **Agents**: `agents/.md` — context-isolated specialists. Shipped: `go-reviewer` (report-only Go diff/file reviewer applying the review-heuristics catalog; `tools:` not `allowed-tools:`). - **Cursor rules**: `rules/*.mdc` — Cursor-only rule guidance with frontmatter (`description`, `alwaysApply`, `globs`, e.g. `globs: ["**/*.go"]`), referenced by the Cursor manifest's `rules` path. Shipped: `rules/go-context.mdc` (mirrors the `go-coding` router). -- **Claude hooks**: `hooks/hooks.json` — object `{ "hooks": { "SessionStart": [...], "PostToolUse": [...] } }`; use `${CLAUDE_PLUGIN_ROOT}` in command paths. Present — wires `session-start.sh` (`SessionStart`) and `format-on-save.sh` (`PostToolUse`, `matcher: "Write|Edit"`). -- **Cursor hooks**: `hooks/cursor-hooks.json` — object `{ "hooks": { "sessionStart": [...], "afterFileEdit": [...] } }`; the command runs from the plugin root (a **workspace-relative** path, **not** `${CLAUDE_PLUGIN_ROOT}`). Present — wires `session-start.sh` (`sessionStart`) and `format-on-save.sh` (`afterFileEdit`). -- **Shared hook scripts**: `hooks/session-start.sh` — detects `go.mod` / `*.go`, prints one Go-standards context line, exits 0 always. `hooks/format-on-save.sh` — after a `*.go` Write/Edit, runs `gofumpt -w` (or `gofmt -w -s`) on that single file; resolves the path from `$CLAUDE_FILE_PATH` or the stdin tool-payload JSON, host-only, silent no-op if no formatter is installed, exits 0 always. Both host-agnostic so either manifest can invoke them. Present. +- **Claude hooks**: `hooks/hooks.json` — object `{ "hooks": { "SessionStart": [...], "PostToolUse": [...] } }`; use `${CLAUDE_PLUGIN_ROOT}` in command paths. Present — wires `session-start.sh` (`SessionStart`) and `format-on-save.sh` + `skill-nudge.sh` (`PostToolUse`, `matcher: "Write|Edit"`). +- **Cursor hooks**: `hooks/cursor-hooks.json` — object `{ "hooks": { "sessionStart": [...], "afterFileEdit": [...] } }`; the command runs from the plugin root (a **workspace-relative** path, **not** `${CLAUDE_PLUGIN_ROOT}`). Present — wires `session-start.sh` (`sessionStart`) and `format-on-save.sh` + `skill-nudge.sh` (`afterFileEdit`). +- **Shared hook scripts**: `hooks/session-start.sh` — detects `go.mod` / `*.go`, prints one Go-standards context line, exits 0 always. `hooks/format-on-save.sh` — after a `*.go` Write/Edit, runs `gofumpt -w` (or `gofmt -w -s`) on that single file; resolves the path from `$CLAUDE_FILE_PATH` or the stdin tool-payload JSON, host-only, silent no-op if no formatter is installed, exits 0 always. `hooks/skill-nudge.sh` — after a `*.go` Write/Edit, names ONE matching go-coding skill for that edit, once per skill per session; delivered as a hook `systemMessage` under Claude Code, a plain line under Cursor; exits 0 always. All three host-agnostic so either manifest can invoke them. Present. - **MCP config** *(optional, not present)*: `.mcp.json` — only if the plugin later integrates an MCP server. There is no companion MCP server today; do not reference one. - **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI's matrix installs `1.26.x` and `1.27.x`; the strict check runs on the 1.26.x (floor) leg, the 1.27.x leg soft-skips it) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. - **Contributor docs**: `docs/` for human-facing references — `install.md`, `testing.md`, `versioning.md`, `authoring.md`. `.github/` holds issue + PR templates, `copilot-instructions.md`, and the CI workflow. (Planning and research working notes are kept locally under `docs/`, **gitignored** — not part of the published plugin.) @@ -49,7 +49,7 @@ The full component surface: - **Skills** — *shipped*: `go-coding` (auto-invoked router) + `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout`. Each routes deeper topics to the enforcing tool and cites authoritative sources. - **Skill** — *deprecated in 0.5.0*: `go-linting`, a redirect stub merged into `go-lint-setup`; removed in 0.6.0. - **Slash commands** — *shipped* as user-invoked skills: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). -- **Agent** — *shipped*: `go-reviewer`, a context-isolated, read-only reviewer applying the review-heuristics catalog (no sub-agent dispatch; treats the diff as untrusted content). +- **Agent** — *shipped*: `go-reviewer`, a context-isolated, report-only reviewer applying the review-heuristics catalog (no sub-agent dispatch; treats the diff as untrusted content). - **Cursor rule** — *shipped*: `rules/go-context.mdc`, scoped to `**/*.go`, mirroring the `go-coding` router for Cursor. ## Development @@ -60,6 +60,7 @@ No build step — the plugin is pure Markdown + JSON. Validate and dogfood local ```bash ./scripts/validate.sh # dual-host parity / frontmatter (soft-skips if no python3) +./scripts/hooks-test.sh # bash tests for hooks/session-start.sh + hooks/skill-nudge.sh claude plugin validate . # manifest + component structure (no extra deps) claude --plugin-dir /path/to/go-coding-plugin # load locally for one session (dogfooding) ``` diff --git a/README.md b/README.md index 935c5d1..704d563 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,14 @@ Or load a local working copy for a single session: `claude --plugin-dir /path/to | Skill `go-coding` | shipped | Auto-invoked router: sends each Go topic to the enforcing tool and the focused skill below; recommends `gopls-lsp`. | | Session-start hook | shipped | Detects a Go workspace (`go.mod`/`*.go`) and prints one standards line; dual-host. | | Format-on-save hook | shipped | After each `Write`/`Edit` of a `*.go` file, runs `gofumpt -w` (or `gofmt -w -s`) on it; dual-host, host-only, silent no-op if no formatter is installed. | +| Skill-nudge hook | shipped | After each `Write`/`Edit` of a `*.go` file, names ONE matching go-coding skill for that edit, once per skill per session; dual-host — delivered as a hook `systemMessage` under Claude Code, a plain line under Cursor. | | Skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` | shipped | Load-on-use standards — each rule cited, framed around the enforcing linter (`modernize`, `errorlint`, `-race`, …). `go-layout` also owns naming, doc comments, and exported-API shape. | | Skill `go-linting` | deprecated in 0.5.0 | Redirect stub merged into `go-lint-setup`, which now owns the golangci-lint v2 config-schema, adoption, and upgrade-breakage content; removed in 0.6.0. | | Agent `go-reviewer` | shipped | Report-only, context-isolated Go reviewer for what linters miss; severity-ranked findings, no sub-agent dispatch. Its grant excludes `Write`/`Edit` but includes `Bash` to run the linters, so no-edit is a contract it keeps rather than a sandbox that enforces it. | | Skills `/go-explain`, `/go-lint-setup` (user-invoked) | shipped | Slash-command skills — idiom/standard lookup; scaffold, adopt, or debug the golangci-lint v2 config in a repo. | | Lint config `references/golangci.v2.yml` | shipped | Reference golangci-lint v2 config (`modernize` + stack linters). | | Cursor rule `go-context.mdc` | shipped | `**/*.go`-scoped guidance mirroring the router for Cursor. | +| Scripts `scripts/hooks-test.sh`, `scripts/usage-report.py` | shipped | Dev tooling, not part of the installed component surface: a bash test harness for the hooks, and a stdlib-only adoption-report generator over local session transcripts. | Guidance is grounded in authoritative sources — [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), the [Google](https://google.github.io/styleguide/go/) and [Uber](https://github.com/uber-go/guide) style guides — and the standard toolchain (`gofmt`/`gofumpt`, `go vet`, `staticcheck`, `golangci-lint`, `go test -race`). @@ -53,6 +55,7 @@ No build step — the plugin is pure Markdown + JSON. Validate locally: ```bash ./scripts/validate.sh # manifests, parity, paths, frontmatter, taught linters, fixers +./scripts/hooks-test.sh # bash tests for hooks/session-start.sh + hooks/skill-nudge.sh claude plugin validate . # manifest + component structure ``` diff --git a/docs/install.md b/docs/install.md index e491425..c66be41 100644 --- a/docs/install.md +++ b/docs/install.md @@ -96,9 +96,10 @@ These are **host-only** dev tools; the plugin still works without them (the form ## Hooks -The plugin ships two host-agnostic hooks (Claude `hooks/hooks.json`, Cursor `hooks/cursor-hooks.json`): +The plugin ships three host-agnostic hooks (Claude `hooks/hooks.json`, Cursor `hooks/cursor-hooks.json`): - **`session-start.sh`** — on session start, detects a Go workspace (`go.mod`/`*.go`) and prints one standards line. - **`format-on-save.sh`** — after each edit of a `*.go` file (Claude `PostToolUse` on `Write`/`Edit`; Cursor `afterFileEdit`), runs **`gofumpt -w`** on that file, or **`gofmt -w -s`** when `gofumpt` is not installed. It is **host-only** (no container round-trip), **edits the file in place**, and is a **silent no-op** when no Go formatter is on `PATH` — so install `gofmt` (ships with Go) or `gofumpt` to benefit. It never blocks an edit. This is per-file formatting only; run `golangci-lint` and your tests via CI/`make` for full-tree checks. +- **`skill-nudge.sh`** — after each edit of a `*.go` file (same trigger as `format-on-save.sh`), names ONE matching go-coding skill for that edit (a `_test.go` file → `go-testing`; goroutine/channel/`sync`/`atomic`/`errgroup` content → `go-concurrency`; `fmt.Errorf`/`errors.*` → `go-errors`), once per skill per session. Delivered as a hook `systemMessage` under Claude Code (reaches the model's context on exit 0) or a plain line under Cursor. It never blocks an edit. > The Cursor wiring targets the `afterFileEdit` event; if your Cursor version exposes a different post-edit event or payload shape, adjust `hooks/cursor-hooks.json` and the path-extraction in `format-on-save.sh` accordingly. diff --git a/docs/testing.md b/docs/testing.md index 49c21ca..675d432 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -7,6 +7,7 @@ exercising the components. ## Validation - **Manifest / component validation** — `./scripts/validate.sh` (also run by CI on every PR): checks both `plugin.json` manifests, dual-host parity (name/version/description/author agree), declared component paths, kebab-case names, hook-config JSON, and SKILL.md / agent / command frontmatter (including `name` == directory/filename, and that agents declare `tools:` not `allowed-tools:`). The wrapper runs `scripts/validate.py`; if Python 3 isn't installed it prints a warning and skips (exit 0) rather than failing — install `python3` for the full local check, or rely on `claude plugin validate .` and CI. CI pins Python so the deep check always runs there. +- **Hook tests** — `./scripts/hooks-test.sh` (also run by CI on every PR): bash tests for `hooks/session-start.sh` and `hooks/skill-nudge.sh`, including a can-fail self-test block that proves the negative-case helpers actually fail on bad input. - **Official validator** — `claude plugin validate .`: checks the manifest and component structure (no extra dependencies). - **Structural review** — run the `plugin-dev:plugin-validator` agent after creating or modifying components. - **Skill quality review** — run the `plugin-dev:skill-reviewer` agent: description-triggering quality, progressive disclosure, content structure. @@ -19,6 +20,7 @@ Install from your working copy (see [install.md](install.md)), then exercise eac - **Session-start hook** — open a repo with a `go.mod`/`*.go`; one Go-standards line should print at session start (and nothing in a non-Go repo). - **`go-coding` router** — ask for a Go review or idiom help; it should route to the enforcing tool and the focused skill. - **Standards skills** — a topic prompt should engage the matching skill (for example error wrapping → `go-errors`, a flaky time-based test → `go-testing`/`go-concurrency`, linter setup → `go-lint-setup`). +- **Skill-nudge hook** — edit a `_test.go` file; the nudge should name `go-coding:go-testing` (as a systemMessage under Claude Code, a plain line under Cursor) and, critically, the model should ACT on it — load the skill — not merely have the line appear in the transcript. A second edit to a `_test.go` file in the same session should be silent (once per skill per session). - **`go-reviewer` agent** — ask for a Go code review; it returns severity-ranked findings and does not spawn sub-agents. - **Slash commands (skills)** — `/go-explain ` and `/go-lint-setup`. - **Cursor rule** — in Cursor, open a `.go` file and confirm `go-context.mdc` attaches. @@ -42,14 +44,18 @@ file instead. `--help` repeats the counting rules. **Counted:** a `Skill` tool invocation whose `skill` input starts with `go-coding:`; a `Task`/`Agent` tool invocation with `subagent_type` `go-coding:go-reviewer`; a user `` invocation naming a go-coding skill. Events are split into main-session, -subagent, and user-invoked, per month, and deduplicated by session. +subagent, and user-invoked, per month. The report renders two tables: "Per skill / agent" +counts every event (so a skill loaded three times in one session counts three times), and +"Sessions with >=1 event, per skill / agent" counts distinct sessions instead — a skill +loaded three times in one session counts once there. The 50% target below is read off the +sessions table: (sessions loading a given focused skill) / (sessions loading +`go-coding:go-coding`). **Not counted:** the SessionStart banner line, or skill-body text echoed back inside tool results — only structured tool invocations and explicit slash-command text count. **Target:** the focused standards skills (`go-errors`, `go-testing`, `go-idioms`, -`go-linting`, `go-lint-setup`, `go-layout`, `go-concurrency`) should load on at least -50% of sessions where the `go-coding` router itself loads, and `go-layout` / -`go-concurrency` specifically should show non-zero counts in any period where the -corresponding work (project layout/API design, or goroutines/channels/context) is -actually touched. +`go-lint-setup`, `go-layout`, `go-concurrency`) should load on at least 50% of sessions +where the `go-coding` router itself loads, and `go-layout` / `go-concurrency` +specifically should show non-zero counts in any period where the corresponding work +(project layout/API design, or goroutines/channels/context) is actually touched. diff --git a/docs/versioning.md b/docs/versioning.md index 4c754a7..1d5d524 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -17,7 +17,7 @@ bump. 1. Bump `version` in **both** manifests (they must agree): `.claude-plugin/plugin.json` and `.cursor-plugin/plugin.json`. Keep `description` and `author` identical across both — `scripts/validate.py` enforces this parity. -2. Run `./scripts/validate.sh` and `claude plugin validate .`. +2. Run `./scripts/validate.sh`, `./scripts/hooks-test.sh`, and `claude plugin validate .`. 3. **Dogfood:** load the working copy (`claude --plugin-dir /path/to/go-coding-plugin`) and exercise the components against a real Go change on **both** hosts — see [testing.md](testing.md). 4. Fold the accumulated `## [Unreleased]` notes into a dated `## [X.Y.Z] - YYYY-MM-DD` section in diff --git a/references/golangci.v2.yml b/references/golangci.v2.yml index 1022d02..1d79f6d 100644 --- a/references/golangci.v2.yml +++ b/references/golangci.v2.yml @@ -4,9 +4,9 @@ # Schema is golangci-lint v2 — a v1 config will NOT parse (run `golangci-lint migrate` on a v1 # config). Needs a golangci-lint v2 release recent enough to know every linter named below; if it # rejects one, bump the pin rather than dropping the line. Pin an exact version in CI in one place -# (see `go-lint-setup` — no version is blessed here on purpose). See the `go-lint-setup` skill for -# what each linter does and why. Keep this in sync with the scaffold block in -# `skills/go-lint-setup/SKILL.md`. +# (see `go-lint-setup` — no version is blessed here on purpose). For what each linter does and why, +# see the inline comments below; for adopting or debugging an existing config, see the +# `go-lint-setup` skill. Keep this in sync with the scaffold block in `skills/go-lint-setup/SKILL.md`. version: "2" linters: diff --git a/scripts/validate.py b/scripts/validate.py index ab4f8a7..06ad1b7 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -187,7 +187,7 @@ def validate_linter_references(): taught.update(re.findall(r"[Tt]he\s+`([a-z0-9-]+)`\s+linter", body)) for name in sorted(taught - allowed): err(f"{md.relative_to(ROOT)}: teaches the '{name}' linter but " - f"references/golangci.v2.yml does not enable it — enable it in all three " + f"references/golangci.v2.yml does not enable it — enable it in both " f"config copies or stop naming it (advice == tooling)") diff --git a/skills/go-lint-setup/SKILL.md b/skills/go-lint-setup/SKILL.md index 78cdc77..d571177 100644 --- a/skills/go-lint-setup/SKILL.md +++ b/skills/go-lint-setup/SKILL.md @@ -5,12 +5,17 @@ argument-hint: optional target path (defaults to .golangci.yml) allowed-tools: Read, Write, Glob, Bash --- -# go-lint-setup — scaffold golangci-lint v2 +# go-lint-setup — scaffold, adopt, or debug golangci-lint v2 > **Bundled `references/` is at the plugin root** (beside `skills/`, two levels above this file) — *not* under this skill. Read `references/golangci.v2.yml` as `${CLAUDE_PLUGIN_ROOT}/references/golangci.v2.yml` on Claude Code, or `../../references/golangci.v2.yml` from this skill's directory, or Glob for the installed `references/golangci.v2.yml` (host-agnostic). -Scaffold the plugin's reference **golangci-lint v2** config into the current repo so its linting -matches the `go-coding` standards. Single interaction. +Scaffold the plugin's reference **golangci-lint v2** config into a repo that has none, or adopt or +debug an existing config already in the repo — schema questions, migration, which linters, +`//nolint`, exclusions. Scaffolding is a single interaction. + +Asked to set up or scaffold a config → Steps 1–3 below. Asked about an existing config (rejected +keys, migration, which linters, `//nolint`, exclusions) → skip to *Adopting or debugging an +existing config*; do not write a file. Steps: From 9be6d9c1320f803f08463bb5b534dda55a395d41 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 22:18:21 +0300 Subject: [PATCH 16/23] fix(hooks): nudge on the edit, not on the whole file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nudge grepped the whole edited file, so "this edit touches an error path" fired for any file that merely defines a sentinel somewhere — measured at 43% of non-test files in one real SDK, 65% in another. Under Claude Code the hook now classifies from the PostToolUse payload, which carries the new text (plus a few lines of context), and says "this edit"; Cursor sends only a path, so that host falls back to the whole file and the message says "this file". Volume was never the problem — the once-per-skill-per-session marker caps a session at three nudges — but the claim has to be true when it fires. Also drops /go-explain from the session banner ahead of the skill's removal, and adds two tests: an unrelated edit inside an error-heavy file stays silent, and a path-only payload still classifies from the file. Co-Authored-By: Claude Opus 4.8 --- hooks/session-start.sh | 2 +- hooks/skill-nudge.sh | 29 +++++++++++++++++++++-------- scripts/hooks-test.sh | 32 +++++++++++++++++++++++--------- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/hooks/session-start.sh b/hooks/session-start.sh index bdd70f5..0efe5a6 100755 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -18,7 +18,7 @@ if is_go_workspace; then for cfg in .golangci.yml .golangci.yaml .golangci.toml .golangci.json; do [ -e "$cfg" ] && lint="" && break done - echo "› Go workspace — go-coding: load go-coding then the skill for your diff (go-errors · go-testing · go-idioms · go-concurrency · go-layout); /go-explain for a one-shot idiom lookup; go-reviewer for a diff review${lint}. gofmt/golangci-lint v2 + gopls-lsp recommended." + echo "› Go workspace — go-coding: load go-coding then the skill for your diff (go-errors · go-testing · go-idioms · go-concurrency · go-layout); go-reviewer for a diff review${lint}. gofmt/golangci-lint v2 + gopls-lsp recommended." fi exit 0 diff --git a/hooks/skill-nudge.sh b/hooks/skill-nudge.sh index fe70fb1..72655ca 100755 --- a/hooks/skill-nudge.sh +++ b/hooks/skill-nudge.sh @@ -3,8 +3,10 @@ # calls for — once per skill per session — printed as a hook systemMessage (Claude Code) or a # plain line (Cursor). Deterministic trigger for the focused skills a usage analysis showed are # rarely loaded. Always exits 0; never blocks an edit. +# +# At most three nudges reach a session (one per skill), each on its first match. set -u -f="${CLAUDE_FILE_PATH:-}"; sid="" +f="${CLAUDE_FILE_PATH:-}"; sid=""; payload="" if [ ! -t 0 ]; then payload="$(cat)" [ -n "$f" ] || f="$(printf '%s' "$payload" | grep -oE '"file_?[Pp]ath"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 | sed -E 's/.*"([^"]+)"$/\1/')" @@ -14,19 +16,30 @@ fi case "$f" in *.go) ;; *) exit 0 ;; esac [ -f "$f" ] || exit 0 [ -n "$sid" ] || sid="ppid$PPID" + +# What gets matched: the edit itself where the host hands it over (a Claude Code PostToolUse +# payload carries the new text in tool_input, plus a few lines of surrounding context in +# tool_response), otherwise the whole file (Cursor's afterFileEdit passes only a path). Matching +# the edit is what keeps the message honest — grepping a whole file says "this edit touches an +# error path" for any file that merely happens to define a sentinel somewhere. +case "$payload" in + *'"tool_input"'*) subject="$payload"; what="this edit";; + *) subject="$(cat "$f")"; what="this file";; +esac + skill=""; topic="" -case "$f" in *_test.go) skill="go-testing"; topic="a test file";; esac -if [ -z "$skill" ] && grep -qE 'go func|chan |<-chan|chan<-|sync\.|atomic\.|errgroup\.' "$f"; then skill="go-concurrency"; topic="goroutines, channels or sync"; fi -if [ -z "$skill" ] && grep -qE 'fmt\.Errorf|errors\.(Is|As|AsType|New|Join)' "$f"; then skill="go-errors"; topic="an error path"; fi +case "$f" in *_test.go) skill="go-testing"; topic="a test file"; what="this edit";; esac +if [ -z "$skill" ] && printf '%s' "$subject" | grep -qE 'go func|chan |<-chan|chan<-|sync\.|atomic\.|errgroup\.'; then skill="go-concurrency"; topic="goroutines, channels or sync"; fi +if [ -z "$skill" ] && printf '%s' "$subject" | grep -qE 'fmt\.Errorf|errors\.(Is|As|AsType|New|Join)'; then skill="go-errors"; topic="an error path"; fi [ -n "$skill" ] || exit 0 marker="${TMPDIR:-/tmp}/go-coding-nudge.${sid}.${skill}" [ -e "$marker" ] && exit 0 : > "$marker" 2>/dev/null || true -# topic/skill above are fixed literals set in this script (no quotes or backslashes), so the -# message below needs no JSON escaping before it goes into printf. If either is ever built from +# what/topic/skill above are fixed literals set in this script (no quotes or backslashes), so the +# message below needs no JSON escaping before it goes into printf. If any is ever built from # variable/external text instead, escape it first — printf does no JSON escaping of its own. -msg="› go-coding: this edit touches ${topic} — load go-coding:${skill} before continuing." -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || printf '%s' "${payload:-}" | grep -q '"hook_event_name"'; then +msg="› go-coding: ${what} touches ${topic} — load go-coding:${skill} before continuing." +if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || printf '%s' "$payload" | grep -q '"hook_event_name"'; then printf '{"systemMessage":"%s"}\n' "$msg" # Claude Code: systemMessage reaches the model's context on exit 0 else printf '%s\n' "$msg" # Cursor afterFileEdit: plain line diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh index c761158..0e038e6 100755 --- a/scripts/hooks-test.sh +++ b/scripts/hooks-test.sh @@ -20,13 +20,19 @@ mkdir -p "$t/go-with-lint" "$t/go-no-lint" "$t/not-go" printf 'module x\n\ngo 1.26.0\n' > "$t/go-with-lint/go.mod"; : > "$t/go-with-lint/.golangci.yml" printf 'module x\n\ngo 1.26.0\n' > "$t/go-no-lint/go.mod" run_case "go repo with lint config names the hop" "$t/go-with-lint" "load go-coding then the skill for your diff" -run_case "go repo with lint config names /go-explain" "$t/go-with-lint" "/go-explain " +run_case "go repo with lint config names go-reviewer" "$t/go-with-lint" "go-reviewer for a diff review" run_case_absent "go repo with lint config omits lint-setup" "$t/go-with-lint" "/go-lint-setup" +run_case_absent "banner names no removed skill" "$t/go-no-lint" "go-explain" run_case "go repo without lint config offers setup" "$t/go-no-lint" "/go-lint-setup" run_case "non-go repo is silent" "$t/not-go" "" -nudge() { # FILE SESSION -> stdout - printf '{"session_id":"%s","tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$2" "$1" | bash "$here/hooks/skill-nudge.sh" +nudge() { # FILE SESSION [EDIT_TEXT] -> stdout. Claude Code shape: a tool_input payload, so the + # hook classifies from the edit text, not from the whole file. + printf '{"session_id":"%s","tool_name":"Edit","tool_input":{"file_path":"%s","new_string":"%s"}}' "$2" "$1" "${3:-}" | bash "$here/hooks/skill-nudge.sh" +} +nudge_path() { # FILE SESSION -> stdout. Cursor shape: a path and no tool_input, so the hook falls + # back to classifying from the whole file. + printf '{"session_id":"%s","file_path":"%s"}' "$2" "$1" | bash "$here/hooks/skill-nudge.sh" } s="test-$$"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* : > "$t/a_test.go"; printf 'package a\nfunc f(){ go func(){}() }\n' > "$t/w.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e.go"; : > "$t/plain.go"; : > "$t/readme.md" @@ -39,23 +45,31 @@ chk_silent() { local name="$1" got="$2"; if [ -z "$got" ]; then echo "ok $name # path explicitly. chk "test file nudges go-testing" "$(nudge "$t/a_test.go" "$s")" "go-coding:go-testing" chk_silent "second test file is silent" "$(nudge "$t/a_test.go" "$s")" -chk "goroutine nudges go-concurrency" "$(nudge "$t/w.go" "$s")" "go-coding:go-concurrency" -chk "fmt.Errorf nudges go-errors" "$(nudge "$t/e.go" "$s")" "go-coding:go-errors" +chk "goroutine edit nudges go-concurrency" "$(nudge "$t/w.go" "$s" 'go func(){}()')" "go-coding:go-concurrency" +chk "fmt.Errorf edit nudges go-errors" "$(nudge "$t/e.go" "$s" 'fmt.Errorf')" "go-coding:go-errors" chk_silent "plain go file is silent" "$(nudge "$t/plain.go" "$s")" chk_silent "non-go file is silent" "$(nudge "$t/readme.md" "$s")" rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* +# Classify from the edit, not the file: an unrelated edit inside an error-heavy file must stay +# silent. Whole-file matching would claim "this edit touches an error path" for every file that +# merely defines a sentinel somewhere, which is most of a real Go repo. +sedit="$s-edit"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sedit."* +chk_silent "unrelated edit in an error-heavy file is silent" "$(nudge "$t/e.go" "$sedit" '// tidy the doc comment')" +chk "path-only payload falls back to the file" "$(nudge_path "$t/e.go" "$sedit")" "go-coding:go-errors" +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sedit."* + # Delivery channel (F4): a fresh fixture + session per case so dedupe cannot silence it, and each # host path forced explicitly rather than relying on the ambient CLAUDE_PLUGIN_ROOT. : > "$t/e2.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e2.go" sjson="$s-json"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sjson."* -chk "nudge is a systemMessage under Claude" "$(CLAUDE_PLUGIN_ROOT=/x nudge "$t/e2.go" "$sjson")" '{"systemMessage":' +chk "nudge is a systemMessage under Claude" "$(CLAUDE_PLUGIN_ROOT=/x nudge "$t/e2.go" "$sjson" 'fmt.Errorf')" '{"systemMessage":' rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sjson."* scursor="$s-cursor"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$scursor."* -# Built directly (not via nudge()) per the sketch: env -u only strips CLAUDE_PLUGIN_ROOT from an -# external-command invocation, and nudge's payload already lacks hook_event_name. -out="$(printf '{"session_id":"%s","tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$scursor" "$t/e2.go" | env -u CLAUDE_PLUGIN_ROOT bash "$here/hooks/skill-nudge.sh")" +# Built directly (not via nudge_path()) per the sketch: env -u only strips CLAUDE_PLUGIN_ROOT from +# an external-command invocation, and the path-only payload already lacks hook_event_name. +out="$(printf '{"session_id":"%s","file_path":"%s"}' "$scursor" "$t/e2.go" | env -u CLAUDE_PLUGIN_ROOT bash "$here/hooks/skill-nudge.sh")" case "$out" in '{'*) echo "FAIL nudge is a plain line under Cursor: got JSON '$out'"; fails=$((fails+1));; *"go-coding:go-errors"*) echo "ok nudge is a plain line under Cursor";; From 16b7d43e00219c2d6f953cafc666af82937ebe02 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 22:18:29 +0300 Subject: [PATCH 17/23] refactor(skills)!: remove go-linting and go-explain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go-linting shipped earlier in this branch as a deprecation stub pointing at go-lint-setup, which had absorbed its content. Delete it outright instead: a redirect that spends always-on description budget to say "load a different skill" earns less than the bytes it costs, and nothing in the tree pointed at it any more. go-explain goes with it. It answered one-shot lookups — "what's the modern way to do X" — which the focused skills already answer, with the same citations and in the context of the actual code; a usage scan recorded a single invocation in four weeks. Removing it leaves one routing story instead of two. The adoption script keeps both names in its known-skill list so a scan of older transcripts still resolves the events they produced. BREAKING CHANGE: /go-explain is gone — ask the topic and let the router load the focused skill. Prompts naming go-linting should name go-lint-setup. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 10 +++++----- README.md | 3 +-- docs/authoring.md | 2 +- docs/testing.md | 2 +- scripts/usage-report.py | 10 ++++++---- skills/go-explain/SKILL.md | 28 ---------------------------- skills/go-linting/SKILL.md | 10 ---------- 7 files changed, 14 insertions(+), 51 deletions(-) delete mode 100644 skills/go-explain/SKILL.md delete mode 100644 skills/go-linting/SKILL.md diff --git a/AGENTS.md b/AGENTS.md index b5bef2b..89a148b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to AI coding assistants (Claude Code, Cursor, and co The **Go Coding Plugin** is an AI plugin by Cadasto B.V. that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, commands, agents, hooks, and Cursor rules. It targets **both Claude Code and Cursor** from a single shared component set. -> **Current status — v0.5.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-linting` (deprecated — merged into `go-lint-setup`), `go-layout`; the report-only `go-reviewer` agent; the user-invoked `/go-explain` and `/go-lint-setup` skills; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` + `skill-nudge` hooks. Do not assume a file is present because it is documented here — check first. +> **Current status — v0.5.0.** A complete dual-host (Claude Code + Cursor) Go-standards set, baselined on **Go 1.26.4+** (Go 1.27 supported; its additions are flagged as hints) as a hard floor (no fallback guidance for 1.25 or older; version annotations remain as provenance), that validates clean (`./scripts/validate.sh` + `claude plugin validate .`): the auto-invoked `go-coding` **router** skill; the focused standards skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout`; the report-only `go-reviewer` agent; the user-invoked `/go-lint-setup` skill; a shipped `references/golangci.v2.yml`; the `rules/go-context.mdc` Cursor rule; and host-agnostic `session-start` + `format-on-save` + `skill-nudge` hooks. Do not assume a file is present because it is documented here — check first. ## Domain Context @@ -31,8 +31,8 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Claude manifest**: `.claude-plugin/plugin.json` — `name`, `version`, `description`, `author` (an **object** `{name, url}` — `claude plugin validate` rejects a string), `license`, `repository`, `keywords`. Claude Code discovers components from the **default folders** (`skills/`, `commands/`, `agents/`, `hooks/`) automatically; no explicit path map is needed. - **Cursor manifest**: `.cursor-plugin/plugin.json` — same metadata **plus** explicit top-level path keys (`skills`, `rules`, `agents`, `commands`, `hooks`). No `mcpServers` — this plugin has no MCP backend. Keep `name`/`version`/`description`/`author` identical to the Claude manifest. -- **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` standards skills. `go-linting` is deprecated in 0.5.0 — a redirect stub merged into `go-lint-setup`, removed in 0.6.0. -- **Slash commands** are authored as **user-invoked skills** (`skills//SKILL.md` with `argument-hint` + `allowed-tools`), not the legacy `commands/` folder — both yield a `/` command, but the skills layout is preferred (current `plugin-dev` guidance). Keep the surface small; put multi-step workflows in auto-invoked skills. Shipped: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). +- **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` standards skills. `go-linting` was merged into `go-lint-setup` and removed in 0.5.0. +- **Slash commands** are authored as **user-invoked skills** (`skills//SKILL.md` with `argument-hint` + `allowed-tools`), not the legacy `commands/` folder — both yield a `/` command, but the skills layout is preferred (current `plugin-dev` guidance). Keep the surface small; put multi-step workflows in auto-invoked skills. Shipped: `/go-lint-setup` (scaffold the golangci-lint v2 config). `/go-explain` was removed in 0.5.0 — a one-shot lookup the focused skills already answer. - **Agents**: `agents/.md` — context-isolated specialists. Shipped: `go-reviewer` (report-only Go diff/file reviewer applying the review-heuristics catalog; `tools:` not `allowed-tools:`). - **Cursor rules**: `rules/*.mdc` — Cursor-only rule guidance with frontmatter (`description`, `alwaysApply`, `globs`, e.g. `globs: ["**/*.go"]`), referenced by the Cursor manifest's `rules` path. Shipped: `rules/go-context.mdc` (mirrors the `go-coding` router). - **Claude hooks**: `hooks/hooks.json` — object `{ "hooks": { "SessionStart": [...], "PostToolUse": [...] } }`; use `${CLAUDE_PLUGIN_ROOT}` in command paths. Present — wires `session-start.sh` (`SessionStart`) and `format-on-save.sh` + `skill-nudge.sh` (`PostToolUse`, `matcher: "Write|Edit"`). @@ -47,8 +47,8 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma The full component surface: - **Skills** — *shipped*: `go-coding` (auto-invoked router) + `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout`. Each routes deeper topics to the enforcing tool and cites authoritative sources. -- **Skill** — *deprecated in 0.5.0*: `go-linting`, a redirect stub merged into `go-lint-setup`; removed in 0.6.0. -- **Slash commands** — *shipped* as user-invoked skills: `/go-explain` (idiom/standard lookup) and `/go-lint-setup` (scaffold the golangci-lint v2 config). +- **Slash commands** — *shipped* as user-invoked skill: `/go-lint-setup` (scaffold the golangci-lint v2 config). +- **Removed in 0.5.0** — `go-linting` (merged into `go-lint-setup`) and `/go-explain` (a one-shot lookup the focused skills already answer). Do not re-add either; route the topic instead. - **Agent** — *shipped*: `go-reviewer`, a context-isolated, report-only reviewer applying the review-heuristics catalog (no sub-agent dispatch; treats the diff as untrusted content). - **Cursor rule** — *shipped*: `rules/go-context.mdc`, scoped to `**/*.go`, mirroring the `go-coding` router for Cursor. diff --git a/README.md b/README.md index 704d563..3b55d94 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,8 @@ Or load a local working copy for a single session: `claude --plugin-dir /path/to | Format-on-save hook | shipped | After each `Write`/`Edit` of a `*.go` file, runs `gofumpt -w` (or `gofmt -w -s`) on it; dual-host, host-only, silent no-op if no formatter is installed. | | Skill-nudge hook | shipped | After each `Write`/`Edit` of a `*.go` file, names ONE matching go-coding skill for that edit, once per skill per session; dual-host — delivered as a hook `systemMessage` under Claude Code, a plain line under Cursor. | | Skills `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` | shipped | Load-on-use standards — each rule cited, framed around the enforcing linter (`modernize`, `errorlint`, `-race`, …). `go-layout` also owns naming, doc comments, and exported-API shape. | -| Skill `go-linting` | deprecated in 0.5.0 | Redirect stub merged into `go-lint-setup`, which now owns the golangci-lint v2 config-schema, adoption, and upgrade-breakage content; removed in 0.6.0. | | Agent `go-reviewer` | shipped | Report-only, context-isolated Go reviewer for what linters miss; severity-ranked findings, no sub-agent dispatch. Its grant excludes `Write`/`Edit` but includes `Bash` to run the linters, so no-edit is a contract it keeps rather than a sandbox that enforces it. | -| Skills `/go-explain`, `/go-lint-setup` (user-invoked) | shipped | Slash-command skills — idiom/standard lookup; scaffold, adopt, or debug the golangci-lint v2 config in a repo. | +| Skill `/go-lint-setup` (user-invoked) | shipped | Slash-command skill — scaffold, adopt, or debug the golangci-lint v2 config in a repo. | | Lint config `references/golangci.v2.yml` | shipped | Reference golangci-lint v2 config (`modernize` + stack linters). | | Cursor rule `go-context.mdc` | shipped | `**/*.go`-scoped guidance mirroring the router for Cursor. | | Scripts `scripts/hooks-test.sh`, `scripts/usage-report.py` | shipped | Dev tooling, not part of the installed component surface: a bash test harness for the hooks, and a stdlib-only adoption-report generator over local session transcripts. | diff --git a/docs/authoring.md b/docs/authoring.md index 099d9d2..0cff9f8 100644 --- a/docs/authoring.md +++ b/docs/authoring.md @@ -20,7 +20,7 @@ The detailed companion to [AGENTS.md](../AGENTS.md) (which is authoritative); th standards skills are the model. - **Skill (user-invoked / slash command)** — a thin one-shot `skills//SKILL.md` that also carries `argument-hint` + `allowed-tools`; use `$ARGUMENTS` in the body. Invoked as `/`. See - `/go-explain`, `/go-lint-setup`. (The legacy `commands/` folder is not used.) + `/go-lint-setup`. (The legacy `commands/` folder is not used.) - **Agent** — a context-isolated specialist. Use **`tools:`** (a YAML block list), **never** `allowed-tools:` — in an agent that key is silently ignored and the agent inherits *all* tools. See `go-reviewer` (report-only, no sub-agent dispatch). diff --git a/docs/testing.md b/docs/testing.md index 675d432..8e085ac 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,7 +22,7 @@ Install from your working copy (see [install.md](install.md)), then exercise eac - **Standards skills** — a topic prompt should engage the matching skill (for example error wrapping → `go-errors`, a flaky time-based test → `go-testing`/`go-concurrency`, linter setup → `go-lint-setup`). - **Skill-nudge hook** — edit a `_test.go` file; the nudge should name `go-coding:go-testing` (as a systemMessage under Claude Code, a plain line under Cursor) and, critically, the model should ACT on it — load the skill — not merely have the line appear in the transcript. A second edit to a `_test.go` file in the same session should be silent (once per skill per session). - **`go-reviewer` agent** — ask for a Go code review; it returns severity-ranked findings and does not spawn sub-agents. -- **Slash commands (skills)** — `/go-explain ` and `/go-lint-setup`. +- **Slash command (skill)** — `/go-lint-setup`. - **Cursor rule** — in Cursor, open a `.go` file and confirm `go-context.mdc` attaches. After editing content, reinstall (or restart the session) to pick up changes. diff --git a/scripts/usage-report.py b/scripts/usage-report.py index c27bfc0..ad1b45f 100755 --- a/scripts/usage-report.py +++ b/scripts/usage-report.py @@ -13,8 +13,8 @@ ``name`` in (``Task``, ``Agent``) and ``input.subagent_type == "go-coding:go-reviewer"``; * a ``user`` message whose text contains a ``...`` - invocation naming one of this plugin's skills (bare, e.g. ``/go-explain``, - or namespaced, e.g. ``/go-coding:go-explain``). + invocation naming one of this plugin's skills (bare, e.g. ``/go-lint-setup``, + or namespaced, e.g. ``/go-coding:go-lint-setup``). Session-start banner text and skill-body text echoed back inside tool results are NOT scanned for matches — only structured ``tool_use`` blocks and user @@ -38,7 +38,9 @@ from pathlib import Path # Skill directory names this plugin ships (used to recognize a bare, unnamespaced -# slash-command invocation such as "/go-explain" as a go-coding command). +# slash-command invocation such as "/go-lint-setup" as a go-coding command). +# "go-explain" and "go-linting" were removed in 0.5.0 but stay listed so a scan of +# older transcripts still resolves the events they produced. GO_SKILL_SHORT_NAMES = { "go-coding", "go-concurrency", "go-errors", "go-explain", "go-idioms", "go-layout", "go-linting", "go-lint-setup", "go-testing", @@ -263,7 +265,7 @@ def build_arg_parser() -> argparse.ArgumentParser: - assistant tool_use block, name "Task" or "Agent", input.subagent_type == "go-coding:go-reviewer" - user invocations naming a go-coding skill (bare, e.g. - "/go-explain", or namespaced, e.g. "/go-coding:go-explain") + "/go-lint-setup", or namespaced, e.g. "/go-coding:go-lint-setup") Not counted: - the SessionStart banner text - skill-body text echoed back inside tool results diff --git a/skills/go-explain/SKILL.md b/skills/go-explain/SKILL.md deleted file mode 100644 index e3ae7de..0000000 --- a/skills/go-explain/SKILL.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: go-explain -description: One-shot lookup/explanation of a single Go idiom, standard, or tool. This skill should be used when the user runs `/go-explain ` or asks to "explain", "look up", or "what's the modern way to do" a specific Go construct (e.g. error wrapping, `synctest`, `wg.Go`, `internal/` layout) — returning the modern form, the enforcing linter, and a cited source. For applying a standard while writing or reviewing code, use the focused go-* skill instead. Not for non-Go languages. -argument-hint: a Go topic (e.g. error wrapping, synctest, wg.Go, min/max, internal layout) -allowed-tools: Read, Grep, Glob ---- - -# go-explain — one-shot Go lookup - -Explain the Go idiom, standard, or tool named in **$ARGUMENTS** — concisely, in one interaction. - -Cover, in a few lines: - -1. **The modern form** — what idiomatic Go does today, with the Go version it landed in. -2. **Over what** — the older pattern it replaces, if any. -3. **Enforcement** — the tool that flags or fixes it (`gofmt`/`gofumpt`, `go vet`, a `golangci-lint` - linter such as `modernize`/`errorlint`, or `go test -race`), with the exact command to run. -4. **Source** — cite one authoritative reference: Effective Go, Go Code Review Comments, the Google - or Uber Go style guide, a `go.dev/blog` post, or `pkg.go.dev`. - -Answer against the **Go 1.26.4+** baseline (Go 1.27 is supported too; its additions are hints, not -floor changes). Name the version an idiom landed in (that's step 1) — that is provenance for the -reader, not a gate on the recommendation. When the user's toolchain is on Go 1.27+, the 1.27 form -may be the better answer instead — say so explicitly and cite the source. For a fuller treatment, route -to the matching skill: `go-errors`, `go-concurrency`, -`go-testing`, `go-idioms`, `go-lint-setup`, or `go-layout`. - -Keep it tight — this is a lookup, not a lecture. If `$ARGUMENTS` is empty, ask what to explain. diff --git a/skills/go-linting/SKILL.md b/skills/go-linting/SKILL.md deleted file mode 100644 index ccaaed5..0000000 --- a/skills/go-linting/SKILL.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: go-linting -description: Deprecated in 0.5.0 — merged into go-lint-setup. This skill should be used only when an old prompt or document still names go-linting; it redirects there. ---- - -# go-linting — deprecated - -This skill's golangci-lint v2 config-schema, adoption, and upgrade-breakage content moved into `go-lint-setup`. -It now lives under `go-lint-setup`'s `## Adopting or debugging an existing config` heading — load that skill instead. -This stub is removed in 0.6.0. From 4d2a4d63f50231d4b53f668a24f765bd100a781d Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 22:18:43 +0300 Subject: [PATCH 18/23] refactor(skills): tighten descriptions; plain English when a person reads it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Descriptions are always-on context, so every word competes with the session's real work. Rewritten trigger-first with the redundant thematic restatements dropped and the concrete trigger tokens kept: 4,619 -> 4,101 characters across the seven surviving skills, 11% off, with no trigger removed. Two colons that YAML would have read as a nested mapping went with them (caught by validate.py). Adds a "Writing for the human" section to the router and a matching paragraph to go-reviewer's output format: anything a person reads — a PR description, a review comment, a question, a design choice put to them — states the effect before the mechanism and expands a Go term or drops it. Identifiers, commands and linter names stay verbatim; the prose around them is what has to be plain. Short, too: a few sentences per point, options plus a recommendation for a decision. Also drops the router's pointer to the removed /go-explain. Co-Authored-By: Claude Opus 4.8 --- agents/go-reviewer.md | 4 ++++ skills/go-coding/SKILL.md | 14 +++++++++++--- skills/go-concurrency/SKILL.md | 2 +- skills/go-errors/SKILL.md | 2 +- skills/go-idioms/SKILL.md | 2 +- skills/go-layout/SKILL.md | 2 +- skills/go-lint-setup/SKILL.md | 2 +- skills/go-testing/SKILL.md | 2 +- 8 files changed, 21 insertions(+), 9 deletions(-) diff --git a/agents/go-reviewer.md b/agents/go-reviewer.md index d2d79b4..df781da 100644 --- a/agents/go-reviewer.md +++ b/agents/go-reviewer.md @@ -140,6 +140,10 @@ Verdict: 3 issues — 1 high, 2 medium. If you find nothing real, say so plainly — **do not invent findings to look thorough.** End with a one-line note of what you did *not* cover (files or paths outside the given scope). +A person reads this report, so write the prose in plain English: say what goes wrong before naming +the mechanism, and expand a Go term the first time it appears or leave it out. Identifiers, +commands and linter names stay verbatim. Keep each finding to the three lines above. + ## Edge cases - **No diff given and none inferable:** ask for the diff/files, or run `git diff` if a branch is in diff --git a/skills/go-coding/SKILL.md b/skills/go-coding/SKILL.md index 76b1bb6..e24c9d7 100644 --- a/skills/go-coding/SKILL.md +++ b/skills/go-coding/SKILL.md @@ -1,6 +1,6 @@ --- name: go-coding -description: This skill should be used when a Go task spans multiple areas, is unspecified, or the question is which tool or standard applies — it routes each topic to the deterministic tool, then to the focused go-* skill that owns it (go-errors, go-concurrency, go-testing, go-idioms, go-lint-setup, go-layout for layout/naming/API design). Go coding-standards router for idiomatic Go — Go 1.26.4+ (Go 1.27 supported; its additions are flagged as hints), golangci-lint v2. For a single, already-identified topic load that skill directly. Loading this router alone does not apply the standards — it names the focused skill to load next. Not for non-Go languages or domain/business rules. +description: Go coding-standards router — Go 1.26.4+ (1.27 supported, its additions flagged as hints), golangci-lint v2. This skill should be used when a Go task spans several areas, is unspecified, or the question is which tool or standard applies — it names the deterministic tool to run, then the focused skill that owns the topic (go-errors, go-concurrency, go-testing, go-idioms, go-layout, go-lint-setup). Loading the router alone does not apply the standards — load the skill it names next. For one already-identified topic, load that skill directly. Go only; not for business rules. --- # go-coding — Go standards router @@ -59,6 +59,15 @@ Apply these even if you load nothing else; they are the rules the focused skills - `ctx` first; no goroutine without an owner that waits for it; `t.Context()` in tests. - Run `gofmt`/`gofumpt` and `golangci-lint run` — never reason out what a tool decides. +## Writing for the human + +Anything a person reads — a PR description, a review comment, a question, a design choice put to +them — goes in plain English, not Go shorthand. State the effect before the mechanism ("the request +keeps running after the caller gives up", not "ctx leak in the errgroup"), and expand a term the +first time it appears or leave it out. Keep it short: a few sentences per point, and a decision they +must make gets the options plus a recommendation, not an essay. Identifiers, commands and linter +names stay verbatim — it is the prose around them that must be plain. + ## Authoritative sources (cite, don't guess) - Effective Go — @@ -76,8 +85,7 @@ If a workflow already owns the reviewer seat, that reviewer loads the focused sk one review seat per diff. Orchestrators: put the "Route, then load" table into every implementer and reviewer brief. -Two user-invoked skills round out the surface: `/go-explain ` for a one-shot idiom lookup, -and `/go-lint-setup` to scaffold the reference golangci-lint v2 config into a repo. +`/go-lint-setup` scaffolds the reference golangci-lint v2 config into a repo. --- *Top-level structure adapted from [`samber/cc-skills-golang`](https://github.com/samber/cc-skills-golang) (MIT © 2026 Samuel Berthe).* diff --git a/skills/go-concurrency/SKILL.md b/skills/go-concurrency/SKILL.md index f6c58a1..2f792fe 100644 --- a/skills/go-concurrency/SKILL.md +++ b/skills/go-concurrency/SKILL.md @@ -1,6 +1,6 @@ --- name: go-concurrency -description: Idiomatic, leak-free Go concurrency. This skill should be used when a diff or question contains go func, chan, select, sync.WaitGroup/Mutex/Once, atomic, errgroup, context.WithCancel/Timeout/Cause, a retry or backoff loop, a worker pool, an HTTP client with per-request cancellation, or a Close on a goroutine-owned resource — goroutine lifetimes and leaks, context propagation and cancellation causes, work that must outlive a request, typed atomics, mutex misuse, data races. Pair with go test -race, go vet and goleak. Time/concurrency testing mechanics live in go-testing (synctest). Not for non-Go languages. +description: Idiomatic, leak-free Go concurrency. This skill should be used when a diff or question contains go func, chan, select, sync.WaitGroup/Mutex/Once, atomic, errgroup, context.WithCancel/Timeout/Cause, a retry or backoff loop, a worker pool, per-request cancellation, or a Close on a goroutine-owned resource — goroutine lifetime and leaks, context propagation and cancel causes, work outliving a request, typed atomics, mutex misuse, data races. Pair with go test -race and goleak. Time-dependent tests belong to go-testing (synctest). Go only. --- # go-concurrency — Go concurrency diff --git a/skills/go-errors/SKILL.md b/skills/go-errors/SKILL.md index d320f8a..5b29ceb 100644 --- a/skills/go-errors/SKILL.md +++ b/skills/go-errors/SKILL.md @@ -1,6 +1,6 @@ --- name: go-errors -description: Idiomatic Go error handling. This skill should be used when the user writes, reviews, or debugs Go error code — wrapping with `%w`, inspecting via `errors.Is`/`errors.AsType`, sentinel vs typed errors, `errors.Join`, unchecked `Close` errors, when panic is legitimate, enum-switch dispatch defaults, keeping payload values out of boundary errors/logs, or chasing a silently-swallowed or context-losing error. Pair with the `errorlint` linter (set it up via `go-lint-setup`). Not for non-Go languages. +description: Idiomatic Go error handling. This skill should be used when the user writes, reviews, or debugs Go error code — wrapping with `%w`, `errors.Is`/`errors.AsType`, sentinel vs typed errors, `errors.Join`, an unchecked `Close`, when panic is legitimate, enum-switch dispatch defaults, keeping payload values out of boundary errors and logs, or chasing a swallowed or context-losing error. Pair with the `errorlint` linter. Go only. --- # go-errors — Go error handling diff --git a/skills/go-idioms/SKILL.md b/skills/go-idioms/SKILL.md index 774b15d..9a5cfad 100644 --- a/skills/go-idioms/SKILL.md +++ b/skills/go-idioms/SKILL.md @@ -1,6 +1,6 @@ --- name: go-idioms -description: Modern idiomatic Go (the `modernize` analyzer set) — Go 1.26+, with Go 1.27 additions noted. This skill should be used when the user writes, reviews, or modernizes Go and wants current-version idioms — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)` and `errors.AsType` (Go 1.26), dropped loop-var copies, or Go 1.27 additions (generic methods, json/v2-backed `encoding/json`, the `atomictypes`/`embedlit`/`slicesbackward`/`unsafefuncs` go fix modernizers) — or asks which modernize fixer owns a rewrite. Framed so advice equals tooling (`go fix ./...`, or `golangci-lint --enable-only=modernize`). Not for golangci-lint configuration (use `go-lint-setup`) or non-Go languages. +description: Modern idiomatic Go (the `modernize` analyzer set) — Go 1.26+, Go 1.27 additions noted. This skill should be used when the user writes, reviews, or modernizes Go, or asks which fixer owns a rewrite — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)`, `errors.AsType`, dropped loop-var copies, and the Go 1.27 additions (generic methods, json/v2-backed `encoding/json`, the `atomictypes`/`embedlit`/`slicesbackward`/`unsafefuncs` fixers). Advice equals tooling — `go fix ./...` or `golangci-lint --enable-only=modernize`. Not for linter configuration (go-lint-setup). Go only. --- # go-idioms — modern Go (modernize) diff --git a/skills/go-layout/SKILL.md b/skills/go-layout/SKILL.md index 841b107..8ae2b26 100644 --- a/skills/go-layout/SKILL.md +++ b/skills/go-layout/SKILL.md @@ -1,6 +1,6 @@ --- name: go-layout -description: Go project layout, package design and API surface. This skill should be used when the user creates a new package or directory, adds or renames an exported identifier, decides between cmd/ and internal/, writes a doc comment on an exported API, or asks about util/common grab-bags, start-flat-then-grow, receiver naming, initialisms, or in-band error values. Also when reviewing a diff that adds a new package or changes a public type or signature. Counters imported ceremony (hexagonal/DDD layers) with the standard-library shape. Pair with golangci-lint's revive rules (var-naming, receiver-naming, exported). Not for error handling (go-errors) or test design (go-testing). +description: Go project layout, package design and API surface. This skill should be used when the user creates a new package or directory, adds or renames an exported identifier, decides between cmd/ and internal/, writes a doc comment on an exported API, or reviews a diff that adds a package or changes a public type or signature — also util/common grab-bags, start-flat-then-grow, receiver naming, initialisms, in-band error values, and hexagonal/DDD ceremony answered with the standard-library shape. Pair with revive (var-naming, receiver-naming, exported). Not for error handling (go-errors) or tests (go-testing). --- # go-layout — layout, naming & API surface diff --git a/skills/go-lint-setup/SKILL.md b/skills/go-lint-setup/SKILL.md index d571177..1d4d9cd 100644 --- a/skills/go-lint-setup/SKILL.md +++ b/skills/go-lint-setup/SKILL.md @@ -1,6 +1,6 @@ --- name: go-lint-setup -description: Scaffold the reference golangci-lint v2 config into a Go repo. This skill should be used when the user runs `/go-lint-setup` or asks to "set up", "scaffold", "add", or "bootstrap" golangci-lint or a `.golangci.yml` for a Go project — it writes the plugin's reference v2 config (modernize + stack linters) and will not overwrite an existing config unprompted, or asks why golangci-lint v2 rejects a config, about migrating a v1 config, which linters the default set enables, how to adopt modernize/errorlint in an existing repo, what `golangci-lint fmt` does, how to write a `linters.exclusions` rule, or how to suppress a finding with `//nolint`. Not for non-Go projects. +description: Scaffold, adopt, or debug the golangci-lint v2 config in a Go repo. This skill should be used when the user runs `/go-lint-setup`, asks to "set up", "scaffold", "add" or "bootstrap" golangci-lint or a `.golangci.yml`, or asks why golangci-lint v2 rejects a config, how to migrate a v1 config, which linters the default set enables, how to adopt modernize/errorlint in an existing repo, what `golangci-lint fmt` does, how to write a `linters.exclusions` rule, or how to suppress a finding with `//nolint`. Writes the reference v2 config (modernize + stack linters); never overwrites an existing one unprompted. Go only. argument-hint: optional target path (defaults to .golangci.yml) allowed-tools: Read, Write, Glob, Bash --- diff --git a/skills/go-testing/SKILL.md b/skills/go-testing/SKILL.md index e6727ad..8e5340d 100644 --- a/skills/go-testing/SKILL.md +++ b/skills/go-testing/SKILL.md @@ -1,6 +1,6 @@ --- name: go-testing -description: Idiomatic Go testing. This skill should be used when the user writes or reviews Go tests, benchmarks, or fuzz targets — any _test.go file, a can-fail control that proves a guard is mutation-detectable, a refusal test that must assert the operation-specific facet rather than a shared sentinel, table-driven tests, `t.Parallel` (and what cannot run under it), `t.Context`, `t.Chdir`/`t.Setenv`, `t.TempDir` vs `t.ArtifactDir`, `t.Output`, `testing.B.Loop`, the race detector, goroutine-leak detection, `testing/synctest` for time/concurrency, fuzzing, golden files, or writing failure messages that actually diagnose. Pair with `go test -race`. Not for non-Go test frameworks; error-wrapping belongs to `go-errors`. +description: Idiomatic Go testing. This skill should be used when the user writes or reviews Go tests, benchmarks or fuzz targets — any _test.go file, table-driven `t.Run`, a can-fail control proving a guard is mutation-detectable, a refusal test asserting the operation-specific facet not a shared sentinel, `t.Parallel` (and what cannot run under it), `t.Context`, `t.Chdir`/`t.Setenv`, `t.TempDir` vs `t.ArtifactDir`, `t.Output`, `testing.B.Loop`, the race detector, goroutine-leak checks, `testing/synctest` for time and concurrency, fuzzing, golden files, or failure messages that actually diagnose. Pair with `go test -race`. Go only; error wrapping belongs to go-errors. --- # go-testing — Go testing From b43f755283234c6dbea1f93a18bf65ae80846cd3 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 22:19:48 +0300 Subject: [PATCH 19/23] docs(changelog): shorten the 0.5.0 entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section had grown to 94 lines of implementation narrative — which helper replaced which assertion idiom, which files a wording sweep touched. A changelog says what changed and why it matters; the rest is what the commits are for. 46 lines now, one short sentence per entry, plus a Removed section for go-linting and /go-explain. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 124 ++++++++++++++++----------------------------------- 1 file changed, 38 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d03ae..d987614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,96 +9,48 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ## [0.5.0] - 2026-09-03 -Makes the `go-coding` router route: a mandatory hop table and minimum checklist, trigger-first -skill descriptions, a post-edit skill nudge, and a friendlier SessionStart banner close the gap a -usage analysis showed between skills being loaded and skills actually being followed. Folds -`go-linting` into `go-lint-setup` — the skill the router and banner actually point at — behind a -deprecation stub, adds a script for measuring skill/agent adoption from local session transcripts, -documents a one-review-seat-per-diff pattern for subagent orchestrators, and adds Go 1.27 support -alongside the existing Go 1.26.4+ floor. +Makes the `go-coding` router route. A usage analysis of local session transcripts found the router +loading often and the focused skills almost never, so this release adds a hop table, a post-edit +nudge, trigger-first descriptions and a banner that names the next step. It also shrinks the +surface — `go-linting` merged into `go-lint-setup`, `/go-explain` removed — adds Go 1.27 support on +the existing Go 1.26.4+ floor, and ships a script for measuring adoption. ### Added -- Skill: `skills/go-coding/SKILL.md` — a "Route, then load" hop table mapping diff content to the - focused skill to load (a `_test.go`/benchmark/fuzz target → `go-testing`; `fmt.Errorf`/`errors.*`/a - sentinel/typed error/enum switch → `go-errors`; a loop/map/slice/string split/`interface{}`/a - struct literal → `go-idioms`; `go func`/`chan`/`sync.`/`atomic.`/`errgroup`/`context.With*` → - `go-concurrency`; a new package/exported identifier/`cmd`-`internal` decision/doc comment → - `go-layout`; `.golangci.y*ml`/an unclear linter complaint → `go-lint-setup`), plus a minimum - checklist to apply when a second skill load isn't affordable; its description now says loading the - router alone does not apply the standards. Orchestrators dispatching implementer/reviewer - subagents carry the table into every brief, since a subagent does not inherit the parent session's - skills. -- Hooks: `hooks/skill-nudge.sh` — a `PostToolUse`/`afterFileEdit` hook that, after a Go file edit, - delivers one nudge naming the go-coding skill the edit calls for (a `_test.go` file → `go-testing`; - goroutine/channel/`sync`/`atomic`/`errgroup` content → `go-concurrency`; `fmt.Errorf`/`errors.*` → - `go-errors`), once per skill per session, via the hook `systemMessage` channel under Claude Code - (reaches the model's context on exit 0) and a plain line under Cursor; wired dual-host in - `hooks/hooks.json` and `hooks/cursor-hooks.json`. A usage analysis showed these focused skills load - far less often than the router itself. -- Hooks: `hooks/session-start.sh` — the SessionStart banner now names the skill-loading hop, lists - the focused skills, points at `/go-explain` and `go-reviewer`, and adds a `/go-lint-setup` prompt - when no `.golangci.y*ml`/`.toml`/`.json` file is found in the workspace. -- Scripts: `scripts/hooks-test.sh` — a test harness covering `hooks/skill-nudge.sh` and the - session-start banner, with a `chk_silent` helper that asserts exact emptiness for the no-nudge - cases instead of a vacuous non-match check, and a can-fail test control: self-tests proving the - negative-case helpers (`chk_silent`, `run_case_absent`) actually fail on bad input rather than - passing vacuously. Runs in CI (`.github/workflows/validate.yml`) alongside `scripts/validate.py`. -- Docs: `README.md` — "Using with subagent orchestrators": implementer and reviewer brief templates - that carry the router's hop table into a dispatched subagent's instructions, so a plan runner's - implementers and reviewers apply the standards even though a subagent does not inherit the parent - session's skills; `go-reviewer` is dispatched directly only when no such reviewer seat exists. -- Scripts, Docs: `scripts/usage-report.py`, `docs/testing.md` — a stdlib-only script that scans local - Claude Code session transcripts for go-coding skill/agent invocations (a `Skill`-tool call prefixed - `go-coding:`, a `go-coding:go-reviewer` subagent dispatch, or a user slash-command invocation) and - renders a monthly adoption report split into main-session, subagent, and user-invoked counts, - deduplicated by session; `docs/testing.md` gains a "Measuring adoption" section naming what is and - isn't counted and the adoption target. A usage analysis of local session transcripts shaped this - release's router and skill-description changes; this script reproduces that measurement so - adoption stays checkable going forward. -- Skills: `skills/go-idioms/SKILL.md` — Go 1.27 forward hints: two † fixers (`atomictypes`, - `slicesbackward`) graduate into the stock `go fix`, alongside new `embedlit`/`unsafefuncs` fixers, - a `waitgroup` → `waitgroupgo` rename, and a dropped `fmtappendf`; a new "Newer in Go 1.27" section - covers generic methods, preferring `encoding/json/v2`/`jsontext` on new/hot paths once on 1.27, and - the default `stdversion` vet check — all framed as additive over the Go 1.26 floor, never as a - requirement. -- Skills: `skills/go-lint-setup/SKILL.md` — a golangci-lint Go-1.27 compatibility note: Go 1.27 - needs golangci-lint ≥ v2.13.0, stated as a compatibility floor rather than a version pin. -- Validation: `.github/workflows/validate.yml` — a Go version matrix (`1.26.x`, `1.27.x`) with - `fail-fast: false`; the floor-minor (`1.26.x`) leg keeps running the strict `go-idioms` - Fixer-column check against `go tool fix help`, the `1.27.x` leg soft-skips it. +- Skill `go-coding` — a "Route, then load" table mapping diff content to the skill to load, and a + minimum checklist for when a second load is not affordable. +- Skill `go-coding`, agent `go-reviewer` — "Writing for the human": anything a person reads (PR + text, review findings, a question) states the effect before the mechanism, in plain English, short. +- Hook `hooks/skill-nudge.sh` — after a Go edit, names one skill the edit calls for, classified from + the edit rather than the whole file. Once per skill per session, so at most three per session; + a `systemMessage` under Claude Code, a plain line under Cursor. +- Script `scripts/hooks-test.sh` — 18 bash tests for both hooks, including can-fail controls that + prove the silence assertions can actually fail. Runs in CI. +- Script `scripts/usage-report.py` — stdlib-only adoption report from local session transcripts; + counting rules and the 50% target in `docs/testing.md`. +- Docs `README.md` — implementer and reviewer brief templates for orchestrators, since a subagent + does not inherit the parent session's skills. +- Skills `go-idioms`, `go-lint-setup` — Go 1.27 hints (generic methods, json/v2-backed + `encoding/json`, the four new `go fix` modernizers, `stdversion` by default under `go test`) and + the golangci-lint ≥ v2.13.0 floor for Go 1.27. +- CI `.github/workflows/validate.yml` — Go matrix `1.26.x` + `1.27.x`, `fail-fast: false`; the + floor leg keeps the strict `go-idioms` Fixer-column check. ### Changed -- Skills: `skills/go-lint-setup/SKILL.md` — absorbs `go-linting`'s golangci-lint v2 config-schema, - adoption, and upgrade-breakage content under a new "Adopting or debugging an existing config" - heading, since `go-lint-setup` is the skill the router and session-start banner actually route to. -- Agent, Skill: `agents/go-reviewer.md`, `skills/go-coding/SKILL.md` — one review seat per diff: when - an orchestrating workflow already provides a reviewer seat, `go-reviewer` is not dispatched beside - it; that reviewer loads `go-coding` and the focused skills for the diff itself and cites the rule - each finding rests on. -- Skills: `skills/go-layout/SKILL.md`, `skills/go-concurrency/SKILL.md`, `skills/go-testing/SKILL.md` - — descriptions rewritten trigger-first, naming the concrete diff content that should load each - skill (a new package/exported identifier/doc comment for `go-layout`; - `go func`/`chan`/`sync.`/`atomic.`/`errgroup`/a retry loop for `go-concurrency`; any `_test.go` - file or can-fail control for `go-testing`), since a usage analysis showed the prior, more abstract - descriptions rarely triggered these skills. -- Skills, Agent, Cursor rule, Docs: `skills/go-idioms/SKILL.md`, `skills/go-testing/SKILL.md`, - `skills/go-concurrency/SKILL.md`, `skills/go-coding/SKILL.md`, `skills/go-explain/SKILL.md`, - `agents/go-reviewer.md`, `rules/go-context.mdc`, `docs/authoring.md`, `docs/install.md`, - `README.md`, `AGENTS.md` — version-floor wording now states Go 1.26.4+ as the hard floor with Go - 1.27 supported, its additions flagged as hints rather than folded into the floor; - `docs/install.md` recommends the latest 1.27.x patch while noting an existing 1.26.4+ toolchain - still satisfies the floor; earlier pre-release ("draft, expected Aug 2026") mentions of Go 1.27 in - `go-concurrency` and `go-testing` are corrected to the released feature set and dated to its actual - 2026-08-19 release; `AGENTS.md` and `docs/authoring.md` describe the CI matrix's two legs instead - of a single pinned Go version. - -### Deprecated -- Skill: `skills/go-linting/SKILL.md` — deprecated: reduced to a three-line stub redirecting to - `go-lint-setup`'s "Adopting or debugging an existing config" section, which now carries the - content that used to live here. Every other internal pointer (the router's routing table and - description, `README.md`, `AGENTS.md`'s inventory, `rules/go-context.mdc`, - `references/golangci.v2.yml`'s header comment, and cross-references from `go-errors`, `go-idioms`, - `go-explain`, and `go-reviewer`) now points at `go-lint-setup` instead. Removal in 0.6.0. +- Skills — descriptions rewritten trigger-first and shortened 11% (4,619 → 4,101 characters). + Always-on context competes with the session's real work. +- Skill `go-lint-setup` — absorbs `go-linting`'s config-schema, adoption and upgrade-breakage + content; re-fronted as "scaffold, adopt, or debug". +- Hook `hooks/session-start.sh` — the banner names the hop and the focused skills, and offers + `/go-lint-setup` only when the workspace has no golangci-lint config. +- Agent `go-reviewer`, skill `go-coding` — one review seat per diff: where a workflow already has a + reviewer, that reviewer loads the skills itself instead of `go-reviewer` being dispatched beside it. +- Skills, agent, Cursor rule, docs — Go 1.26.4+ remains the hard floor; Go 1.27 is supported and its + additions are flagged as hints. `docs/install.md` recommends the latest 1.27.x patch. + +### Removed +- Skill `go-linting` — merged into `go-lint-setup`, the skill the router and banner point at. +- Skill `/go-explain` — one-shot lookups are what the focused skills already do, with the same + citations and the code in view. ## [0.4.1] - 2026-08-25 From 92bfcff68d1b77fb64809c7780868890e1f249da Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 22:21:39 +0300 Subject: [PATCH 20/23] docs(testing): the nudge check covers an edit that does not touch the topic Co-Authored-By: Claude Opus 4.8 --- docs/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index 8e085ac..5f73390 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -20,7 +20,7 @@ Install from your working copy (see [install.md](install.md)), then exercise eac - **Session-start hook** — open a repo with a `go.mod`/`*.go`; one Go-standards line should print at session start (and nothing in a non-Go repo). - **`go-coding` router** — ask for a Go review or idiom help; it should route to the enforcing tool and the focused skill. - **Standards skills** — a topic prompt should engage the matching skill (for example error wrapping → `go-errors`, a flaky time-based test → `go-testing`/`go-concurrency`, linter setup → `go-lint-setup`). -- **Skill-nudge hook** — edit a `_test.go` file; the nudge should name `go-coding:go-testing` (as a systemMessage under Claude Code, a plain line under Cursor) and, critically, the model should ACT on it — load the skill — not merely have the line appear in the transcript. A second edit to a `_test.go` file in the same session should be silent (once per skill per session). +- **Skill-nudge hook** — edit a `_test.go` file; the nudge should name `go-coding:go-testing` (as a systemMessage under Claude Code, a plain line under Cursor) and, critically, the model should ACT on it — load the skill — not merely have the line appear in the transcript. A second edit to a `_test.go` file in the same session should be silent (once per skill per session), and so should an edit that does not itself touch the topic — a doc-comment fix in a file that defines a sentinel elsewhere must not claim the edit touches an error path. - **`go-reviewer` agent** — ask for a Go code review; it returns severity-ranked findings and does not spawn sub-agents. - **Slash command (skill)** — `/go-lint-setup`. - **Cursor rule** — in Cursor, open a `.go` file and confirm `go-context.mdc` attaches. From cdac1988e341ffbd707c190842466e3fd1e790d9 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Thu, 3 Sep 2026 22:44:46 +0300 Subject: [PATCH 21/23] feat(rule,validate): bring the Cursor rule level with the router; check parity and inventories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Cursor mirror had drifted a release behind: it carried the topic table but neither the diff->skill mapping nor the minimum checklist, so a Cursor session got the index and none of the routing. It now carries both, plus the plain-English rule for anything a person reads. Two new checks in validate.py, each one closing a gap that was previously found only by reading two files side by side: - Hook parity — the same hooks/*.sh must be wired for the equivalent event on both hosts (SessionStart/sessionStart, PostToolUse/afterFileEdit), every wired script must exist and be executable, and no script may sit in the tree unwired. - Doc inventories — every shipped skill, agent and hook must be named where the docs claim to list them. One-directional on purpose, so tombstones for removed components stay legal. install.md is held to the hooks alone; it documents what they need on PATH and is not a skill catalogue. Each was run against a deliberately broken tree first: unwiring skill-nudge from Cursor, adding an undocumented skill, and clearing a hook's executable bit all produce the expected error. The inventory check immediately found a real gap — docs/testing.md had no triggering check for format-on-save — which is now added. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- CHANGELOG.md | 6 +++ README.md | 2 +- docs/testing.md | 1 + rules/go-context.mdc | 35 +++++++++++++++ scripts/validate.py | 100 ++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 143 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 89a148b..8991208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ This repo supports **both Claude Code and Cursor**. Shared assets (skills, comma - **Cursor hooks**: `hooks/cursor-hooks.json` — object `{ "hooks": { "sessionStart": [...], "afterFileEdit": [...] } }`; the command runs from the plugin root (a **workspace-relative** path, **not** `${CLAUDE_PLUGIN_ROOT}`). Present — wires `session-start.sh` (`sessionStart`) and `format-on-save.sh` + `skill-nudge.sh` (`afterFileEdit`). - **Shared hook scripts**: `hooks/session-start.sh` — detects `go.mod` / `*.go`, prints one Go-standards context line, exits 0 always. `hooks/format-on-save.sh` — after a `*.go` Write/Edit, runs `gofumpt -w` (or `gofmt -w -s`) on that single file; resolves the path from `$CLAUDE_FILE_PATH` or the stdin tool-payload JSON, host-only, silent no-op if no formatter is installed, exits 0 always. `hooks/skill-nudge.sh` — after a `*.go` Write/Edit, names ONE matching go-coding skill for that edit, once per skill per session; delivered as a hook `systemMessage` under Claude Code, a plain line under Cursor; exits 0 always. All three host-agnostic so either manifest can invoke them. Present. - **MCP config** *(optional, not present)*: `.mcp.json` — only if the plugin later integrates an MCP server. There is no companion MCP server today; do not reference one. -- **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI's matrix installs `1.26.x` and `1.27.x`; the strict check runs on the 1.26.x (floor) leg, the 1.27.x leg soft-skips it) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. +- **Validation**: `scripts/validate.sh` wraps `scripts/validate.py` to check both manifests, dual-host parity, declared component paths, kebab-case names, hook-config JSON, skill/command/agent frontmatter (**agents must use `tools:` not `allowed-tools:`** — flagged as an error), hook parity (the same `hooks/*.sh` wired for the equivalent event on both hosts, each one existing and executable, none left unwired), doc component inventories (every shipped skill, agent and hook named in `README.md`, this file, and `docs/testing.md`; hooks alone in `docs/install.md`), and two *advice == tooling* invariants: every linter taught in a component is enabled in `references/golangci.v2.yml`, and (when a floor-minor Go toolchain is on PATH — CI's matrix installs `1.26.x` and `1.27.x`; the strict check runs on the 1.26.x (floor) leg, the 1.27.x leg soft-skips it) the `go-idioms` Fixer column matches `go tool fix help`. The Python is stdlib-only. `.github/workflows/validate.yml` pins Python + Go and runs the validator strictly. - **Contributor docs**: `docs/` for human-facing references — `install.md`, `testing.md`, `versioning.md`, `authoring.md`. `.github/` holds issue + PR templates, `copilot-instructions.md`, and the CI workflow. (Planning and research working notes are kept locally under `docs/`, **gitignored** — not part of the published plugin.) ### Component surface diff --git a/CHANGELOG.md b/CHANGELOG.md index d987614..0a1e56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ the existing Go 1.26.4+ floor, and ships a script for measuring adoption. the golangci-lint ≥ v2.13.0 floor for Go 1.27. - CI `.github/workflows/validate.yml` — Go matrix `1.26.x` + `1.27.x`, `fail-fast: false`; the floor leg keeps the strict `go-idioms` Fixer-column check. +- Validation `scripts/validate.py` — hook parity (the same script wired for the equivalent event on + both hosts, existing and executable, none left unwired) and doc component inventories (every + shipped skill, agent and hook named where the docs claim to list them). ### Changed - Skills — descriptions rewritten trigger-first and shortened 11% (4,619 → 4,101 characters). @@ -44,6 +47,9 @@ the existing Go 1.26.4+ floor, and ships a script for measuring adoption. `/go-lint-setup` only when the workspace has no golangci-lint config. - Agent `go-reviewer`, skill `go-coding` — one review seat per diff: where a workflow already has a reviewer, that reviewer loads the skills itself instead of `go-reviewer` being dispatched beside it. +- Cursor rule `rules/go-context.mdc` — brought level with the router: the diff→skill mapping, the + minimum checklist, and the plain-English rule for anything a person reads. +- Docs `docs/testing.md` — adds the missing `format-on-save` triggering check. - Skills, agent, Cursor rule, docs — Go 1.26.4+ remains the hard floor; Go 1.27 is supported and its additions are flagged as hints. `docs/install.md` recommends the latest 1.27.x patch. diff --git a/README.md b/README.md index 3b55d94..bc3fafe 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Use `go-reviewer` directly when no such seat exists (an ad-hoc "review this file No build step — the plugin is pure Markdown + JSON. Validate locally: ```bash -./scripts/validate.sh # manifests, parity, paths, frontmatter, taught linters, fixers +./scripts/validate.sh # manifests, parity, paths, frontmatter, hooks, doc inventories, linters, fixers ./scripts/hooks-test.sh # bash tests for hooks/session-start.sh + hooks/skill-nudge.sh claude plugin validate . # manifest + component structure ``` diff --git a/docs/testing.md b/docs/testing.md index 5f73390..f0078e4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -20,6 +20,7 @@ Install from your working copy (see [install.md](install.md)), then exercise eac - **Session-start hook** — open a repo with a `go.mod`/`*.go`; one Go-standards line should print at session start (and nothing in a non-Go repo). - **`go-coding` router** — ask for a Go review or idiom help; it should route to the enforcing tool and the focused skill. - **Standards skills** — a topic prompt should engage the matching skill (for example error wrapping → `go-errors`, a flaky time-based test → `go-testing`/`go-concurrency`, linter setup → `go-lint-setup`). +- **Format-on-save hook** — save a deliberately mis-formatted `*.go` file; `format-on-save.sh` should reformat that one file in place (`gofumpt -w`, or `gofmt -w -s` when `gofumpt` is absent) and say nothing when neither is installed. - **Skill-nudge hook** — edit a `_test.go` file; the nudge should name `go-coding:go-testing` (as a systemMessage under Claude Code, a plain line under Cursor) and, critically, the model should ACT on it — load the skill — not merely have the line appear in the transcript. A second edit to a `_test.go` file in the same session should be silent (once per skill per session), and so should an edit that does not itself touch the topic — a doc-comment fix in a file that defines a sentinel elsewhere must not claim the edit touches an error path. - **`go-reviewer` agent** — ask for a Go code review; it returns severity-ranked findings and does not spawn sub-agents. - **Slash command (skill)** — `/go-lint-setup`. diff --git a/rules/go-context.mdc b/rules/go-context.mdc index 2f1259b..610035a 100644 --- a/rules/go-context.mdc +++ b/rules/go-context.mdc @@ -23,6 +23,41 @@ This Cursor rule mirrors the `go-coding` router skill — apply it when editing | Testing | `go test -race ./...`; `testing/synctest` for time/concurrency | `go-testing` | | Layout, naming & API surface | `golangci-lint run --enable-only=revive`; rest is judgment | `go-layout` | +## Route, then load + +This rule is an index, not the standard. Before writing or reviewing Go, open the focused skill +matching the change — it carries the cited rules and the judgment: + +- any `_test.go`, a benchmark, a fuzz target, a "verified by temporarily breaking it" claim → + `go-testing` +- `fmt.Errorf`, `errors.*`, a sentinel, a typed error, a `switch` over an enum → `go-errors` +- a loop, map, slice, string split, `interface{}`, a struct literal that could be `new(expr)` → + `go-idioms` +- `go func`, `chan`, `sync.`, `atomic.`, `errgroup`, `context.With*`, a `Close` on a goroutine-owned + resource → `go-concurrency` +- a new package, an exported identifier, a `cmd/` or `internal/` decision, a doc comment on an API → + `go-layout` +- `.golangci.y*ml`, a linter complaint you do not understand → `go-lint-setup` + +## Minimum checklist (when opening a skill is not affordable) + +- Wrap with `fmt.Errorf("…: %w", err)`; inspect with `errors.Is` / `errors.AsType` and guard the + result (`ok && v != nil` — a typed-nil pointer satisfies the match). Never swallow an error. +- Every guard has a test that fails when the guard is deleted; a "temporarily broke it by hand" + check is not evidence — commit it as a can-fail test. Table-driven `t.Run` with got/want messages. +- `range n`, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any`; `go fix ./...` before hand-edits. +- `ctx` first; no goroutine without an owner that waits for it; `t.Context()` in tests. +- Run `gofmt`/`gofumpt` and `golangci-lint run` — never reason out what a tool decides. + +## Writing for the human + +Anything a person reads — a PR description, a review comment, a question, a design choice put to +them — goes in plain English, not Go shorthand. State the effect before the mechanism ("the request +keeps running after the caller gives up", not "ctx leak in the errgroup"), and expand a term the +first time it appears or leave it out. Keep it short: a few sentences per point, and a decision they +must make gets the options plus a recommendation. Identifiers, commands and linter names stay +verbatim — it is the prose around them that must be plain. + Ground every judgment call in a cited source — Effective Go, Go Code Review Comments, the Google and Uber Go style guides, `pkg.go.dev`. Don't invent rules. Adopt the shipped golangci-lint v2 config (`references/golangci.v2.yml`, at the **plugin root** — resolve via `${CLAUDE_PLUGIN_ROOT}/references/…` or Glob the installed copy, not under this rule's dir); run `golangci-lint run --fix` for auto-fixable findings. diff --git a/scripts/validate.py b/scripts/validate.py index 06ad1b7..ac55e2c 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -25,6 +25,7 @@ reason it wouldn't run. Usage: python3 scripts/validate.py (from the repo root) """ import json +import os import re import shutil import subprocess @@ -49,6 +50,18 @@ # minor's `go tool fix help` — a newer/older toolchain's list would prove nothing about the # floor, so the check skips on any other minor. Bump together with the documented baseline. GO_FLOOR_MINOR = "1.26" +# Hook events that mean the same thing on each host, so the same scripts must be wired for +# both. Claude event -> Cursor event. +EQUIVALENT_HOOK_EVENTS = {"SessionStart": "sessionStart", "PostToolUse": "afterFileEdit"} +# Docs that inventory the component surface, and which kinds each one claims to cover. +# install.md enumerates the hooks (it documents what they need on PATH) but is not a skill +# catalogue, so it is held to the hook list only. +INVENTORY_DOCS = { + "README.md": ("skills", "agents", "hooks"), + "AGENTS.md": ("skills", "agents", "hooks"), + "docs/testing.md": ("skills", "agents", "hooks"), + "docs/install.md": ("hooks",), +} def err(msg): @@ -274,6 +287,88 @@ def validate_json_file(path: Path, label: str): load_json(path, label) +def _hook_scripts(config: dict) -> dict: + """Map each event name in a hook config to the set of `hooks/*.sh` scripts it wires. + Both host schemas nest differently (Claude groups by matcher, Cursor does not), so walk + whatever is under the event and collect every `command` string found.""" + found = {} + for event, entries in (config.get("hooks") or {}).items(): + scripts = set() + stack = [entries] + while stack: + node = stack.pop() + if isinstance(node, dict): + cmd = node.get("command") + if isinstance(cmd, str): + scripts.update(re.findall(r"hooks/([A-Za-z0-9._-]+\.sh)", cmd)) + stack.extend(node.values()) + elif isinstance(node, list): + stack.extend(node) + found[event] = scripts + return found + + +def validate_hook_parity(): + """Dual-host hook parity: the same scripts must be wired for the equivalent event on both + hosts, every wired script must exist and be executable, and no `hooks/*.sh` may sit in the + tree unwired. Catches the drift where a new hook reaches Claude Code but never Cursor — + previously only findable by reading both configs side by side.""" + claude_path, cursor_path = ROOT / "hooks" / "hooks.json", ROOT / "hooks" / "cursor-hooks.json" + if not (claude_path.is_file() and cursor_path.is_file()): + return + claude, cursor = load_json(claude_path, "Claude hooks"), load_json(cursor_path, "Cursor hooks") + if claude is None or cursor is None: + return + claude_events, cursor_events = _hook_scripts(claude), _hook_scripts(cursor) + wired = set() + for claude_event, cursor_event in EQUIVALENT_HOOK_EVENTS.items(): + here, there = claude_events.get(claude_event, set()), cursor_events.get(cursor_event, set()) + wired |= here | there + for name in sorted(here - there): + err(f"hooks/cursor-hooks.json: '{name}' is wired for Claude's {claude_event} but " + f"not for Cursor's {cursor_event} (dual-host parity)") + for name in sorted(there - here): + err(f"hooks/hooks.json: '{name}' is wired for Cursor's {cursor_event} but not for " + f"Claude's {claude_event} (dual-host parity)") + for event in set(claude_events) - set(EQUIVALENT_HOOK_EVENTS): + wired |= claude_events[event] + for event in set(cursor_events) - set(EQUIVALENT_HOOK_EVENTS.values()): + wired |= cursor_events[event] + for name in sorted(wired): + script = ROOT / "hooks" / name + if not script.is_file(): + err(f"hooks: wired script 'hooks/{name}' does not exist") + elif not os.access(script, os.X_OK): + err(f"hooks/{name}: wired but not executable (chmod +x)") + for script in sorted((ROOT / "hooks").glob("*.sh")): + if script.name not in wired: + err(f"hooks/{script.name}: present in the tree but wired by neither host's hook config") + + +def validate_doc_inventories(): + """Every shipped component must appear in the docs that claim to inventory the surface. + A removed skill leaves stale rows behind and a new hook goes unmentioned — both happened in + this repo. The check is one-directional on purpose: it proves each component IS documented, + not that every name mentioned still exists, so deliberate tombstones ("removed in 0.5.0") + and cross-references stay legal.""" + # Hooks are matched on the stem, since docs legitimately write "the format-on-save hook". + kinds = { + "skills": [d.name for d in sorted((ROOT / "skills").iterdir()) if (d / "SKILL.md").is_file()], + "agents": [m.stem for m in sorted((ROOT / "agents").glob("*.md"))], + "hooks": [s.stem for s in sorted((ROOT / "hooks").glob("*.sh"))], + } + for doc_name, covered in INVENTORY_DOCS.items(): + doc = ROOT / doc_name + if not doc.is_file(): + continue + body = doc.read_text() + for kind in covered: + for component in kinds[kind]: + if component not in body: + err(f"{doc_name}: does not mention the shipped {kind[:-1]} '{component}' — " + f"its component inventory is stale") + + def main(): manifests = {} for subdir, label in ((".claude-plugin", "Claude manifest"), (".cursor-plugin", "Cursor manifest")): @@ -306,12 +401,14 @@ def main(): validate_json_file(ROOT / "hooks" / "hooks.json", "Claude hooks") validate_json_file(ROOT / "hooks" / "cursor-hooks.json", "Cursor hooks") + validate_hook_parity() validate_skills() validate_md_components("agents", require_name=True, is_agent=True) validate_md_components("commands", require_name=False) validate_rules() validate_linter_references() validate_fixer_column() + validate_doc_inventories() if __name__ == "__main__": @@ -322,6 +419,7 @@ def main(): print(f" - {e}") sys.exit(1) print("OK: manifests, dual-host parity, component paths, kebab-case names, " - "hook configs, skills, agents, commands, rules, and taught-linter references are valid") + "hook configs and hook parity, skills, agents, commands, rules, taught-linter " + "references, and doc component inventories are valid") for note in notes: print(f" note: {note}") From ee312c1fe2af0fec72c68d27482e1e19bf3badf7 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Fri, 4 Sep 2026 00:42:54 +0300 Subject: [PATCH 22/23] fix(hooks,skills): classify the added text only; correct four Go 1.27 claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit External review found the nudge still grepping the raw Claude payload. A real PostToolUse payload carries old_string and a tool_response echo of nearby lines, so deleting an fmt.Errorf — or editing beside one — still nudged go-errors. That is the false positive the previous commit claimed to have removed. The hook now extracts tool_input.new_string (Edit) and content (Write) and matches only those; old_string and tool_response are excluded on purpose, and there is no fallback to the file when the extraction is empty, since that would restore the bug. The old fixtures could not see it: they sent new_string alone. Payload builders now mirror production shape, and two independent cases — an edit that only deletes an error path, and an edit beside one — fail against the old implementation. Every case asserts exit 0 as well (Claude Code treats a non-zero PostToolUse hook as failed, and $(...) was swallowing it), plus Write/content, hook_event_name without CLAUDE_PLUGIN_ROOT, and the "this edit" / "this file" wording. 24 cases. Also from the review, each re-verified against its source: - json/v2 said "Prefer ... for new/hot JSON paths". The 1.27 notes say the opposite — v1 now runs on v2 underneath and "users are not required to migrate". Reworded: keep v1; reach for v2 when you want its stricter semantics or Options. - embedlit was described as the t := T{}; t.x = v builder rewrite. It is the promoted-field one: T{U: U{x: 1}} -> T{x: 1}. - unsafefuncs was named in the prose but had no table row. - stdversion was dated to 1.27. The check is older; 1.27 is when go test starts running it by default. - httptest.NewTestServer now carries its signature, which takes a testing.TB that httptest.NewServer does not. Co-Authored-By: Claude Opus 4.8 --- hooks/skill-nudge.sh | 28 ++++++----- scripts/hooks-test.sh | 98 +++++++++++++++++++++++++------------- skills/go-idioms/SKILL.md | 12 +++-- skills/go-testing/SKILL.md | 6 ++- 4 files changed, 93 insertions(+), 51 deletions(-) diff --git a/hooks/skill-nudge.sh b/hooks/skill-nudge.sh index 72655ca..7c01112 100755 --- a/hooks/skill-nudge.sh +++ b/hooks/skill-nudge.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash # PostToolUse / afterFileEdit hook: after a Go file is edited, name ONE go-coding skill the edit -# calls for — once per skill per session — printed as a hook systemMessage (Claude Code) or a -# plain line (Cursor). Deterministic trigger for the focused skills a usage analysis showed are -# rarely loaded. Always exits 0; never blocks an edit. +# calls for — printed as a hook systemMessage (Claude Code) or a plain line (Cursor). Deterministic +# trigger for three focused skills a usage analysis showed load far less often than the router: +# go-testing, go-concurrency, go-errors. Always exits 0; never blocks an edit. # -# At most three nudges reach a session (one per skill), each on its first match. +# A test file always routes to go-testing, even when it also spawns goroutines — the test skill +# owns how to test concurrency. At most three nudges reach a session, one per skill. set -u f="${CLAUDE_FILE_PATH:-}"; sid=""; payload="" if [ ! -t 0 ]; then @@ -17,18 +18,23 @@ case "$f" in *.go) ;; *) exit 0 ;; esac [ -f "$f" ] || exit 0 [ -n "$sid" ] || sid="ppid$PPID" -# What gets matched: the edit itself where the host hands it over (a Claude Code PostToolUse -# payload carries the new text in tool_input, plus a few lines of surrounding context in -# tool_response), otherwise the whole file (Cursor's afterFileEdit passes only a path). Matching -# the edit is what keeps the message honest — grepping a whole file says "this edit touches an -# error path" for any file that merely happens to define a sentinel somewhere. +# Pull one JSON string field out of the payload. The body pattern `(\\.|[^"\\])*` matches an +# escaped character or an ordinary one, so an embedded \" does not end the match early. +json_field() { printf '%s' "$payload" | grep -oE "\"$1\"[[:space:]]*:[[:space:]]*\"(\\\\.|[^\"\\\\])*\""; } + +# What gets matched: the text the edit ADDS, where the host hands it over — Edit's new_string, +# Write's content. old_string and tool_response are deliberately excluded: deleting an fmt.Errorf +# is not an error path this edit introduces, and tool_response echoes surrounding lines the edit +# never touched. Cursor's afterFileEdit passes only a path, so that host matches the whole file +# and the message says so. No fallback from one to the other — falling back to the file on an +# empty extraction would restore exactly the false positives this avoids. case "$payload" in - *'"tool_input"'*) subject="$payload"; what="this edit";; + *'"tool_input"'*) subject="$(json_field new_string; json_field content)"; what="this edit";; *) subject="$(cat "$f")"; what="this file";; esac skill=""; topic="" -case "$f" in *_test.go) skill="go-testing"; topic="a test file"; what="this edit";; esac +case "$f" in *_test.go) skill="go-testing"; topic="a test file";; esac if [ -z "$skill" ] && printf '%s' "$subject" | grep -qE 'go func|chan |<-chan|chan<-|sync\.|atomic\.|errgroup\.'; then skill="go-concurrency"; topic="goroutines, channels or sync"; fi if [ -z "$skill" ] && printf '%s' "$subject" | grep -qE 'fmt\.Errorf|errors\.(Is|As|AsType|New|Join)'; then skill="go-errors"; topic="an error path"; fi [ -n "$skill" ] || exit 0 diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh index 0e038e6..9dedabf 100755 --- a/scripts/hooks-test.sh +++ b/scripts/hooks-test.sh @@ -26,50 +26,82 @@ run_case_absent "banner names no removed skill" "$t/go-no-lint" "go-exp run_case "go repo without lint config offers setup" "$t/go-no-lint" "/go-lint-setup" run_case "non-go repo is silent" "$t/not-go" "" -nudge() { # FILE SESSION [EDIT_TEXT] -> stdout. Claude Code shape: a tool_input payload, so the - # hook classifies from the edit text, not from the whole file. - printf '{"session_id":"%s","tool_name":"Edit","tool_input":{"file_path":"%s","new_string":"%s"}}' "$2" "$1" "${3:-}" | bash "$here/hooks/skill-nudge.sh" +# --- skill-nudge ------------------------------------------------------------------------------- +# Payload builders. p_edit/p_write mirror a real Claude Code PostToolUse payload: hook_event_name, +# tool_input with BOTH old_string and new_string, and a tool_response echoing nearby file text. +# That shape is the point — a hook that greps the raw payload passes a new_string-only fixture and +# still misfires in production, where old_string and tool_response carry the code around the edit. +p_edit() { # SESSION FILE OLD NEW RESPONSE + printf '{"session_id":"%s","hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{"file_path":"%s","old_string":"%s","new_string":"%s"},"tool_response":{"filePath":"%s","originalFile":"%s"}}' \ + "$1" "$2" "$3" "$4" "$2" "${5:-}" } -nudge_path() { # FILE SESSION -> stdout. Cursor shape: a path and no tool_input, so the hook falls - # back to classifying from the whole file. - printf '{"session_id":"%s","file_path":"%s"}' "$2" "$1" | bash "$here/hooks/skill-nudge.sh" +p_write() { # SESSION FILE CONTENT + printf '{"session_id":"%s","hook_event_name":"PostToolUse","tool_name":"Write","tool_input":{"file_path":"%s","content":"%s"}}' "$1" "$2" "$3" +} +p_path() { # SESSION FILE — Cursor afterFileEdit: a path, no tool_input, no hook_event_name + printf '{"session_id":"%s","file_path":"%s"}' "$1" "$2" +} + +# Every case asserts exit 0 as well as the output: Claude Code treats a non-zero PostToolUse hook as +# a failed hook, and a bare $(...) would swallow that. +hook() { # NAME PAYLOAD -> echoes stdout; flags a non-zero exit + local name="$1" out st + out="$(printf '%s' "$2" | bash "$here/hooks/skill-nudge.sh")"; st=$? + [ "$st" -eq 0 ] || { echo "FAIL $name: hook exited $st (a non-zero PostToolUse hook is a failed hook)"; fails=$((fails+1)); } + printf '%s' "$out" } -s="test-$$"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* -: > "$t/a_test.go"; printf 'package a\nfunc f(){ go func(){}() }\n' > "$t/w.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e.go"; : > "$t/plain.go"; : > "$t/readme.md" chk() { local name="$1" got="$2" expect="$3"; case "$got" in *"$expect"*) echo "ok $name";; *) echo "FAIL $name: got '$got'"; fails=$((fails+1));; esac; } chk_silent() { local name="$1" got="$2"; if [ -z "$got" ]; then echo "ok $name"; else echo "FAIL $name: expected silence, got '$got'"; fails=$((fails+1)); fi; } -# chk matches by substring, which is delivery-channel-agnostic: the nudge text appears whether -# wrapped as {"systemMessage":"..."} (Claude Code) or printed as a plain line (Cursor). These six -# cases therefore exercise whichever path is active for the ambient CLAUDE_PLUGIN_ROOT (unset in a -# plain shell, so normally the Cursor/plain path); the two delivery-channel cases below force each -# path explicitly. -chk "test file nudges go-testing" "$(nudge "$t/a_test.go" "$s")" "go-coding:go-testing" -chk_silent "second test file is silent" "$(nudge "$t/a_test.go" "$s")" -chk "goroutine edit nudges go-concurrency" "$(nudge "$t/w.go" "$s" 'go func(){}()')" "go-coding:go-concurrency" -chk "fmt.Errorf edit nudges go-errors" "$(nudge "$t/e.go" "$s" 'fmt.Errorf')" "go-coding:go-errors" -chk_silent "plain go file is silent" "$(nudge "$t/plain.go" "$s")" -chk_silent "non-go file is silent" "$(nudge "$t/readme.md" "$s")" -rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* -# Classify from the edit, not the file: an unrelated edit inside an error-heavy file must stay -# silent. Whole-file matching would claim "this edit touches an error path" for every file that -# merely defines a sentinel somewhere, which is most of a real Go repo. +s="test-$$"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* +: > "$t/a_test.go"; printf 'package a\nfunc f(){ go func(){}() }\n' > "$t/w.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e.go"; : > "$t/plain.go"; : > "$t/readme.md" + +chk "test file nudges go-testing" "$(hook t "$(p_edit "$s" "$t/a_test.go" 'x := 1' 'x := 2')")" "go-coding:go-testing" +chk_silent "second test file is silent" "$(hook t2 "$(p_edit "$s" "$t/a_test.go" 'x := 2' 'x := 3')")" +chk "goroutine edit nudges go-concurrency" "$(hook c "$(p_edit "$s" "$t/w.go" '' 'go func(){}()')")" "go-coding:go-concurrency" +chk "fmt.Errorf edit nudges go-errors" "$(hook e "$(p_edit "$s" "$t/e.go" '' 'return fmt.Errorf(\"x: %w\", err)')")" "go-coding:go-errors" +chk "Write content nudges too" "$(hook w "$(p_write "$s-w" "$t/w.go" 'package a\nfunc f(){ go func(){}() }')")" "go-coding:go-concurrency" +chk_silent "plain go file is silent" "$(hook p "$(p_edit "$s" "$t/plain.go" '' 'const x = 1')")" +chk_silent "non-go file is silent" "$(hook n "$(p_edit "$s" "$t/readme.md" '' 'fmt.Errorf')")" +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$s."* "${TMPDIR:-/tmp}/go-coding-nudge.$s-w."* + +# The whole point of classifying from the edit. Each of these payloads carries fmt.Errorf somewhere +# a naive grep would find it — the old text being deleted, the tool_response echo, the file on disk +# — while the text the edit ADDS is a doc-comment tidy. All must stay silent. sedit="$s-edit"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sedit."* -chk_silent "unrelated edit in an error-heavy file is silent" "$(nudge "$t/e.go" "$sedit" '// tidy the doc comment')" -chk "path-only payload falls back to the file" "$(nudge_path "$t/e.go" "$sedit")" "go-coding:go-errors" -rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sedit."* +chk_silent "edit that only deletes an error path is silent" \ + "$(hook d "$(p_edit "$sedit" "$t/e.go" 'return fmt.Errorf(\"x\")' 'return nil')")" +# Its own session: otherwise the case above would have consumed the go-errors marker and this one +# would pass by dedupe rather than by classifying correctly. +chk_silent "edit beside an error path is silent" \ + "$(hook b "$(p_edit "$sedit-beside" "$t/e.go" '// old comment' '// tidy the doc comment' 'package a\nimport \"fmt\"\nvar e = fmt.Errorf(\"x\")')")" +chk "path-only payload falls back to the file" "$(hook f "$(p_path "$sedit" "$t/e.go")")" "go-coding:go-errors" +chk "path-only payload says 'this file'" "$(hook f2 "$(p_path "$sedit-2" "$t/e.go")")" "this file touches" +chk "edit payload says 'this edit'" "$(hook f3 "$(p_edit "$sedit-3" "$t/a_test.go" '' 'x := 1')")" "this edit touches" +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sedit"* -# Delivery channel (F4): a fresh fixture + session per case so dedupe cannot silence it, and each -# host path forced explicitly rather than relying on the ambient CLAUDE_PLUGIN_ROOT. -: > "$t/e2.go"; printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e2.go" +# Delivery channel: a fresh fixture + session per case so dedupe cannot silence it, and each host +# path forced explicitly rather than relying on the ambient CLAUDE_PLUGIN_ROOT. +printf 'package a\nimport "fmt"\nvar e = fmt.Errorf("x")\n' > "$t/e2.go" sjson="$s-json"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sjson."* -chk "nudge is a systemMessage under Claude" "$(CLAUDE_PLUGIN_ROOT=/x nudge "$t/e2.go" "$sjson" 'fmt.Errorf')" '{"systemMessage":' +out="$(CLAUDE_PLUGIN_ROOT=/x hook j "$(p_edit "$sjson" "$t/e2.go" '' 'fmt.Errorf')")" +chk "CLAUDE_PLUGIN_ROOT delivers a systemMessage" "$out" '{"systemMessage":' rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$sjson."* +# hook_event_name alone must be enough — CLAUDE_PLUGIN_ROOT is not set for hooks in every context, +# and the nudge has to carry the skill name, not just be well-formed JSON. +shook="$s-hookevent"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$shook."* +out="$(printf '%s' "$(p_edit "$shook" "$t/e2.go" '' 'fmt.Errorf')" | env -u CLAUDE_PLUGIN_ROOT bash "$here/hooks/skill-nudge.sh")"; st=$? +[ "$st" -eq 0 ] || { echo "FAIL hook_event_name without CLAUDE_PLUGIN_ROOT: exited $st"; fails=$((fails+1)); } +chk "hook_event_name alone delivers a systemMessage" "$out" '{"systemMessage":' +chk "that systemMessage names the skill" "$out" "go-coding:go-errors" +rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$shook."* + scursor="$s-cursor"; rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$scursor."* -# Built directly (not via nudge_path()) per the sketch: env -u only strips CLAUDE_PLUGIN_ROOT from -# an external-command invocation, and the path-only payload already lacks hook_event_name. -out="$(printf '{"session_id":"%s","file_path":"%s"}' "$scursor" "$t/e2.go" | env -u CLAUDE_PLUGIN_ROOT bash "$here/hooks/skill-nudge.sh")" +# Built directly (not via hook()): env -u only strips CLAUDE_PLUGIN_ROOT from an external-command +# invocation, and the path-only payload already lacks hook_event_name. +out="$(printf '%s' "$(p_path "$scursor" "$t/e2.go")" | env -u CLAUDE_PLUGIN_ROOT bash "$here/hooks/skill-nudge.sh")"; st=$? +[ "$st" -eq 0 ] || { echo "FAIL nudge is a plain line under Cursor: exited $st"; fails=$((fails+1)); } case "$out" in '{'*) echo "FAIL nudge is a plain line under Cursor: got JSON '$out'"; fails=$((fails+1));; *"go-coding:go-errors"*) echo "ok nudge is a plain line under Cursor";; diff --git a/skills/go-idioms/SKILL.md b/skills/go-idioms/SKILL.md index 9a5cfad..78979fa 100644 --- a/skills/go-idioms/SKILL.md +++ b/skills/go-idioms/SKILL.md @@ -93,14 +93,16 @@ moves to 1.27, these are worth reaching for. | Idiom (available from 1.27) | Supersedes / complements | Fixer / linter | Since | Source | |---|---|---|---|---| | Generic methods — a method may declare its own type parameters (interface methods still may not declare type parameters, nor be implemented by generic methods) | a package-level generic helper function bound to the receiver type as a workaround for "methods can't be generic" | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | -| Prefer `encoding/json/v2` + `jsontext` for new/hot JSON paths once on 1.27 | v1 `encoding/json` (still works unchanged; only exact error-message text may shift) | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| `encoding/json/v2` + `jsontext` when you want v2's stricter semantics (invalid UTF-8 and duplicate object names rejected) or its `Options` | v1 `encoding/json` — **keep it**: it now runs on the v2 implementation underneath, keeps its behaviour (only exact error text may shift), gets the faster unmarshal for free, and stays supported. The release notes are explicit: "users are not required to migrate". Opt out of the new backend with `GOEXPERIMENT=nojsonv2` | — | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | | `atomictypes` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | raw `sync/atomic` functions | `atomictypes` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | | `slicesbackward` — graduates into the stock `go fix` (previously † golangci-lint-only, row above) | `for i := len(s)-1; i >= 0; i--` | `slicesbackward` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | -| `embedlit` — folds a field assignment right after a composite literal into the literal | `t := T{}; t.Field = v` builder pattern | `embedlit` | 1.27 | [go.dev/doc/go1.27](https://go.dev/doc/go1.27) | +| `embedlit` — initialise a field promoted from an embedded struct directly in the parent literal: `T{U: U{x: 1}}` → `T{x: 1}` | the nested literal an embedded struct used to require | `embedlit` | 1.27 | [modernize](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize) | +| `unsafefuncs` — `unsafe.Pointer(uintptr(ptr) + uintptr(n))` → `unsafe.Add(ptr, n)` | hand-rolled unsafe pointer arithmetic (`unsafe.Add` itself is 1.17; the fixer is new) | `unsafefuncs` | 1.27 | [modernize](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize) | -- **`go test` now runs the `stdversion` vet check by default** (available from 1.27): it flags use of - stdlib symbols newer than the module's `go` directive — the guardrail that keeps a 1.26-floor - module from silently depending on a 1.27-only symbol. Trust it over manual review for this. Source: +- **`go test` runs the `stdversion` vet check by default from 1.27.** The check itself is not new — + what changes is that it becomes automatic. It flags stdlib symbols newer than the `go` directive in + force for the file, which is the guardrail that keeps a 1.26-floor module from silently depending on + a 1.27-only symbol. Trust it over manual review for this. Source: [go.dev/doc/go1.27](https://go.dev/doc/go1.27). ## Sources diff --git a/skills/go-testing/SKILL.md b/skills/go-testing/SKILL.md index 8e5340d..070ae5f 100644 --- a/skills/go-testing/SKILL.md +++ b/skills/go-testing/SKILL.md @@ -43,8 +43,10 @@ Deterministic backstop: `go test -race ./...` (always, in CI), `go test -bench`, bubble is durably blocked. Reach for it instead of `time.Sleep`-based polling. (Always `synctest.Test` — the pre-stable `synctest.Run` no longer exists.) *Go 1.27 (released 2026-08-19, ) adds `synctest.Sleep` (`time.Sleep` + `Wait` - in one) and `httptest.NewTestServer`, an in-memory server usable inside a bubble. Source: - .* + in one) and `httptest.NewTestServer(t, handler)` — signature + `func NewTestServer(t testing.TB, handler http.Handler) *Server`, note the `testing.TB` first + argument that `httptest.NewServer` does not take — an in-memory server usable inside a bubble. + Sources: , .* - **Fuzzing** (`func FuzzX(f *testing.F)`) for parsers, codecs, and anything consuming untrusted bytes. **Golden files** (an `-update` flag writing `testdata/*.golden`) for large structured output. A golden pins *shape*, not behaviour — when it records something another system executes (SQL, From 20f7a7d7248eea9c92ce14de9f2bfb7d15fcbbc2 Mon Sep 17 00:00:00 2001 From: Sebastian Iancu Date: Fri, 4 Sep 2026 00:47:07 +0300 Subject: [PATCH 23/23] =?UTF-8?q?fix(docs,validate):=20review=20nits=20?= =?UTF-8?q?=E2=80=94=20stale=20inventories,=20gopls=20pin,=20self-testing?= =?UTF-8?q?=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the same external review, the cheaper half: - README's lede said two hooks while the table listed three; AGENTS.md claimed a Cursor `commands` path the manifest does not declare; docs/testing.md said CI runs validate.sh when it calls validate.py directly, and listed neither new check. - gopls was pinned at v0.23's predecessor while the docs recommend Go 1.27. v0.23.0 (July 2026) is the line that adds Go 1.27 support. - The Cursor rule referenced ${CLAUDE_PLUGIN_ROOT}, which is Claude-only, in a Cursor-only file. - go-idioms triggered on "writes, reviews, or modernizes Go" — nearly every Go turn. Narrowed to a rewritable construct or an explicit modernize/go fix ask. - hooks-test.sh covers format-on-save for the first time: it reformats a messy file, is idempotent, leaves non-Go files alone, and stays a silent exit-0 no-op with no formatter on PATH. 28 cases, so "tests for both hooks" is now "all three". - The doc-inventory check matched substrings, so `go-test` would have been satisfied by `go-testing`. Bounded now — which immediately caught two hooks my own testing.md rewrite had dropped by name. And the point behind those: `validate.py --selftest` rebuilds each structural check's failure case in a temp tree and requires the check to catch it, then requires it to go quiet once the defect is removed. Eight checks, run in CI. A green validator over a valid tree never proved the checks still check. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/validate.yml | 2 + AGENTS.md | 2 +- CHANGELOG.md | 31 +++++++--- README.md | 2 +- docs/install.md | 6 +- docs/testing.md | 7 ++- rules/go-context.mdc | 4 +- scripts/hooks-test.sh | 33 +++++++++++ scripts/validate.py | 105 ++++++++++++++++++++++++++++++++- skills/go-idioms/SKILL.md | 2 +- 10 files changed, 175 insertions(+), 19 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ac8f0f7..e995a3a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,5 +29,7 @@ jobs: # CI is strict and deterministic: Python is guaranteed here, so run the # validator directly (the scripts/validate.sh graceful skip is for local use). - run: python3 scripts/validate.py + # A green validator proves the tree is valid, not that the checks still check. + - run: python3 scripts/validate.py --selftest # Bash-only hook test harness; runs on both matrix legs. - run: ./scripts/hooks-test.sh diff --git a/AGENTS.md b/AGENTS.md index 8991208..2ce8cee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ When a recommendation derives from one of the above, attribute it explicitly and This repo supports **both Claude Code and Cursor**. Shared assets (skills, commands, agents) are consumed by both hosts; host-specific manifests and hook configs are kept separate. - **Claude manifest**: `.claude-plugin/plugin.json` — `name`, `version`, `description`, `author` (an **object** `{name, url}` — `claude plugin validate` rejects a string), `license`, `repository`, `keywords`. Claude Code discovers components from the **default folders** (`skills/`, `commands/`, `agents/`, `hooks/`) automatically; no explicit path map is needed. -- **Cursor manifest**: `.cursor-plugin/plugin.json` — same metadata **plus** explicit top-level path keys (`skills`, `rules`, `agents`, `commands`, `hooks`). No `mcpServers` — this plugin has no MCP backend. Keep `name`/`version`/`description`/`author` identical to the Claude manifest. +- **Cursor manifest**: `.cursor-plugin/plugin.json` — same metadata **plus** explicit top-level path keys — `skills`, `agents`, `rules`, `hooks` (a `commands` key would go here too, but this plugin ships no `commands/` folder: `/go-lint-setup` is a user-invoked skill). No `mcpServers` — this plugin has no MCP backend. Keep `name`/`version`/`description`/`author` identical to the Claude manifest. - **Skills**: `skills//SKILL.md` — shared by both hosts. Shipped: `go-coding` (auto-invoked router) plus the focused, load-on-use `go-errors`, `go-concurrency`, `go-testing`, `go-idioms`, `go-layout` standards skills. `go-linting` was merged into `go-lint-setup` and removed in 0.5.0. - **Slash commands** are authored as **user-invoked skills** (`skills//SKILL.md` with `argument-hint` + `allowed-tools`), not the legacy `commands/` folder — both yield a `/` command, but the skills layout is preferred (current `plugin-dev` guidance). Keep the surface small; put multi-step workflows in auto-invoked skills. Shipped: `/go-lint-setup` (scaffold the golangci-lint v2 config). `/go-explain` was removed in 0.5.0 — a one-shot lookup the focused skills already answer. - **Agents**: `agents/.md` — context-isolated specialists. Shipped: `go-reviewer` (report-only Go diff/file reviewer applying the review-heuristics catalog; `tools:` not `allowed-tools:`). diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1e56a..87148d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,14 @@ the existing Go 1.26.4+ floor, and ships a script for measuring adoption. minimum checklist for when a second load is not affordable. - Skill `go-coding`, agent `go-reviewer` — "Writing for the human": anything a person reads (PR text, review findings, a question) states the effect before the mechanism, in plain English, short. -- Hook `hooks/skill-nudge.sh` — after a Go edit, names one skill the edit calls for, classified from - the edit rather than the whole file. Once per skill per session, so at most three per session; - a `systemMessage` under Claude Code, a plain line under Cursor. -- Script `scripts/hooks-test.sh` — 18 bash tests for both hooks, including can-fail controls that - prove the silence assertions can actually fail. Runs in CI. +- Hook `hooks/skill-nudge.sh` — after a Go edit, names one skill the edit calls for. Under Claude + Code it matches only the text the edit adds (`tool_input.new_string`, or `content` for a write), + not the file and not the deleted text; Cursor passes a path alone, so there it matches the file + and says so. Once per skill per session, at most three per session; a `systemMessage` under + Claude Code, a plain line under Cursor. +- Script `scripts/hooks-test.sh` — 28 bash tests over all three hooks, on production-shaped + payloads, asserting exit status as well as output, with can-fail controls proving the silence + assertions can actually fail. Runs in CI. - Script `scripts/usage-report.py` — stdlib-only adoption report from local session transcripts; counting rules and the 50% target in `docs/testing.md`. - Docs `README.md` — implementer and reviewer brief templates for orchestrators, since a subagent @@ -37,10 +40,14 @@ the existing Go 1.26.4+ floor, and ships a script for measuring adoption. - Validation `scripts/validate.py` — hook parity (the same script wired for the equivalent event on both hosts, existing and executable, none left unwired) and doc component inventories (every shipped skill, agent and hook named where the docs claim to list them). +- Validation `scripts/validate.py --selftest` — rebuilds each structural check's failure case in a + temporary tree and requires the check to catch it, and to stay quiet once the defect is removed. + Runs in CI, because a green run over a valid tree says nothing about whether a check still checks. ### Changed -- Skills — descriptions rewritten trigger-first and shortened 11% (4,619 → 4,101 characters). - Always-on context competes with the session's real work. +- Skills — descriptions rewritten trigger-first and shortened 10% (4,619 → 4,163 characters). + Always-on context competes with the session's real work. `go-idioms` in particular no longer + triggers on "writes or reviews Go", which was nearly every Go turn. - Skill `go-lint-setup` — absorbs `go-linting`'s config-schema, adoption and upgrade-breakage content; re-fronted as "scaffold, adopt, or debug". - Hook `hooks/session-start.sh` — the banner names the hop and the focused skills, and offers @@ -48,7 +55,15 @@ the existing Go 1.26.4+ floor, and ships a script for measuring adoption. - Agent `go-reviewer`, skill `go-coding` — one review seat per diff: where a workflow already has a reviewer, that reviewer loads the skills itself instead of `go-reviewer` being dispatched beside it. - Cursor rule `rules/go-context.mdc` — brought level with the router: the diff→skill mapping, the - minimum checklist, and the plain-English rule for anything a person reads. + minimum checklist, and the plain-English rule for anything a person reads. Drops the Claude-only + `${CLAUDE_PLUGIN_ROOT}` reference from a Cursor-only file. +- Skills `go-idioms`, `go-testing` — four Go 1.27 claims corrected against their sources: v1 + `encoding/json` stays the default (the release notes say users are not required to migrate); + `embedlit` folds a promoted field into the parent literal rather than a post-literal assignment; + `unsafefuncs` gains the table row its prose already assumed; `stdversion` is dated to when `go + test` starts running it by default, not to the check's existence. `httptest.NewTestServer` now + carries its signature. +- Docs `docs/install.md` — `gopls` pin moves to v0.23.x, the line that adds Go 1.27 support. - Docs `docs/testing.md` — adds the missing `format-on-save` triggering check. - Skills, agent, Cursor rule, docs — Go 1.26.4+ remains the hard floor; Go 1.27 is supported and its additions are flagged as hints. `docs/install.md` recommends the latest 1.27.x patch. diff --git a/README.md b/README.md index bc3fafe..93abf40 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Go Coding Plugin -An AI plugin by **Cadasto B.V.** that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, an agent, session-start and format-on-save hooks, and a Cursor rule. It targets **both Claude Code and Cursor** from a single shared component set. +An AI plugin by **Cadasto B.V.** that teaches AI coding assistants **idiomatic Go coding standards** — formatting, naming, error handling, concurrency, testing, and project layout — through skills, an agent, three hooks (session-start, format-on-save, skill-nudge), and a Cursor rule. It targets **both Claude Code and Cursor** from a single shared component set. ## Install diff --git a/docs/install.md b/docs/install.md index c66be41..8a4fb57 100644 --- a/docs/install.md +++ b/docs/install.md @@ -55,7 +55,7 @@ At minimum the host should provide: | **`gofmt`** | the Go distribution | `format-on-save.sh` fallback (`gofmt -w -s`) | n/a — always ships with Go | | **`gofumpt`** | `go install` | `format-on-save.sh` primary (`gofumpt -w`), stricter gofmt superset | hook degrades to `gofmt` | | **`goimports`** | `go install` | `goimports` formatter in the golangci-lint v2 config (import grouping/pruning) | import-group formatting skipped | -| **`gopls`** (v0.22.x) | `go install` | the `gopls-lsp` plugin (defs/refs/diagnostics/rename/vulncheck) | no code intelligence | +| **`gopls`** (v0.23.x) | `go install` | the `gopls-lsp` plugin (defs/refs/diagnostics/rename/vulncheck) | no code intelligence | ### Install / upgrade Go (official tarball, Linux) @@ -79,7 +79,7 @@ go version # → go version go1. ```bash go install mvdan.cc/gofumpt@latest # stricter gofmt superset (hook primary) go install golang.org/x/tools/cmd/goimports@latest # import grouping / pruning -go install golang.org/x/tools/gopls@v0.22.0 # language server for the gopls-lsp plugin (pinned: v0.22.x — the gopls line that adds Go 1.26 support; use @latest for the newest patch) +go install golang.org/x/tools/gopls@v0.23.0 # language server for the gopls-lsp plugin (pinned: v0.23.x — the gopls line that adds Go 1.27 support; use @latest for the newest patch) ``` Verify: @@ -89,7 +89,7 @@ go version # → 1.27.x (or 1.26.4+ on the floor) command -v gofmt # ships with Go (in GOROOT/bin) gofumpt --version command -v goimports # goimports has no --version flag -gopls version # → golang.org/x/tools/gopls v0.22.x +gopls version # → golang.org/x/tools/gopls v0.23.x ``` These are **host-only** dev tools; the plugin still works without them (the format hook degrades to `gofmt`, then to a silent no-op). Full-tree `golangci-lint` runs separately — often in a pinned container — so it does not depend on these host binaries. On Go 1.27, that container/pin needs **golangci-lint ≥ v2.13.0** (released 2026-08-19, the release that added Go 1.27 support) — see ; anything older predates 1.27 support. diff --git a/docs/testing.md b/docs/testing.md index f0078e4..1c61483 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -6,8 +6,9 @@ exercising the components. ## Validation -- **Manifest / component validation** — `./scripts/validate.sh` (also run by CI on every PR): checks both `plugin.json` manifests, dual-host parity (name/version/description/author agree), declared component paths, kebab-case names, hook-config JSON, and SKILL.md / agent / command frontmatter (including `name` == directory/filename, and that agents declare `tools:` not `allowed-tools:`). The wrapper runs `scripts/validate.py`; if Python 3 isn't installed it prints a warning and skips (exit 0) rather than failing — install `python3` for the full local check, or rely on `claude plugin validate .` and CI. CI pins Python so the deep check always runs there. -- **Hook tests** — `./scripts/hooks-test.sh` (also run by CI on every PR): bash tests for `hooks/session-start.sh` and `hooks/skill-nudge.sh`, including a can-fail self-test block that proves the negative-case helpers actually fail on bad input. +- **Manifest / component validation** — `./scripts/validate.sh`: checks both `plugin.json` manifests, dual-host parity (name/version/description/author agree), declared component paths, kebab-case names, hook-config JSON, hook parity (the same `hooks/*.sh` wired for the equivalent event on both hosts, each existing and executable, none left unwired), doc component inventories (every shipped skill, agent and hook named where the docs claim to list them), and SKILL.md / agent / command frontmatter (including `name` == directory/filename, and that agents declare `tools:` not `allowed-tools:`). The wrapper runs `scripts/validate.py`; if Python 3 isn't installed it prints a warning and skips (exit 0) rather than failing — install `python3` for the full local check, or rely on `claude plugin validate .` and CI. CI pins Python and calls `python3 scripts/validate.py` directly, so the deep check can never silently skip there. +- **Validator self-test** — `python3 scripts/validate.py --selftest` (also run by CI): rebuilds each structural check's failure case in a temporary tree and requires the check to catch it, so a check that has quietly stopped checking cannot pass as green. +- **Hook tests** — `./scripts/hooks-test.sh` (also run by CI on every PR): bash tests for all three hook scripts (`hooks/session-start.sh`, `hooks/format-on-save.sh`, `hooks/skill-nudge.sh`), including a can-fail self-test block that proves the negative-case helpers actually fail on bad input. - **Official validator** — `claude plugin validate .`: checks the manifest and component structure (no extra dependencies). - **Structural review** — run the `plugin-dev:plugin-validator` agent after creating or modifying components. - **Skill quality review** — run the `plugin-dev:skill-reviewer` agent: description-triggering quality, progressive disclosure, content structure. @@ -54,6 +55,8 @@ sessions table: (sessions loading a given focused skill) / (sessions loading **Not counted:** the SessionStart banner line, or skill-body text echoed back inside tool results — only structured tool invocations and explicit slash-command text count. +Nor is Cursor: the script reads Claude Code transcripts (`~/.claude/projects`) only, so +the numbers describe adoption on one of the two hosts. **Target:** the focused standards skills (`go-errors`, `go-testing`, `go-idioms`, `go-lint-setup`, `go-layout`, `go-concurrency`) should load on at least 50% of sessions diff --git a/rules/go-context.mdc b/rules/go-context.mdc index 610035a..32dbbde 100644 --- a/rules/go-context.mdc +++ b/rules/go-context.mdc @@ -60,4 +60,6 @@ verbatim — it is the prose around them that must be plain. Ground every judgment call in a cited source — Effective Go, Go Code Review Comments, the Google and Uber Go style guides, `pkg.go.dev`. Don't invent rules. Adopt the shipped golangci-lint v2 -config (`references/golangci.v2.yml`, at the **plugin root** — resolve via `${CLAUDE_PLUGIN_ROOT}/references/…` or Glob the installed copy, not under this rule's dir); run `golangci-lint run --fix` for auto-fixable findings. +config (`references/golangci.v2.yml`, at the **plugin root** — search the installed plugin directory +for it, not under this rule's own dir); run `golangci-lint run --fix` for auto-fixable findings, and +read the rest. diff --git a/scripts/hooks-test.sh b/scripts/hooks-test.sh index 9dedabf..3193e64 100755 --- a/scripts/hooks-test.sh +++ b/scripts/hooks-test.sh @@ -109,6 +109,39 @@ case "$out" in esac rm -f "${TMPDIR:-/tmp}/go-coding-nudge.$scursor."* +# --- format-on-save ---------------------------------------------------------------------------- +# Needs a formatter; without gofmt or gofumpt on PATH the hook is a deliberate no-op and there is +# nothing to assert beyond "exits 0", which the last case covers. +fmt_hook() { # PAYLOAD -> exit status recorded, stdout dropped + printf '%s' "$1" | bash "$here/hooks/format-on-save.sh" >/dev/null 2>&1 +} +if command -v gofmt >/dev/null 2>&1 || command -v gofumpt >/dev/null 2>&1; then + printf 'package a\nfunc f( ) {\n}\n' > "$t/messy.go" + before="$(cat "$t/messy.go")" + fmt_hook "$(p_write "$s-fmt" "$t/messy.go" 'x')"; st=$? + after="$(cat "$t/messy.go")" + if [ "$st" -ne 0 ]; then echo "FAIL format-on-save exits 0: exited $st"; fails=$((fails+1)); + elif [ "$before" = "$after" ]; then echo "FAIL format-on-save reformats a messy Go file: file unchanged"; fails=$((fails+1)); + else echo "ok format-on-save reformats a messy Go file"; fi + + # A second run must be a fixpoint — a formatter that keeps rewriting an already-formatted file + # would churn every save. + fmt_hook "$(p_write "$s-fmt" "$t/messy.go" 'x')" + if [ "$(cat "$t/messy.go")" = "$after" ]; then echo "ok format-on-save is idempotent"; else echo "FAIL format-on-save is idempotent: second run changed the file"; fails=$((fails+1)); fi + + printf 'not go source\n' > "$t/keep.md" + fmt_hook "$(p_write "$s-fmt" "$t/keep.md" 'x')" + if [ "$(cat "$t/keep.md")" = 'not go source' ]; then echo "ok format-on-save leaves a non-go file alone"; else echo "FAIL format-on-save leaves a non-go file alone: file was rewritten"; fails=$((fails+1)); fi +else + echo "ok format-on-save cases skipped (no gofmt/gofumpt on PATH)" +fi + +# No formatter on PATH at all: the hook must stay silent and still exit 0. CLAUDE_FILE_PATH is set +# so the hook never needs cat/grep/sed, which an empty PATH would also take away — this case is +# about the missing formatter, not about a crippled shell. +out="$(CLAUDE_FILE_PATH="$t/plain.go" PATH=/nonexistent "$BASH" "$here/hooks/format-on-save.sh" &1)"; st=$? +if [ "$st" -eq 0 ] && [ -z "$out" ]; then echo "ok format-on-save is a silent no-op without a formatter"; else echo "FAIL format-on-save is a silent no-op without a formatter: exit $st, output '$out'"; fails=$((fails+1)); fi + # Can-fail control (F3): prove chk_silent and run_case_absent actually fail on bad input, so a # broken helper (e.g. always echoing "ok") can't hide a real regression above. Each probe runs in a # `$(...)` subshell — already isolated from this shell's `fails` — with its own `fails=0` so the diff --git a/scripts/validate.py b/scripts/validate.py index ac55e2c..fe255f0 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -21,8 +21,17 @@ This plugin has no MCP backend, so there is intentionally no ``.mcp.json`` check. + * dual-host hook parity: the same ``hooks/*.sh`` wired for the equivalent event on both + hosts, each wired script present and executable, and none left unwired; + * doc component inventories: every shipped skill, agent and hook named in the docs that + claim to list them. One-directional, so tombstones for removed components stay legal. + Dependency-free (stdlib only) so the ``scripts/validate.sh`` soft-skip is the *only* -reason it wouldn't run. Usage: python3 scripts/validate.py (from the repo root) +reason it wouldn't run. + +Usage: + python3 scripts/validate.py # verify this tree + python3 scripts/validate.py --selftest # verify the checks themselves still catch things """ import json import os @@ -30,6 +39,7 @@ import shutil import subprocess import sys +import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @@ -364,7 +374,9 @@ def validate_doc_inventories(): body = doc.read_text() for kind in covered: for component in kinds[kind]: - if component not in body: + # Bounded so a shorter name is not satisfied by a longer one that contains it + # ("go-test" must not be answered by "go-testing"). + if not re.search(rf"(? int: + """Every structural check, run against a tree built to break it. A check that has quietly + stopped checking — a renamed field, a regex that no longer matches — still exits 0 on a valid + tree, so 'the suite is green' proves nothing on its own. Each case is also run against the + same tree with the defect removed, so a check that always fires fails too.""" + global ROOT, errors + real_root, real_errors, failures = ROOT, errors, 0 + for label, defect, checks in SELFTEST_CASES: + outcomes = {} + for variant, break_it in (("broken", defect), ("clean", None)): + with tempfile.TemporaryDirectory() as tmp: + ROOT = Path(tmp) + _selftest_tree(ROOT, break_it=break_it) + errors = [] + for check in checks: + check() + outcomes[variant] = list(errors) + ROOT, errors = real_root, real_errors + if not outcomes["broken"]: + print(f"FAIL {label}: the check did not catch it") + failures += 1 + elif outcomes["clean"]: + print(f"FAIL {label}: the check also fires on a clean tree: {outcomes['clean'][0]}") + failures += 1 + else: + print(f"ok {label}") + if failures: + print(f"FAIL: {failures} check(s) do not actually check") + return 1 + print(f"OK: {len(SELFTEST_CASES)} structural checks each caught their own failure case") + return 0 + + if __name__ == "__main__": + if "--selftest" in sys.argv[1:]: + sys.exit(run_selftest()) main() if errors: print(f"FAIL: {len(errors)} problem(s)") diff --git a/skills/go-idioms/SKILL.md b/skills/go-idioms/SKILL.md index 78979fa..342b5f0 100644 --- a/skills/go-idioms/SKILL.md +++ b/skills/go-idioms/SKILL.md @@ -1,6 +1,6 @@ --- name: go-idioms -description: Modern idiomatic Go (the `modernize` analyzer set) — Go 1.26+, Go 1.27 additions noted. This skill should be used when the user writes, reviews, or modernizes Go, or asks which fixer owns a rewrite — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)`, `errors.AsType`, dropped loop-var copies, and the Go 1.27 additions (generic methods, json/v2-backed `encoding/json`, the `atomictypes`/`embedlit`/`slicesbackward`/`unsafefuncs` fixers). Advice equals tooling — `go fix ./...` or `golangci-lint --enable-only=modernize`. Not for linter configuration (go-lint-setup). Go only. +description: Modern idiomatic Go (the `modernize` analyzer set) — Go 1.26+, Go 1.27 additions noted. This skill should be used when a diff or question contains a rewritable construct, when the user asks to modernize Go or run `go fix`, or asks which fixer owns a rewrite — range-over-int, `min`/`max`, `slices`/`maps`, `strings.Cut`, `any` over `interface{}`, iterators, `omitzero` json tags, `os.Root`, `new(expr)`, `errors.AsType`, dropped loop-var copies, and the Go 1.27 additions (generic methods, json/v2-backed `encoding/json`, the `atomictypes`/`embedlit`/`slicesbackward`/`unsafefuncs` fixers). Advice equals tooling — `go fix ./...` or `golangci-lint --enable-only=modernize`. Not for linter configuration (go-lint-setup). Go only. --- # go-idioms — modern Go (modernize)