Skip to content

go-ldd: extract a language-neutral core and generate the plugin directory - #27

Open
buzzdan wants to merge 16 commits into
mainfrom
claude/handoff-phase-1-5-core-c50e75
Open

go-ldd: extract a language-neutral core and generate the plugin directory#27
buzzdan wants to merge 16 commits into
mainfrom
claude/handoff-phase-1-5-core-c50e75

Conversation

@buzzdan

@buzzdan buzzdan commented Sep 9, 2026

Copy link
Copy Markdown
Owner

What this does

Phase 1.5 of the go-ldd roadmap: the Go plugin's source now lives in two places, and the directory the marketplace serves is generated from them.

  • core/ holds the language-neutral text: rules, skills, agents, commands and the repo-brain gate, as Go text/template files with two constructs only (profile scalars and {{include "..."}}).
  • lang/go/ is the Go binding: profile.yaml (11 scalars plus an ignore list), the include snippets (canonical examples, falsifying questions, routing tables, Go-specific blocks), and passthrough/ for files copied through unchanged.
  • tools/ldd-gen (its own Go module; the root gets a go.work, not a go.mod) renders a binding into its plugin directory, checks a directory against its rendering, and reports language residue left in core/.
  • go-linter-driven-development/ is now generated and must not be edited by hand. CLAUDE.md, the root README and docs/generator.md say so; core/README.md is the templating contract and carries the generated Residue section.

Proof that nothing moved

The generated directory differs from main by exactly one file: the CHANGELOG entry under Unreleased (9 lines). Every commit in the series kept task check green. The differential run of the gate's fixture matrix against main's script reports zero differences. Plugin version stays 2.10.0.

Checks

Check Result
task check every plugin directory matches its rendering
task lint-core hard residue: none; Residue section current
task docs:check OK
go test ./... in tools/ldd-gen green (golden test included)
golangci-lint run in tools/ldd-gen 0 issues
task test-gate 43 of 45 on macOS (two cases use GNU sed; unchanged from main, passes on Linux)

