Files
kvitt/CLAUDE.md
T
jacobh2 f1abb0d2ef
Build Kvitt images / build (push) Failing after 51s
Server gate: cargo audit
The server gate becomes cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test
(CLAUDE.md, server/README.md). The pre-commit hook runs cargo audit --no-fetch only when server/Cargo.lock or a
server Cargo.toml is staged, and skips with a message when cargo-audit or the advisory database is missing.
First run: 0 advisories, 0 warnings over 319 crates; no audit.toml needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 19:36:54 +02:00

17 KiB
Raw Blame History

CLAUDE.md

Operating guide for AI agents and humans working on Kvitt. This is the only working-rules file; it says how to work here, not what the product is. For the product, read SPEC.md. It was seeded on 2026-10-17 from the Ebb project's rules after a year of running Opus agents in parallel; keep what fits, delete what does not, but keep one file.

The product in one paragraph. Kvitt is a Splitwise-style shared-expense app for exactly two people, the owner (Jacob, admin) and his spouse, in Sweden. A native Android app (Kotlin, Compose) keeps a full offline copy of the household's expense accounts, expenses, settlements and audit log in Room and syncs with a small Rust server (axum, SQLite) that runs in Docker on a Raspberry Pi 3 at home, reachable at https://icehack.asuscomm.com:8443. Money is SEK in integer öre. The phone computes balances, settle-up proposals and spending analytics itself; the server stores, syncs, schedules recurring expenses, sends push notifications and hosts the APK for in-app updates. Everything else is in SPEC.md.


SOURCE OF TRUTH

  • SPEC.md wins. If a plan, this file or existing code disagrees with the spec, the spec is right.
  • A revised decision updates the spec in the same landing, with an entry in docs/decisions.md (numbered D-001…; never deleted, revised in place with a pointer). Only a docs agent edits SPEC.md and docs/decisions.md (see Numbering).
  • Each PLAN-*.md lists the spec sections to read first. Read them before writing anything.
  • Never encode a threshold, limit or rule that is not in the spec. Ask, or leave a TODO(spec); a wrong number is worse than a missing one.
  • One handoff: docs/HANDOFF.md, rewritten in place. One decisions log. One working-rules file (this one). Delete superseded copies the same day; never keep two sources of truth.

REPOSITORY LAYOUT

