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
| Host | Serves | Observed |
|---|---|---|
https://tndm.loca.zone | the application, proxied to 127.0.0.1:51818 | HTTP 200; /api/decks also HTTP 200 |
https://wiki.tndm.loca.zone | this wiki, from /home/loca/dev/wikis/tndm/current | HTTP 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:
| Field | Value |
|---|---|
id | qwizz-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 |
createdAt | 2026-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
- Import the document.
POST /api/decks/importwithmd_pathormd_url, plus optionallycopy_fromorbase_urland an explicitid(src/routes/api/decks/import/+server.ts:14-24). The response reportssections,frames,imagesRewrittenand asset copy statistics (src/lib/server/importer.ts:16-22). - Create a session.
POST /api/sessionswith{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). - 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). - Move through frames. Scrolling drives an
IntersectionObserverthat tracks the active section (src/routes/s/[sessionId]/+page.svelte:330-341), and a debounced effect emits onenavevent per section, once per second at most (src/routes/s/[sessionId]/+page.svelte:317-327). Reopening a session replays to the lastnavevent (src/routes/s/[sessionId]/+page.svelte:272-275). - Act on a frame. A verdict of
keep,killorrecaptureposts averdictevent (src/routes/s/[sessionId]/+page.svelte:152-154); notes and tasks postnoteandtaskevents carrying text and tags (src/routes/s/[sessionId]/+page.svelte:586-600); a scribble uploads a PNG data URL toPOST /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). - The agent talks back. Any process that can reach the API posts an event with
actor: 'agent'andkind: '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 aspeechevent (src/lib/voice.ts:232-241). The recipe is in api. - Export and merge.
GET /api/sessions/:id/export.mdyields the decision log, which is merged back into the source document by hand or by an agent.
Where to read next
- architecture — process shape,
DATA_DIRlayout, the event bus and the SSE broker. - md-parser — the
mdframescontract, 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.