This repo extracts the build conventions, test harnesses, and pre-publish smoke gate from the upstream io.github.fiftieshousewife:system-out-to-lombok-log4j recipe project into a reusable scaffold for new OpenRewrite recipe projects. It is the source-of-truth for what a fresh recipe project should look like — the upstream project remains the canonical evolving recipe library, and changes flow push-based from there into here (see plan §B8).
JBANG_TEMPLATE_PLAN.md— the full Plan-agent report (Part A: 22 OpenRewrite best-practice findings against the upstream project; Part B: 11-section JBang+picocli scaffolder design). The header table tracks which Part A items have shipped upstream.README.md— what the repo is, placeholder list, JBang quickstart, manual fallback.
- B11.1 done.
template/holds the parameterised scaffold.tests/ci-smoke.shis the bash port that scaffolds + sed-substitutes + runs./gradlew check smokeTest. - B11.2 done (2026-05-04).
jbang/RecipeScaffold.javais the picocliInitport — same end-state asci-smoke.sh, byte-identical scaffold tree (verified). Runs in two flavours:jbang jbang/RecipeScaffold.java init …(preferred) orjavac --release 17 -cp picocli.jarfor environments without JBang.--verifyruns./gradlew check smokeTestafter scaffolding. - B11.3 done (2026-05-04).
add-recipeJBang subcommand. Reads.recipescaffold.yml(written byinit) for project identity, expandstemplate/snippets/recipe-class-{java,scanning}.template+recipe-test.templateintosrc/main/java/<pkg>/recipes/<Name>.java(+ test).--type javaand--type scanningship;yamlandrefasterqueued. CI exercises bothbash-scaffold + add-recipe (java + scanning) + ./gradlew checkandjbang init --verify + add-recipe (java + scanning) + ./gradlew check. - B11.3.1 yaml done (2026-05-04).
add-recipe --type yamlwrites a composition manifest tosrc/main/resources/META-INF/rewrite/<kebab>.ymlplus anEnvironment.builder().scanRuntimeClasspath()test undersrc/test/java/<pkg>/<Name>Test.java.AddRecipewas refactored to aRecipeKindrecord dispatch (mainSnippet,testSnippet,mainInResources); two new snippet-time placeholders shipped ({{recipeId}},{{recipeKebab}}). - B11.3.1 refaster done (2026-05-04).
add-recipe --type refasterwrites an outer-holder class with one nested@RecipeDescriptortemplate-pair undersrc/main/java/<pkg>/<Name>.javaplus a test that instantiates the generated<Name>Recipesaggregate. Template build wiring added:rewrite-templating(annotationProcessor + implementation),error_prone_core(compileOnly with the canonical excludes),-Arewrite.javaParserClasspathFrom=resourcesoncompileJava. All four--typevalues (java, scanning, yaml, refaster) now have CI cells in both the bash- and JBang-scaffold jobs. - TestKit harness done (2026-05-04). Repo now has a Gradle build of its own (
build.gradle.kts,settings.gradle.kts,gradle/libs.versions.toml, wrapper assets,gradle.properties). Main source set points atjbang/so the JBang flow keeps working;jbang/RecipeScaffold.javagainedpackage recipescaffold;so a packaged test can import it.src/test/java/recipescaffold/ScaffoldHarnessTest.javadrivesInit+AddRecipe(all four--typevalues) into a@TempDir, then runsGradleRunneragainst the scaffolded project with-g <tmp> -Dmaven.repo.local=<tmp>. Three CI jobs now:bash-scaffold,jbang-scaffold,harness. - B11.4 done (2026-05-04).
verify-gatesJBang subcommand: walks upward to find.recipescaffold.yml, runs./gradlew check integrationTest smokeTestviaProcessBuilder.inheritIO(). Refuses to run in non-recipescaffold projects (no dropfile = no smokeTest task to invoke). Init'srunGradlehelper extracted to top-level so both flows share it. - B11.3.2 done (2026-05-04).
add-recipe --test-style block|method(defaultblock);methodswaps inrecipe-method-test.template(one-linejava(...)overMath.max(1, 2)with a commented-out before/after hint). Restricted to--type java|scanning. CI cell + harness cell added. upgrade-skillsdone (2026-05-04). Fourth JBang subcommand. Walks upward for.recipescaffold.yml, replaces each upstreamtemplate/.claude/skills/<X>in the scaffolded project's.claude/skills/.--dry-runfor preview. RefactoredfindTemplateDir,findProjectRoot,deleteRecursivelyto top-level helpers; addedcopyDir.- B11.5+ later.
git init+ GitHub remote.
.
├── README.md # repo-level: what it is, JBang quickstart, manual fallback
├── CLAUDE.md # this file — session bootstrap
├── JBANG_TEMPLATE_PLAN.md # the source plan
├── build.gradle.kts # repo-root Gradle build for the TestKit harness
├── settings.gradle.kts # ditto
├── gradle/ # libs.versions.toml + wrapper for the harness build
├── gradlew, gradlew.bat # wrapper scripts (copied from template/)
├── src/test/java/recipescaffold/ # ScaffoldHarnessTest — Init+AddRecipe@TempDir+GradleRunner
├── .claude/skills/ # repo-level skills for working IN this repo
├── tests/ci-smoke.sh # bash scaffold-and-build verifier (kept as v0 fallback)
├── jbang/RecipeScaffold.java # picocli Init + AddRecipe subcommands — the JBang flow
├── template/ # WHAT GETS SCAFFOLDED into the user's new project
│ ├── AGENTS.md # vendor-neutral agent guidance (canonical)
│ ├── CLAUDE.md # Claude-Code-specific notes; forwards to AGENTS.md
│ ├── LICENSE # Apache 2.0
│ ├── .editorconfig
│ ├── .github/
│ │ ├── dependabot.yml
│ │ └── workflows/{gradle,release,wrapper-validation}.yml
│ ├── snippets/ # source-of-truth recipe-skeleton fragments (read by add-recipe)
│ ├── ...everything else that becomes their build...
│ └── .claude/skills/ # the four recipe-authoring skills the scaffolded project ships with
After init, the scaffolded project root holds a .recipescaffold.yml dropfile (recipescaffoldVersion, group, artifact, rootPackage, javaTargetMain, javaTargetTests). add-recipe walks upward from cwd looking for it.
The template/.claude/skills/ vs the repo-level .claude/skills/ distinction matters: the former goes to the scaffolded user, the latter is for the maintainer of THIS repo.
Two distinct dialects:
Init-time placeholders (substituted by tests/ci-smoke.sh and jbang init):
| Placeholder | Meaning |
|---|---|
{{group}}, {{artifact}}, {{rootPackage}} |
Maven group + artifact + Java root package |
{{initialVersion}} |
First version of the scaffolded project |
{{recipeName}}, {{recipeDescription}} |
POM name + description |
{{githubOrg}}, {{githubRepo}} |
For SCM URLs |
{{authorId}}, {{authorName}}, {{authorEmail}} |
POM developer block |
{{javaTargetMain}}, {{javaTargetTests}} |
release for compileJava and compileTestJava |
{{rewritePluginVersion}} |
Snippet versions in template's docs |
__ROOT_PACKAGE__ |
Literal directory marker — renamed at scaffold time |
Snippet-time placeholders (substituted by jbang add-recipe, in template/snippets/*.template only):
| Placeholder | Meaning |
|---|---|
{{package}} |
Java package the recipe (or its test) lives in (= <rootPackage>.recipes by default) |
{{recipeName}} |
Recipe class name (PascalCase) |
{{recipeDisplayName}} |
Returned by getDisplayName() |
{{recipeDescription}} |
Returned by getDescription() |
{{recipeId}} |
OpenRewrite recipe identifier — for yaml: <rootPackage>.<recipeName> (root namespace per example.yml convention); for java/scanning: <package>.<recipeName>. |
{{recipeKebab}} |
kebab-case form of {{recipeName}}, used for the YAML manifest filename. |
Both dialects share the {{name}} syntax. Init-time substitution and the residual check both skip files under <root>/snippets/ so the snippet-time markers survive scaffolding into the user's project intact.
The residual check is (?<!\$)\{\{[a-zA-Z][a-zA-Z0-9]*\}\} — anchored so GitHub Actions ${{ secrets.X }} expressions in release.yml don't trip the gate.
tests/ci-smoke.sh and jbang/RecipeScaffold.java must stay in sync. When you add a new init-time placeholder: extend both substitution lists and the table above. When you add a new snippet, drop it in template/snippets/ and add-recipe picks it up by file name (no script edit needed unless you add a new --type).
The four recipe-authoring skills (new-gradle-project, new-recipe, recipe-testing, smoke-test) plus the ten clean-code skills. The clean-code skills aren't enforced by build tools here (the template strips the cleancode plugin) — invoke them on judgment, not because tooling demands it.
- No emojis in source, docs, or commits unless the user explicitly asks.
- Prefer editing
template/files over creating new ones. New files intemplate/propagate to every future scaffold — only add when the responsibility doesn't fit existing files. - Verify after every template change by running both
tests/ci-smoke.shand the JBang script with--verify. Don't trust placeholder substitution by inspection. - Honest scope notes belong in
README.md— the smoke runner ships SLF4J/Lombok-specific dep blocks markedEDIT FOR YOUR RECIPE'S DEPS. Don't pretend it's fully generic. - AGENTS.md is canonical, CLAUDE.md is the stub in the scaffolded project — vendor-neutral content goes in AGENTS.md.
The supported flow is JBang. Install it once (brew install jbang or curl -Ls https://sh.jbang.dev | bash -s - app setup), then:
jbang jbang/RecipeScaffold.java init --help
jbang jbang/RecipeScaffold.java add-recipe --helpJBang handles compilation, dep resolution (//DEPS info.picocli:picocli:4.7.7), and caching. CI uses the same flow via jbangdev/setup-jbang@main in .github/workflows/ci.yml.
Typical sequence for a fresh project:
jbang jbang/RecipeScaffold.java init --group=… --artifact=… --root-package=… [other required args] -d ./acme-rewrite-recipes --verify
cd acme-rewrite-recipes
jbang <path-to-recipescaffold>/jbang/RecipeScaffold.java add-recipe --name RemoveStaleSuppression
./gradlew checkadd-recipe finds .recipescaffold.yml by walking upward from cwd, so it works from any subdirectory of a scaffolded project.
Fallback only — when JBang is genuinely unavailable (e.g. an air-gapped CI image), the script compiles cleanly with javac --release 17 since //DEPS and //JAVA are Java line comments. Don't make this the default; install JBang.
PICOCLI=/tmp/picocli-cache/picocli-4.7.7.jar
[ -f "$PICOCLI" ] || curl -fsSL -o "$PICOCLI" \
https://repo1.maven.org/maven2/info/picocli/picocli/4.7.7/picocli-4.7.7.jar
javac --release 17 -cp "$PICOCLI" -d /tmp/recipescaffold-build jbang/RecipeScaffold.java
java -cp /tmp/recipescaffold-build:"$PICOCLI" recipescaffold.RecipeScaffold init --help./gradlew test runs ScaffoldHarnessTest from the repo root. It scaffolds via Init.call() into a @TempDir, runs AddRecipe.call() once per --type (java, scanning, yaml, refaster), then drives GradleRunner against the result with -g <tmpHome> -Dmaven.repo.local=<tmpM2> check. The harness is the in-repo equivalent of Maven's archetype:integration-test and runs as a third parallel CI job alongside bash-scaffold and jbang-scaffold. Locally it works on any machine where the forked Gradle daemon JVM has working DNS — including the sandbox we run inside is restrictive enough that the harness's inner gradle subprocess fails to fetch the rewrite plugin from plugins.gradle.org. Don't fight that here; CI is the authoritative gate for harness-level verification.
git initand the GitHub remoterecipescaffold— defer until B11.3+ has settled the directory layout.template/snippets/*.template— B11.3 (add-recipesubcommand source-of-truth fragments).- CI for THIS repo — runs
tests/ci-smoke.sh(or the JBang flow) on every PR. Plan §B11.4.