Imported from newscientist101/apple-turnover (
AGENTS.md). Install upstream withnpx skills add newscientist101/apple-turnover. Copyright stays with the author.
Agent Instructions
This file contains the architecture invariants, task-management rules, and verification conventions that keep the system correct.
Keep the three documentation files separate:
| Document | Owns |
|---|---|
README.md |
Build, run, deploy, user-facing behavior and limits |
AGENT_API.md |
HTTP/WebSocket wire contract, payloads, status codes, frame kinds |
AGENTS.md |
Internal invariants, development workflow, testing and proof requirements |
Do not duplicate wire-contract details here when they belong in AGENT_API.md.
Repository map
cmd/srv/ server entrypoint
cmd/agentcli/ pure-Go client of the documented HTTP API
srv/server.go server lifecycle and page handling
srv/api.go HTTP API and route table
srv/conductor.go in-memory performance state
srv/hub.go listener fan-out
srv/ws.go WebSocket listener endpoint
srv/event.go listener frame encoding and broadcast
srv/static/ browser modules
srv/templates/ HTML shell
scripts/ mutation-testing tools
srv.service systemd unit
There is no database, no browser build step, and no vendored browser dependency. The page loads pinned CodeMirror and @strudel/web URLs from CDNs.
Task tracking: beads (bd)
The repository uses beads for shared task state. On a fresh clone, initialize the shared Dolt-backed database before working:
bd bootstrap
Use bd sync rather than hand-written pull/push loops:
bd sync
Run it before reading the task list and again after completing work. A clean sync means the sync completed; it does not prove that a preceding claim or close succeeded.
Claims are leases, not advisory flags:
bd update <id> --claim
bd heartbeat <id>
bd unclaim <id>
bd reclaim
Verify claims and closes with bd show <id> --json, not with the exit code from a later bd sync.
Do not use bd update --force to replace a live claim. Use bd reclaim for expired leases.
For shared replicas, keep the bd versions aligned with the repository's database schema. Do not independently migrate a remote-backed database on multiple clones.
Dispatch only leaf subtasks; keep parent issues open until their children are complete.
Architecture invariants
Conductor
Conductor owns the entire live performance state.
- Thread-safe for concurrent use.
- Holds code, version, anchor, last agent message, bounded code history, playing state, listener count, and the stored evaluation verdict.
- Nothing is persisted.
- Versions are contiguous, unique, monotonic, never reused, and never skipped.
- Only
Publish/POST /api/codeincrements the version. - Message, anchor, transport, listener-count, and evaluation-result changes do not increment it.
Snapshotmust not expose mutable internal state; history and nested verdict data are copied.RecordEvalResultmust distinguish stored from understood but stale so only a stored verdict is broadcast.
Hub
One goroutine owns the subscriber set.
- Broadcasts encoded frames without depending on the Conductor.
- Slow subscribers are dropped rather than blocking the hub.
- Subscriber queues are bounded at 64 messages.
- Subscriber removal is idempotent; channels close exactly once.
SubscriberCountis a synchronous round trip and may block if the hub is wedged, so tests must bound it.- Hub shutdown and subscriber drop both appear to receivers as channel closure.
- A count hook may create the event frame but must not call back into the Hub from the hub goroutine.
WebSocket
The listener endpoint must never block on a client.
- Subscribe before generating the initial snapshot.
- Write every frame with a 5-second deadline.
- Unsubscribe on every exit path.
- Handle both hub shutdown and subscriber-channel closure.
- A slow listener is dropped; a transient catch-up write failure does not justify inventing a different state model.
/wsis the exact path and uses the same-origin handshake behavior provided by the WebSocket library.- Keepalive pings occur every 30s; listeners that cannot respond within 5s are reaped.
- The HTTP server does not impose a shorter
WriteTimeoutthat would defeat the WebSocket close/write deadlines.
HTTP API
- Apply the 64 KiB request cap centrally.
- Detect oversize before JSON parsing so oversized malformed bodies still return
413. - Reject unknown fields and trailing JSON data.
- Argument-free endpoints reject any supplied field.
- Empty
codeandmessageare errors, not no-ops. - Validate first, commit second, broadcast last.
- Broadcast the same snapshot that the HTTP response returns.
- Keep
/apierrors uniformly JSON. - Keep route handling centralized in
routes(); tests should exercise that real tree.
Event frames
Every listener event has exactly this shape:
{"kind":"<what happened>","snapshot":{...}}
snapshotis the actual snapshot value, not a second bespoke representation.- All frame construction goes through one encoder.
- The event vocabulary is
snapshot,code,message,transport,eval-result,listener-count,anchor. - Event names describe listener-visible changes, not endpoint names; therefore play and hush both use
transport.
Anchor and coherence
- An anchor replaces the entire timeline.
- Both
epochMsandcpsare required. - Validation belongs in
Conductor, not only in the HTTP handler. cpsaccepts only finite positive values up to 1000.- Epoch skew is checked against a supplied reference clock so boundary tests are deterministic.
- A rejected anchor must leave the existing anchor unchanged and broadcast nothing.
- Client commits are bar-aligned, not sample-accurate.
- A newer code version must cancel an older pending commit.
- With no usable anchor, the client commits immediately and marks the state
unscheduled. - Synchronization behavior is proved by executing the served browser JavaScript, not by duplicating its arithmetic in Go tests.
Page and browser assets
GET /must render the complete shell, not merely a status-200 prefix.- Serve the actual browser modules that the page references.
- The browser is the only evaluator and audio producer.
editor.jsmay fall back to the plain textarea if CodeMirror is unavailable; that does not imply audio can work offline.
Standing limitations
These are design properties, not bugs:
- No server audio. The Go process never evaluates JavaScript or produces sound.
- Browser-only evaluation. A server-accepted pattern can still fail in Strudel.
- Bar-aligned synchronization only. There is no cross-machine sample clock.
- Drift is observable, not eliminated. The client reports measured drift and
unscheduledwhen the anchor cannot be used. - No persistence. Restart returns to version 0.
- Network required at browser load. Pinned CDN dependencies are not vendored.
Any change to these limitations changes the API/README contract and must be documented with the implementation.
Verification gate
The standard gate is:
make verify
Equivalent steps:
gofmt -l .
go vet ./...
go build ./...
go test ./... -race -count=1
Run this before committing completed work.
Tests must not depend on:
- external network access;
- databases;
- the production port (
:8000); - already-running services.
Use httptest and loopback instead.
Core test locations
| Area | Primary tests |
|---|---|
| API/integration | srv/integration_test.go |
| WebSocket | srv/ws_test.go |
| Hub | srv/hub_test.go |
| API handlers | srv/api_test.go |
| Conductor/state | srv/conductor_test.go, srv/conductor_eval_test.go |
| Browser timing | srv/coherence_test.go |
| API documentation | srv/agent_api_doc_test.go |
| Browser wiring | srv/session_client_test.go, srv/editor_view_test.go, srv/viz_test.go |
| CLI | cmd/agentcli/main_test.go |
Assert response bodies, not just status codes.
Prefer tests that drive the real route tree and served bytes over tests that reproduce the implementation in the test harness.
Boundedness in tests
A hang is a test failure, not a waiting strategy.
Every potentially blocking operation needs its own bound:
- request context/deadline;
- transport/client timeout;
- watchdog for goroutine/channel waits;
- bounded server shutdown.
Do not rely on an unbounded wg.Wait(), httptest.Server.Close(), or a deadline checked only after a possibly blocking call.
For APIs owned by a single goroutine, bound the API call itself. A parked goroutine after the watchdog fires is acceptable when the test is about to fail.
The overriding rule
A bound may make a hang fast; it must never make a hang pass.
Adding a timeout must not remove the assertion that proves the underlying defect.
Documentation verification
AGENT_API.md is a shipped artifact and is machine-checked against the implementation and a running server.
The checks should prove all of the following:
- documented routes and actual routes match in both directions;
- every event kind is documented;
- documented request shapes are accepted;
- undocumented fields are rejected;
- documented response shapes match live server bytes;
- the document's shape anchors remain present.
Important anchors:
<!-- shape:error -->
<!-- shape:codeRequest -->
<!-- shape:messageRequest -->
<!-- shape:anchorRequest -->
<!-- shape:evalAck -->
<!-- shape:evalResultRequest -->
<!-- shape:snapshot -->
<!-- shape:storedEvalResult -->
<!-- shape:frame -->
<!-- frame-kinds -->
Do not remove or rename these without updating the document tests.
Mutation testing
Mutation testing deliberately introduces defects and requires the test suite to catch them.
make mutation-check
./scripts/mutation-check.sh --list
./scripts/mutation-check.sh <mutation-id>
Rules:
- Mutations must actually change the intended source.
- Mutation anchors must be checked; a no-op mutation is not evidence.
- Mutations must be reverted automatically and byte-for-byte verified afterward.
caughtmeans a test failed as expected.SURVIVEDis a failure.WEAK(compile/panic failure without the intended assertion catch) is a failure.BROKENmeans the mutation anchor no longer matches and must be repaired.- Routed mutations must match at least one test; a regex matching nothing is
SURVIVED. - Run only genuinely expensive mutations through narrow
-runrouting. - Do not run mutation checks concurrently when measuring performance.
A timeout can expose a hang, but the test still needs to fail because the intended invariant was violated.
Feature workflow
- Write a failing test in the harness that owns the behavior.
- Implement the behavior in the layer that owns the invariant.
- Update
AGENT_API.mdwhen the wire contract changes. - Update
AGENTS.mdwhen an internal invariant changes. - Add or update mutation coverage for new behavior.
- Run
make verify. - Commit the completed work.
- Sync/close the corresponding bead only after the code is actually committed.
For browser timing or synchronization, test the served JavaScript with the fake clock/goja harness rather than translating the timing algorithm into Go.
Measurement discipline
- Use a real Git worktree when mutation or binary-build experiments could disturb the main tree.
- Preserve and report command exit codes.
- Do not casually regenerate historical mutation benchmarks.
- When documenting a defect/fix, record concrete evidence: failure mode, relevant stack frames or output, and duration where timing is material.
