Skip to content

docs: add lint/static-analysis gaps brief#829

Merged
kyle-sexton merged 1 commit into
mainfrom
docs/lint-static-analysis-gaps-brief
Jul 21, 2026
Merged

docs: add lint/static-analysis gaps brief#829
kyle-sexton merged 1 commit into
mainfrom
docs/lint-static-analysis-gaps-brief

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Interview contract (Brief) for the lint/static-analysis gap epic: typos hook plugin, Go coverage (batch + hook), lychee-offline + pyright batch additions, .NET runtime guard, hook-observability fleet convention, bespoke-guard routing. Plan section intentionally empty — filled per sub-item by /planning:plan.

Epic + sub-issues follow; will be linked once filed.

No linked issue: this PR lands the Brief only; execution issues are tracked separately.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_019KVQkSXBKY8pcCSNKUnP3g

Interview contract for the lint/static-analysis gap epic: typos hook
plugin, Go coverage (batch + hook), lychee-offline and pyright batch
additions, .NET runtime guard, hook-observability fleet convention,
bespoke-guard routing. Related epics routed to their owning repos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KVQkSXBKY8pcCSNKUnP3g
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 0s —— View job


PR Review: docs/lint-static-analysis-gaps-brief

  • Gather context and read changed files
  • Review PLAN.md for correctness and alignment with CLAUDE.md
  • Check adherence to project conventions
  • Post findings

Summary

This PR adds a single planning document: docs/topics/lint-static-analysis-gaps/PLAN.md. It's a pure docs change — no code, no manifests, no plugin config — so review focuses on completeness, internal consistency, and alignment with project conventions.

Overall: LGTM with minor observations.


What's good

Format is consistent with established patterns. The ## Brief / ## Plan structure, subsection order (TLDR → Goal → Constraints → Acceptance criteria → Captured assumptions → Out of scope → Deferred questions → Related epics), and empty ## Plan with an explicit fill-in note all match the established convention across docs/topics/*/PLAN.md.

Lane assignments are correctly applied. Sub-items correctly map tools to lanes per the two-lane rule:

  • Hook lane (per-file, fast, auto-fixing): typos, Go format (line 27)
  • Batch lane (repo-wide / package-scope): golangci-lint, lychee-offline, pyright, dotnet guard (lines 28–36)
  • The explicit rationale for golangci-lint being batch-only ("package-scope analysis, per its FAQ") is good — it pre-empts a natural question.

Plugin philosophy constraints are correctly restated. The Constraints section (lines 49–55) correctly enumerates repo/user/machine/org-agnostic, cross-platform, setup contract, and graceful degradation — all aligned with docs/PLUGIN-PHILOSOPHY.md.

Time-stamped assertion (line 39). "no native user-visible hook UI exists as of 2026-07-21" is correctly grounded at authoring time, which is the right practice for observability-surface claims that may change.

PR title follows Conventional Commits (docs: …) as required by .github/workflows/pr-title.yml. ✓


Minor observations

1. "rung-4" is undefined in this document (line 62).

"Batch additions … are rung-4 defaults only"

The consumer override ladder taxonomy is internal shorthand. This is fine for a Brief that will be read by people already in context, but worth adding a pointer to wherever "rung-4" is defined (the ecosystem override ladder doc or the plugin philosophy) so the acceptance criteria are unambiguous when the sub-items are executed.

2. Setup-lifecycle convention dependency (line 55).

"New plugins conform to the setup-lifecycle convention once that epic lands."

This creates a blocking dependency on a future epic. The Brief correctly captures it as a constraint, but the sub-issues should explicitly track this dependency so the typos and Go hook plugins don't ship without the setup-lifecycle convention resolved. A note in the epic tracking issue (when filed) linking to the setup-lifecycle epic would close the loop.

3. Sub-item 7 cross-repo routing (lines 43–45).
The bespoke guards are correctly scoped out as a routing decision owned by ci-workflows/standards. The sub-issue for this item should capture the cross-repo dependency explicitly so the "local lane must invoke the same owned source" requirement doesn't get orphaned if the sub-issue is assigned across repos.

4. golangci-lint FAQ citation (line 31).
The FAQ is cited parenthetically ("per its FAQ") without a URL. The fresh-docs mandate in CLAUDE.md targets changes to plugins/manifests/structure specifically, so a planning doc is not strictly subject to it — but adding the URL here would be consistent with the "verified against … fetched YYYY-MM-DD" style used in MIGRATION-PLAYBOOK.md and make the rationale durable if golangci-lint's FAQ stance ever changes.


None of these are blocking. Observations 1 and 2 are worth addressing in the epic issue rather than in this doc. Observation 3 is a tracking hygiene note for sub-issue filing. Observation 4 is a style preference only.

@kyle-sexton
kyle-sexton merged commit 4a289ab into main Jul 21, 2026
24 of 25 checks passed
@kyle-sexton
kyle-sexton deleted the docs/lint-static-analysis-gaps-brief branch July 21, 2026 09:39
kyle-sexton added a commit that referenced this pull request Jul 21, 2026
## Summary

New hook plugin `plugins/typos-format/`: runs `typos --write-changes` on
every `Write`/`Edit`, gated on a consumer typos config
(`typos.toml`/`_typos.toml`/`.typos.toml`/`Cargo.toml`/`pyproject.toml`)
found by an ancestor walk-up, mirroring the shipped
`ruff-format`/`markdown-format` plugin pattern. Residual (unfixable)
findings surface via `additionalContext` with remediation guidance
pointing at `extend-words`/`extend-identifiers`/`extend-ignore-re`
allowlist entries. Advisory only — never blocks the edit.

