Room entities/DAOs/DB, PocketBase Retrofit client + auth interceptor, SyncEngine (push-then-pull, LWW, tombstones, client-generated ids), SettingsStore, AppContainer. Open Library + Google Books merge, ISBN validation, CameraX + ML Kit scanner plumbing. Verified by orchestrator: assembleDebug exit 0; testDebugUnitTest exit 0, 68 tests, 0 failures, 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bThmkmyUUdqQpy3MXFFe5
8.4 KiB
Bookshelf — session handoff
Written 2026-09-06 by the Opus orchestrator, after a sprite restart forced a fresh session.
Read these first, in order
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.- 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 forclaude -pspecifically, and it keeps worker output out of the orchestrator's context). - Read worker output via
logs/<name>.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 <NAME> ./tasks/<NAME>.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/<name>.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 internalRowColumnParentData.weight.weightis a ColumnScope/RowScope member; it needs NO import. Delete the line.- SyncStatusBar.kt was missing
import androidx.compose.runtime.getValue, soval 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:
tasks/gw— aflock-serialized gradle wrapper. Both workers share ONE Gradle project dir; concurrent./gradlewruns clobber each other's outputs. Both prompts forbid./gradlewand requiretasks/gw. Reuse this for every future parallel wave.- 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.ktsorlibs.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
- Migration filename ↔
_migrationsdesync. Worker A renamed1788636563_created_books.jsto...564...to fix an alphabetical-replay ordering bug (bookssorted beforeshelves, breaking the relation). Correct for fresh instances, but the dev DB still had the old name recorded applied, so PocketBase tried to re-createbooksand crash-looped 9 times. Fixed viaUPDATE _migrations SET file=.... If you ever rename a migration, update that table too. DB backup: scratchpaddata.db.bak. - Rule semantics: in PocketBase
""means PUBLIC,nullmeans superuser-only. Confusing these is exactly how the library would end up world-readable. claude -p --output-format jsonwrites its log only at exit; a 0-byte.jsonmeans the worker is still running or was killed, not that it failed.- 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+testexit 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.