PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6, M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a start prompt per plan in prompts/ plus prompts/README.md (dependency order and parallel waves). SPEC v0.2: owner answers D-001 (development-first, M2c split into M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once on Drive), D-003 (Splitwise CSV format, English and Swedish variants, category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of ambiguous contract points stated explicitly in §14.3 to §14.5. HANDOFF §2 lists the plans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
PLAN-M2b: Android skeleton and :domain
1. Goal
Create the Gradle project in android/ with every module of §6.2, the version catalog, the architecture check, the :core:model UI contract and domain types, a :domain that passes every shared test vector including analytics, empty fakes, and an :app that launches to an empty themed screen. After it lands, M5 (data), M6 (design) and M7a to M7f (features) build against the same interfaces in parallel.
2. Read first
- SPEC §6.2 Android app (stack table, module graph, the paragraph on the architecture check).
- SPEC §14.2.4 UI contract: types and repository interfaces in
:core:model. - SPEC §14.5 Domain rules as pure functions (14.5.1 Types, 14.5.2 Functions, 14.5.3 Error tokens, 14.5.4 Analytics, 14.5.5 Test-vector format).
- SPEC §9.1 Splits, §9.3 Balances, §9.4 Settlement allocation proposal, §9.6 Limits and validation.
- SPEC §4 FR-14 Analytics (AC-14.2, AC-14.7, AC-14.10) and FR-19 Localisation and formatting.
- SPEC §14.2 Room entities (only to know what
:datawill hold; M5 writes it). - SPEC §11.2 (row M2b), §11.3 (notes M2b, M7x, M8), §14.7 Module layout and ownership map, §14.8.
- Files:
contracts/test-vectors/*.json,android/gradle.properties,android/gradle/gradle-daemon-jvm.properties,tools/gradle-serial.sh,tools/dependency-report/latest.py,tools/git-hooks/pre-commit,CLAUDE.md(BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS, THE PHONE).
3. Already established
- Kotlin 2.x latest stable; Compose with Material 3;
minSdk36;targetSdkandcompileSdkthe latest stable API; Room with KSP; Hilt; Ktor client (OkHttp engine) with kotlinx.serialization; WorkManager; kotlinx-datetime; Coil; Vico or Canvas charts (decided by M6); FCM only in:adapter:fcm; version catalogandroid/gradle/libs.versions.toml; Gradle daemon on JDK 25 (pinned, existing files); bytecode target Java 17 (§6.2). - Screenshot tests use Roborazzi or AGP Compose Preview Screenshot Testing, whichever is stable at implementation time, recorded in decisions (§6.2).
- Modules and allowed edges exactly as the §6.2 table; features never depend on
:dataor:domain;:core:fakesonly for tests, screenshot tests and debug previews (§6.2, CLAUDE.md Layering). - The architecture check is a Gradle task in
check, owned by M2b; it fails on any other dependency edge and onjava.time,java.ioorandroid.*imports in:core:modeland:domain(§6.2). :core:modelpackagese.hagstedt.kvitt.modelholds the §14.2.4 types and interfaces verbatim; domain input/output types inse.hagstedt.kvitt.model.money(§14.5.1);:domainpackagese.hagstedt.kvitt.domainwith the §14.5.2 functions and §14.5.4 analytics (§14.5).- Wire value of every enum = lowercase name; icon tokens are Strings from
CategoryIcons.ALL, the 30 tokens of §9.7 in that order (§14.2.4). :domainpasses every vector file incontracts/test-vectors/, includingspend_series.jsonandresolve_range.json(Kotlin only) (§14.5, §11.2).- Strings live in
values/strings.xml(English fallback) andvalues-sv/strings.xml; a lint or test in the gate fails on a key missing from either file (AC-19.1). - Application ids: release
se.hagstedt.kvitt, debugse.hagstedt.kvitt.debug, instrumented testsse.hagstedt.kvitt.<module>.test(CLAUDE.md THE PHONE). - No mocking libraries, no
!!, no@Suppress, no lint-baseline additions (CLAUDE.md). - M1 has landed (
c6df2ab); its readings are explicit in SPEC v0.2: domain functions receive only non-deleted rows; analytics vector keys per §14.5.5;bp_out_of_rangemeans above 10000;resolveRangeedge cases per §14.5.4 (howCUSTOMwith a null date signalsvalidation_failedis for M2b to fix and report). :core:model/**and:domain/**become contract files once landed (§14.8).
4. Owned paths
From SPEC §14.7, verbatim:
android/settings.gradle.kts,android/build.gradle.kts,android/gradle.properties,android/gradle/**,android/build-logic/**(owner M2b; after landing: "later plans may add catalog entries and module includes (rebase resolves)").android/core/model/**,android/domain/**(owner M2b; after landing: "contract-change plans only").android/core/fakes/**(owner M2b; after landing: "M7x add files only underfakes/<feature>/").android/app/**up to and including "the M2b shell" (the rowandroid/adapter/fcm/**,android/app/**(after the M2b shell) | M8).
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
Open points the orchestrator must settle in the start prompt's notes (stop and ask if they are not settled): (a) "every module of §6.2" means build files in android/core/design/, android/data/, android/adapter/fcm/ and android/feature/*/, which §14.7 gives to M6, M5, M8 and M7x: whether M2b creates them as empty module shells that ownership then passes on; (b) where the theme of the "empty themed screen" lives, given :core:design belongs to M6; (c) which module holds the "string resource files for both languages" M2b creates; (d) which screenshot-test tool, and whether M2b wires it into check (needs a decision id); (e) whether feature modules use Hilt (hiltViewModel) or receive repositories as parameters, and how cross-feature navigation is expressed, since the feature convention plugin in build-logic fixes it for M7a to M7f; (f) the package of the §14.5.4 analytics types (:core:model money package or :domain); (g) what an "empty scenario" is for repositories whose state has required fields (for example HomeState.me).
5. Deliverables
android/settings.gradle.kts,android/build.gradle.kts: the project with every module of §6.2.android/gradle/libs.versions.toml: every library and plugin of §6.2 with the versions chosen.android/gradle/wrapper/(gradle-wrapper.properties,gradle-wrapper.jar) andandroid/gradlew,android/gradlew.bat.android/gradle.properties,android/gradle/gradle-daemon-jvm.properties: the existing JDK 25 pin and flags kept, merged with what the project needs.android/build-logic/**: convention plugins (pure JVM module, Android library, feature module, application) and the architecture check task wired intocheck.android/core/model/**: §14.2.4 and §14.5.1 types and interfaces, verbatim, plus unit tests for enum wire values andCategoryIcons.ALL.android/domain/**: §14.5.2 functions and §14.5.4 analytics; a vector test runner reading../contracts/test-vectors/*.jsonfor all seven files.android/core/fakes/**: one fake per repository interface of §14.2.4 driven by a scenario type, with an empty scenario each; fakes compute derived values through:domain.android/app/**:Applicationclass,MainActivityshowing an empty themed screen, debugapplicationIdSuffix.debug, manifest.- Module shells for
:core:design,:data,:adapter:fcm,:feature:{onboarding,home,account,expense,settle,analytics,settings}(per open point a). values/strings.xmlandvalues-sv/strings.xmlwith the app name (per open point c), and the missing-translation check (Android lintMissingTranslationas an error, or a test) incheck.- Screenshot-test tooling wired into
check(per open point d) with one screenshot of the empty screen in light and dark. docs/decisions.mdentry for the screenshot tool only if the start prompt grants a decision id and the path (the file is not owned by M2b; otherwise put the text in the report as a contract request).
6. Steps
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-m2b -b agent-m2b main, then in.claude/worktrees/agent-m2b:git reset --hard main. Copyandroid/local.propertiesfrom the main checkout if it exists (cp /Users/jhag/projects/kvitt/android/local.properties android/); if it does not, create it with the single linesdk.dir=/opt/homebrew/share/android-commandlinetools. Never overwrite an existing one, never commit it. Runtools/install-git-hooks.sh. - Read §2 in order. Confirm
contracts/test-vectors/exists onmain; if not, stop and report. - Versions ("latest stable" in §6.2): resolve from official sources and record each with its source: Kotlin (kotlinlang.org releases), Android Gradle Plugin and the latest stable
compileSdk/targetSdkAPI level (developer.android.com release notes), Gradle wrapper (gradle.org releases; must support JDK 25 daemon and the AGP chosen), Compose BOM, Material 3 (developer.android.com/jetpack/androidx/releases), KSP matching the Kotlin version, Room, Hilt, Ktor, kotlinx.serialization, kotlinx.coroutines, kotlinx-datetime, WorkManager, DataStore, Coil, Roborazzi or the AGP screenshot plugin. Check whether the AGP Compose Preview Screenshot Testing plugin is stable; report the finding for open point (d). - Generate the Gradle wrapper with the chosen version; keep
android/gradle.propertiesandandroid/gradle/gradle-daemon-jvm.propertiescontent (JDK 25 pin, auto-download off). - Write
build-logicconvention plugins and the architecture check (allowed edges per the §6.2 table; forbidden importsjava.time,java.io,android.*in:core:modeland:domain); add a failing-case test of the check ifbuild-logicsupports it. - Write
:core:modelexactly from §14.2.4 and §14.5.1 (no added fields). Write:domaintest-first against all seven vector files, then implement §14.5.2 and §14.5.4. - Write
:core:fakes(one fake per interface, scenario-driven, empty scenario each) and the module shells. - Write
:app: themed empty screen, string resources in both languages, missing-translation check, screenshot tooling with one golden in light and dark; open the PNGs before committing (CLAUDE.md Screenshots). - Run
python3 tools/dependency-report/latest.pyand record its lines. - Run the gate (§7). If the owner's phone is attached and the start prompt names this plan as the one on the phone:
PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh installDebug, after/opt/homebrew/share/android-commandlinetools/platform-tools/adb shell pm list packages | grep se.hagstedt.kvittto confirm the ids. - Commit on
agent-m2b,git rebase main, rerun the gate. Never merge, push or remove the worktree.
7. Gate
From SPEC §11.2, verbatim: "Android gate".
Command, from android/: PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check.
Green means: BUILD SUCCESSFUL in this session; the architecture check, :core:model and :domain unit tests (all seven vector files), lint with the missing-translation rule, and the screenshot test all ran (task names listed in the report); test counts from fresh XML under build/test-results/.
8. HUMAN steps
From SPEC §11.2, verbatim: "Plug the phone in once to confirm installDebug works".
Where in the flow: after the gate is green (step 10). The orchestrator relays the step; the agent runs the package check and installDebug itself and reports the result. If the phone is not attached, report the step as outstanding.
9. Out of scope
- Room, DAOs, Ktor client, sync engine, token store, workers, real repositories, APK updater (M5).
- Theme tokens, typography, shared components, charts,
MoneyFormatter,DateFormatter(M6). - Any feature screen (M7a to M7f) and fake scenarios beyond the empty one (M7x add theirs).
- Navigation graph, Hilt bindings to
:data, deep links, FCM, notifications, network security config, release signing (M8). - Server work (M2a and later),
infra/(M2c-dev). - Adding fields to §14.2.4 types (contract change).
10. Report
- Branch
agent-m2b, head commit hash after rebasing onmain. - Gate output: BUILD line, actionable tasks line, the names of the architecture-check, lint and screenshot tasks that ran, unit test counts from fresh XML (per module).
- Versions chosen (Kotlin, AGP, Gradle, compile/target SDK, every catalog entry) with the official source; dependency-report lines.
- Screenshot tool chosen and why (and the decisions text if no decision id was granted).
- How each open point (a) to (g) was resolved, as given in the start prompt.
installDebugresult on the phone, or "outstanding".- Numbering used (decision ids, spec version) or
TODO(spec)occurrences. - Contract requests, open questions, spec gaps (with section numbers).
11. Numbering
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use TODO(spec) if none was given.