- Hook-precision + hook-telemetry convention conformance (schema +
registry row added).
- `/typos-format:setup check|apply` skill.
- 44 black-box contract-test assertions (`typos-format.test.sh`),
including a `--force-exclude` regression case added after independent
review caught the flag was missing (verified empirically: without it,
the hook silently overrode a consumer's own typos excludes).

## Test plan

- [x] `typos-format.test.sh` — 44/44 pass against real `typos-cli
1.44.0`.
- [x] Full fleet `scripts/run-plugin-tests.sh` — all plugin contract
tests pass.
- [x] `scripts/validate-plugins.sh` (manifest schema, catalog sync,
no-npx contract).
- [x] `scripts/sync-hook-utils.sh --check`, `check-silent-skips.sh`,
`check-changelog-parity.sh --check`, `check-cross-plugin-source-drift.sh
--check`, `check-skill-leaf-names.sh --check`, `check-changed-skills.sh
origin/main`.
- [x] `typos`/`shellcheck`/`markdownlint-cli2` self-check on all new
files.
- [x] Two independent fresh-context reviews (code review + security
review via CI): one CRITICAL finding (missing `--force-exclude`) fixed +
regression-tested; two SUGGESTION-level security notes triaged (one
deferred as a fleet-wide pre-existing test-harness pattern, one
out-of-scope for the shared `hook-utils.sh` library); two minor
code-review notes fixed (test assertion strength, marketplace
`relevance` block).

## Review follow-ups filed

- #874 — `ruff-format` is missing its hook-telemetry data schema +
registry row (pre-existing docs-drift gap, surfaced while using it as a
reference plugin).
- #875 — fleet-wide: concurrent `PostToolUse` hook writes have no
locking/ordering, so overlapping-scope hooks (e.g. this plugin +
`ruff-format`, or `eol-normalizer` + any formatter) can silently clobber
each other's fix. Documented as a known limitation in this plugin's
header comment and README; not fixable at the single-plugin level.

## Related

- Closes #831 (epic #830, sub-item 1 of
`docs/topics/lint-static-analysis-gaps/PLAN.md`, contract landed via
#829).

<details>
<summary>Implementation plan (docs/topics/typos-format-hook/PLAN.md,
pruned from the tree after merge)</summary>

# typos-format hook plugin

## Brief

Issue #831 (epic #830, sub-item 1 of
docs/topics/lint-static-analysis-gaps/PLAN.md, merged via PR #829). New
hook
plugin `plugins/typos-format/`: per-file `typos -w` autofix on
Write/Edit,
mirroring the existing `plugins/markdown-format/` and
`plugins/ruff-format/`
patterns. Opt-in via consumer typos config ancestor walk-up (no config =
no-op,
never impose typos' defaults on a repo that hasn't adopted it — same
posture as
ruff-format). Advisory only, hook-precision + hook-telemetry
conventions,
setup skill (check/apply).

Scope: Tier C (no design) — pure pattern replication of two
already-shipped,
gate-passing plugins; no new types, no new architecture.

## Plan

### Phase 1: Hook plugin skeleton

Files (new): `.claude-plugin/plugin.json`, `hooks/hooks.json`,
`hooks/hook-utils.sh` (synced copy of `lib/hook-utils.sh`),
`hooks/typos-format.sh`, `.claude-plugin/marketplace.json` entry.

Hook control flow (modeled on `ruff-format.sh`): kill-switch check,
buffered
stdin read, jq gate, file-path extraction, repo-root resolution,
ancestor
config walk-up (`typos.toml` > `_typos.toml` > `.typos.toml` >
`Cargo.toml`
metadata > `pyproject.toml [tool.typos]`, precedence per crate-ci/typos'
own docs and empirical verification), typos binary resolution from PATH,
`typos --write-changes --force-exclude --format json` fix pass,
exit-code
branching (0 = clean/fixed, 2 = residual findings, other = tool break),
residual-finding parsing with remediation guidance pointing at
`extend-words`/`extend-identifiers`/`extend-ignore-re`, and
hook-telemetry
envelope emission.

Hook-precision conformance: the convention is fleet-wide by intent (its
owner doc states "every plugin hook follows it") though CI-audited
enforcement is currently guardrails-only; this plugin opts in
voluntarily.

Known, documented, out-of-scope risk: concurrent same-file `PostToolUse`
hook writes (no extension filter + no locking) — tracked in #875.

### Phase 2: Setup skill + docs

`skills/setup/SKILL.md` (`check`/`apply` contract; `apply` is
guidance-only
since typos has no per-repo dependency-manager install path, unlike Ruff
or
markdownlint-cli2), `README.md`, `CHANGELOG.md` (starts at 0.1.0).

### Phase 3: Contract test

`hooks/typos-format.test.sh` — black-box Bash contract test mirroring
`ruff-format.test.sh`'s fixture-corpus shape: opt-in gate
(present/absent
config, pyproject with/without `[tool.typos]`,
`typos.toml`/`_typos.toml`
precedence), fix + residual-finding reporting, config-exclude respect
(`--force-exclude`), kill switch, telemetry envelope shape,
missing-binary
and missing-`jq` visibility notices.

### Phase 4: Local CI-equivalent verification

Ran every gate the new directory trips in `plugin-gate`/`hygiene`
locally
before opening the PR (hook-utils sync, plugin manifest validation,
catalog
sync, changelog parity, silent-skip gate, cross-plugin source drift,
skill
leaf names, skill quality gate, full fleet test suite, and a `typos`/
`shellcheck`/`markdownlint-cli2` self-check on the new files).

## Blast radius

LOW — new, additive plugin directory; zero changes to existing plugin
behavior beyond the synced `hook-utils.sh` copy (mechanical, CI-gated)
and
two purely-additive doc/registry rows.

## Open questions (resolved during implementation)

1. Binary-file handling — empirically not an issue; typos handles
arbitrary
   file content without a hard error.
2. Setup `apply` scope — resolved to guidance-only (no auto-install),
   confirmed against crate-ci/typos' own install docs (no universal
per-repo dependency-manager path exists for a standalone Rust binary).

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 21, 2026
…atus (#890)

## Summary

Adds an `opt-in` key to the `dotnet` ecosystem entry — the one
lint-bearing ecosystem (of 8: dotnet, python, typescript, bash,
markdown, powershell, yaml, cross-cutting) that lacked one. `dotnet
format`'s own docs state preferences are read from `.editorconfig` "if
present, otherwise a default set of preferences will be used"; verified
empirically (dotnet SDK 10.0.302) that a project with **no**
`.editorconfig` still fails `dotnet format --verify-no-changes` against
Roslyn's built-in conventions. Without a gate, this imposes formatting
opinions on a repo that never configured any — the same class of
violation `ruff-format`/`typos-format` already avoid at the hook layer.

- `check/SKILL.md` now honors `opt-in` for the lint phase — it never did
before, so dotnet's `check-cmd` ran unconditionally whenever
`.cs`/`.csproj`/etc. changed. Build and test are unaffected; only the
lint phase is gated.
- Both `check/SKILL.md` and `lint/SKILL.md` make an opt-in-unmet skip
**visible** (`skip (opt-in unmet: ...)`) instead of `lint/SKILL.md`'s
prior behavior of silently dropping the ecosystem from output entirely —
this applied to every opt-in-bearing ecosystem, not just dotnet.
- Multi-tool ecosystems (bash's shellcheck-always/shfmt-conditional
split; cross-cutting's per-tool config list) get explicit handling so an
unconditionally-applicable sub-tool is never suppressed just because a
sibling sub-tool's condition is unmet.
- `plugins/toolchain` version 0.5.2 → 0.6.0 (minor — new capability, not
a fix to previously-intended behavior).

## Review history

An independent fresh-context review caught two IMPORTANT findings before
this PR, both fixed and empirically re-verified:

1. The opt-in condition originally required a C#-specific
`.editorconfig` section (`[*.cs]`), but a universal `[*]` section
demonstrably governs `dotnet format`'s output on `.cs` files too
(empirically confirmed: `[*] indent_style=tab` alone triggers real
WHITESPACE violations). Widened the condition to `[*]` or a C#-glob
section.
2. The initial "opt-in-unmet skip suppresses the whole ecosystem row"
rule was correct for single-condition ecosystems (dotnet, python) but
wrong for multi-tool ones (bash, cross-cutting) where one sub-tool's
condition can hold unconditionally — fixed both skill files to gate only
the actually-unmet sub-tool(s).

`plugins/toolchain` has no automated test harness (documentation-driven:
skills read YAML config, the executing agent runs the resolved shell
commands — there's no script to unit-test), so verification here is
empirical testing of the underlying `dotnet format` behavior plus
careful prose review, not a CI-gated test suite.

## Test plan

- [x] `check-jsonschema` against `ecosystem.schema.json` — both
`dotnet.yaml` files (bundled default + example fixture) validate.
- [x] `dotnet format --verify-no-changes` behavior empirically verified
(dotnet SDK 10.0.302): no `.editorconfig` → built-in defaults apply;
`.editorconfig` with only unrelated globs (`[*.md]`) → zero effect;
`.editorconfig` with `[*]` → real effect (matches a `[*.cs]` section).
- [x] `scripts/check-changed-skills.sh origin/main` — both edited skills
PASS, 0 errors.
- [x] `scripts/generate-catalog.mjs --check`,
`scripts/check-changelog-parity.sh --check` — pass.
- [x] `typos` (full repo, matches CI), `markdownlint-cli2` on all
touched files — clean.
- [x] Independent fresh-context code review — 2 IMPORTANT findings, both
fixed and re-verified above.

## Related

- Closes #835 (epic #830, sub-item 5 of
`docs/topics/lint-static-analysis-gaps/PLAN.md`, contract landed via
#829).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
…#910)

## Summary

Two deliverables per issue #832's "Go coverage, both lanes":

1. **`plugins/toolchain/reference/ecosystems/go.yaml`** batch ecosystem
default (+ matching
`docs/conventions/ecosystem-commands/examples/go.yaml` fixture) — `go
build ./...`/`go test ./...` unconditional; `golangci-lint run [--fix]
./...` gated behind an `opt-in` key
(`.golangci.yml`/`.golangci.yaml`/`.golangci.toml`/`.golangci.json`
presence); `project-discovery: ["go.mod"]` for nested-module coverage; a
`go-mod-tidy-drift` gate via `go mod tidy -diff`.
2. **New hook plugin `plugins/go-format/`** — runs `goimports -w` on
every `.go` `Write`/`Edit`, **unconditionally** (no consumer-config
opt-in gate — the one deliberate shape difference from sibling
`ruff-format`/`typos-format`), skipping files carrying Go's `// Code
generated ... DO NOT EDIT.` marker.

## Design decisions

Issue #832 calls for an "implementation-time field survey" to pick the
per-file formatter (gofmt/goimports/gofumpt/golangci-lint fmt) against
three criteria: official/authoritative, maintained, feature-fit.

- **`goimports`** picked: its own docs state it "formats your code in
the same style as gofmt so it can be used as a replacement for your
editor's gofmt-on-save hook" — a direct official statement of intent for
this exact per-file-hook scenario. Actively maintained (golang/tools,
v0.48.0). Handles import add/remove, which bare `gofmt` never does —
necessary since an LLM edit changing symbol usage can leave a broken
import list.
- **`gofumpt`** rejected as the unconditional default: third-party
opinionated superset ("a stricter gofmt"), same risk class as why
`ruff-format`/`dotnet format` gate behind consumer config. No toggle
added in v1 (no consumer has asked; can be added later if requested).
- **`golangci-lint fmt`** rejected for the per-file hook: with no config
present it has zero formatters enabled by default (does nothing) —
corroborates the issue's own "golangci-lint is batch-only by design"
framing, for the correct reason (verified via the tool's own behavior,
not just restated from the brief).
- **`go-format` runs unconditionally** — `goimports` has no meaningful
config-divergence axis when left unconfigured (unlike
ruff/dotnet-format, whose underlying tools genuinely diverge by config),
so gating it would be inert ceremony.
- **`golangci-lint`'s lint step IS gated** — empirically verified
golangci-lint v2 with no config file still applies its own fixed
"standard" linter preset unconditionally, the same
imposed-unconfigured-opinion risk PR #890 (issue #835) fixed for `dotnet
format`. Mirrors the existing `python.yaml`/`dotnet.yaml` `opt-in`
pattern.

Full rationale, stress-test findings, and empirical verification notes:
`docs/topics/832-go-ecosystem/PLAN.md` (pasted below).

## Review history

Two independent fresh-context passes ran before this PR, findings folded
in:

**Plan stress-test** (before implementation) — one HIGH finding: the
"goimports has no config-divergence axis" premise missed that
`goimports` has zero awareness of Go's generated-file convention, and
would silently rewrite generated files. Fixed by adding a marker-skip
guard to the hook before implementation began. Also found a MEDIUM
multi-module gap (`./...` doesn't cross a nested `go.mod` boundary —
fixed by adding `project-discovery: ["go.mod"]`) and corrected two
citation errors (a misattributed `golangci-lint fmt` quote; an
inaccurate "every ecosystem has a matching example fixture" claim).

**Independent code review** (after implementation, before PR) — one
CRITICAL finding: the generated-file guard checked only the file's
*first non-blank line*, missing the common real-world shape where a
copyright/license header (`addlicense`/`goheader`-style tooling)
precedes the marker by a few `//` comment lines — reviewer empirically
reproduced the miss against a real generated-file shape and confirmed
the hook silently rewrote it. Fixed: the guard now scans the file's full
leading comment/blank-line run per Go's own stated convention ("before
the first non-comment, non-blank text in the file"), and two related
defeat vectors caught by the same review (trailing CRLF, leading UTF-8
BOM) are also fixed. Five new regression cases added. Two
SUGGESTION-level findings (an unnecessary `mktemp` for stderr capture;
an inaccurate PLAN.md claim about a nonexistent schema-check script)
were also fixed.

## Test plan

- [x] `plugins/go-format/hooks/go-format.test.sh` — 41/41 passing
(FAKEBIN-pattern contract test, real `goimports` v0.48.0 binary),
including 5 generated-file-guard regression cases (bare marker,
license-header preamble, CRLF, BOM, marker-after-leading-block negative
case).
- [x] `check-jsonschema` against `ecosystem.schema.json` — both
`go.yaml` files (bundled default + example fixture) validate; against
the plugin manifest schema for
`plugins/go-format/.claude-plugin/plugin.json`.
- [x] `scripts/check-changed-skills.sh` / direct `check-skill.sh`
invocation — `go-format:setup`, `toolchain:check`, `toolchain:lint` all
PASS.
- [x] `scripts/sync-hook-utils.sh --check`,
`scripts/check-cross-plugin-source-drift.sh`,
`scripts/check-silent-skips.sh`, `scripts/check-changelog-parity.sh
--check`, `node scripts/validate-plugin-contracts.mjs`, `bash
scripts/validate-plugins.sh` (strict catalog validation) — all pass.
- [x] `typos`, `markdownlint-cli2`, `shellcheck` on all new/changed
files — clean.
- [x] `node scripts/generate-catalog.mjs --check` — in sync.
- [x] Manually verified `go mod tidy -diff` and golangci-lint's
unconfigured-defaults behavior against real installed toolchains (Go
1.26.5, golangci-lint v2) — see PLAN.md.
- [x] Plan stress-test + independent code review — findings above, both
fixed and re-verified.

## Related

- Closes #832 (epic #830, sub-item 2 of
`docs/topics/lint-static-analysis-gaps/PLAN.md`, contract landed via PR
#829).

<details>
<summary>PLAN.md (implementation plan, decisions, and review
history)</summary>

## Brief

Issue #832 (epic #830, sub-item 2 of
`docs/topics/lint-static-analysis-gaps/PLAN.md` lines 26-30). Two
deliverables, one PR:

1. New `plugins/toolchain/reference/ecosystems/go.yaml` batch ecosystem
entry + matching
   `docs/conventions/ecosystem-commands/examples/go.yaml` fixture.
2. New hook plugin `plugins/go-format/` — per-file `goimports` autofix
on Write/Edit.

Scope boundaries: batch additions are rung-4 defaults only (consumer
`.claude/ecosystems/*.yaml`
override ladder unchanged). `govulncheck` is explicitly "optional" per
the brief — NOT shipped as a
default; documented as a consumer-addable local gate only. No `gofumpt`
toggle in v1 (YAGNI — no
consumer has asked).

Success criteria: both new surfaces pass the plugin contract gate +
fleet conformance audit
(acceptance criteria line 61 of the epic Brief); CI/local parity
restored for the Go toolchain gap
identified 2026-07-21 (line 66-67).

## Open Decisions (resolved this session, recorded here for the approval
gate)

1. **Formatter pick for the per-file hook: `goimports`.** Field survey
against the Brief's own
criteria (official/authoritative, maintained, feature-fit), researched
fresh this session:
- `gofmt` — non-configurable by design (go.dev/blog/gofmt,
go.dev/doc/effective_go), but doesn't
manage imports; an LLM edit that adds/removes symbol usage leaves a
broken file.
- `goimports` (golang.org/x/tools, v0.48.0, 2026-07-09; ~7,983 commits,
last updated
2026-07-20) — its own docs state it "formats your code in the same style
as gofmt so it can be
used as a replacement for your editor's gofmt-on-save hook." Direct
official statement of
     intent for exactly this scenario. **Picked.**
- `gofumpt` (mvdan/gofumpt v0.10.0, 2026-05-04) — self-branded "a
stricter gofmt," third-party
opinionated superset. Rejected as the unconditional default (same class
of risk as why
ruff-format/dotnet-format gate behind consumer config); not adding a
toggle in v1.
- `golangci-lint fmt` — rejected for the per-file hook. **Stress-test
correction:** the plan's
original citation was imprecise — `golangci-lint fmt --stdin` IS a
genuine single-file,
non-package-scoped invocation (empirically confirmed: no `go/packages`
loading, formats piped
stdin instantly). The "directories are NOT analyzed recursively... files
must come from the
same package" quote governs `golangci-lint run` (linters), not `fmt`
(formatters) — don't
misattribute it. The correct, empirically-confirmed rejection reason:
with no config file
present, `golangci-lint fmt` has **zero formatters enabled by default**
(unlike `run`'s fixed
"standard" 5-linter preset) — i.e. it silently does nothing, the
opposite of a usable default.
Conclusion (rejected for the hook) is unchanged; only the stated reason
is corrected.
Independently corroborates the Brief's own line 30 ("golangci-lint is
batch-only by design")
     for the `run`/lint half of the tool.
- **Consequence: `go-format` runs `goimports` unconditionally — no
consumer-config opt-in gate.**
This is the one deliberate shape difference from
ruff-format/typos-format/dotnet-format's
opt-in-gated pattern. Rationale: goimports has no meaningful
config-divergence axis when left
     unconfigured, so gating it would be inert ceremony. Qualifies for
`docs/PLUGIN-PHILOSOPHY.md` lines 143-147 lane-1 treatment
(non-conflicting good-practice
     default) where gofumpt/golangci-lint-fmt would not.
- **Stress-test correction (HIGH finding, folded in):** the "no
config-divergence axis" claim
was incomplete — empirically confirmed goimports rewrites files carrying
the canonical
`// Code generated ... DO NOT EDIT.` marker with zero awareness of that
convention, while
golangci-lint's own linters/formatters default to
`issues.exclude-generated: strict` in v2.
Generated Go files (protobuf, mockgen, sqlc, stringer, wire output) are
common; an
unconditional hook would silently rewrite them on any edit. **Fix, not a
re-open of the
unconditional-default decision:** `go-format.sh` adds a generated-file
marker guard — skip
(emit_skipped) when the marker appears anywhere in the file's leading
comment/blank-line run (scanning stops at the first line that is neither
blank nor a `//`
comment), matching Go's own stated convention ("before the first
non-comment, non-blank text
in the file"), not just the first non-blank line. **Independent-review
correction (CRITICAL,
folded in post-stress-test):** the original implementation checked ONLY
the first non-blank
line, on the premise that a preamble before the marker is "uncommon" —
that premise was
false; a license/copyright header (common `addlicense`/`goheader`
tooling output) routinely
precedes the marker by several `//` lines, and this shape is empirically
present in real Go
stdlib-adjacent generated files. Fixed by scanning the full leading
comment/blank block
instead of only line one; also fixed two related defeat vectors caught
by the same review
(trailing CRLF `\r` not stripped before the `$` anchor; a leading UTF-8
BOM defeating the `^`
anchor) and added five regression test cases (license-header preamble,
CRLF, BOM, and a
negative case confirming a marker appearing AFTER the leading block does
NOT suppress a real
edit). This is precision-scoping (same category as `--force-exclude`
giving Ruff per-file skip
     precision), not a consumer-config
walk-up, so it does not undermine Open Decision 1's "unconditional"
framing.

2. **`golangci-lint` lint step gated behind an `opt-in` key** requiring
`.golangci.yml`/`.golangci.yaml`/`.golangci.toml`/`.golangci.json`
present (ancestor walk to
repo root, mirroring `dotnet.yaml`'s `root = true` boundary if an
analogous concept exists for
golangci-lint — verified during Phase 1 to NOT exist, so ceiling at repo
root only). Verified
via WebFetch/WebSearch this session
(golangci-lint.run/docs/configuration,
github.com/golangci/golangci-lint discussions, 2026): golangci-lint v2
with **no config file
present still runs its own fixed "standard" linter preset**
(`linters.default: standard`)
rather than erroring or running nothing — the same
imposed-unconfigured-opinion risk class
PR #890 (issue #835) just fixed for `dotnet format`'s unconfigured
Roslyn defaults, and
consistent with why `python.yaml` already gates ruff behind
`[tool.ruff]`/`ruff.toml` presence.
Mirrors `python.yaml:10` and `dotnet.yaml:34`'s `opt-in` key shape
exactly.

3. **Single PR, closes #832.** Both deliverables are one Brief line ("Go
coverage, both lanes"),
share the same formatter research, and match the one-issue-one-PR
precedent from #831/#835.

## Plan

### Phase 1: `go.yaml` ecosystem batch entry [DONE]

Files:

- `plugins/toolchain/reference/ecosystems/go.yaml` (new) — flat
top-level keys, bundled-fallback
  disclaimer header (mirrors `dotnet.yaml:1-27` style):
  - `globs: ["*.go", "go.mod", "go.sum"]`
- `project-discovery: ["go.mod"]` — **added per stress-test MEDIUM
finding**: empirically
confirmed `go build ./...`/`go test ./...`/`go list ./...` run from a
repo root silently skip
a nested module's packages (a nested `go.mod` bounds `./...` expansion;
a root `go.work` file
does NOT cross module boundaries for this either). Without
`project-discovery`, a monorepo with
a nested Go module gets silent incomplete build/test/lint, directly
undercutting this issue's
own "CI/local parity" success criterion. Mirrors
`python.yaml`/`typescript.yaml`'s existing
    `project-discovery` handling for the same class of problem.
  - `build-cmd: "go build ./..."`
  - `test-cmd: "go test ./..."`
  - `check-cmd: "golangci-lint run ./..."`
  - `fix-cmd: "golangci-lint run --fix ./..."`
  - `opt-in`: text per Open Decision 2 above.