CI (.github/workflows/generate-check.yml, the repo's first workflow) runs all of these on every pull request.

Review rounds

Three review rounds ran before this PR was opened (rule hunters, skeptic, comment critic, and the semantic reviewers on the whole diff, then again on each fix commit). Findings fixed: a missing core/ rendered as empty and would have deleted the plugin; a malformed ignore pattern switched the ignore list off; the Taskfile variable LANG collided with the shell locale; go test ./tools/... from the root is rejected under go.work; plugin: / or another plugin's name passed validation and would have wiped the target; the residue tables counted hits as lines; the empty-directory check compared an error string. The generator now refuses to write anywhere but its own plugin directory.

What stays in the binding for now

skills/documentation/reference.md, scripts/check-repo-brain_test.sh, the testing reference and examples, the worked case studies, and the plugin's own README, CHANGELOG, manifest and hooks are passthrough. core/README.md lists why for each.

Not in this PR

The behavioral eval re-run (cheap and medium tiers against the generated plugin, compared with the recorded baseline) costs real money and runs only after explicit approval.

🤖 Generated with Claude Code

buzzdan and others added 16 commits September 9, 2026 15:07
Add tools/ldd-gen, a small Go module that renders core/ (language-neutral
templates) plus lang/<lang>/ (profile, includes, overrides, passthrough)
into the plugin directory the marketplace serves. In this first step the
whole Go plugin is passthrough under lang/go/passthrough/, so the rendering
is a copy and `ldd-gen -check` passes by construction. Later commits move
one group of files at a time into core/ while the check stays green.

Also add the root go.work (so `go run ./tools/ldd-gen` works from the repo
root without a root go.mod), the Taskfile tasks generate, check, test-gate
and test-gen, the CI workflow that runs them, core/README.md as the
templating contract, and a CLAUDE.md note that the plugin directory is
generated and must not be edited by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The four agents and scripts/check-repo-brain.sh now render from core/.
rule-hunter substitutes the source glob in its worked example; the
overabstraction skeptic is already language-free and moves verbatim; the
comment critic includes its directive list; the lint fixer substitutes the
default lint-fix command and includes its mechanical-issue list, routing
table and hard limits. The gate keeps its driver in core and includes the
Go adapter block, marker lines included, from lang/go/scripts/.

The fixture matrix (check-repo-brain_test.sh) stays passthrough: besides
the conformant fixture it embeds Go in several individual cases, so a split
would need many includes. It is listed for the second language binding.

`task check` stays green and the differential run of the matrix against the
previously committed gate reports zero differences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each rule keeps Principle, Why, Design guidance, Fix pattern and the section
headings in core/. The Canonical example body and the Falsifying questions
body (after the one-line lead-in) move to lang/go/rules/R<n>/ and come back
through includes. These four rules need no other substitution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Same split as the previous commit. R9's monorepo note names the project
marker file, which becomes the profile scalar; nothing else in these six
rules' portable sections is language-specific.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… an include

Same split as the earlier rule commits. R6 substitutes the test-file glob in
its Fix pattern. R10's Design guidance bullet that names the linter config
and its concurrency linters moves to an include, since it is tooling text
rather than a design principle. These two rules are the ones a second
language is expected to override wholesale, so they land last and alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o core

Skill references substitute the plugin name; the refactoring skill's four
tooling blocks (routing table, file and package routing, testing
integration, nolint prohibition) become includes, and its reference.md is
portable as it stands. The review skill substitutes the diff glob and the
project marker and includes its nolint finding and critic prefilter
command. The design skill substitutes the language name and includes its
linter triggers, layout-scan example and package-structure sketch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
linter-driven-development substitutes the language name, command prefix,
plugin name, project marker and default test command; its pre-flight steps
that name project files and tools, and the package-scoped lint line, become
includes. maxims.md is portable as written: the Go proverbs it quotes are
attributed quotes, not Go-only guidance.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…locks are includes

The objective (with the language name substituted), when-to-use, philosophy
and composition ladder are portable and stay in core. The quick start,
reusable infrastructure, the three workflows, key patterns, output format,
checklists and success criteria name testify, httptest, build tags and Go
test commands throughout, so each block becomes one include. reference.md
and examples/ stay passthrough: they are a Go harness catalogue.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The edge line's comment prefix becomes a profile scalar, and the sentence
that says how an edge is verified and which files the gate covers becomes an
include. The remaining Go words (godoc, doc.go, testable examples) are
inline idioms left for the second language binding to resolve.
skills/documentation/reference.md stays passthrough: its toolbox is portable
but its worked examples are Go code, and the seam is recorded with the
residue in the last commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each command file is named core/commands/{{.CmdPrefix}}-<name>.md and renders
to commands/go-ldd-<name>.md; every slash-command mention and Skill
reference is a template scalar. The analyze command's project-discovery and
file-scope blocks, and wire-repo-brain's language-scope paragraph and edge
verification note, become includes; quickfix and review substitute the
default test and lint commands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…entry

`ldd-gen lint-core` scans core/ for language-specific text. Hard residue
(plugin-name literal, linter name, source glob, nolint directive) fails the
command; none remains. Soft residue (nil, ctx, goroutine, interface, godoc
and friends) is counted per token and per file and written between markers
into core/README.md by `task lint-core`, which CI runs and which fails when
the committed section is stale. core/README.md also lists what stays in the
binding and why.

The root README's development steps describe the generate-and-commit flow,
docs/generator.md explains the generator and its checks, and the plugin
CHANGELOG gains an Unreleased entry under Internal: the one intentional
change to the generated directory in this series. The plugin version stays
2.10.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fixes from the pre-merge review, none changing the rendered plugin:

- A missing core/ is refused up front instead of rendering an empty tree
  that would delete every templated plugin file; a walk root that does
  not exist is an error, and a repository with no binding fails -check.
- profile.Parse rejects a plugin name that is not a plain directory name
  and malformed ignore patterns, so a typo can neither point the generator
  at the wrong directory nor switch the ignore list off. Patterns without
  a slash match a name anywhere; the Go profile ignores .DS_Store.
- Overrides carry their own executable bit, and an override with no core
  file to replace is an error. Ignored directories are never removed,
  even when empty. The non-empty-directory check uses errno values, not
  an error string.
- The residue report counts distinct lines, sorts hits with a stable
  tie-break, names bare plugin and command-prefix literals as hard
  residue, and lists Go linter and library names as soft residue.
- The Taskfile variable is BINDING, not LANG, which the shell locale would
  override; CI runs the generator tests from the module directory (the
  root pattern is rejected under go.work) and adds the docs gate; -check
  with -lang is an error; a failed diff command is reported, not hidden.
- Falsifying-question includes start on their first line; the blank line
  lives in the core template, as core/README.md says.
- Comments and docs follow the review: plain words, no overstated claims.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t files

The plugin name check now rejects any separator, the root, and hidden
names, and Generate refuses to write unless the rendering carries
.claude-plugin/plugin.json and the target directory either holds that
manifest too or holds nothing the generator would delete. A wrong plugin
name in a profile can no longer delete other parts of the repository.

Files whose name starts with a dot are skipped as sources, so a .DS_Store
under core/ or a passthrough directory no longer fails the render. An
override that exists but cannot be read is an error instead of a silent
fall back to the core file. Taskfile descriptions say "on disk" like the
rest of the docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… to inputs

Generate now refuses to write when two bindings name the same plugin
directory, when the rendered manifest has no name, or when the target
directory's manifest names a different plugin. Files inside the target that
the rendering owns count as touched even when ignored, so a foreign
directory that happens to hold such a path is refused too.

The input walk skips only the names the profile ignores (.DS_Store), not
every hidden file, so a binding can ship files like .mcp.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… -check

The input walk skips a fixed list of editor and OS droppings (.DS_Store,
._*, *.swp, *~, .#*, Thumbs.db, desktop.ini) plus any name the profile
ignores without a slash, for files and whole directories alike, so an open
editor or Finder cannot break or pollute a rendering while real hidden
plugin files such as .mcp.json still render. -check now also fails when two
bindings name the same plugin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The evals clone lives below the repository root, so the root go.work would
otherwise stop the runner module from building.

Co-Authored-By: Claude Fable 5.1 <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