# Bookshelf — session handoff Written 2026-09-06 by the Opus orchestrator, after a sprite restart forced a fresh session. ## Read these first, in order 1. `docs/SPEC.md` — the authoritative product/technical contract. Unchanged and still correct. Every worker prompt must point at it. Do not restate it; do not let it drift. 2. This file — operational state, what's done, what bit us, what's next. ## Operating model (the user explicitly asked for this — keep it) The user is on the **$20/mo Pro plan** and wants Opus used sparingly. - **Opus = orchestrator only.** Write specs, launch workers, verify results, decide. Do NOT write app code yourself. Do NOT read large files into Opus context. - **Sonnet = all implementation**, via `claude -p` (NOT the Agent tool — the user asked for `claude -p` specifically, and it keeps worker output out of the orchestrator's context). - Read worker output via `logs/.summary` / `jq -r '.result'`, never by cat-ing source. - Quota status: user reported **62% of the 5h window consumed** at ~19:45 on 09-05. Worker A alone cost **$2.40 / 85 turns**. Budget accordingly; prefer resuming a session over restarting one. ### How to launch a worker ``` cd ~/bookshelf && nohup ./tasks/run-task.sh ./tasks/.txt >/dev/null 2>&1 & ``` `tasks/run-task.sh` is quota-aware: on a usage-limit error it sleeps `POLL` (600s) and resumes the SAME session rather than restarting, up to `MAX_WALL` (24h), and does not count quota waits against its 3-strike hard-failure budget. It writes: `logs/.json` (final result), `.err`, `.sid` (session id), `.state` (progress), `.summary`. **HAZARD — do not repeat:** never edit `run-task.sh` while workers are running. Bash reads scripts by byte offset; swapping the file mid-run makes live workers resume inside unrelated code and can spawn duplicate `claude` processes that burn quota on finished work. If you must change it, write a NEW file and use that for the next wave. ## Environment - JDK 21: `~/toolchain/jdk21` (system java is 25 — too new for AGP, do not use it) - Android SDK: `~/toolchain/android-sdk` (platforms;android-37.0, build-tools;37.0.0, platform-tools) - **No KVM, no emulator.** Verify only via `./gradlew assembleDebug`, JVM unit tests, and Paparazzi PNG rendering. Never claim the app was "run". - PocketBase v0.40.2 service, **127.0.0.1:8090, deliberately NOT internet-exposed** (no `--http-port`, so the sprite proxy can't reach it). Restart: `sprite-env services restart pocketbase`. Logs: `/.sprite/logs/services/pocketbase.log`. - Superuser creds: `server/.dev-credentials` (gitignored). ## STATE: what is DONE ### Wave 1A — server: COMPLETE and verified by the orchestrator (not just self-reported) `server/` contains `setup-schema.sh` (idempotent), `create-user.sh`, `pb_hooks/main.pb.js`, `pb_migrations/`, `deploy/` (systemd unit, Dockerfile, compose, backup.sh, README covering Tailscale vs port-forward+Caddy), `README.md`, `.gitignore`. Independently re-verified on 09-06 after fixing the service: | Check | Result | |---|---| | anonymous LIST books/shelves/bookcases | **403 / 403 / 403** | | anonymous self-registration | **403** | | `/api/health` | 200 | **`pb_hooks/main.pb.js` is INTENTIONAL, not scope drift.** PocketBase's `listRule` is a row filter, so anonymous LIST would otherwise return `200 []` instead of an error. The hook forces 403. Keep it; it is why the table above passes. It is auto-loaded by the stock binary. ### Wave 1B — Android scaffold + design system: COMPLETE, verified by the orchestrator The pre-restart worker had gotten much further than the last handoff recorded. On 09-06 the orchestrator found everything on disk (theme, 7 shared components, MainActivity, BookshelfApplication, Paparazzi test, all 8 Literata TTFs) and only THREE compile errors, all the same class of trivial import bug — fixed directly by the orchestrator rather than spending a worker session on two-line edits: - `import androidx.compose.foundation.layout.weight` (x2: BookshelfScaffold.kt, the Paparazzi test) — that resolves to the *internal* `RowColumnParentData.weight`. `weight` is a ColumnScope/RowScope member; it needs NO import. Delete the line. - SyncStatusBar.kt was missing `import androidx.compose.runtime.getValue`, so `val x by transition.animateFloat(...)` had no delegate. | Check | Result | |---|---| | `./gradlew assembleDebug` | **exit 0** — app-debug.apk, 47MB | | `./gradlew testDebugUnitTest` | **exit 0** | | `./gradlew recordPaparazziDebug` | **exit 0** — 10 PNGs, light+dark | Snapshots: `app/app/src/test/snapshots/images/`. The orchestrator eyeballed scaffold-light: warm paper ground, Literata serif title, thin gold hairline rule. Matches the design language. ### Repo is now a git repo `git init` + baseline commit `8bcd9f7` at the 1B-green point. This is deliberate: it lets the orchestrator verify a wave with `git diff --stat` / `git log` instead of reading source files into Opus context, and gives a rollback that isn't a whole-sprite checkpoint restore. Root `.gitignore` covers build outputs, `server/pb_data`, `.dev-credentials`, worker logs. ## STATE: what is IN FLIGHT ### Wave 2 — C (data layer) + D (metadata/scanning): LAUNCHED 09-06 ~01:59Z, running in parallel Prompts: `tasks/C-data.txt`, `tasks/D-metadata.txt`. Sessions: C-data=c2b92ca5-55d7-49b2-8a89-dc36e3ba4c9f, D-metadata=9a2c0de8-e475-4a43-9302-bc66889ee2bd Two coordination devices were put in place before launch; keep them for wave 3: 1. **`tasks/gw` — a `flock`-serialized gradle wrapper.** Both workers share ONE Gradle project dir; concurrent `./gradlew` runs clobber each other's outputs. Both prompts forbid `./gradlew` and require `tasks/gw`. Reuse this for every future parallel wave. 2. **Disjoint file ownership, stated as a hard boundary in each prompt.** C owns data.local, data.remote, data.repo, data.prefs, AppContainer, BookshelfApplication. D owns data.metadata and ui.scan plumbing. NEITHER may touch `app/build.gradle.kts` or `libs.versions.toml` — the orchestrator confirmed every wave-2 dependency is ALREADY declared and wired, so there is no legitimate reason for a worker to edit a build file. D must not wire MetadataRepository into AppContainer (C owns it); D reports the one-line snippet instead, to be applied later. ## STATE: what is NOT done ### Waves 3-4 — not started. Prompts not yet written. - **Wave 3 (after C+D land and are verified):** E — the six screens (setup, library, detail, scan, locations, settings). - **Wave 4:** F — Paparazzi screenshots for the user to judge the look, release keystore + signed APK, top-level README, end-to-end sync test against the live PocketBase. ## Gotchas already paid for — do not rediscover 1. **Migration filename ↔ `_migrations` desync.** Worker A renamed `1788636563_created_books.js` to `...564...` to fix an alphabetical-replay ordering bug (`books` sorted before `shelves`, breaking the relation). Correct for fresh instances, but the dev DB still had the old name recorded applied, so PocketBase tried to re-create `books` and crash-looped 9 times. Fixed via `UPDATE _migrations SET file=...`. **If you ever rename a migration, update that table too.** DB backup: scratchpad `data.db.bak`. 2. Rule semantics: in PocketBase `""` means PUBLIC, `null` means superuser-only. Confusing these is exactly how the library would end up world-readable. 3. `claude -p --output-format json` writes its log only at exit; a 0-byte `.json` means the worker is still running or was killed, not that it failed. 4. System JDK is 25 and will break AGP. Workers must export `JAVA_HOME=~/toolchain/jdk21` (run-task.sh already does). ## Verification standard (hold workers to this) Workers self-report optimistically. Before accepting any wave: - Re-run the security curls above yourself. The user's stated requirement is that this not be "accessible to everyone in the world"; that check is non-negotiable and cheap. - Require `assembleDebug` + `test` exit 0, and confirm artifacts exist on disk. - Treat "I couldn't get Paparazzi working so I skipped screenshots" as a finding to report to the user, not something to paper over — the user explicitly cares how this looks. ## Open questions for the user (not yet asked — deferred, not forgotten) - Where the server will actually live (home box vs a sprite) — only affects the deploy README. - Their two account emails, for `create-user.sh`. Not needed until the app can log in.