Files
jacobh2 76ee957f41 SPEC v0.3: orchestrator resolutions of the plans' open points
Records D-004 and D-015 to D-025: shells and ownership, server seams
(router middleware, rate_limit layer, tasks::start, AuditWriter and PushQueue
traits, next_occurrence_date in kvitt-domain, M3 owns recurring generation,
M4 lands after M3), OpenAPI parsed-value equality, feature convention and
strings, empty fake scenario and resolveRange exception, shared ledger rows and
editor fields in :core:design, no bundled font, pruning at 04:00, development
infrastructure as M2c-dev built it, M2c-pi allowances, release keystore,
production import procedure. Plans and prompts close their open points;
HANDOFF rewritten to the real state; CLAUDE.md: implementation agents never
edit SPEC.md or docs/decisions.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:23:38 +02:00

8.8 KiB

PLAN-M10: First release

1. Goal

Ship the first signed release to both phones from the production server on the Pi: release keystore, signed APK published, both phones paired, push working both ways, nightly backup on Drive, a restore drill, and the memory check. Record the evidence in docs/validation/first-release.md and docs/validation/restore-drill.md.

2. Read first

  1. SPEC §10.6 APK hosting and release, §10.9 Backups and restore, §10.10 Operations, §10.4 Server configuration and CLI (bootstrap, rotate-epoch), §10.7 (step 7, the FCM key on the Pi).
  2. SPEC §4 FR-1, FR-2 (pairing both phones), FR-13 Push notifications, FR-16 In-app update, FR-18 Backups and restore.
  3. SPEC §8.5 Transport (release network security config), §14.4.8 Epoch recovery.
  4. SPEC §11.2 (row M10), §11.3 (note M10), §14.7.
  5. infra/README.md, infra/RESTORE.md, infra/publish-apk.sh, infra/deploy.sh (from M2c-pi); android/app/build.gradle.kts (signing config, from M8).
  6. CLAUDE.md (THE PHONE, THE OWNER: confirm before anything destructive; EXECUTION PROTOCOLS: Reports are deliverables).

3. Already established

  • version_code = git rev-list --count HEAD of the released commit; version_name = YYYY.MM.DD-<short sha>; passed as kvitt.versionCode/kvitt.versionName by the release script (§10.6).
  • Release keystore kvitt-release.jks, alias kvitt, RSA 4096, validity 10000 days, created once by the owner with the keytool command of §10.6 in a directory outside the repository (D-024); android/local.properties holds its absolute path and the secrets in kvitt.release.storeFile, kvitt.release.storePassword, kvitt.release.keyAlias, kvitt.release.keyPassword; the owner keeps a copy in a password manager; losing it means reinstalling (§10.6).
  • infra/publish-apk.sh builds :app:assembleRelease through tools/gradle-serial.sh, writes apk/kvitt-<version_code>.apk and an atomic apk/latest.json, keeps the newest 5 (§10.6).
  • The landing page links "Download Kvitt" to the latest APK; the first install asks to allow "Install unknown apps" (AC-2.5, AC-16.3).
  • Pairing: kvitt-server bootstrap on the Pi prints the admin code (FR-1); the admin creates the partner invite in Settings and shares it (AC-2.1); after pairing the app syncs and asks for notifications (AC-2.9).
  • Push both ways: a change by one user notifies the other user's devices (AC-13.1).
  • Backup nightly 03:30 to Drive folder 1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV; local files deleted once confirmed on Drive (D-002, §10.9).
  • Restore drill follows §10.9 steps 1 to 7 and is documented in docs/validation/restore-drill.md; both phones must sync correctly afterwards (AC-18.2).
  • Memory: docker stats --no-stream with both phones syncing; server RSS under 100 MB (§10.10, §11.2).
  • Production runs on the Pi; development data on the Mac is not migrated (D-001).

4. Owned paths

