Agents use plain HTTP and Authorization: Bearer <token> because they cannot complete the browser’s interactive Authelia one_factor redirect. The token file is /home/loca/dev/tandem-audit/.api-token, mode 600; never print, log, paste, or commit its value. See private page (sign-in required) for the edge and private page (sign-in required) for rotation.

flowchart LR
    H[Human] -->|rules frames| UI[Browser workspace]
    UI -->|posts user events| B[(Append-only event bus)]
    B -->|same ordered events| UI
    B -->|SSE or polling| A[Attached agent]
    A -->|posts agent events| B
    B --> X[export.md]

Tandem etiquette

RuleWorking behavior
Do not spam navTreat it as browser scroll telemetry; skip it unless location matters and never answer it automatically
Use actor: agentNever post as user when the agent is speaking or deciding; the human remains the authority
Honour the append-only logCorrect a decision with a new event; there is no event edit/delete route, and seq/ts belong to the server

Use the recipes in order and replace each quoted replace-with-… value.

1. Attach to a session

Set the endpoint, bearer header, and session id, then fetch the session with its deck so every later frame and section id comes from source.

export TNDM_BASE='https://tndm.loca.zone'
export TNDM_AUTH="Authorization: Bearer $(cat /home/loca/dev/tandem-audit/.api-token)"
export TNDM_SESSION='replace-with-session-id'
curl -sS -H "$TNDM_AUTH" \
  "$TNDM_BASE/api/sessions/$TNDM_SESSION"

Expected: HTTP 200 JSON shaped as {session, deck}; an unknown session returns JSON 404.

2. Tail the bus

Open the SSE stream after the highest server sequence already processed so replay flows directly into live events.

AFTER=0
curl -fsSN -H "$TNDM_AUTH" \
  "$TNDM_BASE/api/sessions/$TNDM_SESSION/stream?after=$AFTER"

Expected: : connected, then one data: {BusEvent} frame per event with seq > AFTER, plus idle : ping comments every 25 seconds.

3. Post a say

Send agent speech to the shared rail while the human has the workspace open.

curl -fsS -X POST -H "$TNDM_AUTH" \
  -H 'content-type: application/json' \
  "$TNDM_BASE/api/sessions/$TNDM_SESSION/events" \
  -d '{"actor":"agent","kind":"say","text":"cookbook recipe check"}'

Expected: {"ok":true,"seq":n} and the full event appears on the stream; only a live arrival may be voiced by the browser.

4. Post a verdict

Rule a frame as the agent using ids read from the attached deck and one canonical verdict: keep, recapture, or kill.

FRAME='replace-with-frame-id'
SECTION='replace-with-section-id'
curl -fsS -X POST -H "$TNDM_AUTH" \
  -H 'content-type: application/json' \
  "$TNDM_BASE/api/sessions/$TNDM_SESSION/events" \
  -d "{\"actor\":\"agent\",\"kind\":\"verdict\",\"frame\":\"$FRAME\",\"section\":\"$SECTION\",\"verdict\":\"keep\"}"

Expected: {"ok":true,"seq":n} followed by the stored verdict on SSE with the server’s sequence and timestamp.

5. Mark a route done

Close one reviewed route by posting its section id without rewriting any earlier frame decisions.

SECTION='replace-with-section-id'
curl -fsS -X POST -H "$TNDM_AUTH" \
  -H 'content-type: application/json' \
  "$TNDM_BASE/api/sessions/$TNDM_SESSION/events" \
  -d "{\"actor\":\"agent\",\"kind\":\"route_done\",\"section\":\"$SECTION\"}"

Expected: {"ok":true,"seq":n} and the section appears in session progress and the export’s sections_done count.

6. Read export.md

Render the converged human-and-agent log as the review document handed back to the source owner.

curl -fsS -H "$TNDM_AUTH" \
  "$TNDM_BASE/api/sessions/$TNDM_SESSION/export.md"

Expected: text/markdown with a YAML statistics header, a tandem-audit heading, and one bullet per exported decision.

Folded response, transport, and round-trip notes
AreaContract
Session selectionSession ids are random; list or create them through api. lastSeq is the resume cursor and doneSections is progress
Frame idsUse <sectionId>/<n> from the deck; never invent one. md-parser defines their construction
Poll alternativeGET /events?after=n is exclusive and durable; persist the highest sequence for every event read, including ignored kinds (src/lib/server/store.ts:237-257)
Host-only alternativeDATA_DIR/sessions/<id>/events.jsonl is one complete JSON event per line and may be tailed only by a process on the host (src/lib/server/store.ts:246-257,347-350)
Stream reconnectReuse the last processed seq; invalid after is 400 and an unknown session is a JSON 404 rather than a dangling stream (src/routes/api/sessions/[id]/stream/+server.ts:16-72)
say deliveryThe rail records history and raises unread state while closed; speech synthesis runs only for a live agent say while its toggle is on, never for replayed backlog (src/routes/s/[sessionId]/+page.svelte:176-178)
Speech evidenceAudio output was not proven on the 2026-08-20 headless evidence pass; see private page (sign-in required)
Cursor ownershipClient-supplied seq and ts are discarded; use the POST response or received event, not a number sent by the client (src/lib/server/store.ts:260-299,334-350)
Self-loop guardAdvance past say and do not automatically answer it, or an agent can answer its own acknowledgements forever
ScribblesA bus event carries only file; the PNG is under DATA_DIR/sessions/<id>/scribbles/ (src/lib/server/store.ts:381-416)
Export bulletsOnly note, task, verdict, speech, and scribble become bullets; by_kind still counts all event kinds (src/lib/server/export.ts:5-27)
Export countersevents counts bullets, frames_touched counts distinct bulleted frames, and sections_done counts distinct route_done targets (src/lib/server/export.ts:89-124)
Round tripHuman reviews the export, accepted items enter the source feedback log, scribble assets stay relative, then the deck is re-imported without disturbing session history; see first-deck
Import scopeImport, image materialization, and re-import safety stay in api rather than being duplicated as a seventh recipe
Folded live receipts from the former payload dumps
DateProbeResult
2026-08-20Throwaway session creationReturned random id, deck id, creation time, and lastSeq: 0
2026-08-20Agent say, note, verdict, route_doneEach append returned {ok: true, seq} and appeared in sequence order
2026-08-20Poll after a cursorReturned only later events; bad after returned 400
2026-08-20SSE replay plus concurrent POSTReplayed the backlog, then delivered the live event on the same stream
2026-08-20Exportevents counted two decision bullets while by_kind counted five total events, proving the counters differ
2026-08-20Re-import same deck idRewrote the deck while preserving the six-event session and its lastSeq
2026-08-20DELETE /eventsReturned 405; correction requires a new event
2026-08-20CleanupThrowaway deck and session were removed; the deleted session then returned 404
2026-08-21Bearer recheckGET /api/decks and GET /api/sessions returned 200 with their prior shapes; write recipes were not rerun wholesale after the edge gate, and commit 09fa602 left the API and event contract unchanged

For the full endpoint and event tables use api; for browser controls use quickstart; for bus internals and the 2026-08-21 no-load-nav policy use private page (sign-in required).