This repo's own backlog. The scaffolded project gets its own template/BACKLOG.md; do not conflate.
The B-numbered items track JBANG_TEMPLATE_PLAN.md Part B; the A-numbered items track Part A (upstream OpenRewrite findings) and only appear here when they touch the template payload.
upgrade-skills— fourth JBang subcommand. Walks upward to find.recipescaffold.yml(or accepts--directory), locates upstreamtemplate/.claude/skills/(or accepts--template-dir), and replaces each skill subdir in the project's.claude/skills/with the upstream copy. Iterates only over upstream subdirs, so any user-added skill in the project is left alone. Supports--dry-runfor preview. Tested locally: tampered SKILL.md overwritten cleanly; second run idempotent; error path (non-scaffolded directory) exits 2 with clean message. Refactor:findTemplateDir,findProjectRoot, anddeleteRecursivelywere moved from per-subcommand private statics to top-levelRecipeScaffoldhelpers, plus a newcopyDir(cousin of Init'scopyTreewithout the.gradle/build/.ideaskip logic). Per-subcommand wrappers retained as thin delegates so the existing call sites are unchanged. Deviates from the BACKLOG-Parked verbiage of "init --upgrade-skillsflag" — a separate subcommand is cleaner than gating most of init behind a flag.
- B11.3.2 —
add-recipe --test-style methodshipstemplate/snippets/recipe-method-test.template. Tighter alternative to the default block-form test: one-linerewriteRun(java("class T { int m() { return Math.max(1, 2); } }"))with a commented-out hint showing how to expand to ajava(before, after)pair when the recipe transforms code.Math.maxis a stand-in so the parser can bind types (OpenRewrite'sRewriteTestrejects LSTs with missing type info — the originalfoo(1, 2)placeholder failed for that reason). Restricted to--type java|scanning; yaml usesEnvironment.builderand refaster references the generated<Name>Recipesaggregate, so neither is meaningfully tighter in one-line form. New AddRecipe option--test-style block|method(defaultblock); validates the value and the type/style combination. Local end-to-end: scaffold + add-recipe (java + scanning + yaml + refaster + java method-style) +./gradlew check— green. CI extends both bash- and jbang-scaffold jobs with a fifth cell. Harness gains a fifthaddRecipecall.
- B11.4 —
verify-gatesJBang subcommand. Walks upward from cwd looking for.recipescaffold.yml(or accepts--directory), validates the dropfile is present (refuses non-recipescaffold projects to keep the smokeTest assumption honest), then runs./gradlew check integrationTest smokeTestviaProcessBuilder.inheritIO()and forwards the exit code. The three tasks are listed explicitly so all run even whencheckis up-to-date — the user is asking "are the gates green right now," not "is anything stale." Reuses Init'srunGradlehelper, which was extracted to a top-level staticRecipeScaffold.runGradle(Path, List<String>)so both Init's--verifyflow and VerifyGates share the same wrapper invocation. Tested locally: exit 2 with clean error when no dropfile present; happy path begins gradle invocation correctly. Plan §B3 priority 3 — deferred git-init dependency lifted (B11.3.x has settled).
- In-repo TestKit harness —
src/test/java/recipescaffold/ScaffoldHarnessTest.javadrivesInit.call()andAddRecipe.call()(one cell per--type: java, scanning, yaml, refaster) into a@TempDir, then runsGradleRunnerwith-g <tmpGradleHome> -Dmaven.repo.local=<tmpM2> --stacktrace checkagainst the scaffolded project. Pattern: Initializr'sProjectGeneratorTestershape + Maven Archetype'sarchetype:integration-testscope, ported to Gradle TestKit. Required scaffolding: a Gradle build at the repo root for the first time (settings.gradle.kts,build.gradle.kts,gradle/libs.versions.toml, wrapper assets copied fromtemplate/,.gitignoreextended for/build/and/.gradle/);package recipescaffold;added tojbang/RecipeScaffold.javaso a packaged test can import it (Java forbids importing default-package types). The build points the main source set atjbang/so the JBang flow keeps working unchanged. CI gains aharnessjob that runs./gradlew test. Local end-to-end inside this Claude sandbox can't complete because the forked Gradle daemon JVM can't resolve DNS for fresh artifact downloads (same gap that hit the refaster verification) — but the harness gets through scaffold + add-recipe (all four types) + TestKit invocation, then fails only on the inner gradle fetching the rewrite plugin. CI and unsandboxed local environments will run end-to-end.
- B11.3.1 (refaster) —
add-recipe --type refastershipstemplate/snippets/recipe-class-refaster.template(outer holder class with one nested@RecipeDescriptortemplate-pair, idiomatic permoderneinc/rewrite-recipe-starter) andtemplate/snippets/recipe-test-refaster.template(instantiates the generated<recipeName>Recipesaggregate, asserts a one-line before→after rewrite). KINDS map gains a fourth entry. Template build wiring (template/build.gradle.kts+gradle/libs.versions.toml):annotationProcessor+implementationoforg.openrewrite:rewrite-templating:1.41.4,compileOnlyofcom.google.errorprone:error_prone_core:2.49.0(with the canonicalauto-service-annotationsanddataflow-errorproneexcludes),compileJavaadds-Arewrite.javaParserClasspathFrom=resources. CI extends both jobs with a fourth--type refastercell. Local./gradlew checkdeferred to CI: the new artifact downloads fail DNS resolution inside the forked Gradle daemon's JVM, even though the parent shell resolves them — the same sandbox/forked-daemon gap that motivates the queued TestKit harness item.
- B11.3.1 (yaml) —
add-recipe --type yamlshipstemplate/snippets/yaml-composition-block.template(thespecs.openrewrite.org/v1beta/recipemanifest with placeholderrecipeList: []+ an inline comment showing the canonical entry shape) andtemplate/snippets/recipe-test-yaml.template(aRewriteTestskeleton that loads the manifest viaEnvironment.builder().scanRuntimeClasspath().build().activateRecipes("<id>")rather thannew RecipeClass()). AddRecipe was refactored from a flatCLASS_SNIPPETSmap to a nestedRecipeKindrecord dispatch withmainSnippet,testSnippet, andmainInResources— yaml routes the manifest tosrc/main/resources/META-INF/rewrite/<kebab>.yml(no package subdir) while java/scanning still writesrc/main/java/<pkg>/<Name>.java. Two new snippet-time placeholders:{{recipeId}}(for yaml =<rootPackage>.<recipeName>, root namespace per the example.yml convention; for java/scanning =<package>.<recipeName>) and{{recipeKebab}}(PascalCase → kebab, used for the manifest filename). Local end-to-end: scaffold + add-recipe (java + scanning + yaml) +./gradlew check— green. CI extends both jobs with a thirdadd-recipe --type yaml SmokeYamlRecipecell.
- B11.3.1 (partial) —
add-recipe --type scanningshipstemplate/snippets/recipe-class-scanning.template(ScanningRecipe<Acc>withgetInitialValue/getScanner/getVisitor+ a nestedAccclass holding aSet<String> seen). AddRecipe dispatches viaCLASS_SNIPPETSmap (java + scanning); unsupported types report the available list. Samerecipe-test.templateis reused — the no-op default still asserts source unchanged. Local end-to-end: scaffold + add-recipe (java) + add-recipe (scanning) +./gradlew check— green. CI extends both jobs with a secondadd-recipe --type scanning SmokeScanRecipecell. yaml/refaster types still queued.
- B11.3 —
add-recipeJBang subcommand. Args:--name <RecipeName>,--type java(initial; B11.3.1 addedscanning),--display-name,--description,--package,-d/--directory,--no-tests,--force. Reads.recipescaffold.ymldropfile (walks upward from cwd if--directorynot given), loadssnippets/recipe-class-java.template+recipe-test.template, substitutes{{package}}/{{recipeName}}/{{recipeDisplayName}}/{{recipeDescription}}, writes tosrc/main/java/<pkg>/recipes/<Name>.java(+ test). Refuses to overwrite without--force; rejects non-PascalCase--name; rejects unsupported--type. - B11.3 — dropfile —
Init.call()writes.recipescaffold.ymlat the output root capturingrecipescaffoldVersion(=RecipeScaffold.VERSION, bumped to0.2.0),group,artifact,rootPackage,javaTargetMain,javaTargetTests. Hand-rolled YAML, no extra deps.tests/ci-smoke.shwrites the same shape so the bash flow's output also feedsadd-recipe. - B11.3 — snippets —
template/snippets/{recipe-class-java,recipe-test}.templateplus aREADME.mddocumenting the placeholder dialect. Lives undertemplate/so the snippet directory ships into every scaffold ANDadd-recipereads it from the user's project after scaffolding. Init substitutor and residual check both skip files under<root>/snippets/so the{{…}}markers survive scaffolding intact. - B11.3 — CI — both
bash-scaffoldandjbang-scaffoldjobs now runadd-recipe SmokeRecipeafter the initial scaffold and re-run./gradlew check.bash-scaffoldgains thejbangdev/setup-jbang@mainstep. Catches snippet-substitution regressions before they ship.
- B11.2 — JBang
Initsubcommand atjbang/RecipeScaffold.java. Picocli, single-file,//DEPS info.picocli:picocli:4.7.7,--verifyruns./gradlew check smokeTestpost-scaffold. Class renamedrecipescaffold→RecipeScaffoldfor Java convention. - Repo-root CI at
.github/workflows/ci.yml: parallel jobs runtests/ci-smoke.shandjbang init --verifyon every push/PR. JDK 21+25 installed; usesjbangdev/setup-jbang@main. - Template additions:
LICENSE(Apache 2.0),AGENTS.md(canonical vendor-neutral agent guidance —CLAUDE.mdis now a stub forwarding to it),.editorconfig,.github/workflows/release.yml(tag-triggered Maven Central publish),.github/workflows/wrapper-validation.yml,.github/dependabot.yml. - Smoke-test regression fixed:
GradleRunnerexportsGRADLE_OPTS=-Dorg.gradle.java.home=<jdk21Home>so nested Gradle 8.14.3's Kotlin DSL evaluator stays off JDK 25 (gradle.propertiesis parsed too late to help).integrationTestsource set dropssourceSets.test.get().outputand addsexclude("org.openrewrite", "rewrite-java-25")onintegrationTestRuntimeClasspath(mirrors upstream).
template/.gradle/removed and.gitignorealready excluded — no longer ships in the scaffold tree.- Permissions sanitised: committed
.claude/settings.jsonwith pattern-matched allows fortests/ci-smoke.sh,javacof the JBang script, andjava -cp recipescaffoldinvocations..claude/settings.local.jsonreset to{}.template/.claude/settings.jsonshipsBash(./gradlew *)for scaffolded users. - Residual placeholder check tightened to
(?<!\$)\{\{[a-zA-Z][a-zA-Z0-9]*\}\}(in bothtests/ci-smoke.shand the JBang script) so GitHub Actions${{ secrets.X }}expressions inrelease.ymldon't false-positive.
- B11.1 —
template/parameterised payload +tests/ci-smoke.shbash scaffold-and-verify (v0 fallback).
- B11.3.2 —
recipe-method-test.template— aRewriteTestskeleton that takes a one-linebefore/afterpair instead of the multi-linejava(...)block in the default test. For when the user wants a tighter assertion form for argument-level transforms. git init+ GitHub remoterecipescaffold. Deferred until B11.3.x has settled the snippet layout fully.
- (none — pick from Queued)
- Refactor
RecipeScaffold.javato multi-file (//SOURCES) or to atooling/recipescaffold/Maven/Gradle module. Trigger: when B11.3 doubles the script's line count andInitandAddRecipestart sharing helpers. Today the script is appropriately sized for one subcommand. - Split
Init.call()intoTemplateLocator,Scaffolder,PlaceholderSubstitutor,GradleVerifier. Same trigger as the refactor above. Each becomes individually unit-testable. Clean Code skillclean-code-functionswould flag the current ~100-line method. - Unit tests for the helpers (substitution correctness, residual detection,
__ROOT_PACKAGE__rename, copyTree skip-list). Trigger: after the module extraction. CI black-box coverage (today) is enough for now. - Extract constants in
RecipeScaffold.java:MARKER_DIR = "__ROOT_PACKAGE__",MARKER_PARENTS = List.of(...),TEXT_EXTENSIONS,RESIDUAL_PATTERN. Cosmetic; bundle into the refactor pass. isTextFile()improvements — extension allowlist is brittle;.editorconfigis currently skipped because of no.extension. Either add toTEXT_NAMESor switch to content-based detection.bump-versionssubcommand — TOML-aware Maven Central checker, subset of Ben-Manes but one-step. Plan §B3 priority 4.releasesubcommand — verify-gates → backlog confirm → version bump → tag → push. Plan §B3 priority 5; risky to automate, leave as documented workflow.- Native image via GraalVM for faster cold-start. Plan §B10. Only if startup becomes a user complaint.
- JBang catalog (
jbang-catalog.json) entry so users canjbang recipescaffold@fiftiesHousewife init …instead of pointing at the raw URL. Cheap once the GitHub remote exists.
maxandersen/rewrite-jbang— JBang-distributed runner for OpenRewrite recipes (validates our single-file picocli +jbang app installpattern; different scope — they run recipes, we scaffold the project that authors them).
These ship in upstream first; sync into the template when stable. Tracked here only because they touch template payload or build conventions.
- A14 — extract our own
recipe-library-baseconvention plugin (replaces parts of the template's hand-rolledbuild.gradle.kts). - A16 — publish-on-tag CI workflow (template now ships
release.yml; upstream still queued). - A18 —
recipes.csv+community-recipesPR (upstream first, then template gains it via therecipe-libraryplugin from A14). - A22 — BOM-aligned versions in
libs.versions.toml(template's TOML still pins individual8.79.6entries the BOM should align). - A10 —
MethodMatcherconversion for theSystemOutdetector (upstream-only; here for completeness).