- `install-hint`: **resolved per stress-test finding** — do NOT bake in
a pinned
`curl|sh -s -- -b ... vX.Y.Z` one-liner (upstream's own install docs
explicitly state
`go install`/`go get` "aren't guaranteed to work" for golangci-lint, and
a version-pinned
    curl\|sh command drifts immediately). Use a durable pointer instead:
`"Install golangci-lint: https://golangci-lint.run/docs/welcome/install/
| Go toolchain:
    https://go.dev/dl/"` — same durable-pointer style as `dotnet.yaml`'s
    `"Install .NET SDK from https://dot.net"`.
- `gates`: one entry, `go-mod-tidy-drift`, `trigger-globs: ["go.mod",
"go.sum"]`, `cmd:
"go mod tidy -diff"` — **confirmed live** via `go help mod tidy` (Go
1.26.5 installed this
session): "-diff causes tidy not to modify go.mod or go.sum but instead
print the necessary
changes as a unified diff. It exits with a non-zero code if the diff is
not empty." Introduced
Go 1.23 (2024) — note this as an implicit minimum-Go-version
prerequisite in `context/go.md`.
`remediation: "Run go mod tidy and commit the updated go.mod/go.sum."`
- `notes`: mention `govulncheck` as an available consumer-addable local
gate (not shipped by
default per Open Decision brief-wording), pointing at
`.claude/ecosystems/go.local.yaml`.
- `docs/conventions/ecosystem-commands/examples/go.yaml` (new) — richer
worked-example fixture
mirroring `examples/dotnet.yaml`'s structure (same keys as above plus
the `gates` block spelled
out). **Correction (stress-test finding):** only 3 of the 8 existing
bundled ecosystem yamls
(`bash`, `python`, `dotnet`) actually have a matching example fixture —
`markdown`, `powershell`,
`typescript`, `cross-cutting`, `yaml` do not, and no CI gate enforces
1:1 coverage. Add
`examples/go.yaml` anyway (it's good practice and dotnet/python both
have one), but don't claim
in the PR body that this closes a universal-coverage gap — it doesn't
exist as a gap.
- `plugins/toolchain/skills/check/context/go.md` (new) — Go-specific
gotchas: `go test ./...`
module-root requirement (must run from the module root or a path
containing `go.mod`;
`project-discovery` above handles nested-module walking), GOFLAGS
interactions,
golangci-lint's default-"standard"-preset caveat tied to the opt-in
gate, AND (stress-test
MEDIUM finding) golangci-lint's own config discovery falls back to the
user's **home directory**
with no `root = true`-equivalent stop marker when no repo-level config
is found — a stray
`~/.golangci.yml` on a developer's machine makes local runs diverge from
a clean CI container;
document this as a known local/CI divergence source. Mirrors the
existing
  `context/dotnet.md`/`context/python.md` shape.
