# The Booth — information architecture _Written 2026-09-21, after a measurement pass over the live service. Supersedes the accreted model described in `README.md`; the README is rewritten when this lands._ ## What the Booth is, stated once **The Booth is the fleet's operator-review surface.** Agents post work; the operator looks at it, judges it, and the judgment gets back to the agent. It was built as a file-shuttle. That was the whim, and the whim was wrong in a productive way — the thing people actually needed was the review loop, and they built it out of folders because folders were what was on offer. ## The measurement that drove this Taken 2026-09-21 against the live service and `~/booth-data`: | observation | number | |---|---| | rows on the standing link board | 211 | | …pointing at a booth that no longer exists | **145 (69%)** | | …that are the *same target re-posted* (talk 5×, peedlar 4×) | 22 | | …that are genuinely durable targets | 40 (19%) | | live booths | 24 | | …carrying the `.forever` escape hatch | **13 (54%)** | | largest booth | 270 images, rendered as one flat wall | | distinct agent handles posting | 17 | | commits to ship the asks primitive, in one day | 5 | Two of those are load-bearing and neither was obvious before counting: - **69% rot is structural, not drift.** `booth link` is an `O_APPEND` write with no identity and no stated rule, so re-announcing a bench creates a row rather than updating one, and a booth URL rots the moment its booth is swept. - **54% kept means the escape hatch is carrying the main load.** A 24h TTL with a boolean override is not "ephemeral with an exception"; it is two lifetimes wearing one lifetime's clothes, and the operator is doing the sorting by hand. ## The defect behind every tacked-on part > **One lifetime (24h from last touch) and one shape (a folder), serving five > jobs with different lifetimes and different shapes.** Every bolt-on is a correct local fix to that one global mismatch: | bolt-on | the mismatch it patches | |---|---| | `.forever` | "this one shouldn't expire" | | the link board | "this one isn't a folder" | | `inline.py` (a placeholder DSL) | "this one isn't a gallery" | | `wrap_verbatim_html` (6 regexes into arbitrary HTML) | "the Booth doesn't own this page" | | the floating amber asks chip + `/b//asks` | "the operator can't see the question" | Five mechanisms exist to get **one question next to one artifact**. That is the definition of accretion, and the git log shows its signature: every feature ships, then takes 2–5 patches for cases the single shape did not anticipate. ## The five jobs | # | job | example on the live service | what it needs | |---|---|---|---| | 1 | **Review a set** | `sindra20-engines` (197 files), `pancake-v3-full` (270) | grid, sections, zoom, filter, flag-the-winners | | 2 | **Review a change** | `pancake-v3-full` **vs** `pancake-v4-full` | pairing by name, A/B | | 3 | **Read a report** | `pewpew-ui-brief`, `run07-decisions` | prose + figures + nav, chrome the Booth owns | | 4 | **Decide** | asks | the question beside the artifact | | 5 | **Announce** | *145 dead link rows* | "operator, look at this" | Job 5 is the one nobody named. The link board was absorbing it: an agent posts a booth URL because **a booth has no way to announce itself** — the index card shows a name, an item count and a countdown, but not who made it or what it is for. Enforcing the link rule without giving job 5 a home just makes it homeless. --- # The model Four nouns. Everything else is a view over them. ## 1. Booth — a review surface with a derived lifetime A folder, as before. What changes is that **lifetime is derived from state, not set by a boolean**: - Default: swept 24h after last activity. Unchanged, and still the contract that means nobody cleans up after themselves. - **An open mark pins its booth.** A booth the operator still owes an answer to cannot be swept. (Today it can — a real defect, and one of the reasons `.forever` gets pressed.) - **Viewing a booth page is activity.** If he is still looking at it, it is still alive. Deliberate act, so it cannot be triggered by browsing the index. - `keep` survives as an explicit, reasoned pin — not as the only way to say "not yet". > Expected effect: the 54% kept rate falls to the booths that are genuinely > durable references. **This is a prediction, and it is measurable** — re-count > the `.forever` rate a fortnight after this lands. If it does not move, the > diagnosis was wrong and the boolean was doing something else. Each booth carries `.booth.json`: `{handle, title, why, created}` — written by the CLI from `$ALTHING_HANDLE`. **This is what closes job 5.** The index card gains provenance and a one-line purpose, and the index becomes the "what landed" feed the link board was being used as. ## 2. Item — one record, resolved once, carried everywhere Today `build_gallery()` folds a `.txt` sidecar into an item — and `booth_view_file()` passes only `{name, file, file_url}` to the zoom template. **The caption is not lost in rendering; it is never sent.** Same for the doc view. So: one `booth_items(booth)` resolver returns the full record — `{rel, kind, caption, marks, flagged, blurred, section, url}` — and the gallery, the zoom view, the doc view, the compare view and the zip manifest all read from it. The zoom-loses-annotation bug is then fixed *by construction*, not by patching one more template and hoping the next one remembers. **Sections come from subfolders, which already exist on disk and are currently thrown away.** `pewpew-ui-brief` has `integration/` and `blueprint/`; `dfa-concepts` has `source/`. `rglob` flattens them into one wall. Rendering the structure the poster already created costs nothing and is most of the navigation fix. ## 3. Mark — operator judgment attached to a target The unification, and the reason "comments, votes and asks" stop being three tacked-on things: > **An ask is the session asking the operator. An annotation is the operator > telling the session. A vote is the operator pointing at the good ones.** All > three are *operator judgment attached to an artifact*. One primitive. ``` MARK target : the booth, or one item in it shape : pick — one of N options the session declared in advance (was: ask) note — free text the operator volunteered (was: comment) flag — this one / not this one (was: vote) writer : the operator, in the browser reader : the session — `booth marks [--wait]` ``` One storage model, one read path, one rendering slot (beside the artifact), one place where "is anything still open?" is computed. `pick` keeps today's ask semantics wholesale, including partial answers and the multi-question form — that behaviour is operator-settled (2026-09-09) and is preserved, not redesigned. `flag` is the one that makes a 270-image booth tractable, and it closes a loop that is currently running through chat: `golden-candidates`, `sindra-finalists` and the `pancake-*` ladders are all the operator picking winners from a set and then *telling the session in conversation*. Flags make that readable by the session that posted the set. ## 4. Bench — a running thing, registered **Not a booth.** The operator's rule, stated by him and now enforced in code: > Links are for durable middle-to-long-term testing surfaces — jackdaw's current > bench, talk's current bench — the things that get promoted to Homepage when > fully deployed. ``` BENCH id : normalized URL (the identity — re-posting UPDATES, never appends) name : what it is owner : the agent handle that registered it state : live → promoted (to Homepage) → retired health : last_checked, last_ok (probed; flagged when dead, never auto-deleted) ``` - Stored at `~/booth-data/.benches.json` — a dotfile, outside the booth namespace, so it can neither be swept nor mistaken for a booth. - `booth bench add ""` upserts on the normalized URL. The 5 `talk` rows and 4 `peedlar` rows collapse to one each, by construction. - **`booth link` refuses a `…:8090/b/…` URL** and names the right surface. It survives as a deprecated alias rather than vanishing — 17 handles have the muscle memory, and the teaching moment belongs at the point of use. - Liveness is *flagged*, not enforced. A bench that stops answering gets a marker and a date; deleting is the operator's call. Nothing here deletes the operator's data on a timer. ### Migrating the 211 existing rows 1. `links.md` is archived verbatim to `~/booth-data/links/links-archive-.md` **and** committed to this repo. Nothing the operator wrote is destroyed. 2. The ~40 durable rows seed the registry, deduplicated. 3. The 145 dead rows and 26 live booth-links are not carried over — the first point at nothing, the second are booths, which the index now announces itself. --- # The frame: who owns the page Operator ruling, 2026-09-21: **the page declares itself, the Booth mounts into it.** A report includes one line: ```html ``` …and the Booth's chrome — nav, marks, annotations, the way home — mounts through a real DOM API. Marks land at `data-booth-mark=""` anchors, which keeps the 2026-09-09 ruling that *asks render inline beside the artifact, not on a separate page*. If the line is absent, the Booth injects it at **one** insertion point, so every existing verbatim booth keeps working untouched. **What this deletes**, and this is the whole point of the decision: - `booth/inline.py` — 114 lines of placeholder DSL, entirely - `wrap_verbatim_html` and its six regexes against arbitrary HTML (`_HEAD_CLOSE_RE`, `_HTML_OPEN_RE`, `_DOCTYPE_RE`, `_BODY_CLOSE_RE`, `_HTML_CLOSE_RE`, `_ICON_RE`) and the doctype/charset-ordering constraints they are threading - `_BACK_CHIP`, `asks_chip` — two floating chips positioned by guessed offsets - `GET /b//asks` — the standalone page that existed only because a verbatim booth could not show its own asks Regex-injecting into arbitrary author HTML is the single most fragile thing in the service, and it is load-bearing for the operator's most important workflow. A declared seam costs the author one line and removes the whole class. --- # Navigation Sized for the real case — 270 items — not the demo case. - **Sections** from subfolders (free; the data is already there). - **A sticky rail**: total, per-section counts, and filters — all / flagged / annotated / unanswered. Jump-to-section. - **Keyboard in the grid, not only in the viewer.** Today prev/next exists only *inside* the zoom view: `←/→` move, `f` flags, `n` opens a note, `Enter` zooms, `Esc` backs out. A 270-item review is a keyboard task. - **Zoom keeps the item's annotations** — the same item record, so the caption, notes and flag state are all present at full size. - **Compare**, detected rather than configured: when two subfolders share filenames, offer pair-by-name A/B. This is the `pancake-v3-full` / `pancake-v4-full` dance — currently four separate booths diffed by eye across two browser tabs — collapsed into one booth with two subfolders. --- # Non-goals, kept from the original and still right - **No auth.** LAN/mesh-internal, and blur stays explicitly cosmetic. Anything that must not be seen by whoever can reach `:8090` must not be in a booth. - **No database.** The filesystem stays the state; `ls ~/booth-data` still tells you everything. Marks, benches and booth metadata are all flat files. - **No upload API for booths.** A session makes a folder. That is the whole API, and it is why every agent family can use this without a client.