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
This commit is contained in:
2026-09-06 01:58:37 +00:00
commit 6c17e42037
79 changed files with 3773 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
You are implementing the SERVER half of the Bookshelf project.
READ FIRST: ~/bookshelf/docs/SPEC.md — it is authoritative. Follow it exactly.
A PocketBase v0.40.2 instance is ALREADY RUNNING at http://127.0.0.1:8090
(managed by `sprite-env services`; restart with `sprite-env services restart pocketbase`,
logs at /.sprite/logs/services/pocketbase.log). Superuser credentials are in
~/bookshelf/server/.dev-credentials. Binary + pb_data are in ~/bookshelf/server/.
Deliver, in ~/bookshelf/server/:
1. setup-schema.sh — idempotent bash (curl+jq only, portable to any Linux box).
Takes PB_URL, PB_EMAIL, PB_PASS from env or args. Creates/updates the three
collections exactly per SPEC, sets ALL API rules, creates the indexes, and
locks down the users collection (createRule=null). Safe to re-run. This is the
script the owner will run on their home server, so make its output clear and its
errors actionable.
2. Run it against the live instance and VERIFY, with real curl calls:
- anonymous GET of books/shelves/bookcases is REJECTED (expect 4xx) — this is the
single most important check in the whole task, do not hand-wave it
- anonymous user-registration attempt is REJECTED
- an authenticated user CAN do full CRUD on all three collections
- relations resolve; file upload to books.cover works (use a small generated PNG)
Print the actual status codes you observed. If any check fails, FIX IT and re-verify.
3. create-user.sh — superuser-driven account creation (the owner and their wife).
No self-registration path may exist.
4. deploy/ — with a README.md that a competent-but-not-expert person can follow:
- bookshelf.service (systemd, runs as non-root, Restart=always)
- Dockerfile + docker-compose.yml
- backup.sh (sqlite-safe backup of pb_data, plus how to restore)
- guidance on reaching a home server from outside: Tailscale (recommended)
vs port-forward + Caddy/TLS. Be concrete about the tradeoffs. HTTPS is
required because the app sends passwords.
5. Confirm pb_migrations/ captured the schema so a fresh instance self-provisions.
Test this: point a throwaway instance at a NEW empty data dir with the same
migrations, start it, confirm collections appear, then delete the throwaway.
6. server/README.md — quickstart.
Do not modify ~/bookshelf/app or ~/bookshelf/docs.
Keep ~/bookshelf/server/.dev-credentials out of version control (write .gitignore).
Finish with a <=25 line report: what you built, the verification status codes, and
anything you had to deviate from in the SPEC and why.
+49
View File
@@ -0,0 +1,49 @@
You are creating the Android project SCAFFOLD + DESIGN SYSTEM for Bookshelf.
READ FIRST: ~/bookshelf/docs/SPEC.md — authoritative, follow exactly.
Environment (already installed, do not reinstall):
JAVA_HOME=~/toolchain/jdk21 (JDK 21) ANDROID_HOME=~/toolchain/android-sdk
Installed: platforms;android-37.0, build-tools;37.0.0, platform-tools
No emulator/KVM available — you CANNOT run the app. Verify by compiling.
Create a Gradle project at ~/bookshelf/app. Deliver:
1. Working Gradle build: settings.gradle.kts, build.gradle.kts, gradle.properties,
gradle/libs.versions.toml (version catalog), app/build.gradle.kts, the Gradle
wrapper (download a wrapper JAR compatible with AGP for compileSdk 37 + JDK 21),
local.properties pointing at the SDK, and a sensible .gitignore.
Pick versions that ACTUALLY RESOLVE — check Maven Central / dl.google.com rather
than guessing. Declare every library from the SPEC in the catalog now, even ones
later waves will use, so downstream workers never touch build files.
2. AndroidManifest with the permissions the SPEC implies (camera, internet) and a
single MainActivity hosting Compose.
3. ui/theme: Color.kt, Type.kt, Theme.kt implementing the SPEC palette for BOTH
light and dark. Dynamic color OFF. Bundle the Literata variable/static TTF in
res/font (download from the Google Fonts GitHub repo, it is OFL — include the
license file). Full Material3 ColorScheme for both modes, mapped thoughtfully:
this app must read as warm paper and mahogany, never as default-Material purple.
4. A small reusable component set in ui/theme or ui/components that later waves will
build every screen from — at minimum: BookshelfScaffold (topbar w/ serif title +
gold hairline rule), PaperSurface, GoldDivider, BookCover (Coil, correct 2:3
aspect, letterpress-ish placeholder when no cover, graceful error state),
PrimaryButton/SecondaryButton, EmptyState, SyncStatusBar. Make these genuinely
nice — the owner explicitly cares that this "feels like books". Restrained and
typographic beats skeuomorphic.
5. Paparazzi configured so Compose renders to PNG on the JVM (no device). Add
screenshot tests for the component set in BOTH light and dark, run
`./gradlew recordPaparazziDebug` (or the equivalent task), and confirm real PNGs
land on disk. If Paparazzi will not cooperate with this AGP/Compose combination
after a genuine effort, say so plainly in your report and fall back to Robolectric
+ Roborazzi; do not silently skip visual verification.
6. A placeholder MainActivity screen that renders the component set, so wave 3 has a
living style reference.
MUST end green: `./gradlew assembleDebug` and `./gradlew test` both pass.
Iterate until they do — a broken build blocks every downstream worker.
Write ~/bookshelf/app/BUILD_NOTES.md recording the EXACT resolved versions (AGP,
Gradle, Kotlin, KSP, Compose BOM, Room, Retrofit, Coil, CameraX, ML Kit, WorkManager,
Paparazzi) and any compatibility traps you hit. Downstream workers depend on this.
Do not modify ~/bookshelf/server or ~/bookshelf/docs.
Finish with a <=25 line report: versions, what builds, Paparazzi status, deviations.
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# run-task.sh <task-name> <prompt-file> [cwd]
# Quota-aware worker runner: survives 5-hour usage-limit caps by sleeping until
# quota refreshes and resuming the SAME session instead of restarting from zero.
set -u
NAME="$1"; PROMPT_FILE="$2"; CWD="${3:-$HOME/bookshelf}"
L="$HOME/bookshelf/logs"; mkdir -p "$L"
LOG="$L/${NAME}.json"; ERR="$L/${NAME}.err"; SIDF="$L/${NAME}.sid"; ST="$L/${NAME}.state"
MAX_WALL="${MAX_WALL:-86400}" # 24h total patience
POLL="${POLL:-600}" # 10 min between quota probes
MAX_HARD_FAILS="${MAX_HARD_FAILS:-3}"
export JAVA_HOME="$HOME/toolchain/jdk21"
export ANDROID_HOME="$HOME/toolchain/android-sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH"
export GRADLE_USER_HOME="$HOME/.gradle"
cd "$CWD" || exit 1
# Stable session id so a killed run can be resumed rather than restarted.
if [ ! -s "$SIDF" ]; then
python3 -c "import uuid;print(uuid.uuid4())" > "$SIDF"
FRESH=1
else
FRESH=0 # sid pre-seeded (recovered) or left by an earlier attempt
fi
SID="$(cat "$SIDF")"
say(){ echo "[$(date -Is)] $NAME: $*" >> "$ST"; }
say "start sid=$SID fresh=$FRESH wall=${MAX_WALL}s poll=${POLL}s"
CONT_PROMPT="Continue the task you were working on, from wherever you left off. \
Your original instructions are earlier in this conversation; re-read them and any \
files you already wrote before doing more work. Do not restart from scratch and do \
not redo completed work. Finish the task and give the final report."
deadline=$(( $(date +%s) + MAX_WALL ))
attempt=0; hard=0; quota_waits=0
while [ "$(date +%s)" -lt "$deadline" ]; do
attempt=$((attempt+1))
if [ "$attempt" -eq 1 ] && [ "$FRESH" -eq 1 ]; then
say "attempt $attempt: fresh (--session-id)"
claude -p --model sonnet --permission-mode bypassPermissions \
--output-format json --add-dir "$HOME/bookshelf" \
--session-id "$SID" < "$PROMPT_FILE" > "$LOG" 2>"$ERR"
else
say "attempt $attempt: resume $SID"
printf '%s' "$CONT_PROMPT" | claude -p --model sonnet \
--permission-mode bypassPermissions --output-format json \
--add-dir "$HOME/bookshelf" --resume "$SID" > "$LOG" 2>"$ERR"
fi
rc=$?
blob="$(cat "$LOG" "$ERR" 2>/dev/null | head -c 20000)"
# 1) quota / rate limit -> wait it out, do NOT burn a hard-fail
if printf '%s' "$blob" | grep -qiE 'usage limit|limit will reset|limit resets|rate_limit_error|rate limit exceeded|429|too many requests|overloaded_error'; then
quota_waits=$((quota_waits+1))
say "QUOTA hit (wait #$quota_waits). sleeping ${POLL}s then probing again."
sleep "$POLL"
continue
fi
# 2) session vanished -> start clean once
if printf '%s' "$blob" | grep -qiE 'no conversation found|session not found|could not resume'; then
say "session $SID unresumable; starting fresh"
python3 -c "import uuid;print(uuid.uuid4())" > "$SIDF"; SID="$(cat "$SIDF")"; FRESH=1; attempt=0
continue
fi
# 3) success
isErr="$(jq -r '.is_error // false' "$LOG" 2>/dev/null)"
if [ "$rc" -eq 0 ] && [ "$isErr" != "true" ]; then
say "SUCCESS after $attempt attempt(s), $quota_waits quota wait(s)"
break
fi
# 4) genuine failure
hard=$((hard+1))
say "hard failure #$hard (rc=$rc is_error=$isErr)"
if [ "$hard" -ge "$MAX_HARD_FAILS" ]; then say "GIVING UP after $hard hard failures"; break; fi
sleep 60
done
[ "$(date +%s)" -ge "$deadline" ] && say "WALL CLOCK EXCEEDED"
{
echo "=== $NAME attempts=$attempt quota_waits=$quota_waits hard_fails=$hard ==="
jq -r '"cost=$" + ((.total_cost_usd//0)|tostring) + " turns=" + ((.num_turns//0)|tostring) + " err=" + ((.is_error//"?")|tostring)' "$LOG" 2>/dev/null
echo "--- result (tail) ---"
jq -r '.result // "no result"' "$LOG" 2>/dev/null | tail -c 1800
} > "$L/${NAME}.summary"