Files
bookshelf/docs/HANDOFF.md
T
claude 6c17e42037 Baseline: wave 1A server complete, wave 1B Android scaffold + design system green
assembleDebug, testDebugUnitTest, and recordPaparazziDebug all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bThmkmyUUdqQpy3MXFFe5
2026-09-06 01:58:37 +00:00

6.8 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

  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/<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.

STATE: what is NOT done

Wave 1B — Android scaffold + design system: INCOMPLETE (killed mid-run by the restart)

Present: gradle wrapper, gradle/libs.versions.toml, app/build.gradle.kts, AndroidManifest.xml, proguard-rules.pro, Literata OFL license. Missing/unverified: ui/theme (Color/Type/Theme), the shared component set, MainActivity, Paparazzi setup, and any evidence the build compiles.

Its session SURVIVED and is resumable — prefer this over a restart (saves quota): claude -p --model sonnet --permission-mode bypassPermissions --output-format json \ --add-dir ~/bookshelf --resume 6823e72a-69c1-486e-ae5a-18abab84529b with a "continue where you left off, don't restart" prompt. (Worker A's session, for reference, is 5e3bd183-252c-4224-99b5-91779761ccbc.) First thing the resumed worker must do: get ./gradlew assembleDebug GREEN. Everything downstream is blocked on it.

Waves 2-4 — not started. Prompts not yet written.

  • Wave 2 (parallel, after 1B is green):
    • C — data layer: Room entities/DAOs/DB, PocketBase Retrofit client + auth interceptor, SyncEngine (push-then-pull, LWW, tombstones, client-generated 15-char ids), SettingsStore.
    • D — metadata + scanning: Open Library + Google Books merge, ISBN-13 checksum validation, CameraX + ML Kit continuous scanning.
  • Wave 3 (after C+D): 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.