orchestration: wave 4 prompts; decouple guard poll from lease renewal

Guard now polls every 30s but renews every 15 min. Coupling them meant a wave that
finished just after a renewal sat undetected for a full interval with the sprite
pinned hot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 10:54:03 +00:00
parent 9bf5ff4d6c
commit 0088fda095
3 changed files with 165 additions and 3 deletions
+81
View File
@@ -0,0 +1,81 @@
You are Worker F1 on the Bookshelf project (~/bookshelf). Wave 4, part 1:
PROVE THE SYNC + COVER PIPELINE WORKS AGAINST THE REAL SERVER.
FIRST READ (contract — follow exactly, do not restate):
~/bookshelf/docs/SPEC.md (see "Sync design", "PocketBase schema")
~/bookshelf/docs/HANDOFF.md (state + hazards already paid for — read the hazards)
Waves 1-3 are DONE and verified: server, data layer, metadata/scanning, all six screens.
91 unit tests pass. But EVERYTHING so far was tested against FAKES. Worker C's own
report flagged the biggest hole:
"Cover pipeline is best-effort, not fully spec-complete. Download-on-create and
multipart upload-on-sync are UNTESTED against a real PocketBase (only via fakes).
Logic is sound but should get a real-server smoke test."
That is your job. This is the single most likely place a real bug is hiding.
## The live server
- PocketBase v0.40.2 at http://127.0.0.1:8090 (NOT internet-exposed, by design).
`curl -s http://127.0.0.1:8090/api/health` currently returns 200.
- Superuser creds: `server/.dev-credentials` (gitignored). Schema setup: `server/setup-schema.sh`
(idempotent). Create a normal app user with `server/create-user.sh`.
- If the server is down: `sprite-env services restart pocketbase`;
logs at `/.sprite/logs/services/pocketbase.log`.
- Create a THROWAWAY test user for this (e.g. livetest@example.com). Do NOT invent or
use the owner's real email addresses — they have not been provided.
## Your scope
Build a real end-to-end exercise, driven from JVM code (no emulator on this box), that
against the LIVE server proves:
1. auth: login as the test user, token used by PbAuthInterceptor
2. push: create bookcase -> shelf -> book locally (client-generated 15-char ids) and
PUSH them; verify by reading them back through the REST API that the ids were
preserved and NOT remapped
3. pull: mutate a record server-side, pull, confirm last-write-wins on `updated`
4. tombstones: soft-delete locally, push, confirm `deleted=true` propagates and the
record is filtered from local queries
5. **COVER ROUND-TRIP (the point of this task):** attach a cover image to a book,
multipart-upload it to the `cover` file field, then fetch the stored file back
through `/api/files/...` and assert the bytes match what was uploaded. Use a small
locally-generated PNG — do NOT depend on openlibrary.org being reachable.
6. cursor handling: the per-collection `updated` cursor advances and a second pull
is a no-op (no duplicate rows).
## CRITICAL — do not poison the normal test suite
This test hits a real server and MUST NOT run in the default `testDebugUnitTest`.
Machines without PocketBase must stay green. Gate it behind a system property, e.g.
`assumeTrue(System.getProperty("liveServer") != null)`, and provide a runner script
`server/live-sync-test.sh` that sets it. State plainly in your report how it is invoked.
## Boundaries — Worker F2 is running RIGHT NOW in this repo
- F2 owns `app/app/build.gradle.kts` (it is adding release signing), the root `README.md`,
and all Paparazzi/screenshot code. DO NOT TOUCH THOSE. If you need a build change,
REPORT it, do not make it.
- You own `server/` scripts and the live-test code. Put it in its own package
(e.g. `org.modg.bookshelf.livesync`) so it cannot collide with F2's screenshot tests.
- You MAY fix genuine bugs you find in data.repo / data.remote / data.local — that is
the expected outcome. Every fix must keep the existing 91 tests passing.
## Build/verify — use the serialized wrapper, never ./gradlew directly
~/bookshelf/tasks/gw assembleDebug
~/bookshelf/tasks/gw testDebugUnitTest
## Definition of done — actually run these, wait for them, report REAL exit codes
1. `~/bookshelf/tasks/gw assembleDebug` exits 0.
2. `~/bookshelf/tasks/gw testDebugUnitTest` exits 0, ZERO failures, and the live test is
SKIPPED in that run (prove it).
3. The live test, run explicitly against 127.0.0.1:8090, PASSES all six points above.
4. Report the actual bugs you found. "Everything worked first try" on an untested
pipeline is a suspicious claim — if that is genuinely the outcome, show the evidence
(the assertions that ran, the bytes compared).
## Do not repeat an earlier worker's mistake
A worker once ended its turn saying "tests are running in the background, I'll report
when done." That is a FAILURE. Run it, WAIT, report observed exit codes. A truthfully
reported gap is worth more than a false green.
## Report (short — read by a token-constrained orchestrator)
- real exit codes; how to invoke the live test; PASS/FAIL per numbered point above
- every bug found and how you fixed it
- anything you could NOT verify, and why
+73
View File
@@ -0,0 +1,73 @@
You are Worker F2 on the Bookshelf project (~/bookshelf). Wave 4, part 2:
SHOW THE OWNER WHAT IT LOOKS LIKE, AND SHIP AN INSTALLABLE APK.
FIRST READ (contract — follow exactly, do not restate):
~/bookshelf/docs/SPEC.md (see "Design language", "Screens", "Quality bar")
~/bookshelf/docs/HANDOFF.md (state + hazards already paid for — read the hazards)
Waves 1-3 are DONE and verified: 91 unit tests pass, all six screens exist, and there
is an existing Paparazzi setup (`ComponentGalleryPaparazziTest`, 10 PNGs of the shared
components) that ALREADY WORKS — `~/bookshelf/tasks/gw recordPaparazziDebug` is green.
Follow its patterns rather than inventing a new approach.
Context that shapes this task: there is NO EMULATOR on this box (no KVM). Nothing in
this project has ever actually been RUN — only compiled and unit-tested. Your screenshots
are the ONLY way the owner can judge how their library app looks, and the signed APK is
the only way they can run it for real. Both matter more than usual.
## Your scope
1. **Paparazzi screenshots of all SIX screens** — setup, library, detail, scan,
locations, settings — each in BOTH light and dark. Render them with realistic
populated state (a plausible shelf of books, covers as placeholder art), NOT empty
states, plus the library empty state as its own shot since SPEC calls it out.
The scan screen cannot show a live camera in Paparazzi — render its overlay/reticle
and the result bottom sheet over a static backdrop, and say so in your report.
2. **Release signing + signed APK.** Generate a release keystore, wire a signingConfig
into `app/app/build.gradle.kts`, and produce a signed release APK. Keystore and its
passwords MUST be gitignored — put credentials in `app/keystore.properties` (gitignored)
and read them from the build file, with a graceful fallback so the build still works
for anyone without the keystore. Record the passwords in `app/keystore.properties` and
state them in your report so the owner can rebuild.
`minifyEnabled` is currently false for release — if you enable R8, you must prove the
signed APK still builds AND that Room/Retrofit/kotlinx-serialization/ML Kit survive it
(proguard rules). If you cannot prove it, leave R8 off and say so.
3. **Top-level `README.md`** — what the project is, the two-person shared-library model,
architecture (offline-first Room + PocketBase, push-then-pull LWW sync), how to build,
how to deploy the server (point at `server/deploy/`), how to install the APK, and an
honest "current limitations" section. Reuse the real content in `server/README.md` and
`docs/SPEC.md`; do not contradict them.
## Boundaries — Worker F1 is running RIGHT NOW in this repo
- F1 owns `server/` and live-server sync testing, in package `org.modg.bookshelf.livesync`.
DO NOT touch `server/` or anything under that package.
- F1 may be fixing bugs in data.repo / data.remote / data.local. DO NOT edit those
packages. If a screenshot needs a data-layer change, REPORT it instead.
- You own `app/app/build.gradle.kts`, the root `README.md`, and all screenshot code.
- Do not modify the six screens' behavior to make them easier to screenshot. If a screen
is hard to render in isolation, that is a finding to report, not a licence to change it.
## Build/verify — use the serialized wrapper, never ./gradlew directly
~/bookshelf/tasks/gw assembleDebug
~/bookshelf/tasks/gw testDebugUnitTest
~/bookshelf/tasks/gw recordPaparazziDebug
~/bookshelf/tasks/gw assembleRelease
## Definition of done — actually run these, wait for them, report REAL exit codes
1. All four commands above exit 0. `testDebugUnitTest` keeps ZERO failures (91 tests
currently pass — do not break them).
2. Screenshot PNGs exist on disk for all six screens x light/dark. List their paths.
3. A signed release APK exists. Give its exact path, size, and the output of
`~/toolchain/android-sdk/build-tools/37.0.0/apksigner verify --print-certs <apk>`
(or an explanation if that tool is unavailable).
4. Do NOT commit the keystore or its passwords to git. Verify with `git status`.
## Do not repeat an earlier worker's mistake
A worker once ended its turn saying "tests are running in the background, I'll report
when done." That is a FAILURE. Run it, WAIT, report observed exit codes. A truthfully
reported gap is worth more than a false green.
## Report (short — read by a token-constrained orchestrator)
- real exit codes for all four commands
- the screenshot PNG paths, and the signed APK path + size + signer info
- anything in SPEC's design language you think the screens get WRONG, now that you can
finally see them rendered. Be candid — the owner explicitly cares how this looks.
+10 -2
View File
@@ -14,6 +14,7 @@ SENT="$1"; shift
TASKS=("$@")
L="$HOME/bookshelf/logs"; OUT="$L/$SENT"; LEASE="bookshelf-wave"
RENEW="${RENEW:-900}" # renew every 15 min against a 60 min lease
POLL="${POLL:-30}" # but CHECK for completion every 30s (see loop below)
GUARD_LOG="$L/wave-guard.log"
lease_hold() {
@@ -28,11 +29,18 @@ trap 'lease_release; log "guard exiting, lease released"; exit 0' TERM INT
log "guard start: sentinel=$SENT tasks=${TASKS[*]} renew=${RENEW}s"
lease_hold; log "lease '$LEASE' acquired (3600s)"
last_renew=$(date +%s)
# Poll FREQUENTLY (POLL) but renew SLOWLY (RENEW). Coupling the two, as the first
# version did, meant a wave that finished right after a renewal sat undetected for a
# full RENEW interval with the sprite pinned hot the whole time.
while pgrep -f 'run-task\.sh|run-resume\.sh' >/dev/null; do
sleep "$RENEW"
lease_hold
sleep "$POLL"
now=$(date +%s)
if [ $(( now - last_renew )) -ge "$RENEW" ]; then
lease_hold; last_renew=$now
log "lease renewed; workers still running"
fi
done
log "workers stopped; writing $SENT"