Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .codex/rules/default.rules
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)

Expand All @@ -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 ──────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/fast-forward-queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/promotion-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/queue-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
21 changes: 7 additions & 14 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,13 +17,12 @@ 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
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@<ref>`, so `@v0`
# has to keep meaning "the newest v0.x.y".
Expand All @@ -47,7 +43,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 }}

Expand All @@ -57,16 +53,13 @@ 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
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
13 changes: 10 additions & 3 deletions .tituskirch-skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
}
},
"docs": {
"preset": "package"
"preset": "package",
"render": "duxt"
},
"work": {
"tracker": "github",
Expand Down Expand Up @@ -78,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",
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)* | — |
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)* | — |
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const fingerprints: Record<string, Fingerprint> = {
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$/]
Expand Down
14 changes: 14 additions & 0 deletions src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ const registry: Record<string, Template[]> = {
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',
Expand Down
7 changes: 7 additions & 0 deletions tests/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'), '');
Expand Down
18 changes: 13 additions & 5 deletions tests/discover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand Down
13 changes: 13 additions & 0 deletions tests/fixtures/gradle.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# region gitignore-sync
# stacks: core, gradle
# ─────────────────────────────────────────

# endregion

# Gradle (apps/minecraft)
.gradle/
.kotlin/
build/

# Secrets (infra/.env)
.env
18 changes: 18 additions & 0 deletions tests/fixtures/gradle.out
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/templates.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
],
"turborepo@v1": [".turbo"],
"rust@v1": ["/target", "**/*.rs.bk"],
"gradle@v1": [".gradle/", ".kotlin/", "build/"],
"playwright@v1": [
"test-results/",
"playwright-report/",
Expand Down