- `plugins/toolchain/skills/check/SKILL.md` — add `go` to the
covered-ecosystems list (currently:
dotnet, python, typescript, bash, powershell, markdown) and its alias
table if one applies (no
  common alias needed — "go" is already short).
- `plugins/toolchain/skills/lint/SKILL.md` — same covered-ecosystems
list addition if `lint` also
enumerates them explicitly (verify during implementation; python's
opt-in-gated lint precedent
from #835 touched both `check/SKILL.md` and `lint/SKILL.md`, so `go`
likely needs the same
  two-file touch).
- `plugins/toolchain/.claude-plugin/plugin.json` — version bump (minor:
new ecosystem capability).
**Verify current version live** (`git show
origin/main:plugins/toolchain/.claude-plugin/plugin.json`
at rebase time — do not assume 0.6.0 is still current, #833/#834 may
have already bumped it).
- `plugins/toolchain/CHANGELOG.md` — `[Unreleased]`/new version entry
under Added.

**Sanity Check:** `check-jsonschema --schemafile
docs/conventions/ecosystem-commands/ecosystem.schema.json
<file>` passes for both `plugins/toolchain/reference/ecosystems/go.yaml`
and
`docs/conventions/ecosystem-commands/examples/go.yaml`.
**Independent-review correction:** no CI
job or repo script actually validates
`reference/ecosystems/*.yaml`/`examples/*.yaml` against this
schema today (confirmed by grepping `.github/workflows/ci.yml` — its
four `check-jsonschema` steps
cover only marketplace/plugin manifests, dependabot, and workflow
files); the check above is a
manual `check-jsonschema` CLI run, not an existing repo script being
reused. Both files validated
clean this way. This is a pre-existing gap in the repo's own CI
coverage, out of scope for this
issue — noted here rather than silently left as an inaccurate claim.

