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 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 21:42:31 -05:00
co-authored by Claude Opus 4.8
parent 0adf1319f6
commit c24c53ea01
5 changed files with 179 additions and 0 deletions
+61
View File
@@ -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)
+17
View File
@@ -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"
}
]
}
]
}
}
+35
View File
@@ -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.