SPEC.md, PLAN-*.md     what to build; prompts/ holds a ready-to-paste start prompt per plan and the orchestrator prompt
contracts/             openapi.yaml, test-vectors/*.json, sync and push examples: the cross-team contracts (SPEC §14)
docs/HANDOFF.md        state of the work and how it is run
docs/decisions.md      decisions that changed or refined the spec
docs/ux/               the UX sprint deliverables (SPEC §13)
docs/spikes/, docs/validation/   what the device and the data showed (reports are deliverables)
android/               the Android app: one Gradle project (settings.gradle.kts, gradle/libs.versions.toml), modules as in SPEC §14.7
server/                the Rust backend: one Cargo workspace, crates kvitt-domain, kvitt-server, kvitt-import (SPEC §14.7)
infra/                 docker compose stack for the Pi (caddy, server, cert-sync, backup), image build scripts, ops runbooks
tools/                 gradle-serial.sh, clean-builds.sh, install-git-hooks.sh, git-hooks/, dependency-report/latest.py

Who owns which directory during parallel work is in SPEC §14.7. An agent edits only the paths its plan owns.


ORCHESTRATION: HOW WORK IS RUN

The owner talks to one orchestrator session. The orchestrator plans, briefs, lands, gates, installs and answers; it never writes code or repository changes itself, however small (docs and config included). Every change goes through an Opus implementation agent. If the orchestrator slips and edits something, it keeps the work and hands it to an agent to own; it never reverts or redoes it.

  • One Opus agent per task, in its own git worktree (.claude/worktrees/agent-<id>, branch of the same name). The agent's first step is git reset --hard main, then copying android/local.properties, android/app/google-services.json and anything in infra/secrets/ it needs (all git-ignored) from the main checkout. It commits on its branch, rebases on main, runs the gate, and reports; it never lands, pushes or removes its worktree.
  • The orchestrator lands: git merge --ff-only <branch> in the main checkout, re-runs the gate on main, installs on the phone if attached, removes the worktree (git worktree remove --force --force, falling back to rm -rf + git worktree prune) and deletes the branch. No merge commits. Never rebase main.
  • Briefs are the whole context an agent has. A brief states: the goal, what to read first (file paths), what is already established, the exact numbers it may use (next free decision, spec version, schema), the setup steps, the gate, what to report. If the orchestrator cannot write a clear brief, it does not understand the task yet.
  • Agents' reports are not shown to the owner. The orchestrator extracts the decision: one line per finding, fixed or not, and relays every owner step in chat.
  • Subagents are Opus. Haiku or Sonnet only for a read-only scan the owner explicitly asks for that way.
  • Numbering (decisions, spec versions, schema versions, contract requests) is handed out by the orchestrator in the brief. Two parallel agents get disjoint ranges; the second to land renumbers to be contiguous on rebase. Implementation agents never edit SPEC.md or docs/decisions.md: they put proposed decision entries and spec text, numbered from their range, in their report, and the orchestrator has them recorded by a docs agent, landed together with the implementation.
  • The UX sprint (U1, U2) runs in the owner's own Claude session in ~/orca/workspaces/kvitt/UX-sprint on branch UX-sprint, with no orchestrator. Its results reach the main line only through docs/ux/proposed-decisions.md (owner answers, proposed decisions and spec text, no numbers): the orchestrator numbers them and has a docs agent record them in SPEC.md and docs/decisions.md (SPEC v0.8 recorded UX-1 to UX-8 as D-047 to D-054).
  • Parallel agents each have their own worktree, build through tools/gradle-serial.sh (one Gradle build at a time on this machine), and take turns on the phone: one agent on the phone at a time, named in the brief.

THE OWNER

  • Owner steps go in the chat reply, never only in a file. Exact section headings and button labels as they appear on screen, in order, with the expected result. Taps and yes/no observations only.
  • Never ask the owner to read numbers, tables or logs off the phone. Pull them over USB (adb pull of the app's external files, adb shell run-as <package> cat for internal files, adb logcat -d), summarise with a script, aggregates only. Every debug feature writes a file that can be pulled.
  • Do yourself what you can do: package checks, installs, pulls, gates. HUMAN steps are for taps, permissions, sign-ins and looking at a screen.
  • Confirm before anything destructive on the owner's data. "Delete everything" on a real-data install waits for the owner's yes.
  • The owner's own files (for example a personal todo) are not read or edited by agents unless asked.
  • When the owner has answered a question, record the answer in the decisions log or the handoff the same day; stale open questions cost the owner's time twice.

THE PHONE

Two phones: the owner's Samsung Galaxy S26 and the spouse's Galaxy S26+, both Android 16. Agents only ever touch the phone attached to this MacBook over USB, which is the owner's unless the brief says otherwise.

App Application ID
Real app, release se.hagstedt.kvitt
Real app, debug se.hagstedt.kvitt.debug
Instrumented-test APKs se.hagstedt.kvitt.<module>.test
  • adb is at /opt/homebrew/share/android-commandlinetools/platform-tools/adb on this MacBook; it is not on the shell PATH.
  • Use adb only for this project's apps. Do not browse, copy or change anything else on the device.
  • Before the first install in a session, adb shell pm list packages | grep <id> and confirm the ids above still hold.
  • Never clear or uninstall a build that holds the owner's real data without asking. Instrumented tests install their own APK and never touch the real app's data.
  • Never log personal content (values, names, titles, notes), in debug builds either. Logs, screenshots and database files from the device stay out of git; committed reports carry aggregates.

BUILD AND VERIFY

Everything builds locally on this MacBook. No hosted CI, no remote build service.

Two projects, two gates. The Android app is the Gradle project in android/; the server is the Cargo workspace in server/. A change that touches both runs both gates.

JDK. Gradle runs on JDK 25 (current LTS): android/gradle/gradle-daemon-jvm.properties asks for 25 and android/gradle.properties points Gradle at Homebrew's openjdk@25 (auto-download off). The shell's java is a macOS stub or a newer non-LTS: run Gradle as PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh <tasks> and never set JAVA_HOME. Do not install another JDK or change the pin without asking.

Android SDK. android/local.properties must contain sdk.dir (/opt/homebrew/share/android-commandlinetools on this MacBook). Add the line if missing; never overwrite the file, it also holds client ids and tokens; never commit it.

# Android gate, from android/ (the script finds the repository root itself and runs android/gradlew):
../tools/gradle-serial.sh check       # THE Android gate: build, unit tests, lint, architecture rules, screenshot tests
../tools/gradle-serial.sh installDebug   # install the debug app on the attached phone
# Server gate, from server/:
cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test   # THE server gate
# Either directory:
tools/install-git-hooks.sh            # once per checkout
tools/clean-builds.sh [--dry-run]     # delete Gradle build output in every checkout and worktree (disk)
python3 tools/dependency-report/latest.py   # version drift of android/gradle/libs.versions.toml
  • tools/gradle-serial.sh check (Android) and cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test (server) are the only gates. Every check a plan asks for must be reachable from one of them (the OpenAPI match test and the test-vector tests included). Report gates with the real result lines (BUILD line, actionable tasks, test counts from fresh XML only). Never report a check as passing that you did not run in this session.
  • One Gradle build at a time. Parallel builds exhaust memory. Always tools/gradle-serial.sh; it waits on a lock in /tmp and then runs android/gradlew of your worktree.
  • Disk. Three agents plus the main checkout can fill a disk. ~/.gradle/init.d/cache-retention.gradle.kts (user-level, outside the repo) keeps ~/.gradle/caches at 7 days with cleanup on every build; Gradle 9 accepts build-cache retention only there. Run tools/clean-builds.sh after removing a worktree or when free space is under about 5 GB. Deleting ~/.gradle/caches/build-cache-1 and old ~/.gradle/caches/<version> folders is allowed; the next build is a slow full one. An agent that hits "no space left on device" loses only uncommitted work: free space and resume it, do not respawn.
  • Version drift: run the dependency report at the start of every plan. A library more than one stable minor behind gets a line in the plan's report; a toolchain component more than one line behind triggers an upgrade plan first.
  • Screenshots. Goldens are committed; check fails when a render differs. Re-record only for an intended change and open every changed PNG before committing. Code that compiles is not evidence of a good screen.
  • Every schema change gets a migration and a migration test (sqlx migration on the server, Room migration on the phone). User data stored in the database is not derived data and must survive every migration.
  • Contracts are versioned. A change to any contract file (contracts/openapi.yaml, the SQLite schema, the sync protocol, the Room entities, contracts/test-vectors/*.json, the push payload) requires a spec version bump and a docs/decisions.md entry, landed together with it: the implementation agent proposes both in its report (numbered from the brief) and a docs agent records them (Numbering). Representational edits to contracts/openapi.yaml (key order, $ref naming, description wording) need neither (SPEC §14.8).
  • Cargo builds are not serialised by gradle-serial.sh; run at most one cargo build/cargo test per worktree at a time and never build Docker images for the Pi while a Gradle build runs.

ARCHITECTURE RULES

Enforced by an architecture check inside check where possible. Fill in from SPEC; the shape that worked:

  • Layering (Android): feature → core:model, core:design; core:design → core:model; domain → core:model; data → core:model, domain; core:fakes → core:model, domain; adapter:fcm → core:model. Only :app sees everything and binds the :data repositories to the :core:model interfaces through Hilt. Features never see :data or :domain (SPEC §6.2). Pure modules (:core:model, :domain) import only kotlin.*, kotlinx.* and the project's own packages: no java.time, no java.io, no Android.
  • Vendor SDKs live in one adapter module each and map to the project's own records inside it. Sources are plugins behind a registry; nothing names a vendor outside its adapter. Source ids are open strings, never enums.
  • The UI contract is the contract: features consume *State classes through repository interfaces; a contract change is made in the model, the fakes and the real repositories in the same change.
  • Fakes for every repository, driven by scenarios, so screens render and screenshot-test without a device. Fakes never reach a release build.
  • Fast-moving APIs: confirm current usage from the official documentation at implementation time. Do not rely on memory.
  • Licences first: no vendor binary and no model file is committed, bundled or downloaded by a build before the owner has cleared its licence.
  • Server layering: kvitt-domain has no I/O and depends only on serde (serde_json only as a dev-dependency for the vector tests; ids are plain strings); kvitt-server and kvitt-import depend on it, never the reverse. Money and split rules exist only in kvitt-domain and :domain, and both pass the same contracts/test-vectors/*.json.
  • No unwrap()/expect() in server non-test code, start-up included; no unsafe; errors map to the error codes of SPEC §14.3. Enforced per crate by #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] at every crate root, main.rs included (D-007); a new crate adds the same line.

ANTI-PATTERNS

  • No silencing: no !!, no @Suppress / @SuppressLint, no lint-baseline additions to make a check pass. Fix the type or the code.
  • No mocking libraries (Mockito, MockK). Inject the dependency and use a typed fake or a recorded fixture.
  • No empty catch. Handle or log explicitly; never swallow CancellationException.
  • Testing integrity: never delete, disable or loosen a failing test to get green. Fix the code, or change the test with a stated reason.
  • No hard-coded colours, sizes or strings in feature modules. Tokens from one design file; strings from resources, every supported language.
  • No placeholder text in previews or screenshots.
  • No assumptions: read a file before modifying or citing it.
  • Never commit: local.properties, keystores, .aar or other vendor binaries, model files, client ids, tokens, personal data, google-services.json, service-account keys, TLS keys, SQLite databases, backups, the Splitwise CSV, built APKs.

EXECUTION PROTOCOLS

  • Read before edit: read the whole target file first. After editing, check for duplicated lines and intact imports.
  • Stay in scope: each plan has an "Out of scope" list. Respect it; note ideas in the plan's report instead of building them.
  • Stay inside the repository. Nothing outside it without an explicit allowance from the owner (the user-level Gradle init script was one).
  • Stop and ask when: a step could disturb the owner's real data or another app on the phone, the build needs a tool the owner has not installed, a licence is not cleared, or the spec is ambiguous in a way that changes a decision.
  • Always commit finished work once the gate is green. Never commit mid-change or over a failing check. Never commit on main; one branch per task.
  • Push and pull requests need an explicit ask.
  • Reports are deliverables: docs/spikes/, docs/validation/, complete, aggregates only, with a "Results (owner's phone)" section when a measurement is still to come.
  • Navigation: git ls-files to discover, git grep to search; never ls -R, never find without -maxdepth.

OUTPUTS (orchestrator replies)

Default reply: 1–5 lines. Lead with the answer; a yes/no question gets yes or no on line one. One line per finding. Never re-narrate a diff or an agent report; extract the decision. No "want me to X?" when X is obvious. May run long: owner steps (exact labels, expected results), raw failure output, an open trade-off that needs the owner's decision, and the reports a plan asks for.