From c24c53ea013e045e0f90f550e4c3a5a7a23e0e7b Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 30 Jun 2026 21:42:31 -0500 Subject: [PATCH] chore: add CLAUDE.md, SPDX-header hook, and preflight skill - CLAUDE.md: build gotchas (JDK 17-21, AGP built-in Kotlin, KSP-not-KAPT), test stack, the SPDX header rule, Conventional Commits, and commands. - .claude/settings.json + hooks/check-spdx.py: PostToolUse hook that warns (non-blocking) when a .kt/.kts file lacks the SPDX license header. - .claude/skills/preflight: runs the fast CI gate (assembleDebug + testDebugUnitTest + lintDebug) locally. Co-Authored-By: Claude Opus 4.8 --- .claude/hooks/check-spdx.py | 61 ++++++++++++++++++++++++++++++ .claude/settings.json | 17 +++++++++ .claude/skills/preflight/SKILL.md | 35 +++++++++++++++++ .gitignore | 3 ++ CLAUDE.md | 63 +++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 .claude/hooks/check-spdx.py create mode 100644 .claude/settings.json create mode 100644 .claude/skills/preflight/SKILL.md create mode 100644 CLAUDE.md diff --git a/.claude/hooks/check-spdx.py b/.claude/hooks/check-spdx.py new file mode 100644 index 0000000..efe53ed --- /dev/null +++ b/.claude/hooks/check-spdx.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +"""PostToolUse (Write|Edit) hook: warn when a Kotlin source file is missing the +SPDX license header that every LibreMail source file must carry. + +Non-blocking: prints a JSON warning (systemMessage + additionalContext so Claude +adds the header) and always exits 0. Any error is swallowed so the tool flow is +never broken by this check. +""" +import json +import sys + +REQUIRED = "SPDX-License-Identifier" +HEADER_LINE = "// SPDX-License-Identifier: GPL-3.0-or-later" + + +def main() -> int: + try: + data = json.load(sys.stdin) + except Exception: + return 0 + + path = (data.get("tool_response") or {}).get("filePath") \ + or (data.get("tool_input") or {}).get("file_path") + if not path: + return 0 + + lower = path.lower() + if not (lower.endswith(".kt") or lower.endswith(".kts")): + return 0 + + try: + with open(path, "r", encoding="utf-8", errors="replace") as fh: + content = fh.read() + except OSError: + # File missing (e.g. a delete) or unreadable — nothing to check. + return 0 + + if REQUIRED in content: + return 0 + + msg = f"SPDX header missing in {path}" + out = { + "systemMessage": msg, + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": ( + f'{path} is missing the license header. Add "{HEADER_LINE}" as the ' + "first line — every LibreMail source file carries it." + ), + }, + } + print(json.dumps(out)) + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception: + sys.exit(0) diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..16f10ec --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "python .claude/hooks/check-spdx.py", + "timeout": 30, + "statusMessage": "Checking SPDX header" + } + ] + } + ] + } +} diff --git a/.claude/skills/preflight/SKILL.md b/.claude/skills/preflight/SKILL.md new file mode 100644 index 0000000..07fc071 --- /dev/null +++ b/.claude/skills/preflight/SKILL.md @@ -0,0 +1,35 @@ +--- +name: preflight +description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + lintDebug) before pushing or opening a PR. Mirrors the merge gate; does NOT run emulator E2E. Use before treating a change as done. +--- + +# /preflight + +Run the same fast checks CI enforces on every PR, in order, and report the outcome. + +## Preconditions + +- The Gradle daemon must run on **JDK 17–21**. AGP 9.2 fails on JDK 25+. If a build errors + with a JDK/AGP version mismatch, check `java -version` / `JAVA_HOME` and point it at a 17–21 + JDK (e.g. Android Studio's bundled JBR) before retrying. +- PowerShell: invoke the wrapper as `.\gradlew`. Git Bash / the Bash tool: `./gradlew`. + +## Steps + +Run these three, stopping at the first failure: + +```bash +./gradlew :app:assembleDebug +./gradlew :app:testDebugUnitTest +./gradlew :app:lintDebug +``` + +To keep going and collect every failure in one pass, add `--continue`. + +## Reporting + +- If all three pass, say so plainly (e.g. "preflight green: build, unit tests, lint"). +- On failure, surface the actual Gradle error. For test failures, point at the report under + `app/build/reports/tests/testDebugUnitTest/`; for lint, `app/build/reports/lint-results-debug.html`. +- Do **not** run emulator/E2E (`connectedDebugAndroidTest`) here — that's CI's job unless the + user explicitly asks. diff --git a/.gitignore b/.gitignore index 13cc684..f0c4891 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ captures/ # Kotlin .kotlin/ + +# Claude Code — personal settings (the shared settings.json IS committed) +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..46ccb4c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +LibreMail is a GPL-3.0 Android email client (Kotlin, Jetpack Compose, Material 3). +See `@README.md` for OAuth/account setup (Gmail, Outlook), the tech stack, and the +offline-first architecture — this file only covers what isn't obvious from the code. + +## Build, test, lint + +Use a **JDK 17–21** for the Gradle daemon. AGP 9.2 does **not** support JDK 25 — if +`JAVA_HOME` points at 25+, builds fail. Commands (PowerShell: use `.\gradlew`): + +```bash +./gradlew :app:assembleDebug # build debug APK +./gradlew :app:testDebugUnitTest # JVM unit tests +./gradlew :app:lintDebug # Android lint +# single unit test: +./gradlew :app:testDebugUnitTest --tests "org.libremail.data.SomeClassTest" +``` + +E2E/instrumented tests need a booted emulator: `./gradlew :app:connectedDebugAndroidTest`, +or via Gradle Managed Devices `./gradlew e2eGroupDebugAndroidTest` (whole matrix) / +`./gradlew api29DebugAndroidTest` (one API level). The managed-device list in +`app/build.gradle.kts` must stay in lockstep with the E2E matrix in `.github/workflows/ci.yml`. + +**Before treating a change as done**, run the fast CI gate: `assembleDebug` + +`testDebugUnitTest` + `lintDebug` (the `/preflight` skill does this). Emulator E2E is +left to CI unless asked. + +## Build-config gotchas + +- **Built-in Kotlin (AGP 9.x).** Kotlin compilation is handled by AGP's built-in Kotlin; + the Kotlin version (2.4.0) is pinned via the root `build.gradle.kts` buildscript classpath. + **Never apply the `org.jetbrains.kotlin.android` plugin** — it throws a ClassCastException + against AGP 9's DSL. (The `kotlin-android` alias in `libs.versions.toml` exists but must + not be used.) `libs.versions.toml` still supplies all *library* versions. +- **KSP, not KAPT** for all annotation processing (Hilt, Room). +- Room schemas are exported to `app/schemas` and validated by migration tests — commit + schema changes. +- OAuth client IDs come from `secrets.properties` (git-ignored) via `BuildConfig`; the + build works without it (empty/placeholder values). + +## Code conventions + +- Sources live under `app/src/{main,test,androidTest}/kotlin/`; package root `org.libremail` + (applicationId `org.libremail.app`). +- **Every source file starts with** `// SPDX-License-Identifier: GPL-3.0-or-later` (or the + `` form for XML/Markdown). All 117 current `.kt` files follow this. +- `kotlin.code.style=official`. + +## Testing + +JVM unit tests use JUnit4 + `kotlin.test`, **Turbine** for `Flow`, **MockK** for mocks, +**GreenMail** for a real in-process IMAP/SMTP server, and coroutines-test. `org.json` is +pulled in as a real dependency for unit tests because `android.jar`'s version is a no-op stub. + +## Repo etiquette + +- Branch off `main`; branch names like `feat-…` / `fix-…`. PRs target `main` and must pass + the `CI passed` gate. +- **Conventional Commits** for commit subjects and PR titles: `type(scope): summary` + (`feat`, `fix`, `chore`, …), matching existing history.