diff --git a/.gitignore b/.gitignore
index 56ef34e..62ec7a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,4 +30,5 @@ bin/
.DS_Store
### Logs ###
+logs/
*.log
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..26cad2b
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,26 @@
+# CLAUDE.md
+
+Guidance for Claude Code (and other agents) working in this repository.
+
+## What this is
+
+A Java project template: Gradle (Kotlin DSL), Checkstyle, PMD, google-java-format (via Spotless), JaCoCo, SLF4J/Logback, and a unit/integration test split (`src/test` vs `src/int-test`). See `README.md` for the full command reference.
+
+## Conventions to follow
+
+- **Logging: SLF4J, never `System.out`/`System.err`.** Get a logger with `private static final Logger LOG = LoggerFactory.getLogger(ThisClass.class);` and use parameterized messages (`LOG.info("thing happened: {}", value)`), not string concatenation. PMD's `SystemPrintln` rule (bestpractices, unexcluded) will fail the build on raw console I/O in `src/main`.
+- **Formatting is not manual.** Don't hand-format Java files or argue with Checkstyle about whitespace/import order — run `./gradlew spotlessApply` and let google-java-format do it. Checkstyle and PMD are deliberately scoped to things the formatter doesn't cover (naming, imports, coding problems, bug patterns); they're not fighting the formatter.
+- **Unit vs integration tests are not interchangeable.** `src/test` (the `test` task) is for fast, isolated unit tests. `src/int-test` (the `intTest` task) is for broader/slower tests. Don't add integration-style tests under `src/test`, and don't assume `./gradlew test` alone exercises integration coverage — see the JaCoCo note below.
+- **The 80%-line / 70%-branch coverage gate (`jacocoTestCoverageVerification`) is intentionally wired to combined unit + integration execution data** (`executionData(tasks.test.get(), intTestTask.get())` in `build.gradle.kts`). If you add a new source set or test task, JaCoCo's default conventions won't pick it up automatically — you have to wire its exec data in explicitly, the same way, or coverage numbers will silently look wrong (lower than reality) without the gate failing to catch it, or vice versa.
+- **PR descriptions must list Jira story keys** per `.github/pull_request_template.md` — that's the org's convention (see the Trident Jira Project Management Playbook), not something to improvise away.
+
+## Known gotchas / do-not-"fix"-without-checking
+
+- **`googleJavaFormat("1.28.0")` is pinned deliberately, not stale.** Spotless 8.9.0's reflective glue adapter (`GoogleJavaFormatFormatterFunc`) breaks against newer google-java-format releases (1.37.0 reproducibly threw `InvocationTargetException` in testing here). If you bump Spotless, re-verify the gjf version still works before bumping it too — don't just grab "latest" for both.
+- **`gradle/gradle-daemon-jvm.properties` pins the Gradle daemon itself to JDK 25**, separately from the project's `java.toolchain` (also 25). This was necessary because an ambient JDK 27 on the dev machine caused flaky in-process formatter failures. Don't remove this file assuming the project toolchain setting alone is enough — they serve different purposes (daemon JVM vs. compile/run JVM).
+- **PMD's `classpath` is explicitly given the toolchain's `jrt-fs.jar`** (`build.gradle.kts`, near the PMD config) to stop it from guessing "current platform" JDK classes and printing a warning every run. If PMD warnings about aux classpath reappear after a Gradle/PMD version bump, check this block still resolves correctly rather than re-adding `PMD_JAVA_DISABLE_AUX_CLASSPATH_WARNINGS` workarounds (that env var doesn't reliably propagate into PMD's forked worker process anyway — verified in testing).
+- **`LoosePackageCoupling` and `GuardLogStatement` are deliberately excluded from PMD's design/bestpractices rules** (`config/pmd/ruleset.xml`) — the former needs explicit package config to be meaningful, the latter is largely redundant once logging is parameterized via SLF4J. Don't silently re-add them without updating the ruleset's comments explaining why they're off.
+
+## Before considering a change "done"
+
+Run `./gradlew check` (compiles, formats-checks, Checkstyle, PMD, unit + integration tests, coverage gate) clean before reporting work finished. CI (`.github/workflows/ci.yml`) runs the same tasks as separate matrix jobs so each shows up as its own PR check.
diff --git a/build.gradle.kts b/build.gradle.kts
index 3128399..902c186 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -38,6 +38,9 @@ configurations.named("intTestRuntimeOnly") {
val junitVersion = "6.1.3"
dependencies {
+ implementation("org.slf4j:slf4j-api:2.0.20")
+ runtimeOnly("ch.qos.logback:logback-classic:1.6.5")
+
testImplementation(platform("org.junit:junit-bom:$junitVersion"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
diff --git a/config/pmd/ruleset.xml b/config/pmd/ruleset.xml
index a5f3dbe..98fd784 100644
--- a/config/pmd/ruleset.xml
+++ b/config/pmd/ruleset.xml
@@ -11,9 +11,8 @@
+
-
-
diff --git a/src/main/java/com/example/app/App.java b/src/main/java/com/example/app/App.java
index 2901455..b427a1a 100644
--- a/src/main/java/com/example/app/App.java
+++ b/src/main/java/com/example/app/App.java
@@ -1,11 +1,16 @@
package com.example.app;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
public final class App {
+ private static final Logger LOG = LoggerFactory.getLogger(App.class);
+
private App() {}
public static void main(String[] args) {
- System.out.println(greeting());
+ LOG.info(greeting());
}
static String greeting() {
diff --git a/src/main/resources/.keep b/src/main/resources/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..7f38e19
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,25 @@
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n
+
+
+
+
+ logs/app.log
+
+ logs/app.%d{yyyy-MM-dd}.%i.log.gz
+ 10MB
+ 30
+ 1GB
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n
+
+
+
+
+
+
+
+