Skip to content

feat: usage-driven optimization and Go 1.27 support (v0.5.0) - #5

Merged
sebastian-iancu merged 23 commits into
mainfrom
feat/usage-driven-optimization
Sep 3, 2026
Merged

sebastian-iancu merged 23 commits into
mainfrom
feat/usage-driven-optimization

Conversation

@sebastian-iancu

@sebastian-iancu sebastian-iancu commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Usage-driven optimization (v0.5.0)

A usage analysis of this plugin found the router loaded often but the focused skills it routes to
rarely followed, several skills with no concrete trigger words, and the two slash commands
effectively undiscovered. This release turns those findings into changes, shrinks the surface, and
adds Go 1.27 support (the floor stays Go 1.26.4+).

What changed

Routing

  • go-coding gains a Route, then load diff→skill table and a minimum checklist for when a second
    skill load is not affordable; its description leads with the trigger.
  • go-layout, go-concurrency and go-testing descriptions name the words a prompt or diff
    actually contains.
  • The SessionStart banner names the router→skill hop and go-reviewer, and offers /go-lint-setup
    only when the workspace has no golangci-lint config.
  • A PostToolUse/afterFileEdit hook (hooks/skill-nudge.sh, dual-host) names one matching skill
    after a Go edit. Under Claude Code it matches only the text the edit adds — new_string, or
    content for a write — not the file, not the deleted text, and not the surrounding lines the
    payload echoes back; Cursor passes a path alone, so there it matches the file and says so. Once
    per skill per session, at most three per session. Delivered as a hook systemMessage under
    Claude Code, where the model actually sees it; a plain line under Cursor.

Smaller surface

  • go-linting is removed, its content merged into go-lint-setup (re-fronted as "scaffold, adopt,
    or debug").
  • /go-explain is removed: one-shot lookups are what the focused skills already do, with the same
    citations and the code in view. One invocation in four weeks of transcripts.
  • All seven remaining descriptions rewritten trigger-first and 10% shorter, with no trigger dropped.

Other

  • "Writing for the human" in the router and go-reviewer: anything a person reads — PR text, review
    findings, a question — states the effect before the mechanism, in plain English, kept short.
  • One review seat per diff, plus implementer/reviewer brief templates for orchestrators (a subagent
    does not inherit the parent session's skills).
  • scripts/usage-report.py measures skill and agent adoption from local transcripts;
    docs/testing.md documents what counts and the target.
  • Go 1.27: cited hints in go-idioms (the new go fix modernizers, json/v2-backed encoding/json,
    generic methods, stdversion by default), a golangci-lint ≥ v2.13.0 note, install docs
    recommending 1.27.x on a 1.26.4+ floor, and a CI matrix over 1.26.x and 1.27.x.
  • rules/go-context.mdc (the Cursor mirror) is level with the router again: it had 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.
  • scripts/hooks-test.sh: 28 bash tests over all three hooks, on production-shaped payloads,
    asserting exit status as well as output, with committed self-tests proving the silence
    assertions can fail. Runs in CI on both Go legs.
  • scripts/validate.py gains two checks that previously needed a human reading two files side by
    side: hook parity (the same hooks/*.sh wired for the equivalent event on both hosts, each
    existing and executable, none left unwired) and doc inventories (every shipped skill, agent
    and hook named where the docs claim to list them). Both were run against a deliberately broken
    tree first. The inventory check found a real gap on its first run — no triggering check for
    format-on-save in docs/testing.md — now added.
  • python3 scripts/validate.py --selftest rebuilds each structural check's failure case in a
    temporary tree, 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.

Verification

  • ./scripts/validate.sh, ./scripts/hooks-test.sh (28/28), python3 scripts/validate.py --selftest (8/8), claude plugin validate . all green at
    the branch head.
  • Always-on context: the descriptions loaded into every session total 5,425 characters, against
    5,658 on main — the surface costs less than the released version while carrying more trigger
    words. (An intermediate commit on this branch peaked at 6,513.)
  • Live trigger checks need an interactive session and have not been run: the five checks under
    Local triggering tests in docs/testing.md, including that the model acts on a nudge rather
    than the line merely appearing.
  • CI runs on push.

Addressed from the external review (2026-09-04)

  • Nudge matched the raw payload, not the edit. A real PostToolUse payload carries old_string
    and a tool_response echo, so deleting an fmt.Errorf — or editing beside one — still nudged
    go-errors. Fixed, and two fixtures now fail against the old implementation.
  • Four Go 1.27 claims re-verified against their sources. encoding/json/v2 no longer says
    "prefer" (the release notes say users are not required to migrate); embedlit is the
    promoted-field rewrite, not the builder one; unsafefuncs gained its table row; stdversion is
    dated to when go test starts running it by default. httptest.NewTestServer carries its
    signature.
  • Doc drift: README lede, the Cursor commands claim in AGENTS, the CI line in docs/testing.md,
    the gopls pin (now v0.23.x, the Go 1.27 line), and the Claude-only variable in the Cursor rule.
  • Not changed, with reasons. The Cursor rule keeps both the hop table and the minimum checklist:
    the checklist is explicitly the fallback for when opening a skill is not affordable, and stripping
    Cursor back to a bare index gives that host less than it has today. golangci-lint run --fix
    stays in the rule — it is editing guidance; the ban on in-place flags belongs to go-reviewer,
    which has it. Live triggering on both hosts is still outstanding and still blocks the tag.

Notes for reviewers

  • Three defects in the implementation plan's own code were caught here and fixed: a multi-file ls
    existence check that misfired whenever any one name was missing; a silence assertion that could
    never fail; and a nudge printed to stdout, which under Claude Code reaches only the transcript and
    never the model.
  • The nudge's false-positive rate is what drove the edit-vs-file change: matching the whole file
    claimed "this edit touches an error path" for 43% of non-test files in one real Go SDK and 65% in
    another, because most Go files define a sentinel somewhere.
  • Hook timeouts are in seconds ("timeout": 5).

🤖 Generated with Claude Code

sebastian-iancu and others added 23 commits September 3, 2026 15:20
…tion 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.
… trigger words, section nesting

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.
…-fail self-tests; hooks-test in CI

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.
…up; 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.
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…eads it

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
… topic

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ck parity and inventories

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 <noreply@anthropic.com>
… claims

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 <noreply@anthropic.com>
…testing checks

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 <noreply@anthropic.com>
@sebastian-iancu
sebastian-iancu marked this pull request as ready for review September 3, 2026 21:57
@sebastian-iancu
sebastian-iancu merged commit 685d260 into main Sep 3, 2026
2 checks passed
@sebastian-iancu
sebastian-iancu deleted the feat/usage-driven-optimization branch September 3, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant