What TNDM is

TNDM turns an arbitrary markdown document into an ordered deck of reviewable frames, then lets a human and a coding agent work through those frames together over one shared, append-only event log. Accepted feedback is rendered back out as markdown and merged into the source document.

“Any markdown document” is literal, not aspirational: the only entry point is parse(md: string, opts?: ParseOptions): FrameDeck (src/lib/mdframes/index.ts:197), it takes a string, and it is documented as never throwing — malformed input degrades to a deck with zero sections rather than an error (src/lib/mdframes/index.ts:192-196, fallback at src/lib/mdframes/index.ts:479-490). Images, pipe tables, fenced code and callouts each become frames or section findings; see md-parser for the full rule set.

The round trip is equally concrete. GET /api/sessions/:id/export.md (src/routes/api/sessions/[id]/export.md/+server.ts:7-20) renders the session as a YAML stats header plus one bullet per decision. Only five of the nine event kinds earn a bullet — note, task, verdict, speech and scribble — because nav and status are transport chatter and say is the agent talking out loud (src/lib/server/export.ts:17-27). That filtered log is what gets merged back into the source document.

Nothing in TNDM is specific to any one document type. The first deck happens to be a screenshot gallery, but the parser only knows about headings, images, tables, code fences and callouts.

The two URLs

HostServesObserved
https://tndm.loca.zonethe application, proxied to 127.0.0.1:51818HTTP 200; /api/decks also HTTP 200
https://wiki.tndm.loca.zonethis wiki, from /home/loca/dev/wikis/tndm/currentHTTP 404 at the time of writing

The wiki host answers 404 because /home/loca/dev/wikis/tndm/current does not exist until the first production build creates it — build.sh only makes that symlink on a non-check build (/home/loca/dev/wikis/build.sh:128-133). The vhost itself is deployed and holds a valid certificate. operations covers the build and the swap.

The first deck

The first document imported into TNDM is the QuizWizz UI/UX feedback tour gallery produced on the WRDP bench. GET /api/decks returns exactly one deck:

FieldValue
idqwizz-2026-08-20
title🎞️ QuizWizz UI/UX Feedback Tour — Complete Shot Gallery
source/home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md
createdAt2026-08-20T08:45:02.982Z

Measured from GET /api/decks/qwizz-2026-08-20, the live deck holds 76 sections (11 at level 2, 65 at level 3) and 323 frames: 320 image, 2 table, 1 code. Of the images, 315 resolve under /shots/ and 5 under /boards/, 65 carry the finding badge, and the sections carry 139 findings between them. Because the deck was imported with copy_from, every image src was rewritten to the deck’s own asset route, for example /api/decks/qwizz-2026-08-20/assets/boards/board-A-public.png (rewrite rule at src/lib/server/importer.ts:234-236).

Those are live numbers, not the parser’s contract numbers. The frozen test fixture at src/lib/mdframes/fixtures/qwizz-tour.md asserts 321 image frames, 316 shots and 66 finding badges (src/lib/mdframes/mdframes.test.ts:66-77). The live deck is one image frame, one shot and one badge lower because the WRDP source document withdrew v01-d-p02-player-intro.png after the fixture was frozen: the fixture still carries that frame at src/lib/mdframes/fixtures/qwizz-tour.md:1480-1481, while the live source lists it as blocked(no-such-state) at /home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md:51 and records the withdrawal at lines 1493-1494 of the same file. Every other contract number matches, which is the point of freezing a fixture. md-parser explains why it is never re-baselined.

How a review session runs

  1. Import the document. POST /api/decks/import with md_path or md_url, plus optionally copy_from or base_url and an explicit id (src/routes/api/decks/import/+server.ts:14-24). The response reports sections, frames, imagesRewritten and asset copy statistics (src/lib/server/importer.ts:16-22).
  2. Create a session. POST /api/sessions with {deckId} returns the session record (src/routes/api/sessions/+server.ts:7-14). Session ids are 8 random bytes in hex (src/lib/server/store.ts:156), and the deck is read first so a bad deck id fails before anything is written (src/lib/server/store.ts:154).
  3. Open /s/<sessionId>. The loader fetches the session with its deck, then the full event history, then the deck list for the picker; the last two failures are tolerated so the page still renders (src/routes/s/[sessionId]/+page.ts:6-33).
  4. Move through frames. Scrolling drives an IntersectionObserver that tracks the active section (src/routes/s/[sessionId]/+page.svelte:330-341), and a debounced effect emits one nav event per section, once per second at most (src/routes/s/[sessionId]/+page.svelte:317-327). Reopening a session replays to the last nav event (src/routes/s/[sessionId]/+page.svelte:272-275).
  5. Act on a frame. A verdict of keep, kill or recapture posts a verdict event (src/routes/s/[sessionId]/+page.svelte:152-154); notes and tasks post note and task events carrying text and tags (src/routes/s/[sessionId]/+page.svelte:586-600); a scribble uploads a PNG data URL to POST /api/sessions/:id/scribble, which stores the file and logs only its filename (src/lib/Scribble.svelte:183-188, src/lib/server/store.ts:383-386).
  6. The agent talks back. Any process that can reach the API posts an event with actor: 'agent' and kind: 'say'. It arrives over the SSE stream, lands in the dialog rail, and is spoken by the browser when the speaker toggle is on (src/routes/s/[sessionId]/+page.svelte:93). Speech captured from the microphone goes the other way as a speech event (src/lib/voice.ts:232-241). The recipe is in api.
  7. Export and merge. GET /api/sessions/:id/export.md yields the decision log, which is merged back into the source document by hand or by an agent.
  • architecture — process shape, DATA_DIR layout, the event bus and the SSE broker.
  • md-parser — the mdframes contract, parsing rules and the frozen-fixture numbers.
  • api — every endpoint, the event schema, import safety and the agent-attach recipe.
  • operations — the systemd unit, nginx, TLS, backups and the wiki build.