From SPEC §14.7, verbatim: infra/** row, after-landing column "M10 updates runbooks" (that is infra/README.md, infra/RESTORE.md), and docs/validation/<file> | "the milestone that names the file", here docs/validation/first-release.md and docs/validation/restore-drill.md.

Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.

5. Deliverables

  • docs/validation/first-release.md: release version, publish result, pairing of both phones, push both ways, update check, backup on Drive, memory check result, outstanding items; a "Results (owner's phone)" section for anything still to come; aggregates only.
  • docs/validation/restore-drill.md: each §10.9 restore step as run, timings, the epoch rotation, both phones' re-push and balance check, issues found.
  • infra/README.md, infra/RESTORE.md: corrections found during the release and the drill.
  • On the Pi (not in git): published apk/kvitt-<version_code>.apk and apk/latest.json; a production household with both users.

6. Steps

  1. Worktree. From the main checkout /Users/jhag/projects/kvitt: git worktree add .claude/worktrees/agent-m10 -b agent-m10 main, then in .claude/worktrees/agent-m10: git reset --hard main. Copy android/local.properties, android/app/google-services.json and infra/secrets/ from the main checkout; the keystore stays outside the repository and is referenced by absolute path from local.properties (never copied into a checkout or git). Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M2c-pi, M3, M4 and M8 have landed and the Pi stack is healthy (infra/deploy.sh output, /health); if not, stop and report.
  3. Keystore (H-1). After the owner created it, add the four kvitt.release.* keys to the main checkout's android/local.properties (never overwrite other lines).
  4. Deploy the current main image to the Pi with infra/deploy.sh <tag>; publish the APK with infra/publish-apk.sh; check GET /apk/latest returns the new version_code.
  5. Bootstrap production: docker compose exec server kvitt-server bootstrap on the Pi (over SSH); give the code to the owner through the orchestrator.
  6. Owner's phone (H-2 to H-4): install from the landing page, pair, allow notifications. Confirm with adb shell pm list packages | grep se.hagstedt.kvitt that the release id is installed and does not clash with the debug build.
  7. Partner (H-5): the owner creates the partner invite in Settings and shares it; the partner installs, pairs, completes the profile step.
  8. Push both ways (H-6): one test expense from each phone (deleted afterwards); each phone confirms the notification.
  9. Backup: trigger docker compose exec backup /backup.sh; confirm on Drive with rclone lsf; confirm the next nightly run the following day if the session allows, else leave it in "Results (owner's phone)".
  10. Restore drill (confirm with the owner before step 1 of §10.9 because it stops production): follow infra/RESTORE.md exactly with the latest backup; both phones re-push after the new epoch; the agent compares balances from the server with what the phones hold (pulled over USB from the owner's phone; the partner phone by yes/no). Fix the runbook where it was wrong.
  11. Memory check: docker stats --no-stream on the Pi while both phones sync; record the server's memory.
  12. Write the two validation files. Commit on agent-m10, git rebase main. Never merge, push or remove the worktree.

7. Gate

From SPEC §11.2, verbatim: "docs/validation/first-release.md and docs/validation/restore-drill.md complete; server RSS under 100 MB". Green means: both files exist with every section filled (no TODO other than a "Results (owner's phone)" item named as outstanding); the docker stats line recorded in first-release.md shows the server below 100 MB with both phones syncing.

8. HUMAN steps

From SPEC §11.2, verbatim: "Create the release keystore; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive".

  • H-1, step 3, on the owner's Mac, in a directory outside the repository (D-024): keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000; choose the passwords, store them in the password manager, and tell the agent the keystore's absolute path and where the passwords are entered (the agent never echoes them).
  • H-2, step 6, on the owner's phone: open the invite link or https://icehack.asuscomm.com:8443/i/<code>, tap "Download Kvitt", open the download.
  • H-3, step 6: when Android asks, allow "Install unknown apps" for the browser, install, open Kvitt, pair with the code, allow notifications.
  • H-4, step 6 and later updates: allow "Install unknown apps" for Kvitt itself when the in-app update first asks (AC-16.3).
  • H-5, step 7, on the partner's phone: the same as H-2 and H-3 with the partner invite link, then the profile step.
  • H-6, step 8: each phone answers yes/no: a notification about the other's test expense arrived.

9. Out of scope

  • New features or fixes in app or server code (findings go in the report; fixes are new plans).
  • The Splitwise import (M11).
  • Gitea Actions changes and Pi provisioning beyond runbook corrections (M2c-pi).
  • Deleting anything on Drive.

10. Report

  • Branch agent-m10, head commit hash after rebasing on main.
  • Released version_code and version_name; image tag deployed.
  • Gate: the two files' section list and the docker stats server line.
  • Restore drill result in one line; runbook corrections.
  • Versions: none expected beyond what landed; state any tool used (keytool, rclone) and its version.
  • Proposed decision entries (never written to SPEC.md or docs/decisions.md by this agent) or TODO(spec) occurrences.
  • HUMAN steps done and outstanding, worded for relay.
  • 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.