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:
@@ -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.
|
||||
@@ -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.
|
||||
Executable
+94
@@ -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"
|
||||
Reference in New Issue
Block a user