### Phase 2: `plugins/go-format/` hook plugin [DONE]

Files (full new plugin directory, mirroring `plugins/typos-format/`
structure):

- `.claude-plugin/plugin.json` — `userConfig.go_format_enabled`
(boolean, default `true`), version
  `0.1.0`, keywords `["go","golang","goimports","formatter","hook"]`.
- `hooks/hooks.json` — `PostToolUse`, matcher `Write|Edit`, command
  `"${CLAUDE_PLUGIN_ROOT}"/hooks/go-format.sh`, timeout 15.
- `hooks/hook-utils.sh` — initial copy via `scripts/sync-hook-utils.sh`
(never hand-copy).
- `hooks/go-format.sh` — control flow mirrors `ruff-format.sh`'s shape:
- Extension pre-filter on `*.go` (jq-free, before requiring jq) — like
ruff-format, unlike
    typos-format's no-filter shape.
- `hook::check_enabled "GO_FORMAT"`, `hook::buffer_stdin`,
`hook::require_jq`,
    `hook::read_file_path`, `hook::repo_root`.
- **No ancestor consumer-config walk-up** (goimports is unconditional
per Open Decision 1) —
document this simplification explicitly in a short header comment
referencing this PLAN's
rationale, not just silently omitting the walk-up
ruff-format/typos-format both have.
- Binary resolution: PATH-only (`command -v goimports`) — no
`.venv`-style walk (Go has no
per-project virtualenv concept; mirrors typos-format's PATH-only
resolution).
- Invocation: **resolved via empirical stress-test verification (real
goimports v0.48.0
binary):** `goimports -w -l "$FILE"` in one pass — `-w` writes the fix,
`-l` (list-only)
combined with `-w` still lists the filename if changes were needed,
giving a single-pass
fix+detect (simpler than ruff-format's two-pass shape). Exit-code
semantics confirmed: **`-l`
ALWAYS exits 0**, even when it lists a file needing changes — there is
no exit-1-style
"findings" signal like ruff/typos have. Detect "changes were made" from
**non-empty stdout**
(the listed filename), not from exit code. Non-zero exit (confirmed:
exit 2, parseable message
on **stderr**) occurs only on a genuine parse/syntax error — surface
that as a finding-text
message (mirrors how ruff-format surfaces a mid-edit syntax error as a
finding, not a
tool-break), matching the doctrine even though the underlying signal
shape differs from ruff.
Skip entirely (before invoking goimports) when the file matches the
generated-file marker guard
    from Open Decision 1's stress-test correction above.
- Telemetry: `hook::emit_telemetry "go-format" "PostToolUse" <status>
"$start" "$data_json"
"$REPO_ROOT"`; `status` semantics per the shared doctrine (`ok` = ran to
judgment, `skipped` =
    tool broke/missing prerequisite).
  - Always exits 0 (advisory only).
- `hooks/go-format.test.sh` — FAKEBIN-pattern contract test mirroring
`ruff-format.test.sh`/`typos-format.test.sh`: gate-off, non-`.go`
extension skip, clean file,
import-added-by-edit auto-added, import-removed-by-edit auto-removed,
**generated-file marker
skip** (new case per the Open Decision 1 stress-test correction),
missing-binary dim-9 visibility
(once-per-session), missing-jq dim-9 visibility, kill-switch, telemetry
envelope shape assertions

(`schema_version`/`timestamp`/`hook`/`hook_event`/`status`/`duration_ms`/`data`),
a
syntax-error-mid-edit case surfaced as a finding (per the confirmed `-l`
exit-2/stderr behavior
  above, not a tool-break).
- `skills/setup/SKILL.md` — `check`/`apply`, `disable-model-invocation:
true`. `check` probes Bash,
jq, `goimports` on PATH, the `go_format_enabled` toggle, hook
registration. `apply`: **resolved
per stress-test finding — guidance-only, no write path**, matching
`typos-format`'s pattern
(`plugins/typos-format/skills/setup/SKILL.md:57-64`), not
`ruff-format`'s `.venv`-install path.
`go install golang.org/x/tools/cmd/goimports@latest` writes to the
machine-global
`$GOBIN`/`$GOPATH/bin` (not project-scoped) and `@latest` is not
idempotent-pinned (silently
drifts over time) — structurally the same "no per-repo
dependency-manager, machine-level binary"
case as typos-format, not ruff-format's project-`.venv` case. Document
the goimports install
pointer (`go install golang.org/x/tools/cmd/goimports@latest` — a plain,
uncaveated `go install`,
unlike golangci-lint's own install docs which explicitly warn `go
install`/`go get` "aren't
guaranteed to work" for that tool specifically — don't let that caveat
bleed across into this
  SKILL.md's goimports guidance).
- `README.md` — mirror typos-format's structure; explicitly document the
"no config-gate,
unconditional default" design choice (the one plugin in the family
without an opt-in section) —
  say so plainly, don't silently omit the section.
- `CHANGELOG.md` — `[0.1.0]` initial release entry, telemetry-conformant
from day one (matches
  typos-format's precedent, not ruff-format's later-follow-up pattern).
- `docs/conventions/hook-telemetry/data/go-format.schema.json` (new) —
findings shape decided from
Phase 2's live verification of goimports' actual diagnostic output
(flat-string per ruff-format's
shape unless goimports emits something structured — verify, don't
assume).
- `docs/conventions/hook-telemetry/README.md` — add Implementers table
row for `go-format`.
- `.claude-plugin/marketplace.json` — new entry: `category:
"development"`,
  `tags: ["go","golang","goimports","formatter","hook"]`,
`relevance: {topic: "Go", signals: {filesRead: ["**/*.go"], cli:
["goimports"]}}` (mirrors
  ruff-format/typos-format's relevance-block shape exactly).
- `README.md` (repo root) — regenerate catalog block via `node
scripts/generate-catalog.mjs`.

**Sanity Check:** `plugins/go-format/hooks/go-format.test.sh` passes
100% when run directly
(`bash plugins/go-format/hooks/go-format.test.sh`), and
`scripts/sync-hook-utils.sh --check`
reports no drift for the new copy.

### Phase 3: Cross-cutting verification + PR [DOING]

- Rebase onto latest `origin/main` (expect a benign conflict on
`plugins/toolchain/.claude-plugin/plugin.json` +
`plugins/toolchain/CHANGELOG.md` against
#833/#834's already-merged or still-in-flight changes — resolve by
reapplying this lane's version
  bump on top of theirs, not by discarding either).
- Run the full local CI-equivalent gate set: hygiene
(schema/markdownlint/typos/shellcheck/exec-bit),
hook-utils-sync, cross-plugin-source-drift, silent-skip-gate,
changelog-parity-gate,
skill-quality-gate + portability-lint, plugin-gate
(`scripts/validate-plugin-contracts.mjs` +
  both new/changed test suites), `scripts/generate-catalog.mjs --check`.
- Mandatory fresh-context independent code review (per this session's
established discipline).
- `gh pr create` — closes #832, body includes the two locked Open
Decisions above (formatter pick +
opt-in-gate rationale) so reviewers see the reasoning, not just the
diff.
- Monitor CI, process every review thread to resolution (GraphQL
`resolveReviewThread` for
  bot-authored addressed threads — this repo's ruleset requires
`required_review_thread_resolution`), merge, remove worktree, delete
branch, confirm issue
  auto-closed.
- `/planning:plan close-out`: paste this PLAN.md into the PR body
`<details>` block, prune
  `docs/topics/832-go-ecosystem/` before merge.

**Sanity Check:** `gh pr view <N> --json state -q .state` returns
`MERGED`; `gh issue view 832
--json state -q .state` returns `CLOSED`.

## Review history

An independent fresh-context code review ran before PR creation and
found one CRITICAL and two
lower-severity items, all addressed and re-verified before opening the
PR:

1. **CRITICAL — generated-file guard only checked the first non-blank
line, missing the common
license-header-then-marker layout** (and two related defeat vectors:
CRLF, UTF-8 BOM).
Reviewer empirically reproduced the miss against a real generated-file
shape (a copyright
header plus a `stringer`-style marker) and confirmed the hook silently
rewrote it. Fixed: the guard now
scans the file's full leading comment/blank-line run (stopping at the
first non-comment,
non-blank line) per Go's own stated convention, strips a trailing CRLF
and leading BOM per
line before matching. Five new regression cases added (license-header
preamble, CRLF, BOM,
and a negative case proving a marker appearing after the leading block
does NOT suppress a
   real edit) — see Open Decision 1 above for the full before/after.
2. **SUGGESTION — stderr capture used an unnecessary `mktemp` file**,
inconsistent with every
other hook in the repo's simpler command-substitution idiom. Simplified
to match.
3. **SUGGESTION — this PLAN's own Phase 1 Sanity Check claimed an
existing repo script validates
ecosystem yaml against `ecosystem.schema.json`; no such CI job or script
exists.** Corrected the
claim (see Phase 1's Sanity Check above) — the schema validation itself
was and remains correct
(verified manually via `check-jsonschema`), only the "reuse an existing
script" framing was
wrong. This is a pre-existing repo-wide CI-coverage gap, out of scope
here.

**On-PR review round** (`claude-review`, `security-review`, and Codex,
both pushes) surfaced eight
more findings. Six confirmed and fixed, two evaluated and consciously
left as-is:

1. **CRITICAL-equivalent (Codex P2, independently confirmed via `go help
generate` live) —
the generated-file guard still missed a `/* ... */` block-comment
preamble.** The authoritative
convention text ("This line must appear before the first non-comment,
non-blank text in the
file") does not restrict "comment" to `//` style; the guard only treated
`//` lines as
comment-continuation, so a block-comment license header (e.g. `/*
Copyright ... */` before
`// Code generated`) caused premature loop termination — the same
failure class as the
pre-PR CRITICAL finding, just a different comment syntax. **Not accepted
as "zero practical
risk"** (one review pass argued this) — fixed properly: the guard now
tracks open `/* */`
blocks and continues scanning through them. New regression case (5f)
added.
2. **Real correctness gap (Codex P2) — `goimports` ran with no `-local`
grouping prefix,** so a
repo already formatting with `-local` (a common Go convention wired into
CI/editor config)
would have every edit re-collapse its local-import grouping back into
the third-party group —
empirically confirmed this materially changes output (verified with a
real third-party import
present). This directly undercut Open Decision 1's "no config-divergence
axis" premise a
second time. Fixed: the hook now derives `-local` from the edited file's
own module path
(`go list -m`, walks to the nearest `go.mod` via Go's own resolution)
when a `go` toolchain is
present — zero new consumer-config surface, covers the single most
common `-local` use case
(self-grouping), gracefully degrades to goimports' plain default when
`go` is absent or the
   file isn't in a resolvable module. New regression case (4b) added.
3. **Security SUGGESTION (both security-review passes) — missing `--`
end-of-flags separator
before `$FILE`.** Low-confidence defense-in-depth; fixed alongside the
`-local` change.
4. **Codex P2 — `go-mod-tidy-drift` gate only triggered on
`go.mod`/`go.sum`, missing source-only
tidy drift** (e.g. removing the last usage of a dependency leaves
`go.mod` over-declared while
`go build`/`go test` still pass). One review pass called this an
acceptable tradeoff citing the
`python.yaml` precedent; **not accepted** — the epic's own stated
success criterion is
"CI/local parity," and leaving this gap directly contradicts it. Fixed:
`trigger-globs` widened
to include `*.go`. Remediation text also now notes the Go 1.23+ floor
for `go mod tidy -diff`
   (a separate LOW finding from the first review pass).
5. **Codex P2 — `/toolchain:lint`'s "Per-project walking" list
enumerated python/typescript only,
omitting `go`** despite `go.yaml` declaring `project-discovery:
["go.mod"]` — a monorepo with a
nested Go module would have `/toolchain:lint go` run `golangci-lint run
./...` from the wrong
   root. Fixed: added a `go` bullet.
6. **LOW — README described the generated-file guard as checking only
the "first non-blank line,"**
stale after the pre-PR CRITICAL fix. Corrected to describe the actual
leading-block scan.
7. **COSMETIC, evaluated and left as-is — BOM-stripping runs on every
loop line, not just the
first.** A UTF-8 BOM can only appear at byte 0, so this is a harmless
no-op after line one, not
a bug. Left unchanged (the fix would add branching complexity for zero
behavioral gain).
8. **COSMETIC — key ordering differed between the bundled `go.yaml`
(`gates` before
`install-hint`) and the example fixture (`install-hint` before
`gates`).** Aligned the bundled
file to the example's ordering (which matches the `dotnet.yaml` example
precedent).

## Blast radius

**MEDIUM.** New plugin + new ecosystem entry, but both are close
pattern-replications of two
already-merged, already-reviewed precedents (typos-format PR #872,
dotnet opt-in gate PR #890) in
this same epic. The two genuine judgment calls (goimports-unconditional,
golangci-lint-opt-in) are
each backed by fresh primary-source research and a direct analogy to an
already-accepted precedent
in this repo — not novel territory. No cross-module architectural
change, no data-model change, no
multi-tenant concern. Several implementation-time facts (exact goimports
flags, exact golangci-lint
install command, `go mod tidy` dry-run flag syntax) are flagged as
**verify-live, don't assume** —
this is where a stress-test/implementation-time slip is most likely, not
in the two locked design
decisions.

## Stress-test summary

Blast radius MEDIUM — no CRITICAL/HIGH triggers (no cross-module
integration, no data-model change,
no multi-tenant posture) per `context/stress-test-triggers.md`'s
criteria, but the two load-bearing
judgment calls (Open Decisions 1 and 2) warrant the mandatory Step 3
fresh-context plan-reviewer
pass before implementation, specifically pressure-testing: (a) is
"goimports unconditional, no
opt-in" actually safe, or does goimports have a config-divergence axis
this session's research
missed; (b) is the golangci-lint opt-in gate correctly scoped (does it
match how `check-cmd`
composes with `fix-cmd` the same way python/dotnet's gates do, and is
the ancestor-walk boundary
choice — repo-root-only, no `root = true`-equivalent — actually correct
for golangci-lint's own
config discovery, which may differ from EditorConfig's semantics).

*(Dispatched separately as the mandatory Step 3 fresh-context
plan-reviewer sub-agent — findings
folded in before implementation begins.)*

## Execution shape

Sequential — Phase 1 (ecosystem yaml) and Phase 2 (hook plugin) touch
disjoint files and share no
data dependency (Phase 2 doesn't consume Phase 1's output), so they are
parallel-safe by the
file-overlap test, but the combined scope is small enough (~14 files
total) that the coordination
overhead of a two-agent split isn't worth it for a MEDIUM-blast-radius,
largely-mechanical
replication lane. Single main-session sequential execution, Phase 1 then
Phase 2, then Phase 3
cross-cutting verification gates both. Phase 3 is fully
sequential-dependent on both.

## Open questions

None blocking — all flagged "verify live during implementation" items
are execution-time fact
lookups (exact CLI flags/install commands), not design decisions
requiring further user input.

## Handoff to implementation

### User-approval gates

None beyond initial plan approval — no `[FALLBACK]` tags, no
scope-expansion proposals anticipated.
If live verification during Phase 1/2 surfaces that `goimports` or
`golangci-lint` behave
materially differently than researched (e.g., goimports turns out to
have a real config-divergence
axis), STOP and re-open Open Decision 1 rather than silently proceeding.

### Execution shape (`[EXEC-SHAPE]` tagged)

- `[EXEC-SHAPE]` Single PR bundling both deliverables (Open Decision 3).
- `[EXEC-SHAPE]` Sequential single-main-session execution (Execution
shape section above).
- `[EXEC-SHAPE]` `go-format` hook plugin structural simplification (no
ancestor config walk-up) —
  Open Decision 1's consequence.

### Mechanical work

Commit boundaries: one commit per phase is reasonable (Phase 1, Phase 2,
then fixup commits for
review findings) but not mandated — squash-merge means the final PR
history is one commit anyway.
Verification checkpoints: run the full local gate set after each phase,
not just once at the end,
to catch cross-phase interactions (e.g., `plugin.json`/`CHANGELOG.md`
version-bump conflicts
between the toolchain-plugin edit in Phase 1 and any repo-root catalog
regen in Phase 2) early.
Sequential fallback: N/A (already sequential).

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
…opic is fully landed (#1004)

## Summary

`docs/topics/lint-static-analysis-gaps/PLAN.md` is a contract-tier topic
doc per `docs/conventions/topic-docs/README.md`: committed on the task
branch only, pruned before merge once the topic's outcome lands
(§"Contract-slice lifecycle"). It was added by PR #829 and never pruned.
Epic #830 and all 6 sub-issues (#831-#836) plus the related
setup-lifecycle epic (#837) are now merged and closed, so the topic's
outcome has fully landed — pruning now, mirroring the same fix already
applied to #836's leaked plan doc in PR #966.

Content preserved for the record (from commit `4a289ab389`, PR #829):

<details>
<summary>Epic Brief (pruned)</summary>

# Lint / static-analysis gaps

## Brief

### TLDR

Close the fleet's lint/static-analysis gaps with one epic in this repo
(typos hook plugin, Go
coverage in both lanes, lychee-offline + pyright batch additions, .NET
runtime guard,
hook-observability convention, bespoke-guard routing), plus two epics
owned elsewhere
(setup-lifecycle convention here as its own design effort; standards
adoption sweep +
ecosystem-declaration in `melodic-software/standards`).

### Goal

Local lanes catch what CI gates, so agents fix findings at edit time
instead of burning
commit–push–CI round-trips. Every addition follows the plugin
philosophy: consumer-config-driven,
zero shipped opinions, runtime detection over assumptions, advisory
hooks that auto-fix silently
and surface only residual unfixables.

Epic sub-items (discuss-first honored — file only on explicit request;
epic + sub-issues shape):

1. **typos hook plugin** — per-file autofix (`typos -w`), consumer
`_typos.toml` ancestor walk-up,
false-positive remediation via consumer allowlist entries
(`extend-words` /
`extend-identifiers` / `extend-ignore-re`), advisory residual-only
context, hook-precision +
   hook-telemetry conventions.
2. **Go coverage, both lanes** — new `go` batch ecosystem default
(golangci-lint v2, format,
`go mod tidy`; govulncheck optional) and a Go format hook plugin.
Formatter selection
(gofmt / goimports / gofumpt / `golangci-lint fmt`) is an
implementation-time field survey per
the pick-for-the-problem discipline; criteria: official/authoritative,
maintained,
feature-fit. golangci-lint is batch-only by design (package-scope
analysis, per its FAQ).
3. **lychee-offline** added to `cross-cutting.yaml` (on-disk link/anchor
integrity; gating in CI,
   no network).
4. **pyright** added to `python.yaml` check-cmd (CI gates it; local
batch was ruff-only).
5. **.NET batch guard** — `dotnet` ecosystem entry detects
analyzer/`.editorconfig` configuration
presence at runtime each run; skips with a visible notice when absent.
No assumptions about
   consumer state; .NET stays batch-lane only.
6. **Hook-observability fleet convention** — every fleet hook emits
`statusMessage` (during run),
`systemMessage` (failure/notable action), and the hook-telemetry OTel
envelope. Grounded in
current official hooks docs at authoring time (no native user-visible
hook UI exists as of
2026-07-21; OTel events + author-emitted messages are the sanctioned
surfaces). Optional
sub-item: upstream feature request for a native verbose-hooks UI toggle.
7. **Bespoke CI guards** (comment-hygiene, exec-bit,
machine-specific-paths,
reference-integrity) — sub-issue routed to `ci-workflows`/`standards`:
local lane must invoke
the same owned source (pointer-not-copy), which needs a small
distribution decision those
   repos own.

### Constraints

- Plugin philosophy governs: repo/user/machine/org-agnostic, two-lane
convention posture,
native-first, cross-platform (Windows/macOS/Linux), setup contract,
graceful degradation.
- Lane rule (locked): fast (<~2s), per-file, auto-fixing,
consumer-config-discovering tool =
hook plugin; slow / repo-wide / package-scope tool = toolchain batch
entry; both when both fit.
- Hooks auto-fix silently, never block, surface only residual unfixables
(markdown-format
  pattern); hook-precision convention bounds false-positive noise.
- New plugins conform to the setup-lifecycle convention once that epic
lands.

### Acceptance criteria

- Each epic sub-item lands as its own planned change with the normal
pipeline
  (explore/research → plan → implement → review).
- typos + Go hook plugins pass the plugin contract gate and fleet
conformance audit.
- Batch additions (go, lychee-offline, pyright, dotnet guard) are rung-4
defaults only —
  consumer `.claude/ecosystems/*.yaml` override ladder unchanged.
- Hook-observability convention documented as an owner doc (convention
registry row) and adopted
  by every fleet hook; conformance audited.
- CI/local parity: gaps identified 2026-07-21 (Go toolchain,
lychee-offline, pyright) have local
  coverage.

### Captured assumptions

- No work-machine tool-install restriction (winget/brew acceptable).
User to correct if wrong.
- Single Go repo today (`ci-runner`); Go hook plugin justified by
completeness preference
  (user choice) despite one consumer.

### Out of scope

- Standards distribution-model redesign — model is settled (ADR-0001,
accepted 2026-07-10);
dissatisfaction, if it persists after reading the ADR rationale, is an
ADR-supersede
  discussion in `standards`.
- Consumer-config adoption gaps (ruff/pyright targets, dotnet-analysis
to itinerary-planner /
medley, TS/JS component admission, medley lychee) — standards epic
below.
- Setup-lifecycle convention design — own epic below.

### Deferred questions

- YAML lint/format plugin — arbiter: USER-RESERVED. Trigger: a CI YAML
gate lands in the fleet,
or Biome ships YAML support (unshipped as of the 2026 roadmap). Facts:
yamllint validate-only;
  yamlfmt autofixes but imposes defaults without consumer config.
- gitleaks per-edit hook — arbiter: USER-RESERVED. Trigger: a local
leaked-secret incident.
Facts: `dir` mode (detect/protect deprecated v8.19), entropy
false-positive noise, no autofix.
- `dotnet format whitespace --folder` fast path — arbiter:
USER-RESERVED. Whitespace-only today
(bypasses MSBuild/restore; the only sub-2s path — full/style/analyzer
modes pay project-load,
~1.3s minimum single file). Links:
<https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format>,
<dotnet/format#757>. Trigger: an MSBuild-free
style/analyzer path
  appears upstream.
- Upstream feature request for native verbose-hooks UI — arbiter:
/planning:plan (optional
  sub-item of the observability convention).

### Related epics (owned elsewhere)

- **Setup-lifecycle convention** (this repo, own design session) — DONE
(#837, PR #990).
- **Standards adoption sweep + ecosystem-declaration enhancement**
(`melodic-software/standards`) — still open, separate repo
(`standards#230`).

## Plan

(Empty — `/planning:plan` fills this per epic sub-item.)

</details>

## Verification

- [x] `git rm -r` only — no other changes; no build/test impact.
- [x] Content fully preserved in git history (commit `4a289ab389`) and
in this PR body.

## Related

No linked issue — epic #830 is already closed (manually, since all 6
sub-issues plus the related setup-lifecycle epic #837 were done); this
is a follow-up hygiene fix, not new epic work. Related PRs: #829 (added
the brief), #966 (same fix applied to #836's leaked plan doc), #990
(#837, the last related item to land before this cleanup).

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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