From 19d939b8ba1221c4207a6527762875b6d175a3ba Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Sun, 6 Sep 2026 20:06:33 +0200 Subject: [PATCH 1/7] chore(deps): bump kirchDev/workflows from v0.8.0 to v0.8.2 --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/fast-forward-queue.yml | 2 +- .github/workflows/promotion-pr.yml | 2 +- .github/workflows/queue-branch.yml | 2 +- .github/workflows/release-please.yml | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55b4977..876277d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,4 +23,4 @@ permissions: jobs: check: name: CI - uses: kirchDev/workflows/.github/workflows/_ci-check.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_ci-check.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 526d79d..6597f60 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,4 +20,4 @@ jobs: packages: read actions: read contents: read - uses: kirchDev/workflows/.github/workflows/_codeql.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_codeql.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 diff --git a/.github/workflows/fast-forward-queue.yml b/.github/workflows/fast-forward-queue.yml index 65260f5..47d36f2 100644 --- a/.github/workflows/fast-forward-queue.yml +++ b/.github/workflows/fast-forward-queue.yml @@ -25,7 +25,7 @@ jobs: pull-requests: read checks: read statuses: read - uses: kirchDev/workflows/.github/workflows/_fast-forward-queue.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_fast-forward-queue.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 with: pr: ${{ inputs.pr }} secrets: diff --git a/.github/workflows/promotion-pr.yml b/.github/workflows/promotion-pr.yml index d8c467d..10a32bf 100644 --- a/.github/workflows/promotion-pr.yml +++ b/.github/workflows/promotion-pr.yml @@ -15,4 +15,4 @@ permissions: jobs: promotion-pr: name: Promotion PR - uses: kirchDev/workflows/.github/workflows/_promotion-pr.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_promotion-pr.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 diff --git a/.github/workflows/queue-branch.yml b/.github/workflows/queue-branch.yml index 28af885..40a8135 100644 --- a/.github/workflows/queue-branch.yml +++ b/.github/workflows/queue-branch.yml @@ -16,6 +16,6 @@ jobs: permissions: contents: read pull-requests: write - uses: kirchDev/workflows/.github/workflows/_queue-branch.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_queue-branch.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 secrets: BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index aefb0b5..95ddeb0 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -26,7 +26,7 @@ jobs: permissions: contents: write pull-requests: write - uses: kirchDev/workflows/.github/workflows/_release-please.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_release-please.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 with: # This package is consumed as `kirchDev/gitignore-sync@`, so `@v0` # has to keep meaning "the newest v0.x.y". @@ -47,7 +47,7 @@ jobs: # `build-script` is left at its default: this repo's `build` writes the npm # artifact itself. coverage-report needs `build:npm` there only because its # `build` produces an Action bundle instead. - uses: kirchDev/workflows/.github/workflows/_publish-npm.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_publish-npm.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 with: tag-name: ${{ needs.release-please.outputs.tag-name }} @@ -67,6 +67,6 @@ jobs: permissions: contents: read id-token: write - uses: kirchDev/workflows/.github/workflows/_publish-npm.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0 + uses: kirchDev/workflows/.github/workflows/_publish-npm.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2 with: prerelease: true From c23a15e8919dfea0f9981710f43eeb3782f2b218 Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Tue, 8 Sep 2026 14:02:51 +0200 Subject: [PATCH 2/7] chore: declare the docs tree as duxt-rendered --- .tituskirch-skills.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.tituskirch-skills.json b/.tituskirch-skills.json index 155cdba..052bf69 100644 --- a/.tituskirch-skills.json +++ b/.tituskirch-skills.json @@ -48,7 +48,8 @@ } }, "docs": { - "preset": "package" + "preset": "package", + "render": "duxt" }, "work": { "tracker": "github", From b56942eb69695aa4060be20a4121d4162aced09f Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Wed, 9 Sep 2026 15:57:59 +0200 Subject: [PATCH 3/7] chore: name work profiles after their branch strategy Rename fleet to parallel-worktree and fleet-with-queue to parallel-worktree-queue, so the name states the isolation the profile buys, and add a parallel profile for parallel work on the shared branch. --- .tituskirch-skills.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.tituskirch-skills.json b/.tituskirch-skills.json index 052bf69..abdc1f0 100644 --- a/.tituskirch-skills.json +++ b/.tituskirch-skills.json @@ -79,14 +79,20 @@ ] }, "profiles": { - "fleet": { + "parallel": { + "work": { + "parallel": true, + "concurrency": 4 + } + }, + "parallel-worktree": { "work": { "concurrency": 8, "branch": "worktree", "parallel": true } }, - "fleet-with-queue": { + "parallel-worktree-queue": { "work": { "concurrency": 8, "branch": "worktree", From 55aba23c47e54fc4b473729451b3f5434e21a546 Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Thu, 10 Sep 2026 13:31:24 +0200 Subject: [PATCH 4/7] fix(config): correct the codex policy match examples --- .codex/rules/default.rules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.codex/rules/default.rules b/.codex/rules/default.rules index 700dc70..8647fe0 100644 --- a/.codex/rules/default.rules +++ b/.codex/rules/default.rules @@ -130,7 +130,7 @@ prefix_rule( pattern = ["rm", ["-rf", "-fr", "-Rf", "-fR"], "/"], decision = "forbidden", justification = "Recursive delete from the filesystem root", - match = ["rm -rf /", "rm -rf /etc"], + match = ["rm -rf /", "rm -fr /"], not_match = ["rm -rf node_modules", "rm -rf dist"], ) @@ -153,7 +153,7 @@ prefix_rule( pattern = [["dd", "mkfs", "shred", "fdisk", "parted"]], decision = "forbidden", justification = "Writes to block devices; never needed from a repo", - match = ["dd if=/dev/zero of=/dev/sda", "mkfs.ext4 /dev/sdb1"], + match = ["dd if=/dev/zero of=/dev/sda", "mkfs -t ext4 /dev/sdb1"], ) # ──────────────────────────── databases ────────────────────────────────────── From 837266825ac63ec76608a66335aa7c1cbe128eea Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Thu, 10 Sep 2026 14:58:11 +0200 Subject: [PATCH 5/7] fix(ci): publish prereleases only from main --- .github/workflows/release-please.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 95ddeb0..bf97902 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -6,9 +6,6 @@ on: push: branches: - main - # dev reaches this workflow for the prerelease job only; release-please - # itself is gated to the release branch below. - - dev # PERMISSIONS PER JOB, NOT PER WORKFLOW: a workflow-level block caps every # called body, and the publish jobs need `id-token: write` for npm's Trusted @@ -20,8 +17,7 @@ concurrency: jobs: release-please: name: Release Please - # `dev` reaches this workflow for the prerelease job below; release-please - # itself only ever runs on the release branch. + # Release-please only ever runs on the release branch. if: github.ref_name == 'main' permissions: contents: write @@ -57,12 +53,9 @@ jobs: # the stable release, and publishing a prerelease from them would shadow it. if: >- ${{ - github.ref_name == 'dev' - || ( - github.ref_name == 'main' - && !startsWith(github.event.head_commit.message, 'chore(main): release') - && !startsWith(github.event.head_commit.message, 'chore: release') - ) + github.ref_name == 'main' + && !startsWith(github.event.head_commit.message, 'chore(main): release') + && !startsWith(github.event.head_commit.message, 'chore: release') }} permissions: contents: read From a1a6dfaf494b31cdc342242d4770fef425997843 Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Wed, 16 Sep 2026 12:15:56 +0200 Subject: [PATCH 6/7] feat(templates): add a gradle stack Gradle build output (.gradle/, .kotlin/, build/), fingerprinted on build.gradle* or settings.gradle* up to one level below the root. Named after the build tool, not the language: Maven writes target/ instead. No *.jar, since the wrapper jar is committed. --- src/detect.ts | 4 ++++ src/templates/index.ts | 14 ++++++++++++++ tests/detect.test.ts | 7 +++++++ tests/discover.test.ts | 18 +++++++++++++----- tests/fixtures/gradle.in | 13 +++++++++++++ tests/fixtures/gradle.out | 18 ++++++++++++++++++ tests/templates.lock.json | 1 + 7 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/gradle.in create mode 100644 tests/fixtures/gradle.out diff --git a/src/detect.ts b/src/detect.ts index 32b0623..613d880 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -69,6 +69,10 @@ const fingerprints: Record = { turborepo: { source: 'repo', files: ['turbo.json'] }, // Root `Cargo.toml` only — see the comment on the template. rust: { source: 'repo', files: ['Cargo.toml'] }, + gradle: { + source: 'repo', + patterns: [/^(?:build|settings)\.gradle(?:\.kts)?$/] + }, playwright: { source: 'repo', patterns: [/^playwright\.config\.[cm]?[jt]s$/] diff --git a/src/templates/index.ts b/src/templates/index.ts index d75ab9a..a64eba7 100644 --- a/src/templates/index.ts +++ b/src/templates/index.ts @@ -171,6 +171,20 @@ const registry: Record = { lines: ['/target', '**/*.rs.bk'] } ], + // Gradle's build output, not "Java": what a JVM repo ignores follows the + // build tool, and Maven writes to `target/` instead. `build/` is unanchored + // because every subproject gets its own. No `*.jar` — the wrapper jar under + // `gradle/wrapper/` is committed, and toptal's block would swallow a new one. + // `.kotlin/` is the Kotlin Gradle plugin's cache, written for `.kts` scripts + // too. One repo in the estate, `mc-network`, keeps its build in + // `apps/minecraft/`. + gradle: [ + { + stack: 'gradle', + version: 1, + lines: ['.gradle/', '.kotlin/', 'build/'] + } + ], playwright: [ { stack: 'playwright', diff --git a/tests/detect.test.ts b/tests/detect.test.ts index bc74d9d..6dc1381 100644 --- a/tests/detect.test.ts +++ b/tests/detect.test.ts @@ -36,6 +36,13 @@ describe('detect', () => { ); }); + it('fingerprints gradle from a build script one level down', () => { + const dir = fresh(); + mkdirSync(join(dir, 'apps')); + writeFileSync(join(dir, 'apps', 'build.gradle.kts'), ''); + expect(names(dir)).toContain('gradle'); + }); + it('fingerprints dotenv from a committed .env.example', () => { const dir = fresh(); writeFileSync(join(dir, '.env.example'), ''); diff --git a/tests/discover.test.ts b/tests/discover.test.ts index c08fe39..fb471b8 100644 --- a/tests/discover.test.ts +++ b/tests/discover.test.ts @@ -53,17 +53,25 @@ describe('isDirectoryKeeper', () => { describe('skipDirectories', () => { it('derives the dependency and build directories from the templates', () => { const skip = skipDirectories(); - for (const dir of ['node_modules', 'vendor', 'dist', 'coverage', '.git']) { + for (const dir of [ + 'node_modules', + 'vendor', + 'dist', + 'coverage', + 'build', + '.git' + ]) { expect(skip).toContain(dir); } }); - // Taking the last segment of `/public/build` would skip every `build/`, and - // of `/public/storage` every `storage/` — including the stubs a scan should - // find and label. + // Taking the last segment of `/public/storage` would skip every `storage/` — + // including the stubs a scan should find and label. `build` is absent from + // the list on purpose: `gradle` ignores a bare `build/`, so it is skipped for + // that reason, not because `/public/build` leaked into the set. it('never turns a path pattern into a bare directory name', () => { const skip = skipDirectories(); - for (const dir of ['storage', 'build', 'hot', 'ssr', 'schemas']) { + for (const dir of ['storage', 'hot', 'ssr', 'schemas']) { expect(skip).not.toContain(dir); } }); diff --git a/tests/fixtures/gradle.in b/tests/fixtures/gradle.in new file mode 100644 index 0000000..7af85fb --- /dev/null +++ b/tests/fixtures/gradle.in @@ -0,0 +1,13 @@ +# region gitignore-sync +# stacks: core, gradle +# ───────────────────────────────────────── + +# endregion + +# Gradle (apps/minecraft) +.gradle/ +.kotlin/ +build/ + +# Secrets (infra/.env) +.env diff --git a/tests/fixtures/gradle.out b/tests/fixtures/gradle.out new file mode 100644 index 0000000..2f11177 --- /dev/null +++ b/tests/fixtures/gradle.out @@ -0,0 +1,18 @@ +# region gitignore-sync +# stacks: core, gradle +# ───────────────────────────────────────── + +# region core@v1 +.DS_Store +# endregion + +# region gradle@v1 +.gradle/ +.kotlin/ +build/ +# endregion + +# endregion + +# Secrets (infra/.env) +.env diff --git a/tests/templates.lock.json b/tests/templates.lock.json index 2179dc1..d8e23b9 100644 --- a/tests/templates.lock.json +++ b/tests/templates.lock.json @@ -51,6 +51,7 @@ ], "turborepo@v1": [".turbo"], "rust@v1": ["/target", "**/*.rs.bk"], + "gradle@v1": [".gradle/", ".kotlin/", "build/"], "playwright@v1": [ "test-results/", "playwright-report/", From 0d11c792c57bc661ac73ad8a6b362bab39837086 Mon Sep 17 00:00:00 2001 From: Titus Kirch Date: Wed, 16 Sep 2026 12:15:58 +0200 Subject: [PATCH 7/7] docs: record the gradle stack and its build/ trade-off --- AGENTS.md | 4 +++- CLAUDE.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 65f4a34..57d4981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,6 +137,7 @@ Every template was derived from the 28 locally cloned `kirchDev` / `TitusKirch` | `nuxt` | `nuxt.config.*` | 1 | | `tauri` | `src-tauri/` | 1 | | `storybook` | `.storybook/` | 1 | +| `gradle` | `build.gradle*` / `settings.gradle*` | 1 | | `rust` | a **root** `Cargo.toml` | 0 | | `playwright` | `playwright.config.*` | 0 | | `vscode` | a `.vscode/` here *(machine)* | — | @@ -168,6 +169,7 @@ What the brief said stays true, just narrower than the old prompt copy implied: Deliberate omissions, each for a reason worth keeping: - **`composer.lock` is in no template.** A library ignores it, an application commits it — a project decision, so it belongs in the free zone. +- **The JVM stack is `gradle`, not `java`.** What a JVM repo ignores follows the build tool — Gradle writes `build/`, Maven `target/` — so a `java` stack would render lines pointing at nothing in half the repos. `*.jar` is left out because the Gradle wrapper jar is committed. A `maven` stack waits until a repo needs one. - **`dist` belongs to `node` alone**, not to `go`. goreleaser writes there too, but a line may live in only one stack (below), and `node` claims it. - **`.terraform.lock.hcl` is not ignored** — it is meant to be committed. - **No `prisma` stack**, even though one repo ignores `src/generated/prisma/`. The path is configurable, so it is a project rule, and the free zone is exactly where it belongs. @@ -193,7 +195,7 @@ A keeper is recognised by what it says, not where it sits, so the idiom holds fo **A recursive scan must not walk into generated output**, and the skip list comes from two places rather than a hand-kept list: -- **the templates** — a stack that ignores a build directory is a stack whose output must not be scanned, so `node_modules`, `dist`, `.turbo` and the rest maintain themselves. Only a *bare* name qualifies: taking the last segment of `/public/build` would skip every `build/` in the tree, and of `/public/storage` every Laravel stub the scan is meant to find. +- **the templates** — a stack that ignores a build directory is a stack whose output must not be scanned, so `node_modules`, `dist`, `.turbo` and the rest maintain themselves. Only a *bare* name qualifies: taking the last segment of `/public/build` would skip every `build/` in the tree, and of `/public/storage` every Laravel stub the scan is meant to find. (`build/` *is* skipped everywhere since `gradle` ignores it bare — Gradle output, accepted for that reason.) - **the repository's own `.gitignore`, inherited downwards** — a directory the repo ignores is generated, so the scan has no business there, exactly as git has none. This is what catches output no template knows: `event-management`'s `.stryker-tmp/` held two full copies of the repo and tripled every number until this rule landed. A directory holding its own `.git` is skipped too: a submodule or an agent worktree is a separate repository, and `app`'s four worktrees otherwise multiplied the report fivefold. diff --git a/CLAUDE.md b/CLAUDE.md index 65f4a34..57d4981 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,7 @@ Every template was derived from the 28 locally cloned `kirchDev` / `TitusKirch` | `nuxt` | `nuxt.config.*` | 1 | | `tauri` | `src-tauri/` | 1 | | `storybook` | `.storybook/` | 1 | +| `gradle` | `build.gradle*` / `settings.gradle*` | 1 | | `rust` | a **root** `Cargo.toml` | 0 | | `playwright` | `playwright.config.*` | 0 | | `vscode` | a `.vscode/` here *(machine)* | — | @@ -168,6 +169,7 @@ What the brief said stays true, just narrower than the old prompt copy implied: Deliberate omissions, each for a reason worth keeping: - **`composer.lock` is in no template.** A library ignores it, an application commits it — a project decision, so it belongs in the free zone. +- **The JVM stack is `gradle`, not `java`.** What a JVM repo ignores follows the build tool — Gradle writes `build/`, Maven `target/` — so a `java` stack would render lines pointing at nothing in half the repos. `*.jar` is left out because the Gradle wrapper jar is committed. A `maven` stack waits until a repo needs one. - **`dist` belongs to `node` alone**, not to `go`. goreleaser writes there too, but a line may live in only one stack (below), and `node` claims it. - **`.terraform.lock.hcl` is not ignored** — it is meant to be committed. - **No `prisma` stack**, even though one repo ignores `src/generated/prisma/`. The path is configurable, so it is a project rule, and the free zone is exactly where it belongs. @@ -193,7 +195,7 @@ A keeper is recognised by what it says, not where it sits, so the idiom holds fo **A recursive scan must not walk into generated output**, and the skip list comes from two places rather than a hand-kept list: -- **the templates** — a stack that ignores a build directory is a stack whose output must not be scanned, so `node_modules`, `dist`, `.turbo` and the rest maintain themselves. Only a *bare* name qualifies: taking the last segment of `/public/build` would skip every `build/` in the tree, and of `/public/storage` every Laravel stub the scan is meant to find. +- **the templates** — a stack that ignores a build directory is a stack whose output must not be scanned, so `node_modules`, `dist`, `.turbo` and the rest maintain themselves. Only a *bare* name qualifies: taking the last segment of `/public/build` would skip every `build/` in the tree, and of `/public/storage` every Laravel stub the scan is meant to find. (`build/` *is* skipped everywhere since `gradle` ignores it bare — Gradle output, accepted for that reason.) - **the repository's own `.gitignore`, inherited downwards** — a directory the repo ignores is generated, so the scan has no business there, exactly as git has none. This is what catches output no template knows: `event-management`'s `.stryker-tmp/` held two full copies of the repo and tripled every number until this rule landed. A directory holding its own `.git` is skipped too: a submodule or an agent worktree is a separate repository, and `app`'s four worktrees otherwise multiplied the report fivefold.