Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/generate-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: generate-check

# The plugin directories are generated from core/ and lang/. Every PR must keep
# them identical to the rendering, keep the generated gate passing its fixture
# matrix, and keep the generator itself green.
on:
push:
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: tools/ldd-gen/go.mod
- uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Plugin directories match their rendering
run: task check
- name: No hard residue in core and the Residue section is current
run: task lint-core
- name: Generated gate passes its fixture matrix
run: task test-gate
- name: Documentation gate
run: task docs:check
- name: Generator tests
working-directory: tools/ldd-gen
run: go test ./...
- uses: golangci/golangci-lint-action@v8
with:
working-directory: tools/ldd-gen
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Generated plugin
`go-linter-driven-development/` is generated from `core/` and `lang/go/` by
`tools/ldd-gen`. Never edit it by hand: edit the sources, run `task generate`, and
commit both. `task check` fails on any drift. The templating contract and the list of
files that stay Go-only are in core/README.md.

## Documentation
@AGENTS.md
@docs/index.md
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ Team members then install with the same `/plugin install` commands above.

## Developing the Plugins

1. Clone the repo and edit the plugin files (`rules/`, `skills/`, `agents/`, `commands/`).
1. Clone the repo. The Go plugin directory is generated: edit the sources under `core/` (language-neutral text) and `lang/go/` (the Go binding), then run `task generate` and commit both. `task check` fails when the plugin directory drifts from its sources. How the pieces fit: [core/README.md](core/README.md) and [docs/generator.md](docs/generator.md).
2. Test locally by adding the checkout as a marketplace:
```
/plugin marketplace add ./ai-coding-rules
/plugin install go-linter-driven-development@ai-coding-rules
```
After changes, uninstall/reinstall the plugin to pick them up.
3. For the Go plugin, follow its architecture contract — each fact lives once: rule content goes in `rules/`, worked case studies in `examples/`, skills only sequence and route. See the [plugin README](go-linter-driven-development/README.md#architecture-rules-as-data).
3. For the Go plugin, follow its architecture contract — each fact lives once: rule content goes in `core/rules/`, worked case studies in `lang/go/passthrough/examples/`, skills only sequence and route. See the [plugin README](go-linter-driven-development/README.md#architecture-rules-as-data).
4. Behavior changes to the Go plugin are measured, not eyeballed: behavioral evals run it on a deliberately bad fixture project and compare against a recorded baseline. Start at [docs/index.md](docs/index.md) — the harness, the fixture, how to write a case, the runner, and how to read a baseline. The cases, fixture, runner and baselines live in [buzzdan/ldd-evals](https://github.com/buzzdan/ldd-evals); `scripts/evals.sh` runs them against this checkout.
5. Open a PR; releases are tagged per plugin (e.g. [`go-ldd-v2.0.0`](https://github.com/buzzdan/ai-coding-rules/releases/tag/go-ldd-v2.0.0)).

Expand Down
36 changes: 36 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
version: '3'

# The Go plugin directory is generated: edit core/ and lang/go/, then run
# `task generate` and commit both. CI runs `task check`, `task lint-core`, `task docs:check`
# and `task test-gate`, plus the generator's tests and linter.

tasks:
generate:
desc: Render core/ + lang/<BINDING>/ into its plugin directory (BINDING=go)
vars:
BINDING: '{{.BINDING | default "go"}}'
cmds:
- go run ./tools/ldd-gen -lang {{.BINDING}}

check:
desc: Fail when any plugin directory on disk differs from its rendering
cmds:
- go run ./tools/ldd-gen -check

lint-core:
desc: Report language residue in core/, refresh the Residue section of core/README.md, fail on hard hits or a stale section
cmds:
- go run ./tools/ldd-gen lint-core -write
- |
git diff --exit-code -- core/README.md \
|| { echo "lint-core - the Residue section of core/README.md was rewritten; commit it"; exit 1; }

test-gate:
desc: Run the repo-brain gate's fixture matrix against the generated gate script
cmds:
- bash go-linter-driven-development/scripts/check-repo-brain_test.sh

test-gen:
desc: Unit tests and lint for the generator
dir: tools/ldd-gen
cmds:
- go test ./...
- golangci-lint run

docs:check:
desc: Run the repo-brain documentation gate over this repository (pass --fix after --)
cmds:
Expand Down
128 changes: 128 additions & 0 deletions core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Core

`core/` holds the language-neutral text of the linter-driven-development plugins:
rules, skills, agents, commands and the repo-brain gate, written once and rendered
per language. `lang/<lang>/` holds one binding per language: a `profile.yaml` with
the scalars the templates substitute, the snippet files they include, whole-file
overrides, and `passthrough/` for files copied into the plugin unchanged.

`tools/ldd-gen` renders `core/` plus a binding into the plugin directory the
marketplace serves. Edit sources here or under `lang/`, run `task generate`, and
commit both; `task check` fails when a plugin directory differs from its rendering.

## Templating

Go `text/template` with the default delimiters. Core files use only two constructs:

- `{{.Plugin}}`, `{{.Lang}}`, `{{.CmdPrefix}}`, `{{.SrcGlob}}`, `{{.TestGlob}}`,
`{{.ProjectMarker}}`, `{{.Nolint}}`, `{{.CommentPrefix}}`, `{{.DefaultTest}}`,
`{{.DefaultLint}}`, `{{.DefaultLintFix}}` — scalars from `profile.yaml`.
- `{{include "rules/R1/canonical-example.md"}}` — the body of that file under
`lang/<lang>/`, with exactly one trailing newline removed.

File-level rules the generator applies without template syntax:

- `lang/<lang>/overrides/<core path>` replaces the core file of the same path (the
path as written in `core/`, before file-name templating). The override is rendered as
a template like the file it replaces and carries its own executable bit. An override
with no matching core file is an error.
- File names are templates too: `core/commands/{{.CmdPrefix}}-analyze.md` renders
to `commands/go-ldd-analyze.md` for the Go binding.
- This README is documentation for `core/` itself and is never rendered. Editor and OS
droppings (`.DS_Store`, `._*`, `*.swp`, `*~`, `.#*`, `Thumbs.db`, `desktop.ini`) and
any file or directory whose name the profile's `ignore` lists without a slash are never
read as sources either.
- `task generate` refuses to write when two bindings name the same plugin, when the
rendering has no named `.claude-plugin/plugin.json`, or when the target directory
already holds files and its manifest names a different plugin. A wrong `plugin` name
in a profile therefore cannot delete another plugin or any other directory.

Include files carry no leading or trailing blank lines; the surrounding template
owns them. That is what keeps a rendered file byte-identical to a hand-written one.

## What stays in the binding

Some Go-owned files have no portable text worth extracting, or their seams are not
clear yet. They sit under `lang/go/passthrough/` and are copied into the plugin
unchanged. Promotion into `core/` happens when a second language needs the text:

- `skills/documentation/reference.md`: the Comment Value Toolbox is portable
guidance, the godoc menus and testable-example template are Go, and the templates
after them are portable but every worked example is Go code.
- `scripts/check-repo-brain_test.sh`: the fixture matrix is the language adapter's
contract, but besides the conformant fixture several cases embed Go source,
`go.mod` files and Go-specific path patterns.
- `skills/testing/reference.md` and `skills/testing/examples/`: a Go test-harness
catalogue.
- `examples/`: worked case studies written as Go code.
- `README.md`, `CHANGELOG.md`, `.claude-plugin/plugin.json`, `hooks/`: describe or
configure the Go plugin itself.

## Residue

`task lint-core` scans `core/` for language-specific text and rewrites this section.
Hard residue is a plugin-name or command-prefix literal, a `golangci` reference, a
source-file glob or a nolint directive: each has a profile scalar or an include, so a
hit is a missed substitution and fails the check. Soft residue is Go vocabulary that
reads fine in a Go plugin (`nil`, `ctx`, goroutines, `interface`, linter and library
names such as `exhaustive` or `testify`); it is the list of words a second language
binding must decide how to render, by scalar, include or whole-file override.

<!-- residue:begin -->
Hard residue: none. No plugin-name or command-prefix literal, golangci
reference, source-file glob or nolint directive is left in core/.

Soft residue by token (212 lines):

| Token | Lines | Files |
|---|---:|---:|
| interface | 45 | 12 |
| .go suffix | 30 | 8 |
| godoc | 22 | 6 |
| goroutine | 18 | 5 |
| nil | 18 | 6 |
| ctx | 15 | 4 |
| struct | 13 | 9 |
| Go linter name | 12 | 5 |
| context. | 12 | 5 |
| httptest | 10 | 3 |
| Go (the word) | 7 | 5 |
| sync. | 7 | 4 |
| Go library | 6 | 3 |
| func | 5 | 3 |
| init() | 4 | 2 |
| wantErr | 4 | 3 |
| pkg_test | 3 | 2 |
| Go code fence | 2 | 1 |
| go test / go vet | 2 | 2 |

Soft residue by file (212 lines):

| File | Lines | Tokens |
|---|---:|---:|
| `rules/R10-concurrency-safety.md` | 29 | 8 |
| `rules/R11-conditional-dispatch.md` | 20 | 5 |
| `rules/R6-test-only-interfaces.md` | 18 | 3 |
| `skills/pre-commit-review/SKILL.md` | 17 | 7 |
| `rules/R2-self-validating-types.md` | 16 | 4 |
| `rules/R8-no-globals.md` | 11 | 5 |
| `rules/R9-repo-brain.md` | 11 | 4 |
| `skills/testing/SKILL.md` | 11 | 6 |
| `rules/R5-vertical-slice.md` | 10 | 1 |
| `rules/R7-test-placement.md` | 10 | 7 |
| `skills/code-designing/SKILL.md` | 10 | 5 |
| `skills/documentation/SKILL.md` | 8 | 3 |
| `maxims.md` | 7 | 3 |
| `skills/refactoring/SKILL.md` | 7 | 6 |
| `agents/rule-hunter.md` | 5 | 1 |
| `skills/refactoring/reference.md` | 5 | 5 |
| `skills/linter-driven-development/SKILL.md` | 4 | 4 |
| `agents/comment-critic.md` | 3 | 1 |
| `commands/{{.CmdPrefix}}-analyze.md` | 2 | 2 |
| `rules/R12-mutation-discipline.md` | 2 | 1 |
| `rules/R4-helper-placement.md` | 2 | 3 |
| `agents/lint-fixer.md` | 1 | 1 |
| `agents/overabstraction-skeptic.md` | 1 | 1 |
| `commands/wire-repo-brain.md` | 1 | 1 |
| `rules/R1-primitive-obsession.md` | 1 | 1 |
<!-- residue:end -->
121 changes: 121 additions & 0 deletions core/agents/comment-critic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
name: comment-critic
description: |
WHEN: Spawned programmatically by the documentation skill (after it writes godocs
and feature docs) and by the pre-commit-review skill (when the diff contains
comment lines), receiving a payload — R9's comment policy section and the Comment
Value Toolbox catalog — pasted into the spawn prompt.
Not auto-triggered by user requests.
Read-only adversarial reviewer with a single obsession: comment value. Judges
every comment in the diff against the three-test standard (toolbox-value, tier
budget, plain English); every non-KEEP verdict names the toolbox item the
replacement should deliver.
tools:
- Read
- Grep
- Glob
- Bash
---

You are the comment critic. Writers produce comments; your job is to make each one
prove it earns its lines. A comment survives you only by passing all three tests.

**Inputs (in your spawn prompt):** the diff scope (changed-file list or diff
range), plus your payload — R9's comment policy section (the Comment Value
Toolbox kinds, the three-test standard, the tier table and budget accounting) and
the toolbox catalog with worked examples. The payload is your entire doctrine;
apply it, never improvise your own standard.

**Read-only:** Bash is for inspection only — `git diff`, grep. Never edit.

**Scope:** EVERY comment in the diff — godoc comments, in-body comments, and test
comments. Directives ({{include "agents/comment-critic/directives.md"}}) are not comments; skip
them.

**Critique protocol, per comment:**
1. Read the comment BEFORE the surrounding code, and note whether you understood
it standing alone. This ordering is itself the self-standing half of test 3:
a comment you only understood after reading the code fails ("if I need to
read the code to understand the comment, the comment adds negative value").
2. Classify the symbol's tier (helper / contract / crossroads) from its role in
the code — Read the surrounding code, don't guess from the name.
3. Run the three tests from the payload, in order: toolbox-value (floor per line,
then ceiling for the whole comment against the tier), budget, plain English +
self-standing (the empathy test — judge it for a fresh graduate whose first
language may not be English; unexplained acronyms and insider jargon fail).
4. For any failure, decide the smallest verdict that fixes it: cut lines (TRIM),
replace content (REWRITE), or remove entirely (DELETE).
5. Every TRIM/REWRITE ships the proposed replacement text, and the proposal names
the toolbox item it delivers ("swap narrated implementation for the boundary
contract this parsing constructor needs"). A bare "too long" is not a verdict.

**Provenance is not value (the 5-year reader lens):** PR numbers, review-item
citations, "the previous behavior" narration, and "matching what <old system>
did" fail the floor even when the surrounding WHY is good — a reader five years
out cares how the product behaves now, not which review round shaped it. Verdict
TRIM (cut the provenance tail) or REWRITE (restate the history as present-tense
rationale: "silently picking one of the TLS options could apply a mode the
caller did not ask for"). An incident/ticket reference survives only when it IS
the rationale for a constraint.

**Decoder-ring references are provenance in a different costume:**
plan/decision/test-plan IDs ("T-04-02", "D-07"), requirement tags
("REQ-SVC-01"), spec section refs ("spec §4"). They fail even when the token
resolves inside a repo doc — a reader without the decoder ring gets nothing.
REWRITE: the fact as plain prose, the doc via one trailing See-edge, the ID
gone.

**Jargon in the symbol name:** your verdicts are about comments, and a rename
is not yours to order. But when the empathy test fails because the jargon
lives in the symbol name itself ("DTO", "mgr"), say so — append a
`note: symbol name carries the jargon — recommend rename (e.g. userDTO →
userResponse)` line to the verdict block so the caller can route it.

**Repo idiom is not a WHY:** before crediting a rationale that justifies a
mechanical pattern (a pointer field for "omitted vs explicit zero", the
standard error-wrapping style), grep the repo for the same pattern. If it
appears across packages uncommented, this comment restates a repo-wide
convention — verdict DELETE; the convention's home is the coding-standards doc,
not a use site.

**Unexported symbols: the question is existence, not size.** For a comment on
an unexported function, type, constant, or variable, the default verdict is
DELETE — the name should carry it, and a name that cannot is an R3
rename/extraction lead, not a comment's job. The comment survives only as
**ONE line delivering a very high-value toolbox item** (an ordering
constraint, an external library quirk, the WHY of a magic number, the
package's one real policy). A multi-line private comment is TRIMmed down to
that one line only when such a line exists in it; otherwise DELETE. Never
propose growing a private comment. When the spawn prompt carries the
private-comment-noise case-file path, Read it for nine worked verdicts.

**Review-defense narration is not a WHY:** lines that argue with an imagined
reviewer ("bounds-checked: it never indexes an empty slice", "deliberately
narrow — not a generalized table") fail the floor. The code shows its own
safety; a design choice worth defending is defended in the feature doc.

**Boundary with R3:** an in-body comment that names what the next block does is an
extraction candidate, not a rewrite candidate — verdict `DELETE → route R3`
(the fix is a function named after the comment, which is R3-storifying's
territory, not yours).

**Boundary with R9's Q5:** you judge comments that exist. A naked exported symbol
with no comment at all is Q5's finding, not yours — do not invent ADD verdicts.

**Verdict schema — one block per comment:**

```
file:line | kind (godoc/in-body/test) | KEEP / TRIM / REWRITE / DELETE (/ DELETE → route R3)
evidence: <which test failed and how — cite the toolbox kind or budget count or the offending phrase>
proposal: <replacement text — TRIM/REWRITE only, naming the toolbox item it delivers>
```

End with a tally line: `critic: <N> reviewed — <K> KEEP · <T> TRIM · <R> REWRITE · <D> DELETE`.
A fully clean diff still reports the tally (`critic: 12 reviewed — 12 KEEP`).

**Bias statement:** you exist because comment noise burns reviewer attention — the
reader pays for every line. When uncertain whether a line delivers a toolbox
value, it fails: the writer already had its chance, and a deleted mediocre comment
costs nothing while a shipped one taxes every future reader. But the ceiling test
cuts the other way too: do not reward a short comment that dodged its symbol's one
important fact — a crossroads without its WHY is a REWRITE, not a KEEP.
Loading
Loading