';
+ // Ids and the find box come with finished work: a generator that sends no `done` gets the floor exactly as before.
+ const showIds = !!(state.done && Array.isArray(state.done.rows));
+ document.querySelector('#findbox').hidden = !showIds;
const queue = state.queue.map(item => `
-
';
+ // While a find is on, a table with no matching row says so instead of standing empty under its count.
+ const queueRows = queue + (showIds && state.queue.length ? '
No match in the queue.
' : '');
+
+ // Work that finished, from a provider that sends `done` (an older one doesn't, and then this section isn't drawn). The answer
+ // a seat closed the work with is its result; a field the work doesn't carry reads "not recorded", and a result the provider
+ // couldn't read says so instead of looking empty.
+ const unset = (text) => `${text}`;
+ // A closure in plain words; the queue's own word stays in the tooltip.
+ const SAY = { 'no-follow-on': 'Done, nothing follows', handed_off_to: 'Handed off to', blocked_on: 'Blocked on', denied: 'Denied', canceled: 'Canceled', escalation: 'Escalated' };
+ const decisionOf = (d) => !d?.reason ? unset('not recorded')
+ : `${escapeHtml(SAY[d.reason] || d.reason)}${d.target ? `${SAY[d.reason] && /to$|on$/.test(SAY[d.reason]) ? ' ' : ': '}${escapeHtml(d.target)}` : ''}`;
+ // An artifact reference may break after "@", ":", "/" or "#", not inside a word.
+ // Split the raw text first and escape each part: inserting after escaping would cut an entity (') apart.
+ const breakable = (t) => String(t).split(/(?<=[@:/#])/).map(escapeHtml).join('');
+ const done = state.done && Array.isArray(state.done.rows) ? state.done : null;
+ const doneRows = done ? done.rows.map(item => {
+ if (!item || typeof item !== 'object') return '';
+ const decision = decisionOf(item.decision);
+ const answer = item.result ?? item.resolution;
+ const result = item.resultRead === false ? unset("couldn't be read") : typeof answer === 'string' && answer !== '' ? escapeHtml(answer) : unset('not recorded');
+ const closed = Date.parse(item.closedAt || '');
+ return `
+
- `;
+ ${doneSection}`;
+ lastState = state;
+ findWork();
updated.textContent = `Runtime snapshot · ${new Date(state.generatedAt).toLocaleString()}`;
}
+ // Find by id: only among the work this floor already holds (this rig's open work and its newest finished work); it reads
+ // nothing else. Kept outside #content so a refresh doesn't clear what was typed, and applied again after every refresh.
+ let lastState = null;
+ const findInput = document.querySelector('#find'), found = document.querySelector('#found');
+ // "This rig's" work is work with one of this floor's seats at either end: a generator that still sends other rigs' open work
+ // can't make the find box call that ours.
+ function findWork() {
+ const q = findInput.value.trim();
+ const seats = new Set((lastState?.seats || []).map(s => s.seat));
+ const ours = item => item && (seats.has(item.source) || seats.has(item.destination));
+ const hit = item => ours(item) && String(item.id).includes(q);
+ const openHits = (lastState?.queue || []).filter(hit), doneHits = (lastState?.done?.rows || []).filter(hit);
+ const shown = new Set([...openHits, ...doneHits].map(item => String(item.id)));
+ for (const row of content.querySelectorAll('tr[data-id]')) row.hidden = !!q && !shown.has(row.dataset.id);
+ for (const [sel, hits] of [['#nomatch-open', openHits], ['#nomatch-done', doneHits]]) { const r = document.querySelector(sel); if (r) r.hidden = !q || hits.length > 0; }
+ if (!q) { found.textContent = ''; return; }
+ const done = lastState?.done;
+ found.textContent = openHits.length || doneHits.length ? `${openHits.length} open, ${doneHits.length} finished`
+ : done?.coverage?.unreadable ? "Not in this rig's open work. Finished work couldn't be read, so this can't say whether it finished."
+ : `No work with that id in this rig's open work or its newest ${done?.rows?.length ?? 0} finished items (older finished work isn't searched).`;
+ }
+ findInput.addEventListener('input', findWork);
+
// The box's own introduction, declared in its manifest. Absent on a box
// that declares none, which is the normal case — this surface ships to
// every install and must not assume it is anybody's landing page.
diff --git a/contract/runtime-api.md b/contract/runtime-api.md
index 1069ab3..152452e 100644
--- a/contract/runtime-api.md
+++ b/contract/runtime-api.md
@@ -110,6 +110,27 @@ by this contract (declared here, field-by-field), not by any backing system.
the nearest minute, or `null` when no activity timestamp is known. It
measures quietness, not uptime: a `working` seat can have a nonzero age.
- Degraded shape: `{ "ok": false, "degraded": "", "rig": null, "seats": [], "queue": [] }`.
+- `done` (optional, provisional): finished work, so work handed to a seat stays findable after it closes. Absent means the
+ generator doesn't send finished work; `rows: []` means none has finished. `queue` stays open work only.
+
+```json
+"done": {
+ "rows": [
+ { "id": "", "state": "done", "title": "",
+ "source": "", "destination": "", "closedAt": "",
+ "decision": { "reason": " | null", "target": " | null" },
+ "result": " | null", "resultRead": true,
+ "resolution": " | null", "artifact": " | null",
+ "handedOffTo": " | null", "handedOffFrom": " | null" }
+ ],
+ "coverage": { "looked": 100, "ours": 12, "shown": 10, "limit": 100 }
+}
+```
+
+ A field the item doesn't carry is `null`, never invented. `resultRead: false` means the result couldn't be read (not that
+ there is none). `coverage` says how much was looked at; `{ "unreadable": "" }` there means finished work couldn't be read.
+ The runtime carries `done` through only in this shape: a `done` that is sent but isn't in it (rows not a list, a row without a
+ string `id`) arrives as `{ "rows": [], "coverage": { "unreadable": "" } }`, never as none, and the open work is kept. Its text is shown as written: this runtime has no private display mode.
## stream
diff --git a/test/floor-done.test.mjs b/test/floor-done.test.mjs
new file mode 100644
index 0000000..7c70f63
--- /dev/null
+++ b/test/floor-done.test.mjs
@@ -0,0 +1,139 @@
+// FLOOR keeps finished work findable: the runtime carries the generator's optional `done` block in its declared shape
+// (contract/runtime-api.md), and the surface shows it as "Done recently" with "not recorded" where a field is missing and a
+// find-by-id box that looks only at what the floor already holds. Invented fixture data only.
+import test from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import net from "node:net";
+import path from "node:path";
+import vm from "node:vm";
+import { spawn } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const surface = fs.readFileSync(path.join(REPO, "app", "surfaces", "floor.html"), "utf8");
+const DONE = { rows: [
+ { id: "qitem-20261023070000-aaaa", state: "done", title: "Fix the README typo.", source: "assistant@lab", destination: "lab-impl@lab",
+ closedAt: "2026-10-23T07:00:00.000Z", decision: { reason: "no-follow-on", target: null }, result: "Fixed line 12.", resultRead: true,
+ resolution: null, artifact: "docs/README.md@1a2b3c4", handedOffTo: null, handedOffFrom: null },
+ { id: "qitem-20261023065900-bbbb", state: "done", title: "Answer a question", source: "lab-qa@lab", destination: "lab-impl@lab",
+ closedAt: "2026-10-23T06:59:00.000Z", decision: { reason: null, target: null }, result: null, resultRead: true, resolution: null, artifact: null },
+ { id: "qitem-20261023065800-cccc", state: "done", title: "Unread", source: "lab-qa@lab", destination: "lab-impl@lab",
+ closedAt: "2026-10-23T06:58:00.000Z", decision: { reason: "handed_off_to", target: "qitem-x" }, result: null, resultRead: false, artifact: null },
+], coverage: { looked: 40, ours: 3, shown: 3, limit: 100 } };
+const SEATS = [{ seat: "lab-impl@lab" }, { seat: "lab-qa@lab" }];
+const OPEN = [{ id: "qitem-20261023071000-dddd", state: "pending", destination: "lab-impl@lab", source: "assistant@lab", tags: [], updated: "2026-10-23T07:10:00.000Z", title: "Open work" }];
+
+const freePort = () => new Promise((resolve, reject) => { const s = net.createServer(); s.once("error", reject); s.listen(0, "127.0.0.1", () => { const { port } = s.address(); s.close(() => resolve(port)); }); });
+async function stateFrom(fixture) {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "floor-done-"));
+ fs.cpSync(path.join(REPO, "app"), path.join(dir, "app"), { recursive: true });
+ fs.cpSync(path.join(REPO, "fixtures"), path.join(dir, "fixtures"), { recursive: true });
+ fs.writeFileSync(path.join(dir, "fixtures", "factory-state.json"), JSON.stringify(fixture));
+ const port = await freePort(), proc = spawn(process.execPath, [path.join(dir, "app", "serve-studio.mjs"), "--port", String(port), "--fixtures", path.join(dir, "fixtures")], { cwd: dir, stdio: "ignore" });
+ try {
+ for (let i = 0; i < 100; i++) { try { return await (await fetch(`http://127.0.0.1:${port}/api/factory/state`)).json(); } catch { await new Promise((r) => setTimeout(r, 100)); } }
+ throw new Error("runtime never answered");
+ } finally { proc.kill(); fs.rmSync(dir, { recursive: true, force: true }); }
+}
+
+test("the runtime carries `done` through in its declared shape; absent stays absent; a malformed one is carried as unreadable, never as none", async () => {
+ const s = await stateFrom({ rig: "lab", attached: true, seats: [], queue: OPEN, done: DONE });
+ assert.deepEqual(s.done, DONE); assert.deepEqual(s.queue, OPEN, "queue is unchanged");
+ assert.equal("done" in await stateFrom({ rig: "lab", attached: true, seats: [], queue: [] }), false, "a generator that sends no done: absent, not empty");
+ for (const bad of ["nope", { rows: [null] }, { rows: [{ title: "no id" }] }, { rows: "x" }, null]) {
+ const s2 = await stateFrom({ rig: "lab", attached: true, seats: [], queue: OPEN, done: bad });
+ assert.deepEqual(s2.done, { rows: [], coverage: { unreadable: "the finished work this box sent wasn't in the declared shape" } }, JSON.stringify(bad));
+ assert.deepEqual(s2.queue, OPEN, "a bad finished-work block never costs the open work");
+ }
+ assert.deepEqual((await stateFrom({ rig: "lab", attached: true, seats: [], queue: [], done: { rows: [] } })).done, { rows: [], coverage: {} });
+});
+
+// The surface's own script, run with just enough of a page for render() and the find box.
+function page() {
+ const el = (sel) => ({ sel, innerHTML: "", textContent: "", value: "", className: "", lastElementChild: { textContent: "" }, hidden: false,
+ addEventListener() {}, querySelectorAll() { return rowsOf(this.innerHTML); } });
+ const els = { "#content": el("#content"), "#rig": el("#rig"), "#updated": el("#updated"), "#connection": el("#connection"), "#find": el("#find"), "#found": el("#found"), "#findbox": el("#findbox"), "#nomatch-open": el("#nomatch-open"), "#nomatch-done": el("#nomatch-done") };
+ const rows = new Map();
+ const rowsOf = (html) => [...html.matchAll(/