diff --git a/agents/darkwing/work/queue-58/BUILD.md b/agents/darkwing/work/queue-58/BUILD.md
new file mode 100644
index 000000000..dab22d232
--- /dev/null
+++ b/agents/darkwing/work/queue-58/BUILD.md
@@ -0,0 +1,200 @@
+# Row 58 (#1547): bus read, task priority and requirement
+
+Brief: `docs/plans/2026-10-10_design-implementation.md`, section "Bus read:
+task priority and requirement" (blob `a360554c`). Owner Darkwing; reviewers
+Dewey and Filbert.
+
+The candidate is `build.patch` against the base in `base.txt`, 8 files in
+`files.txt`. `candidate-manifest.sha256` holds the sha256 of each file after
+the patch.
+
+I built it on `72b16a5b`. Row 57 (#1546) then landed as `f78ba57d` and
+changed four of these files: `broker.mjs`, both READMEs and the webui
+`bus.test.mjs`. `git apply --3way` put the patch on `ae428a54` with no
+conflict. Row 57 adds the decisions reads and this row changes the
+`tasks` rows, so the hunks sit in different places; I read the merged
+`broker.mjs` and both changes are intact. `build.patch`, the manifest and
+the mutants are on `ae428a54`. The gate ran on `f2f2861b`, after row 62
+landed; no candidate file changed between the two.
+
+## The problem
+
+The brief says priority isn't in `task_current.fields`. It is:
+`taskFields()` in `packages/tasks/src/digest.mjs` puts `priority` in every
+live snapshot, `checkTask()` refuses a task without an integer priority, and
+the Tasks table already showed it from the fields. What the read model
+lacked was a named priority field, and the requirement, which sat only in
+the `task.created` body (`packages/tasks/src/verbs.mjs`). The list never
+reads that body, since the list has no trail. So for priority this row
+lifts an existing value to a top-level key; the requirement is new data in
+the list. The brief asks for both fields in the task read model, a
+projection rebuild test, and the two columns in the Tasks view.
+
+## What changed
+
+### The bus `tasks` rows
+
+`Broker.#taskRows()` in `packages/bus/src/broker.mjs` builds the rows the
+`tasks` verb returns. Each is `task_current`'s row as before, with two keys
+added:
+
+- `priority` is the current row's `fields.priority`. It is null for a
+ tombstone, whose fields are only `gone` (and `project` on a move), and for
+ a read with no priority. Vikunja's 0 stays 0.
+- `requirement` is the requirement in the first `task.created` for the same
+ Vikunja task id in the business. A move to another project changes the
+ project in the ref but not the task id, so the moved task keeps the
+ requirement it was created with. It is null for a task the stack didn't
+ create.
+
+Both are computed from append-only rows at each read; nothing new is stored.
+I chose that over a schema change. `store.mjs` checks the DDL against
+`SCHEMA_SHA256` and an open bus against the reference digest, so a new
+column or a changed `task_current` view needs a schema version and a
+migration of every existing `bus.sqlite`. A read-time projection needs
+neither, and a reopened bus gives the same rows by construction.
+
+`taskView()`, the trusted S3 read, is unchanged and still returns
+`task_current`'s rows only. Its comment now names `#taskRows` as the verb's
+read.
+
+The requirement read is one query per `tasks` call over the business's
+`task.created` events. It isn't indexed by kind; a business with many
+thousands of created tasks would make each Tasks refresh scan them. That is
+fine at slice 1 sizes, and a later row can add an index if a real business
+needs one.
+
+### The Tasks view
+
+`packages/webui/src/public/bus.js`:
+
+- `task()` takes `requirement` from the bus row first, then from the trail's
+ `task.created`, so the list, which reads no trail, now has it. Priority is
+ the row's, then the last snapshot that read the task (a tombstone's page),
+ then null. Before, a missing priority became 0 and showed "unset". A
+ poller read always has one, so "none" shows only for a row written without
+ it, like the browser test's #9.
+- The Tasks table gains a Requirement column after Task. Requirement and
+ Priority show a muted "none" when the row has none, in the list and on the
+ task page.
+- `bus.css`: the tasks table's minimum width goes from 900px to 1020px for
+ the extra column. The browser test's overflow checks still pass.
+
+For a live row the bus `priority` and the fields' `priority` are the same
+value, so preferring the row's changes nothing visible today. I kept it so
+the view reads the field the brief puts in the read model; the mutant that
+drops it survives for that reason (below).
+
+### Docs
+
+`packages/bus/README.md` describes the two keys under "Shared reads and
+adapter boundary". `packages/webui/README.md` adds one bullet on the
+columns to the S5 section.
+
+## Scope
+
+The brief owns `packages/bus/`, `packages/tasks/`, the webui Tasks endpoint
+and tests. The webui `bus.js`, `bus.css` and README edits are the Tasks view
+the brief asks for. Nothing in `packages/tasks/` changed: the poller already
+writes `priority` in each snapshot's fields, and `task.create` writes
+`requirement` in `task.created`. Field writers are row 61.
+
+## Tests
+
+New `packages/bus/tests/task-read.test.mjs`, 3 tests. The seed has six demo
+tasks: created by the stack, changed outside it since, never seen by the
+stack, created then gone, created then moved to another project, and a
+second `task.created` for the moved task's new ref. A second business reuses
+one task id with its own requirement.
+
+1. "tasks rows carry priority and requirement for every session that reads
+ tasks": the human, a reader and a role session get the same rows. A later
+ poll's priority wins over the stack's write, 0 stays 0, tombstones have
+ none, the moved task keeps its first requirement, and the other business
+ sees only its own. Each row minus the two keys equals `taskView(...,
+ 'current')`'s row, and `taskView` has neither key.
+2. "the read model rebuilds from the ledger": the rows after a close and
+ reopen equal the rows before. Each row's priority equals its current
+ snapshot's in `trail`, and its requirement equals the first `task.created`
+ for its task id in `events`. A read writes nothing.
+3. "an empty business and one with snapshots but no task.created read
+ cleanly".
+
+All three fail on the base broker (`out/task-read-on-base.log`).
+
+`packages/webui/tests/bus.test.mjs`: the endpoint test asserts the row's
+`priority` and `requirement` reach `/api/bus/tasks` as null, not missing.
+
+`packages/webui/tests/bus-browser.test.mjs`:
+
+- The done task (#9) is now written with no priority.
+- The Tasks list check reads each table's header and each row's Requirement
+ and Priority cells: #8 `none`/`high`, #7 `REQ-SLICE-1`/`high`, #9
+ `none`/`none`. It replaces the `/high/` match.
+- #7's page shows `Priorityhigh`. The moved task's page (#10, a tombstone)
+ shows `Priorityhigh` from its last snapshot.
+- #9's page shows `Requirementnone` and `Prioritynone`. This visit comes
+ after the "never read" failure check, which needs #9's trail unread; an
+ earlier visit made that check fail.
+
+## Mutants
+
+`out/mutants.sh` edits one source file per mutant, runs the one test file
+meant to fail it, and restores the file from the build worktree's index,
+which holds the candidate. Output: `out/mutants.txt`, from the run on `ae428a54` at 22:22 UTC on
+2026-10-11. 16 of 17 fail, as they did on `72b16a5b`; each webui kill
+failed on the intended assertion.
+
+| Mutant | Fails |
+|---|---|
+| `tasks-verb-unchanged`: the verb returns `task_current` as before | task-read 1, 2, 3 |
+| `no-priority`: priority always null | task-read 1, 2 |
+| `zero-priority-null`: `\|\| null`, so 0 becomes null | task-read 1, 2 |
+| `tombstone-priority-zero`: `?? 0` | task-read 1, 2, 3 |
+| `requirement-by-ref`: keyed by the whole ref | task-read 1, 2 |
+| `requirement-last-wins`: the last `task.created` wins | task-read 1, 2 |
+| `requirement-any-business`: events from every business | task-read 1 |
+| `requirement-undefined`: no `?? null` | task-read 1, 2, 3 |
+| `task-view-adds-keys`: `taskView` grows the keys too | task-read 1 |
+| `endpoint-drops-keys`: the rows lose both keys | webui bus endpoint test |
+| `list-requirement-from-trail`: requirement only from the trail | browser |
+| `no-requirement-column` | browser |
+| `priority-default-zero`: `?? 0` in the view | browser |
+| `tombstone-no-fallback`: no snapshot fallback | browser |
+| `detail-priority-raw`: the task page prints the raw value | browser |
+| `list-priority-raw`: the list prints the raw value | browser |
+| `ignore-row-priority`: the view reads only the fields' priority | survives |
+
+`ignore-row-priority` is equivalent today, as said above.
+
+## Gate
+
+`out/gate/gate.sh` ran on 2026-10-11 from 21:57 to 22:02 UTC. Its worktree
+was at `f2f2861b` with `build.patch` applied (`out/gate/apply.txt`) and
+`node_modules` linked to the main checkout's. It ran each suite in turn and
+wrote each log to `out/gate/`. The brief names bus, tasks and webui plus
+every `scripts/test-*.sh`.
+
+| Suite | Result |
+|---|---|
+| packages/bus | 83 pass, 0 fail |
+| packages/tasks | 71 pass, 0 fail |
+| packages/webui | 40 pass, 0 fail, the browser tests included |
+| test-auth | 15 passed |
+| test-conductor | 17 passed |
+| test-config | 24 passed |
+| test-discord | 66 passed |
+| test-extension-package | 18 passed |
+| test-foundation | 44 passed |
+| test-queue | 27 passed |
+| test-release | 14 passed |
+| test-task | 26 passed |
+
+`test-task` ran with `DOCKER_HOST` set to a missing socket and skipped its
+live recall pair. `test-queue` skipped its canonical-root check, since the
+gate worktree isn't the queue's root. Sage's gate rerun covers both.
+After the gate a `gate_default` Docker network was left with no
+containers, and I removed it.
+
+Candidate manifest sha256:
+`e63e4c4f756229a9e5bba276c908e272fae0a71ae4f0a1c47e7e47b630bc63e6`.
diff --git a/agents/darkwing/work/queue-58/base.txt b/agents/darkwing/work/queue-58/base.txt
new file mode 100644
index 000000000..d252227d4
--- /dev/null
+++ b/agents/darkwing/work/queue-58/base.txt
@@ -0,0 +1 @@
+ae428a5409a9171e0879e3ed843319e68be20ff8
diff --git a/agents/darkwing/work/queue-58/build.patch b/agents/darkwing/work/queue-58/build.patch
new file mode 100644
index 000000000..96dc94991
--- /dev/null
+++ b/agents/darkwing/work/queue-58/build.patch
@@ -0,0 +1,367 @@
+diff --git a/packages/bus/README.md b/packages/bus/README.md
+index 5b57b7fc..7fa51f92 100644
+--- a/packages/bus/README.md
++++ b/packages/bus/README.md
+@@ -221,7 +221,11 @@ await read.decisions('roles'); await read.decisions('recent');
+
+ S4 and S5 must import this client-only module. Neither opens `bus.sqlite`.
+ The tasks projection reports the schema’s `task_current` view, including tombstones; the caller
+-must not pretend a missing/inaccessible task is open or completed. Trail joins
++must not pretend a missing/inaccessible task is open or completed. Each row adds two keys,
++computed at each read and never stored. `priority` is the current row's `fields.priority`, null
++for a tombstone or a read with none (Vikunja's 0 stays 0). `requirement` is the requirement of the
++first `task.created` for the same Vikunja task id in the business, so a task moved to another
++project keeps it; null for a task the stack didn't create. Trail joins
+ human request, messages/deliveries, decisions/resolutions, task snapshots and
+ session/claim evidence. Broker timestamps advance monotonically across all
+ its writes, including after reopen; per-table `seq` stays the primary table
+diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs
+index 84db8799..595ca232 100644
+--- a/packages/bus/src/broker.mjs
++++ b/packages/bus/src/broker.mjs
+@@ -303,6 +303,26 @@ export class Broker {
+ id,
+ );
+ }
++ // Row 58 (#1547): the 'tasks' rows, each with the task's priority and requirement. Priority
++ // is the current row's; a tombstone has none. The requirement is the first task.created for
++ // the same Vikunja task id, which a move to another project keeps; null for a task the stack
++ // didn't create. Both come from append-only rows at each read; nothing is stored.
++ #taskRows(business) {
++ const taskId = (ref) => ref.slice(ref.indexOf('/') + 1);
++ const requirement = new Map();
++ for (const e of this.#store.all(
++ "SELECT subject,json_extract(body,'$.requirement') AS requirement FROM events WHERE business=? AND kind='task.created' ORDER BY seq",
++ business,
++ ))
++ if (!requirement.has(taskId(e.subject))) requirement.set(taskId(e.subject), e.requirement);
++ return this.#store
++ .all('SELECT * FROM task_current WHERE business=? ORDER BY task_ref', business)
++ .map((r) => {
++ const fields = JSON.parse(r.fields);
++ const priority = fields.priority ?? null;
++ return { ...r, fields, priority, requirement: requirement.get(taskId(r.task_ref)) ?? null };
++ });
++ }
+ #checkAuthority(s, action, { decision = null, target = null } = {}) {
+ this.#agent(s);
+ if (
+@@ -418,7 +438,8 @@ export class Broker {
+ s.run,
+ ).lastInsertRowid;
+ }
+- // Trusted S3 reads. Agents read the same rows through the 'tasks' view verb.
++ // Trusted S3 reads. Agents read the same rows through the 'tasks' view verb, which adds
++ // priority and requirement (#taskRows).
+ taskView(business, view, arg = null) {
+ this.#business(business);
+ const parse = (r) => ({ ...r, fields: JSON.parse(r.fields) });
+@@ -830,10 +851,7 @@ export class Broker {
+ "SELECT * FROM role_claims c WHERE business=? AND op='claim' AND seq=(SELECT max(seq) FROM role_claims WHERE business=c.business AND role=c.role) ORDER BY role",
+ s.business,
+ );
+- if (verb === 'tasks')
+- return this.#store
+- .all('SELECT * FROM task_current WHERE business=? ORDER BY task_ref', s.business)
+- .map((r) => ({ ...r, fields: JSON.parse(r.fields) }));
++ if (verb === 'tasks') return this.#taskRows(s.business);
+ return this.#trail(s, identifier(a.subject));
+ }
+ // Row 57 (#1546): the Inbox's "Routed to roles" and "Recent" lists, for human and
+diff --git a/packages/bus/tests/task-read.test.mjs b/packages/bus/tests/task-read.test.mjs
+new file mode 100644
+index 00000000..2f353d1b
+--- /dev/null
++++ b/packages/bus/tests/task-read.test.mjs
+@@ -0,0 +1,151 @@
++import test from 'node:test';
++import assert from 'node:assert/strict';
++import { mkdtempSync, rmSync } from 'node:fs';
++import { tmpdir } from 'node:os';
++import { join } from 'node:path';
++import { Store } from '../src/store.mjs';
++import { Broker } from '../src/broker.mjs';
++// Row 58 (#1547): each 'tasks' row carries the task's priority and requirement, read from the
++// append-only task_snapshots and task.created rows, for every session that may read tasks.
++const roles = { pm: { authority: { withinRole: ['message.send', 'task.create'], crossRole: [] } } };
++const businesses = {
++ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles },
++ other: { id: 'other', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles },
++};
++const at = '2026-10-08T12:00:00.000Z';
++// task_current keeps a poll only when it was read after the last self snapshot, whose `at` is the
++// broker's clock, so a read after a write must be later than now.
++const later = new Date(Date.now() + 3600e3).toISOString();
++const digest = 'c'.repeat(64);
++const fields = (extra = {}) => ({ bucket: 11, done: false, priority: 0, ...extra });
++const poll = (task_ref, f, extra = {}) => ({ task_ref, updated: at, digest, fields: f, via: 'board', read_at: at, ...extra });
++const gone = (task_ref, f) => poll(task_ref, f, { via: 'task', read_at: later });
++const tasks = (b, cap) => b.request(cap, { verb: 'tasks', args: {} });
++const pick = (rows) => rows.map((r) => [r.task_ref, r.priority, r.requirement]);
++function open(root) {
++ const store = new Store(root);
++ const b = new Broker({ store, businesses });
++ const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
++ const agent = (business) => {
++ const cap = b.bindLaunch({ business, role: 'pm', run: `${business}-pm`, harness: 'pi' });
++ b.request(cap, { verb: 'role.claim' });
++ return cap;
++ };
++ return { store, b, human, reader: b.bindReader({ business: 'demo' }), agent };
++}
++function setup(t) {
++ const root = mkdtempSync(join(tmpdir(), 'bus-task-read-'));
++ const bus = { root, ...open(root) };
++ t.after(() => {
++ bus.store.close();
++ rmSync(root, { recursive: true, force: true });
++ });
++ return bus;
++}
++// The pm creates a task the way packages/tasks does: a self snapshot and task.created.
++function create(b, cap, human, business, task_ref, requirement, f) {
++ const { request } = b.request(human, { verb: 'message.send', args: { to: 'pm', body: 'please' } });
++ b.recordTask({
++ cap,
++ business,
++ snapshots: [{ task_ref, updated: at, digest, fields: f }],
++ events: [{ kind: 'task.created', subject: task_ref, body: { request, requirement } }],
++ });
++}
++// Six demo tasks: created by the stack (3/7), changed outside it since (3/8), never seen by the
++// stack (3/9), created then gone (3/10), created then moved to project 4 (3/11 -> 4/11), and a
++// second task.created for 4/11's id.
++function seed(bus) {
++ const { b, human, agent } = bus;
++ const pm = agent('demo');
++ create(b, pm, human, 'demo', 'vikunja:3/7', 'REQ-SLICE-1', fields({ priority: 3 }));
++ create(b, pm, human, 'demo', 'vikunja:3/8', 'REQ-SLICE-2', fields({ priority: 1 }));
++ create(b, pm, human, 'demo', 'vikunja:3/10', 'REQ-SLICE-3', fields({ priority: 4 }));
++ create(b, pm, human, 'demo', 'vikunja:3/11', 'REQ-MOVE-1', fields({ priority: 2 }));
++ b.recordTask({
++ business: 'demo',
++ snapshots: [
++ poll('vikunja:3/8', fields({ priority: 5 }), { read_at: later }),
++ poll('vikunja:3/9', fields()),
++ gone('vikunja:3/10', { gone: 'no-access' }),
++ gone('vikunja:3/11', { gone: 'moved', project: 4 }),
++ poll('vikunja:4/11', fields({ priority: 2, bucket: 21 }), { via: 'reconcile', read_at: later }),
++ ],
++ });
++ create(b, pm, human, 'demo', 'vikunja:4/11', 'REQ-MOVE-2', fields({ priority: 2, bucket: 21 }));
++ // The same task id in another business has its own requirement.
++ const { request } = b.request(b.bindHuman({ business: 'other', human: 'jason', via: 'cli', outsideAgent: true }), {
++ verb: 'message.send',
++ args: { to: 'pm', body: 'please' },
++ });
++ b.recordTask({
++ cap: agent('other'),
++ business: 'other',
++ snapshots: [{ task_ref: 'vikunja:3/7', updated: at, digest, fields: fields({ priority: 1 }) }],
++ events: [{ kind: 'task.created', subject: 'vikunja:3/7', body: { request, requirement: 'REQ-OTHER-1' } }],
++ });
++ return pm;
++}
++const EXPECTED = [
++ ['vikunja:3/10', null, 'REQ-SLICE-3'],
++ ['vikunja:3/11', null, 'REQ-MOVE-1'],
++ ['vikunja:3/7', 3, 'REQ-SLICE-1'],
++ ['vikunja:3/8', 5, 'REQ-SLICE-2'],
++ ['vikunja:3/9', 0, null],
++ ['vikunja:4/11', 2, 'REQ-MOVE-1'],
++];
++
++test('tasks rows carry priority and requirement for every session that reads tasks', (t) => {
++ const bus = setup(t);
++ const pm = seed(bus);
++ for (const cap of [bus.human, bus.reader, pm]) assert.deepEqual(pick(tasks(bus.b, cap)), EXPECTED);
++ // The priority is the current row's: a later poll wins over the stack's write (3/8), and Vikunja's
++ // 0 stays 0, not null (3/9). A tombstone has no priority (3/10, 3/11).
++ // The requirement follows the task id: 4/11 keeps the one 3/11 was created with, not the second
++ // task.created, and a task the stack never created has none (3/9).
++ const other = bus.b.bindReader({ business: 'other' });
++ assert.deepEqual(pick(tasks(bus.b, other)), [['vikunja:3/7', 1, 'REQ-OTHER-1']]);
++ // Nothing else in the row changes: it is task_current's row with two keys added.
++ const current = bus.b.taskView('demo', 'current');
++ assert.deepEqual(
++ tasks(bus.b, bus.reader).map(({ priority, requirement, ...r }) => ({ ...r })),
++ current.map((r) => ({ ...r })),
++ );
++ assert.equal('priority' in current[0] || 'requirement' in current[0], false);
++});
++
++test('the read model rebuilds from the ledger: a reopened bus and each trail give the same values', (t) => {
++ const bus = setup(t);
++ seed(bus);
++ const before = tasks(bus.b, bus.reader);
++ bus.store.close();
++ Object.assign(bus, open(bus.root));
++ const after = tasks(bus.b, bus.reader);
++ assert.deepEqual(after, before);
++ assert.deepEqual(pick(after), EXPECTED);
++ // Each value is what the append-only rows say: the priority of the current snapshot, and the
++ // requirement of the first task.created for the task's id, read through trail.
++ for (const row of after) {
++ const trail = bus.b.request(bus.reader, { verb: 'trail', args: { subject: row.task_ref } });
++ const snap = trail.find((r) => r.table === 'task_snapshots' && r.seq === row.seq);
++ assert.equal(snap.fields.priority ?? null, row.priority, row.task_ref);
++ const id = row.task_ref.split('/')[1];
++ const created = bus.store
++ .all("SELECT subject, body FROM events WHERE business='demo' AND kind='task.created' ORDER BY seq")
++ .find((e) => e.subject.split('/')[1] === id);
++ assert.equal(created ? JSON.parse(created.body).requirement : null, row.requirement, row.task_ref);
++ }
++ // A read writes nothing.
++ const count = () => bus.store.get('SELECT (SELECT count(*) FROM events) + (SELECT count(*) FROM task_snapshots) n').n;
++ const n = count();
++ tasks(bus.b, bus.human);
++ tasks(bus.b, bus.reader);
++ assert.equal(count(), n);
++});
++
++test('an empty business and one with snapshots but no task.created read cleanly', (t) => {
++ const bus = setup(t);
++ assert.deepEqual(tasks(bus.b, bus.reader), []);
++ bus.b.recordTask({ business: 'demo', snapshots: [poll('vikunja:5/1', { bucket: 0, done: false })] });
++ assert.deepEqual(pick(tasks(bus.b, bus.reader)), [['vikunja:5/1', null, null]]);
++});
+diff --git a/packages/webui/README.md b/packages/webui/README.md
+index e43118d5..88df712a 100644
+--- a/packages/webui/README.md
++++ b/packages/webui/README.md
+@@ -87,6 +87,10 @@ note is `agents/dewey/work/wui/SLICE1-VIEWS.md`.
+ since the list has no trail. The task page reads the trail and tells a task
+ the stack never created from one changed in Vikunja after the stack wrote it.
+ A stale poll read is shown in the snapshots and never as current.
++- The Tasks table has Requirement and Priority columns, from the `tasks` row's
++ `requirement` and `priority` (row 58, #1547). Either one shows "none" when
++ the row has none. A tombstone's page shows the priority of the last snapshot
++ that read the task.
+ - All bus- and agent-authored text is rendered inert: controls show as control
+ pictures or `[U+XXXX]`, and Markdown stays as source.
+ - Views refresh on the board's ten-second interval and stop on Pause. Focus,
+diff --git a/packages/webui/src/public/bus.css b/packages/webui/src/public/bus.css
+index 39cf2991..acf496a5 100644
+--- a/packages/webui/src/public/bus.css
++++ b/packages/webui/src/public/bus.css
+@@ -25,7 +25,7 @@ body.on-bus .frame,body.on-bus.has-inspector .frame{grid-template-columns:240px
+ .s1-plain{list-style:none;padding:0;margin:0 0 10px;display:grid;gap:6px}.s1-plain li{overflow-wrap:anywhere}
+ .chips .chip{gap:6px}.panel{margin-bottom:16px}.panel h3{font-size:1rem;margin:14px 0 6px}
+ /* Tables */
+-table.s1-table td,table.s1-table th{overflow-wrap:anywhere}table.s1-tasks{min-width:900px}table.s1-tasks .s1-title{min-width:220px;max-width:340px}table.s1-agents{min-width:820px}
++table.s1-table td,table.s1-table th{overflow-wrap:anywhere}table.s1-tasks{min-width:1020px}table.s1-tasks .s1-title{min-width:220px;max-width:340px}table.s1-agents{min-width:820px}
+ table.s1-table tbody th{font-weight:600;text-transform:none;letter-spacing:0;color:var(--text);font-size:.9rem;white-space:normal}
+ .s1-done summary{list-style:none}.s1-done summary h2{display:inline}.s1-done summary::before{content:'▸ ';color:var(--muted)}.s1-done[open] summary::before{content:'▾ '}.s1-done{margin-bottom:20px}
+ .s1-marker{text-decoration:none}.s1-launch{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
+diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js
+index affb9b7b..3be22af5 100644
+--- a/packages/webui/src/public/bus.js
++++ b/packages/webui/src/public/bus.js
+@@ -105,8 +105,10 @@
+ return {
+ ref, project: project && +project, id: id && +id, title: shape.title ?? null, description: shape.description ?? '',
+ bucket: f.gone ? null : f.bucket, done: !!f.done, assignees: Array.isArray(shape.assignees) ? shape.assignees : [],
+- requirement: created?.body?.requirement ?? null, request: created?.body?.request ?? null,
+- due: shape.due_date ?? null, priority: shape.priority ?? 0, percent: shape.percent_done ?? 0,
++ // The bus 'tasks' row carries both (row 58). A tombstone's row has no priority, so the detail
++ // view falls back to the last snapshot that read the task.
++ requirement: row.requirement ?? created?.body?.requirement ?? null, request: created?.body?.request ?? null,
++ due: shape.due_date ?? null, priority: row.priority ?? shape.priority ?? null, percent: shape.percent_done ?? 0,
+ changedAt: row.at, seq: row.seq, external,
+ conflicts: evs.filter(e => e.kind === 'task.conflict').map(e => ({ at: e.at, ...e.body })),
+ missing: f.gone ? { reason: f.gone, at: row.at, project: missingEv?.body?.project ?? null } : null,
+@@ -287,10 +289,11 @@
+ return out.join(' ');
+ }
+ const title = t => `#${esc(t.id)} ${t.title === null ? '(no title in the current read)' : txt(t.title)}`;
++ const priority = p => p === null ? 'none' : esc(PRIORITY[p] || p);
+ function taskRow(t) {
+- return `
| ${title(t)} | ${t.assignees.map(user).join(', ') || 'none'} | ${due(t.due)} | ${esc(PRIORITY[t.priority] || t.priority)} | ${esc(t.percent)}% | ${when(t.changedAt)} | ${markers(t) || 'none'} |
`;
++ return `| ${title(t)} | ${t.requirement ? txt(t.requirement) : 'none'} | ${t.assignees.map(user).join(', ') || 'none'} | ${due(t.due)} | ${priority(t.priority)} | ${esc(t.percent)}% | ${when(t.changedAt)} | ${markers(t) || 'none'} |
`;
+ }
+- const taskTable = (rows, label) => `${['Task', 'Assignees', 'Due', 'Priority', 'Done', 'Changed', 'Markers'].map(h => `| ${h} | `).join('')}
${rows.map(taskRow).join('')}
`;
++ const taskTable = (rows, label) => `${['Task', 'Requirement', 'Assignees', 'Due', 'Priority', 'Done', 'Changed', 'Markers'].map(h => `| ${h} | `).join('')}
${rows.map(taskRow).join('')}
`;
+ function freshness(rows) {
+ const reads = rows.filter(r => r.source === 'poll' && r.read_at).map(r => r.read_at).sort();
+ return `Newest Vikunja read among these tasks: ${reads.length ? when(reads.at(-1)) : 'none'}. Poller status: from the S3 status read, not built yet`;
+@@ -318,7 +321,7 @@
+ const decisions = t.decisions.length ? `Decisions${t.decisions.map(d => `${txt(short(d.id))} ${txt(first(d.question))}`).join('
')}` : '';
+ const sub = t.missing ? `No longer readable in Vikunja (${txt(t.missing.reason)}${t.missing.project ? `, now in project ${txt(t.missing.project)}` : ''}) since ${when(t.missing.at)}.` : `${t.done ? 'Done' : `In bucket ${esc(t.bucket)}`}, last changed ${when(t.changedAt)}. ${readNote()}`;
+ return `${head(title(t), `${sub} ${markers(t)}`)}
+- Task
- Ref
${txt(t.ref)} ${gap('The Vikunja address')}- Assignees
- ${t.assignees.map(user).join(', ') || 'none'} ${gap('Bot names')}
- Requirement
- ${txt(t.requirement || 'none')}
- Due
- ${due(t.due)}
- Priority
- ${esc(PRIORITY[t.priority] || t.priority)}
- Done
- ${esc(t.percent)}%
${ext}${conflicts}${decisions}
++ Task
- Ref
${txt(t.ref)} ${gap('The Vikunja address')}- Assignees
- ${t.assignees.map(user).join(', ') || 'none'} ${gap('Bot names')}
- Requirement
- ${txt(t.requirement || 'none')}
- Due
- ${due(t.due)}
- Priority
- ${priority(t.priority)}
- Done
- ${esc(t.percent)}%
${ext}${conflicts}${decisions}
+ ${t.description ? `Description
${txt(t.description)}
` : ''}
+ Snapshots ${t.snapshots.length}
${t.snapshots.map(s => `- ${clock(s.at)} ${esc(bucketText(s.fields))} ${s.source === 'self' ? `written by ${txt(s.role)}` : `${txt(s.via)} read sent ${esc(String(s.readAt).slice(11, 19))}`}${s.stale ? '; stale read, not shown' : ''}
`).join('')}
+ `;
+diff --git a/packages/webui/tests/bus-browser.test.mjs b/packages/webui/tests/bus-browser.test.mjs
+index aebd3c30..9faa15eb 100644
+--- a/packages/webui/tests/bus-browser.test.mjs
++++ b/packages/webui/tests/bus-browser.test.mjs
+@@ -46,7 +46,7 @@ function seed(root) {
+ b.recordTask({ cap: pm, business: 'demo', events: [{ kind: 'task.conflict', subject: 'vikunja:32/7', body: { verb: 'task.update', expected: digest('a'), actual: digest('b') } }] });
+ // A task the stack never wrote, a done task and one no longer readable.
+ b.recordTask({ business: 'demo', snapshots: [{ task_ref: 'vikunja:32/8', updated: iso(), digest: digest('c'), fields: { ...fields, title: 'Foreign task', bucket: 2, assignees: [] }, via: 'cursor', read_at: iso() }], events: [{ kind: 'task.changed.external', subject: 'vikunja:32/8', body: { via: 'cursor', digest: digest('c'), previous: null, changed: [] } }] });
+- b.recordTask({ cap: pm, business: 'demo', snapshots: [{ task_ref: 'vikunja:32/9', updated: iso(), digest: digest('d'), fields: { ...fields, title: 'Done task', done: true, percent_done: 100 } }] });
++ b.recordTask({ cap: pm, business: 'demo', snapshots: [{ task_ref: 'vikunja:32/9', updated: iso(), digest: digest('d'), fields: { ...fields, title: 'Done task', done: true, percent_done: 100, priority: undefined } }] });
+ b.recordTask({ cap: pm, business: 'demo', snapshots: [{ task_ref: 'vikunja:32/10', updated: iso(), digest: digest('e'), fields: { ...fields, title: 'Moved task' } }] });
+ b.recordTask({ business: 'demo', snapshots: [{ task_ref: 'vikunja:32/10', updated: iso(), digest: digest('f'), fields: { gone: 'moved' }, via: 'task', read_at: later() }], events: [{ kind: 'task.missing', subject: 'vikunja:32/10', body: { reason: 'moved', project: 40 } }] });
+ // An open blocking decision that replaced an earlier one, seen by the human; one routed to PM; one resolved.
+@@ -168,7 +168,14 @@ test('S5 pages in a browser: real broker rows, inert text, Copy, routes, failure
+ assert.equal(await b.evaluate('[...document.querySelectorAll("#bus-view .badge")].filter(e=>/^last read from Vikunja \\((board|cursor)\\)$/.test(e.textContent)).length'), 2);
+ assert.doesNotMatch(tasks, /refused write|not created by the stack/);
+ assert.match(tasks, /#10 \(no title in the current read\)/);
+- assert.match(tasks, /user 5/); assert.match(tasks, /2026-10-20/); assert.match(tasks, /high/);
++ assert.match(tasks, /user 5/); assert.match(tasks, /2026-10-20/);
++ // Requirement and priority come from the bus row (row 58). The list reads no trail, so #7's
++ // requirement is the row's. #8 and #9 have no task.created, and #9 was written with no priority.
++ assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#bus-view table.s1-tasks")].map(t=>[[...t.tHead.rows[0].cells].map(c=>c.textContent),...[...t.tBodies[0].rows].map(r=>[r.cells[1].textContent,r.cells[4].textContent])])'), [
++ ['Task Requirement Assignees Due Priority Done Changed Markers'.split(' '), ['none', 'high']],
++ ['Task Requirement Assignees Due Priority Done Changed Markers'.split(' '), ['REQ-SLICE-1', 'high']],
++ ['Task Requirement Assignees Due Priority Done Changed Markers'.split(' '), ['none', 'none']],
++ ]);
+ assert.match(tasks, /Done 1/); assert.match(tasks, /No longer readable 1/); assert.match(tasks, /missing: moved/);
+ assert.match(tasks, /Bucket names, bot names and the Vikunja address: not in the Q1 module/);
+ assert.match(tasks, /Poller status: from the S3 status read, not built yet/);
+@@ -178,6 +185,7 @@ test('S5 pages in a browser: real broker rows, inert text, Copy, routes, failure
+ await go('#/tasks/vikunja%3A32%2F7', '#7');
+ const t7 = await text('#bus-view');
+ assert.match(t7, /RequirementREQ-SLICE-1/);
++ assert.match(t7, /Priorityhigh/);
+ assert.match(t7, /Changed in Vikunja after the stack wrote it: bucket, 2 new comments/);
+ assert.match(t7, /task\.update: Vikunja changed after the stack read it \(expected aaaaaaaa, found bbbbbbbb\)/);
+ assert.ok(t7.includes(s.ids.open.slice(0, 8)) && t7.includes(s.ids.old.slice(0, 8)));
+@@ -194,6 +202,8 @@ test('S5 pages in a browser: real broker rows, inert text, Copy, routes, failure
+ assert.match(await text('#bus-view'), /Read from Vikunja for the first time/);
+ await go('#/tasks/vikunja%3A32%2F10', '#10');
+ assert.match(await text('#bus-status'), /No longer readable in Vikunja \(moved, now in project 40\)/);
++ // A tombstone's bus row has no priority; the detail shows the last snapshot that read the task.
++ assert.match(await text('#bus-view'), /Priorityhigh/);
+
+ // The trail with filters.
+ await go('#/trail/task/vikunja%3A32%2F7?kind=task', 'Trail for task #7');
+@@ -238,6 +248,11 @@ test('S5 pages in a browser: real broker rows, inert text, Copy, routes, failure
+ await b.evaluate('document.querySelector("#bus-view [data-retry]").click()');
+ await wait('document.querySelector("#bus-view h1").textContent.startsWith("Trail for task #9")');
+ await noOverflow('refused');
++ // #9 was written with no priority and has no task.created. This runs after the never-read
++ // check above, which needs #9's trail unread.
++ await go('#/tasks/vikunja%3A32%2F9', '#9');
++ assert.match(await text('#bus-view'), /Requirementnone/);
++ assert.match(await text('#bus-view'), /Prioritynone/);
+
+ // Back to the board: it still works.
+ await b.evaluate('document.querySelector("#sections a[href=\\"#/\\"]").click()');
+diff --git a/packages/webui/tests/bus.test.mjs b/packages/webui/tests/bus.test.mjs
+index 2d9ca6ba..cf477052 100644
+--- a/packages/webui/tests/bus.test.mjs
++++ b/packages/webui/tests/bus.test.mjs
+@@ -67,6 +67,9 @@ test('bus routes serve the four reads from the reader view, unescaped JSON for t
+ assert.equal(tasks.body.rows.length, 1);
+ assert.equal(tasks.body.rows[0].source, 'poll');
+ assert.equal(tasks.body.rows[0].fields.title, HOSTILE);
++ // The bus row's priority and requirement (row 58) reach the page: this poll has no priority and
++ // no task.created, so both are null, not missing.
++ assert.deepEqual([tasks.body.rows[0].priority, tasks.body.rows[0].requirement], [null, null]);
+
+ const agents = await read('/api/bus/agents');
+ assert.deepEqual(agents.body.rows.map(r => [r.role, r.op]), [['coder', 'claim']]);
diff --git a/agents/darkwing/work/queue-58/candidate-manifest.sha256 b/agents/darkwing/work/queue-58/candidate-manifest.sha256
new file mode 100644
index 000000000..9545beece
--- /dev/null
+++ b/agents/darkwing/work/queue-58/candidate-manifest.sha256
@@ -0,0 +1,8 @@
+ef35b56f728caeb7c2f2094f654ec03cc68cab36ccaa942b7eaf23812cb14cd1 packages/bus/README.md
+1dcb833f4555bc808a3b46d000c27376a387d4dd05825ff74b59551bd3a07d48 packages/bus/src/broker.mjs
+a61476e7a7b14036f250e824e7ed545031d9a4cc58d91ec0c577f2b09ab4fc46 packages/bus/tests/task-read.test.mjs
+034b6758820fd183bbb1c8182d207c60234842fb180efcc6b4d839068accf8b8 packages/webui/README.md
+847e21519333c9f03dc4a726cf51c03552cb638c2de4e844431fb661740e534e packages/webui/src/public/bus.css
+f5908e144a66839779cfbc97eaef98bd58f819bda32d0259ae9b358915fb8508 packages/webui/src/public/bus.js
+b9d496c16162c72e370acc589c95ac3db8649ca4d00f2c270cfdad9f34a08656 packages/webui/tests/bus-browser.test.mjs
+d2f1e1c6bd19e92c0c5ab24eaaed469eac5a5ae11db0dca7eb6c603f5a99c674 packages/webui/tests/bus.test.mjs
diff --git a/agents/darkwing/work/queue-58/files.txt b/agents/darkwing/work/queue-58/files.txt
new file mode 100644
index 000000000..087c9a81f
--- /dev/null
+++ b/agents/darkwing/work/queue-58/files.txt
@@ -0,0 +1,8 @@
+packages/bus/README.md
+packages/bus/src/broker.mjs
+packages/bus/tests/task-read.test.mjs
+packages/webui/README.md
+packages/webui/src/public/bus.css
+packages/webui/src/public/bus.js
+packages/webui/tests/bus-browser.test.mjs
+packages/webui/tests/bus.test.mjs
diff --git a/agents/darkwing/work/queue-58/out/gate/apply.txt b/agents/darkwing/work/queue-58/out/gate/apply.txt
new file mode 100644
index 000000000..5b6e3086a
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/apply.txt
@@ -0,0 +1,8 @@
+packages/bus/README.md: OK
+packages/bus/src/broker.mjs: OK
+packages/bus/tests/task-read.test.mjs: OK
+packages/webui/README.md: OK
+packages/webui/src/public/bus.css: OK
+packages/webui/src/public/bus.js: OK
+packages/webui/tests/bus-browser.test.mjs: OK
+packages/webui/tests/bus.test.mjs: OK
diff --git a/agents/darkwing/work/queue-58/out/gate/bus-tests.log b/agents/darkwing/work/queue-58/out/gate/bus-tests.log
new file mode 100644
index 000000000..06610aef3
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/bus-tests.log
@@ -0,0 +1,91 @@
+✔ launch identity is stamped, payload identity is refused and stale holder cannot send (174.296301ms)
+✔ decision classes route from policy; gated resolution is human-only, choice and target must match (343.006282ms)
+✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (315.245487ms)
+✔ launch events require a human CLI capability; generic emit cannot forge authority events (650.035825ms)
+✔ within-role decisions close atomically and invalid options or blocking omissions refuse (308.088429ms)
+✔ observer capabilities read human inbox but cannot mutate or forge launch identity (359.713005ms)
+✔ task action subjects and linked decision trail are complete and ordered (331.696051ms)
+✔ launch binding is durable and reconnecting requires the identical trusted record (160.940716ms)
+✔ business isolation includes inherited object names and cross-business message references (310.527465ms)
+✔ authority never transfers between action, run, target, unresolved or replaced role holder (466.789147ms)
+✔ task projection uses schema current view, skipping earlier and equal-start polls (145.643876ms)
+✔ revocation permanently bars the old run from reclaiming first, including after broker restart (215.468908ms)
+✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (139.789942ms)
+✔ both arbiters require human resolution when their cross-role route is themselves (199.561263ms)
+✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (3.396001ms)
+✔ only validated broker references load; returned data and exceptions cannot expose a known token (12.520871ms)
+✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (7.115523ms)
+✔ expiry refuses use and env references never become client data (1.598587ms)
+✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (6.291056ms)
+✔ opaque tokens shorter than 16 characters refuse before use (3.655408ms)
+✔ roles lists open decisions routed to a role, and never the human inbox (261.152654ms)
+✔ recent lists closed decisions of every route, newest close first, with how each closed (363.303086ms)
+✔ recent stops at RECENT rows (1829.735569ms)
+✔ every row is what trail already gives the same session for that id (391.336494ms)
+✔ a role session can't list decisions, so it can't read another role's open decisions (647.611132ms)
+✔ decisions is a read: one business, strict arguments, nothing written (373.694874ms)
+✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (156.043937ms)
+✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (165.019157ms)
+✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (206.43812ms)
+✔ endLaunch leaves a claim another run took alone (207.183829ms)
+✔ refuse records action.refused against the caller with the code only (302.67497ms)
+✔ launches off refuses role.launch with launch-revoked until launches on (520.083612ms)
+✔ broker process: launch ops authorize role.launch, record refusals and end runs (510.509834ms)
+✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (9.444502ms)
+✔ process reader gets own kernel identity without exposing environment values (3.344547ms)
+✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (2.167083ms)
+✔ real pid 1 remains inspectable when its environment is protected (0.615375ms)
+✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (236.544443ms)
+✔ missing and foreign-business citations refuse and roll back message and grant (268.628699ms)
+✔ cross-role sends still need a matching resolved decision and consume it once (279.325314ms)
+✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (245.619623ms)
+✔ startup token refusal returns safe code without value or partial listening broker (53.708461ms)
+✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (247.669476ms)
+✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (221.533169ms)
+✔ trusted host registers later launches; socket clients never have a registration verb (660.145876ms)
+✔ runtime excludes declared project roots even when host supplies no repoRoots (97.67311ms)
+✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (358.170086ms)
+✔ v3b prototype refusals, views and append-only mutations (1839.297938ms)
+✔ gated approval authorizes once, survives store reopen, and fresh approval works (308.616906ms)
+✔ another run cannot consume an approval; a failed check leaves it usable (288.290129ms)
+✔ two scheduled callers have exactly one grant and one consumed refusal (228.173145ms)
+✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (893.540551ms)
+✔ class drift gated to cross-role refuses before consumption (458.933673ms)
+✔ class drift cross-role to gated refuses before consumption (485.671212ms)
+✔ class drift gated to within-role refuses before consumption (314.087389ms)
+✔ class drift cross-role to within-role refuses before consumption (421.253693ms)
+✔ class drift within-role to gated refuses before consumption (202.694147ms)
+✔ class drift within-role to cross-role refuses before consumption (192.462176ms)
+✔ message.send consumes approval and prevents a later send or authorize (192.996937ms)
+✔ role.revoke consumes approval and prevents a later revoke or authorize (260.913993ms)
+✔ creates private WAL store and excludes a second writer until explicit close (151.485052ms)
+✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (208.102989ms)
+✔ existing empty database and symlink runtime directory refuse, never initialize over damage (220.863553ms)
+✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (228.228095ms)
+✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (312.964785ms)
+✔ async transactions refuse before invoking their function (379.311304ms)
+✔ tasks rows carry priority and requirement for every session that reads tasks (390.857173ms)
+✔ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (394.863888ms)
+✔ an empty business and one with snapshots but no task.created read cleanly (320.277311ms)
+✔ recordTask keeps sync reads and a role write apart (201.473518ms)
+✔ read_at must be one canonical UTC format, so the projection compares strings safely (147.47186ms)
+✔ a bad entry refuses the whole record (135.729423ms)
+✔ taskView reads the projection for one business (150.972983ms)
+✔ requestTask hands only a holder and a task verb to the handler, and records refusals (471.337904ms)
+✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (766.565026ms)
+✔ without an adapter the server refuses every task verb (461.189217ms)
+✔ the runtime refuses an invalid adapter and closes a valid one (297.711316ms)
+✔ the process loads the S3 adapter from plain-data trackers (572.225242ms)
+✔ socket capability stamps launch identity; shared views use wire, no SQL client (182.026154ms)
+✔ two wire claims serialize; a lost reply never automatically retries (256.975151ms)
+✔ malformed, oversized and identity-forging envelopes refuse without echoing input (137.32736ms)
+✔ client preserves UTF-8 when a response divides a multibyte character (13.085301ms)
+✔ committed mutation followed by dropped reply reports unknown and is never retried (162.326848ms)
+ℹ tests 83
+ℹ suites 0
+ℹ pass 83
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 4462.072351
diff --git a/agents/darkwing/work/queue-58/out/gate/gate.sh b/agents/darkwing/work/queue-58/out/gate/gate.sh
new file mode 100644
index 000000000..bdb6f4de4
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/gate.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Row 58 gate: the brief's suites (bus, tasks, webui) plus every scripts/test-*.sh, one at a time,
+# in a detached worktree of the current HEAD with build.patch applied.
+export TMPDIR=~/darkwing-scratch/r58/tmp
+G=~/darkwing-scratch/r58/gate; O=~/darkwing-scratch/r58/out/gate; mkdir -p $O; rm -f $O/summary.txt; cd $G
+# node_modules is a link to the main checkout's; test-discord needs node_modules/.bin/pi.
+[ -x node_modules/.bin/pi ] || { echo "node_modules not linked" >> $O/summary.txt; exit 1; }
+for s in bus tasks webui; do
+ env -u NODE_TEST_CONTEXT node --test "packages/$s/tests/*.test.mjs" > $O/$s-tests.log 2>&1; echo "$s rc=$?" >> $O/summary.txt
+done
+for s in scripts/test-*.sh; do
+ n=$(basename $s .sh)
+ if [ $n = test-task ]; then DOCKER_HOST=unix:///nonexistent bash $s > $O/$n.log 2>&1; else bash $s > $O/$n.log 2>&1; fi
+ echo "$n rc=$?" >> $O/summary.txt
+done
+echo DONE >> $O/summary.txt
diff --git a/agents/darkwing/work/queue-58/out/gate/summary.txt b/agents/darkwing/work/queue-58/out/gate/summary.txt
new file mode 100644
index 000000000..d75d858cc
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/summary.txt
@@ -0,0 +1,13 @@
+bus rc=0
+tasks rc=0
+webui rc=0
+test-auth rc=0
+test-conductor rc=0
+test-config rc=0
+test-discord rc=0
+test-extension-package rc=0
+test-foundation rc=0
+test-queue rc=0
+test-release rc=0
+test-task rc=0
+DONE
diff --git a/agents/darkwing/work/queue-58/out/gate/tasks-tests.log b/agents/darkwing/work/queue-58/out/gate/tasks-tests.log
new file mode 100644
index 000000000..c0abc7d42
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/tasks-tests.log
@@ -0,0 +1,79 @@
+✔ the boot config is checked before anything starts (100.208812ms)
+✔ a business with no tracker entry refuses task verbs (131.404895ms)
+✔ credential.expiring and .expired are recorded once per instance (288.714629ms)
+✔ a token file that changes on disk records credential.changed (104.313151ms)
+✔ autostart polls, reconciles and retries a startup the tracker was down for (233.576715ms)
+✔ a refusal a restart must clear is not retried by the poll (114.672832ms)
+✔ a poll that fires while two are queued is dropped (104.669646ms)
+✔ close waits for a running verb and refuses one that has not started (200.265835ms)
+✔ the bundled Vikunja is the pinned upstream image the runbook names (1.672948ms)
+✔ every published port is on 127.0.0.1, and no secret is in the file (1.181256ms)
+✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (449.868334ms)
+✔ the project list holds recorded project objects in the recorded list envelope (119.339078ms)
+✔ the recorded task bodies pass the checks S3 applies to every read (1.067817ms)
+✔ the client works against the fake over real HTTP with the platform fetch (371.652461ms)
+✔ a correct install starts, and the first reconcile records tasks that already exist (144.272318ms)
+✔ verbs refuse while a business is starting and after startup refused it (141.755188ms)
+✔ startup refuses a token that can do more than its role needs (544.807904ms)
+✔ startup refuses an unsupported version and flags an untested one (330.856873ms)
+✔ startup refuses a board that the runbook did not install (370.431419ms)
+✔ startup refuses a project the sync bot cannot read (159.048155ms)
+✔ startup refuses a configured label the pm bot cannot see (93.8654ms)
+✔ startup refuses an expired credential and a missing sync credential (168.359766ms)
+✔ an unreachable tracker refuses with tracker-unavailable (92.029783ms)
+✔ an edit in the UI is recorded once, with the fields that changed (244.274002ms)
+✔ a move between open buckets is seen on the board, though updated does not change (216.953978ms)
+✔ a person's comment is counted and a bot's is not (401.544165ms)
+✔ the hourly reconcile catches a comment through comment_count (241.866718ms)
+✔ a task closed in the UI leaves the open view with its done bucket (377.893029ms)
+✔ a task that leaves the board is recorded as deleted, moved or out of reach (350.418814ms)
+✔ a poll that read before a verb wrote does not overwrite the verb (180.479449ms)
+✔ a tracker fault during a tick is reported and the next tick catches up (164.843975ms)
+✔ a malformed answer refuses the tick with tracker-shape (142.738851ms)
+✔ no token value reaches the database, the log or a refusal (281.352052ms)
+✔ the first look at a task counts only comments inside the window (139.326447ms)
+✔ with descendants off the adapter reads no project list and no child, as in v0 (139.555066ms)
+✔ a three-level tree is read per project, and a project outside it is not (130.069472ms)
+✔ a watch-only child keeps the business ready, and a failed child read refuses as transient (247.612384ms)
+✔ a root without the installed buckets refuses with descendants on (104.903462ms)
+✔ the project list refuses without read_all, and skips archived projects and saved filters (309.634411ms)
+✔ descendants must be true or false (97.483334ms)
+✔ a move inside the tree is a moved tombstone plus a snapshot under the new ref (189.581872ms)
+✔ a child made after startup joins the tree at the next reconcile (153.088527ms)
+✔ a child that leaves the tree has its tasks recorded missing, and the ledger keeps their history (197.714088ms)
+✔ a child deleted or hidden between reconciles drops out at the next tick, and the root still refuses (237.555435ms)
+✔ a reconcile that fails keeps the previous tree (129.94486ms)
+✔ a closed task the cursor missed takes its own project's done bucket (134.588777ms)
+✔ closed tasks read at startup take their own project's done bucket (96.11233ms)
+✔ a closed task the cursor returns takes its own project's done bucket (134.17724ms)
+✔ a task moved out of a child that leaves the tree at the same time is moved, not no-access (143.184339ms)
+✔ a task in a bucket outside the five keeps its raw bucket id (112.941716ms)
+✔ bucket 0 on a watch-only task never becomes a bucket or a state (208.792424ms)
+✔ a child whose kanban view is deleted still refuses the tick, and the reconcile reads it watch-only (143.794754ms)
+✔ a child dropped at a tick comes back at the next reconcile once the bot reads it again (122.34508ms)
+✔ task.create needs a recorded human request and a requirement id (221.502964ms)
+✔ only labels named in the business file can be written (283.717269ms)
+✔ task.schedule sets and clears a due date and relations (408.864349ms)
+✔ assign and reassign move the role bots and record task.assigned (323.62119ms)
+✔ task.update.assigned is for the assignee and records task.state (315.101254ms)
+✔ a wrong expected digest records task.conflict and writes nothing (249.286928ms)
+✔ a cross-role verb needs a resolved decision, used once (242.349666ms)
+✔ task.close needs a verdict; after it every verb refuses with task-done (224.338739ms)
+✔ a lost answer is settled by a re-read and never retried (251.653339ms)
+✔ a create whose answer is lost is reported uncertain, and the poll finds the task (187.103147ms)
+✔ a task the sync bot cannot read refuses and records nothing (174.91843ms)
+✔ verbs and polls for one business run one at a time (222.178174ms)
+✔ a due date with milliseconds is written to the second (227.828746ms)
+✔ every write landed and the final read failed: the verb succeeds and records what it wrote (157.643377ms)
+✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (105.90067ms)
+✔ a create whose final read fails succeeds and records task.created (105.899049ms)
+✔ an edit between the last write and the final read shows as external on the next poll (129.748344ms)
+✔ task.created is recorded when a later label write fails (100.075318ms)
+ℹ tests 71
+ℹ suites 0
+ℹ pass 71
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 4094.414646
diff --git a/agents/darkwing/work/queue-58/out/gate/test-auth.log b/agents/darkwing/work/queue-58/out/gate/test-auth.log
new file mode 100644
index 000000000..ac2287606
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-auth.log
@@ -0,0 +1,17 @@
+OK status with missing harness credential exits 3 and still lists accounts
+OK status reports harness credential (read-only) + mosaic accounts
+OK api key material never reaches output
+OK oauth token material never reaches output
+OK unparseable credential file exits 2
+OK symlinked credential file exits 4
+OK env-side credential names reported
+OK env var values never reach output
+OK accounts without an accounts dir reports none and creates nothing
+OK accounts lists files and marks the active one
+OK loose account perms flagged in listing
+OK agent --auth with missing account file refuses (exit 4)
+OK agent --auth with non-0600 account file refuses
+OK agent --auth with invalid account name refuses
+OK auth.sh without valid config refuses
+
+selftest: 15 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-conductor.log b/agents/darkwing/work/queue-58/out/gate/test-conductor.log
new file mode 100644
index 000000000..69b3150f6
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-conductor.log
@@ -0,0 +1,55 @@
+Note: switching to 'f2f2861b7f736b6b3623ef85bdd261c2394671b2'.
+
+You are in 'detached HEAD' state. You can look around, make experimental
+changes and commit them, and you can discard any commits you make in this
+state without impacting any branches by switching back to a branch.
+
+If you want to create a new branch to retain commits you create, you may
+do so (now or later) by using -c with the switch command. Example:
+
+ git switch -c
+
+Or undo this operation with:
+
+ git switch -
+
+Turn off this advice by setting config variable advice.detachedHead to false
+
+Not currently on any branch.
+nothing to commit, working tree clean
+Note: switching to 'f2f2861b7f736b6b3623ef85bdd261c2394671b2'.
+
+You are in 'detached HEAD' state. You can look around, make experimental
+changes and commit them, and you can discard any commits you make in this
+state without impacting any branches by switching back to a branch.
+
+If you want to create a new branch to retain commits you create, you may
+do so (now or later) by using -c with the switch command. Example:
+
+ git switch -c
+
+Or undo this operation with:
+
+ git switch -
+
+Turn off this advice by setting config variable advice.detachedHead to false
+
+OK dry-run: allowed change, exit 0, nothing committed (exit 0)
+OK dry-run committed nothing
+OK apply: allowed change exits 0 (exit 0)
+OK apply: attribution in commit subject
+OK apply: target tree clean after commit
+OK disallowed path refused (exit 1)
+OK disallowed path: target untouched
+OK syntax gate refused broken .mjs (exit 1)
+OK syntax gate: target untouched
+OK suite failure refused (exit 1)
+OK suite failure: target reverted to clean
+OK disabled policy refused (exit 2)
+OK disabled policy: target untouched
+OK failed run refused (exit 1)
+OK failed run: target untouched
+OK missing run exits 4 (exit 4)
+OK invalid policy exits 2 (exit 2)
+
+selftest: 17 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-config.log b/agents/darkwing/work/queue-58/out/gate/test-config.log
new file mode 100644
index 000000000..76c05ef74
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-config.log
@@ -0,0 +1,26 @@
+OK absent adapter defaults to pi
+OK adapter mock validates (exit 0)
+OK unsupported adapter exits 2 (exit 2)
+OK env exports adapter
+OK bootstrap creates default when absent (exit 0)
+OK bootstrap wrote config file
+OK bootstrap is idempotent on existing config (exit 0)
+OK bootstrap did not rewrite existing config
+OK validate missing config exits 3 (exit 3)
+OK malformed JSON exits 2 (exit 2)
+OK unsupported configVersion exits 2 (exit 2)
+OK unknown top-level key exits 2 (exit 2)
+OK unknown execution key exits 2 (exit 2)
+OK unsupported backend exits 2 (exit 2)
+OK unsupported environment exits 2 (exit 2)
+OK relative dataRoot exits 2 (exit 2)
+OK non-canonical dataRoot exits 2 (exit 2)
+OK filesystem root dataRoot exits 2 (exit 2)
+OK home directory dataRoot exits 2 (exit 2)
+OK dataRoot containing config dir exits 2 (exit 2)
+OK control character in provider exits 2 (exit 2)
+OK symlinked config file exits 2 (exit 2)
+OK env exports resolve correctly
+OK failed validation modified nothing
+
+selftest: 24 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-discord.log b/agents/darkwing/work/queue-58/out/gate/test-discord.log
new file mode 100644
index 000000000..b013f9ffa
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-discord.log
@@ -0,0 +1,70 @@
+toolchain: node v26.8.1
+
+OK syntax: packages/discord/src/approvals.mjs
+OK syntax: packages/discord/src/authorize.mjs
+OK syntax: packages/discord/src/binding.mjs
+OK syntax: packages/discord/src/cli.mjs
+OK syntax: packages/discord/src/connector.mjs
+OK syntax: packages/discord/src/context.mjs
+OK syntax: packages/discord/src/engine-pi.mjs
+OK syntax: packages/discord/src/errors.mjs
+OK syntax: packages/discord/src/gateway.mjs
+OK syntax: packages/discord/src/git.mjs
+OK syntax: packages/discord/src/journal.mjs
+OK syntax: packages/discord/src/notify.mjs
+OK syntax: packages/discord/src/rest.mjs
+OK syntax: packages/discord/src/setspark.mjs
+OK syntax: packages/discord/src/tools.mjs
+OK syntax: packages/discord/src/web.mjs
+OK syntax: packages/discord/bin/git-credential.mjs
+OK syntax: packages/discord/extension/tools.mjs
+OK syntax: packages/discord/tests/approvals.test.mjs
+OK syntax: packages/discord/tests/authorize.test.mjs
+OK syntax: packages/discord/tests/binding.test.mjs
+OK syntax: packages/discord/tests/connector.test.mjs
+OK syntax: packages/discord/tests/context.test.mjs
+OK syntax: packages/discord/tests/engine.test.mjs
+OK syntax: packages/discord/tests/fake-pi.mjs
+OK syntax: packages/discord/tests/gateway.test.mjs
+OK syntax: packages/discord/tests/git.test.mjs
+OK syntax: packages/discord/tests/helpers.mjs
+OK syntax: packages/discord/tests/journal.test.mjs
+OK syntax: packages/discord/tests/notify.test.mjs
+OK syntax: packages/discord/tests/recover.test.mjs
+OK syntax: packages/discord/tests/rest.test.mjs
+OK syntax: packages/discord/tests/setspark.test.mjs
+OK syntax: packages/discord/tests/tools.test.mjs
+OK syntax: packages/discord/tests/web.test.mjs
+OK syntax: packages/discord/fixtures/claim-worker.mjs
+OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
+OK syntax: scripts/discord.sh
+OK syntax: scripts/discord-service.sh
+OK packages/discord declares no dependencies
+OK no bot-token-shaped string in packages/discord
+OK fixture binding uses placeholder ids only
+OK fixture binding validates
+OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
+OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
+OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
+OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
+OK real pi with protocol vault adds reserve_id to the git verbs
+OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
+OK real pi refuses a git key on a read-only root (fail closed)
+OK real pi with the pilot flags (--no-tools) exposes no tool at all
+OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
+OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
+OK node --test packages/discord/tests/ (ℹ pass 178)
+OK scripts/discord.sh --help exits 0
+OK scripts/discord.sh check without a binding exits 4
+OK scripts/discord.sh recover without a binding exits 4
+OK scripts/discord.sh reload without a binding exits 4
+OK scripts/discord-service.sh without a command exits 4
+OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
+OK service install writes the rendered unit (0644) and leaves no temp file
+OK service install a second time reports unchanged
+OK systemd-analyze verify accepts the rendered unit
+OK service uninstall removes the unit file
+OK service install with an unknown flag exits 4
+OK service install with USER unset finishes and names the account for lingering
+
+discord suite: 66 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-extension-package.log b/agents/darkwing/work/queue-58/out/gate/test-extension-package.log
new file mode 100644
index 000000000..bd73f3f68
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-extension-package.log
@@ -0,0 +1,21 @@
+OK initial ordinary-file install
+OK installed tree matches canonical source
+OK installed tree has no symlinks
+OK check detects installation drift
+OK sync refuses to overwrite installation drift
+OK check detects an extra destination file
+OK check detects an extra destination directory
+OK check rejects a destination symlink
+OK sync accepts a canonical source update
+OK updated installation matches canonical source
+scripts/test-extension-package.sh: line 14: 2242379 Killed "$@" > /dev/null 2>&1
+OK forced interruption kills the replacing process
+OK next invocation recovers old consistent installation
+OK interrupted replacement rolled back
+OK sync succeeds after interruption recovery
+OK unlocked stale lock file does not block
+OK active lock refuses a concurrent sync
+OK source symlink fails closed
+OK nested second entrypoint fails closed
+
+extension package selftest: 18 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-foundation.log b/agents/darkwing/work/queue-58/out/gate/test-foundation.log
new file mode 100644
index 000000000..6338e1e6d
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-foundation.log
@@ -0,0 +1,53 @@
+toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
+
+OK syntax: scripts/foundation-inspect.mjs
+OK syntax: scripts/foundation/strict-json.mjs
+OK syntax: scripts/foundation/canonical.mjs
+OK syntax: scripts/foundation/resolve.mjs
+OK syntax: scripts/foundation/validate-record.mjs
+OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
+OK syntax: scripts/foundation/canonical.test.mjs
+OK syntax: scripts/foundation/cli.test.mjs
+OK syntax: scripts/foundation/fixtures.test.mjs
+OK syntax: scripts/foundation/resolve.test.mjs
+OK syntax: scripts/foundation/strict-json.test.mjs
+OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
+OK fixture generator runs
+OK checked-in fixtures/bundles equal a fresh generation
+OK checked-in fixtures/raw equal a fresh generation
+OK checked-in fixtures/index.json equal a fresh generation
+OK checked-in demo bundles equal a fresh generation
+OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
+OK node --test scripts/foundation/ (ℹ pass 80)
+OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
+ platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
+ node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
+ schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
+ profile column (schema-valid records only): profile-valid 510, profile-invalid 30
+ profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
+OK oracle: zero schema-column disagreements with the pinned checker
+OK oracle: strict-only profile refusals are counted and asserted
+OK demo: permitted read preview exits 0 (exit 0)
+OK demo: permitted file.change preview exits 0 (exit 0)
+OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
+OK demo: revoked registration is refused (exit 3) (exit 3)
+OK demo: message is not authority (exit 3) (exit 3)
+OK usage: no arguments exits 2 (exit 2)
+OK io: missing file exits 4 (exit 4)
+OK io: directory exits 4 (exit 4)
+OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
+OK bound: oversize fixture exits 2 (exit 2)
+OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
+OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
+OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
+OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
+OK text output starts with the disclaimer
+OK json output is valid JSON with result allowed and exactly the charter §7 fields
+OK json golden matches byte-for-byte
+OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
+OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
+OK canary never printed (bundle run and credential-file run)
+OK a non-bundle JSON file is refused at the shape gate, not read into output
+OK no field of the non-bundle file is echoed
+
+selftest: 44 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-queue.log b/agents/darkwing/work/queue-58/out/gate/test-queue.log
new file mode 100644
index 000000000..823724a93
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-queue.log
@@ -0,0 +1,35 @@
+toolchain: node v26.8.1, git version 2.55.0
+
+OK syntax: packages/queue/src/cli.mjs
+OK syntax: packages/queue/src/errors.mjs
+OK syntax: packages/queue/src/io.mjs
+OK syntax: packages/queue/src/lock.mjs
+OK syntax: packages/queue/src/queue.mjs
+OK syntax: packages/queue/src/review.mjs
+OK syntax: packages/queue/src/store.mjs
+OK syntax: packages/queue/tests/commit.test.mjs
+OK syntax: packages/queue/tests/data.test.mjs
+OK syntax: packages/queue/tests/dispatch.test.mjs
+OK syntax: packages/queue/tests/helpers.mjs
+OK syntax: packages/queue/tests/lock.test.mjs
+OK syntax: packages/queue/tests/migration.test.mjs
+OK syntax: packages/queue/tests/review.test.mjs
+OK syntax: packages/queue/tests/store.test.mjs
+OK syntax: packages/queue/tests/write.test.mjs
+OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
+OK syntax: packages/queue/tests/fixtures/kill-at.mjs
+OK syntax: packages/queue/tests/fixtures/lock-child.mjs
+OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
+OK syntax: scripts/queue-commit.sh
+OK syntax: scripts/git-hooks/pre-commit
+OK syntax: scripts/mosaic
+OK queue-commit.sh, the guard and scripts/mosaic are executable
+OK packages/queue declares no dependencies
+ℹ tests 150
+ℹ pass 150
+ℹ fail 0
+OK node --test packages/queue/tests/
+OK scripts/mosaic queue help
+skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r58/gate) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
+
+queue suite: 27 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-release.log b/agents/darkwing/work/queue-58/out/gate/test-release.log
new file mode 100644
index 000000000..7202b85d4
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-release.log
@@ -0,0 +1,16 @@
+OK valid RELEASE resolves (exit 0)
+OK invalid RELEASE exits 1 (exit 1)
+OK missing RELEASE exits 1 (exit 1)
+OK valid RELEASE leaves image tag consistent with version
+OK status safe on empty state (exit 0)
+OK status created no pointer
+OK fault-injected activation refuses (exit 1)
+OK refused activation wrote no pointer
+OK refusal logged exactly once with valid fields
+OK healthy activation succeeds (exit 0)
+OK pointer written with valid fields
+OK repeat activation succeeds (log grows) (exit 0)
+OK log is append-only across activations
+OK rollback without previous refuses (exit 1)
+
+selftest: 14 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/test-task.log b/agents/darkwing/work/queue-58/out/gate/test-task.log
new file mode 100644
index 000000000..cd89fe9b2
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/test-task.log
@@ -0,0 +1,32 @@
+OK valid task validates (exit 0)
+OK unknown task key exits 2 (exit 2)
+OK unsupported taskVersion exits 2 (exit 2)
+OK invalid task id exits 2 (exit 2)
+OK empty prompt exits 2 (exit 2)
+OK NUL in expectExact exits 2 (exit 2)
+OK out-of-range timeout exits 2 (exit 2)
+OK missing mission file exits 4 (exit 4)
+OK task with valid mission validates (exit 0)
+OK invalid mission exits 2 (exit 2)
+OK validate missing task exits 4 (exit 4)
+OK validation does not modify the task file
+OK prune dry-run exits 0 (exit 0)
+OK dry-run deleted nothing
+OK prune --keep=2 --yes removes oldest (exit 0)
+OK kept exactly 2 newest runs
+OK newest run kept, oldest pruned
+OK append-only receipt written (3 entries)
+OK sessions/workspaces untouched by prune
+OK prune with invalid keep exits 4 (exit 4)
+skip adapter seam cases (docker daemon unavailable)
+skip workspace/capability cases (docker daemon unavailable)
+skip live task cases (docker unavailable)
+OK onboard without name exits 4 (non-interactive) (exit 4)
+OK onboard --name renders profile (exit 0)
+OK profile written
+OK canon structure: required filled, optional placeholdered
+OK canon sections present
+skip live recall pair: user recall run, recalled user name (docker daemon unavailable)
+OK no agent identity on headless run
+
+selftest: 26 passed, 0 failed
diff --git a/agents/darkwing/work/queue-58/out/gate/webui-tests.log b/agents/darkwing/work/queue-58/out/gate/webui-tests.log
new file mode 100644
index 000000000..3571a4d1a
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/gate/webui-tests.log
@@ -0,0 +1,49 @@
+✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (6561.92218ms)
+Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
+✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (8652.977054ms)
+✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (9402.853483ms)
+✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (383.653418ms)
+✔ decisions route serves the roles and recent lists from the reader view (397.233539ms)
+✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (300.364963ms)
+✔ bus refusals map to statuses and never carry a path or a stack (195.330377ms)
+✔ humanCall speaks the human transport protocol without blocking the server (2685.907663ms)
+✔ busReader takes --business, else the live bus host, else refuses (518.612191ms)
+✔ humanCall kills a transport that runs past its timeout (547.154367ms)
+✔ serve refuses a --business value that is not a business id (659.088475ms)
+✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (6474.338594ms)
+✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (3689.648064ms)
+✔ conversation view: a newer session with no readable history keeps the marker (2751.485722ms)
+✔ conversation view: seats without history say so and offer no reply (2065.566145ms)
+✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (6206.855618ms)
+✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (57413.788144ms)
+✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (2.996407ms)
+✔ credential state from the date alone, as the bus states it (0.309707ms)
+✔ queue: rows, one row, ids and methods, notes; no path or id from a row (2742.128278ms)
+✔ queue: a refused read fails closed with the store's text, redacted (2078.396556ms)
+✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (4254.745108ms)
+✔ business: names, vars on the allowlist, authority, credential metadata only (100.44112ms)
+✔ business: missing or invalid file refuses with the module's text, redacted (13.973272ms)
+✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (13.932099ms)
+✔ settings: release, board, loopback address, notifier binding name only (677.704624ms)
+✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (6660.681779ms)
+✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (25216.516956ms)
+✔ loopback host and board origin fail closed (13.778016ms)
+✔ real board fixture passes through WebUI; assets and isolated seen/reply work (178.313944ms)
+✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (176.017018ms)
+✔ unreachable board reports URL; CLI rejects unsupported options (2276.784001ms)
+✔ tokens.css and icons.svg are byte copies of docs/design (13.136971ms)
+✔ JetBrains Mono files match the digests in their sources file, with the OFL text (13.333094ms)
+✔ shipped strings are unchanged, and the web keeps its write boundary (7.351991ms)
+✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (11479.249425ms)
+✔ five states on Board, Inbox, Tasks, Agents and Trail (3931.883085ms)
+✔ Queue, Business and Settings in their five states, with no move button (9250.948248ms)
+✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (3547.734575ms)
+✔ the server log names no config path when the system config refuses (426.523147ms)
+ℹ tests 40
+ℹ suites 0
+ℹ pass 40
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 58109.727949
diff --git a/agents/darkwing/work/queue-58/out/mutants.sh b/agents/darkwing/work/queue-58/out/mutants.sh
new file mode 100644
index 000000000..320feb2aa
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/mutants.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Row 58 (#1547) mutants. Each edits one source file in the build worktree, runs the test file that
+# should fail it, then restores the file from the worktree index, which holds the candidate. Only
+# the one test file runs, since other seats' suites shared the machine; the gate runs every suite.
+export TMPDIR=~/darkwing-scratch/r58/tmp
+cd ~/darkwing-scratch/r58/wt
+B=packages/bus/src/broker.mjs; W=packages/webui/src/public/bus.js
+BT=packages/bus/tests/task-read.test.mjs; WE=packages/webui/tests/bus.test.mjs; WB=packages/webui/tests/bus-browser.test.mjs
+run() { # name file sed-expr test-file
+ sed -i "$3" "$2"
+ if git diff --quiet -- "$2"; then echo "$1: MUTATION DID NOT APPLY"; return; fi
+ env -u NODE_TEST_CONTEXT node --test --test-timeout=60000 "$4" > ~/darkwing-scratch/r58/tmp/mut-$1.log 2>&1
+ echo "$1: rc=$? $(grep -E '^ℹ (pass|fail) ' ~/darkwing-scratch/r58/tmp/mut-$1.log | tr '\n' ' ')"
+ grep -E '^✖ ' ~/darkwing-scratch/r58/tmp/mut-$1.log | sed 's/ ([0-9.]*ms)$//; s/^/ /' | sort -u | head -4
+ git checkout -q -- "$2"
+}
+# The broker's 'tasks' rows.
+run tasks-verb-unchanged $B 's/ if (verb === .tasks.) return this.#taskRows(s.business);/ if (verb === "tasks") return this.#store.all("SELECT * FROM task_current WHERE business=? ORDER BY task_ref", s.business).map((r) => ({ ...r, fields: JSON.parse(r.fields) }));/' $BT
+run no-priority $B 's/ const priority = fields.priority ?? null;/ const priority = null;/' $BT
+run zero-priority-null $B 's/ const priority = fields.priority ?? null;/ const priority = fields.priority || null;/' $BT
+run tombstone-priority-zero $B 's/ const priority = fields.priority ?? null;/ const priority = fields.priority ?? 0;/' $BT
+run requirement-by-ref $B "s/ const taskId = (ref) => ref.slice(ref.indexOf('\/') + 1);/ const taskId = (ref) => ref;/" $BT
+run requirement-last-wins $B 's/ if (!requirement.has(taskId(e.subject))) requirement.set/ requirement.set/' $BT
+run requirement-any-business $B "s/ FROM events WHERE business=? AND kind='task.created' ORDER BY seq\",/ FROM events WHERE (business=? OR 1) AND kind='task.created' ORDER BY seq\",/" $BT
+run requirement-undefined $B 's/requirement: requirement.get(taskId(r.task_ref)) ?? null }/requirement: requirement.get(taskId(r.task_ref)) }/' $BT
+run task-view-adds-keys $B 's/ const parse = (r) => ({ ...r, fields: JSON.parse(r.fields) });/ const parse = (r) => ({ ...r, fields: JSON.parse(r.fields), priority: null, requirement: null });/' $BT
+# The endpoint passes both keys through.
+run endpoint-drops-keys $B 's/ return { ...r, fields, priority, requirement: requirement.get(taskId(r.task_ref)) ?? null };/ return { ...r, fields };/' $WE
+# The Tasks view.
+run list-requirement-from-trail $W 's/ requirement: row.requirement ?? created?.body?.requirement ?? null,/ requirement: created?.body?.requirement ?? null,/' $WB
+run no-requirement-column $W "s/\['Task', 'Requirement', 'Assignees'/['Task', 'Assignees'/; s|\${t.requirement ? txt(t.requirement) : 'none'} | ||" $WB
+run priority-default-zero $W 's/priority: row.priority ?? shape.priority ?? null,/priority: row.priority ?? shape.priority ?? 0,/' $WB
+run tombstone-no-fallback $W 's/priority: row.priority ?? shape.priority ?? null,/priority: row.priority ?? null,/' $WB
+run detail-priority-raw $W 's|Priority${priority(t.priority)}|Priority${esc(PRIORITY[t.priority] \|\| t.priority)}|' $WB
+run list-priority-raw $W 's|${due(t.due)} | ${priority(t.priority)} | |${due(t.due)} | ${esc(PRIORITY[t.priority] \|\| t.priority)} | |' $WB
+# Expected to survive: for a live row the bus priority is fields.priority, which shape is too.
+run ignore-row-priority $W 's/priority: row.priority ?? shape.priority ?? null,/priority: shape.priority ?? null,/' $WB
+git diff --quiet -- $B $W && echo "restored: sources match the candidate index" || echo "NOT RESTORED"
diff --git a/agents/darkwing/work/queue-58/out/mutants.txt b/agents/darkwing/work/queue-58/out/mutants.txt
new file mode 100644
index 000000000..c69956c47
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/mutants.txt
@@ -0,0 +1,60 @@
+tasks-verb-unchanged: rc=1 ℹ pass 0 ℹ fail 3
+ ✖ an empty business and one with snapshots but no task.created read cleanly
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+no-priority: rc=1 ℹ pass 1 ℹ fail 2
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+zero-priority-null: rc=1 ℹ pass 1 ℹ fail 2
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+tombstone-priority-zero: rc=1 ℹ pass 0 ℹ fail 3
+ ✖ an empty business and one with snapshots but no task.created read cleanly
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+requirement-by-ref: rc=1 ℹ pass 1 ℹ fail 2
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+requirement-last-wins: rc=1 ℹ pass 1 ℹ fail 2
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+requirement-any-business: rc=1 ℹ pass 2 ℹ fail 1
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+requirement-undefined: rc=1 ℹ pass 0 ℹ fail 3
+ ✖ an empty business and one with snapshots but no task.created read cleanly
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+ ✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values
+task-view-adds-keys: rc=1 ℹ pass 2 ℹ fail 1
+ ✖ failing tests:
+ ✖ tasks rows carry priority and requirement for every session that reads tasks
+endpoint-drops-keys: rc=1 ℹ pass 7 ℹ fail 1
+ ✖ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape
+ ✖ failing tests:
+list-requirement-from-trail: rc=1 ℹ pass 0 ℹ fail 1
+ ✖ failing tests:
+ ✖ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes
+no-requirement-column: rc=1 ℹ pass 0 ℹ fail 1
+ ✖ failing tests:
+ ✖ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes
+priority-default-zero: rc=1 ℹ pass 0 ℹ fail 1
+ ✖ failing tests:
+ ✖ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes
+tombstone-no-fallback: rc=1 ℹ pass 0 ℹ fail 1
+ ✖ failing tests:
+ ✖ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes
+detail-priority-raw: rc=1 ℹ pass 0 ℹ fail 1
+ ✖ failing tests:
+ ✖ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes
+list-priority-raw: rc=1 ℹ pass 0 ℹ fail 1
+ ✖ failing tests:
+ ✖ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes
+ignore-row-priority: rc=0 ℹ pass 1 ℹ fail 0
+restored: sources match the candidate index
diff --git a/agents/darkwing/work/queue-58/out/task-read-on-base.log b/agents/darkwing/work/queue-58/out/task-read-on-base.log
new file mode 100644
index 000000000..ecd88f516
--- /dev/null
+++ b/agents/darkwing/work/queue-58/out/task-read-on-base.log
@@ -0,0 +1,171 @@
+✖ tasks rows carry priority and requirement for every session that reads tasks (457.263216ms)
+✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (404.191686ms)
+✖ an empty business and one with snapshots but no task.created read cleanly (88.520491ms)
+ℹ tests 3
+ℹ suites 0
+ℹ pass 0
+ℹ fail 3
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 1017.801953
+
+✖ failing tests:
+
+test at packages/bus/tests/task-read.test.mjs:98:1
+✖ tasks rows carry priority and requirement for every session that reads tasks (457.263216ms)
+ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ + actual - expected
+
+ [
+ [
+ 'vikunja:3/10',
+ + undefined,
+ + undefined
+ - null,
+ - 'REQ-SLICE-3'
+ ],
+ [
+ 'vikunja:3/11',
+ + undefined,
+ + undefined
+ - null,
+ - 'REQ-MOVE-1'
+ ],
+ [
+ 'vikunja:3/7',
+ + undefined,
+ + undefined
+ - 3,
+ - 'REQ-SLICE-1'
+ ],
+ [
+ 'vikunja:3/8',
+ + undefined,
+ + undefined
+ - 5,
+ - 'REQ-SLICE-2'
+ ],
+ [
+ 'vikunja:3/9',
+ + undefined,
+ + undefined
+ - 0,
+ - null
+ ],
+ [
+ 'vikunja:4/11',
+ + undefined,
+ + undefined
+ - 2,
+ - 'REQ-MOVE-1'
+ ]
+ ]
+
+ at TestContext. (file:///home/jwoltje/darkwing-scratch/r58/wt/packages/bus/tests/task-read.test.mjs:101:57)
+ at Test.runInAsyncScope (node:async_hooks:226:14)
+ at Test.run (node:internal/test_runner/test:1402:25)
+ at Test.start (node:internal/test_runner/test:1262:17)
+ at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
+ generatedMessage: true,
+ code: 'ERR_ASSERTION',
+ actual: [ [ 'vikunja:3/10', undefined, undefined ], [ 'vikunja:3/11', undefined, undefined ], [ 'vikunja:3/7', undefined, undefined ], [ 'vikunja:3/8', undefined, undefined ], [ 'vikunja:3/9', undefined, undefined ], [ 'vikunja:4/11', undefined, undefined ] ],
+ expected: [ [ 'vikunja:3/10', null, 'REQ-SLICE-3' ], [ 'vikunja:3/11', null, 'REQ-MOVE-1' ], [ 'vikunja:3/7', 3, 'REQ-SLICE-1' ], [ 'vikunja:3/8', 5, 'REQ-SLICE-2' ], [ 'vikunja:3/9', 0, null ], [ 'vikunja:4/11', 2, 'REQ-MOVE-1' ] ],
+ operator: 'deepStrictEqual',
+ diff: 'simple'
+ }
+
+test at packages/bus/tests/task-read.test.mjs:117:1
+✖ the read model rebuilds from the ledger: a reopened bus and each trail give the same values (404.191686ms)
+ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ + actual - expected
+
+ [
+ [
+ 'vikunja:3/10',
+ + undefined,
+ + undefined
+ - null,
+ - 'REQ-SLICE-3'
+ ],
+ [
+ 'vikunja:3/11',
+ + undefined,
+ + undefined
+ - null,
+ - 'REQ-MOVE-1'
+ ],
+ [
+ 'vikunja:3/7',
+ + undefined,
+ + undefined
+ - 3,
+ - 'REQ-SLICE-1'
+ ],
+ [
+ 'vikunja:3/8',
+ + undefined,
+ + undefined
+ - 5,
+ - 'REQ-SLICE-2'
+ ],
+ [
+ 'vikunja:3/9',
+ + undefined,
+ + undefined
+ - 0,
+ - null
+ ],
+ [
+ 'vikunja:4/11',
+ + undefined,
+ + undefined
+ - 2,
+ - 'REQ-MOVE-1'
+ ]
+ ]
+
+ at TestContext. (file:///home/jwoltje/darkwing-scratch/r58/wt/packages/bus/tests/task-read.test.mjs:125:10)
+ at Test.runInAsyncScope (node:async_hooks:226:14)
+ at Test.run (node:internal/test_runner/test:1402:25)
+ at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
+ at Test.postRun (node:internal/test_runner/test:1542:19)
+ at Test.run (node:internal/test_runner/test:1467:12)
+ at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
+ generatedMessage: true,
+ code: 'ERR_ASSERTION',
+ actual: [ [ 'vikunja:3/10', undefined, undefined ], [ 'vikunja:3/11', undefined, undefined ], [ 'vikunja:3/7', undefined, undefined ], [ 'vikunja:3/8', undefined, undefined ], [ 'vikunja:3/9', undefined, undefined ], [ 'vikunja:4/11', undefined, undefined ] ],
+ expected: [ [ 'vikunja:3/10', null, 'REQ-SLICE-3' ], [ 'vikunja:3/11', null, 'REQ-MOVE-1' ], [ 'vikunja:3/7', 3, 'REQ-SLICE-1' ], [ 'vikunja:3/8', 5, 'REQ-SLICE-2' ], [ 'vikunja:3/9', 0, null ], [ 'vikunja:4/11', 2, 'REQ-MOVE-1' ] ],
+ operator: 'deepStrictEqual',
+ diff: 'simple'
+ }
+
+test at packages/bus/tests/task-read.test.mjs:146:1
+✖ an empty business and one with snapshots but no task.created read cleanly (88.520491ms)
+ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ + actual - expected
+
+ [
+ [
+ 'vikunja:5/1',
+ + undefined,
+ + undefined
+ - null,
+ - null
+ ]
+ ]
+
+ at TestContext. (file:///home/jwoltje/darkwing-scratch/r58/wt/packages/bus/tests/task-read.test.mjs:150:10)
+ at Test.runInAsyncScope (node:async_hooks:226:14)
+ at Test.run (node:internal/test_runner/test:1402:25)
+ at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
+ at Test.postRun (node:internal/test_runner/test:1542:19)
+ at Test.run (node:internal/test_runner/test:1467:12)
+ at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
+ generatedMessage: true,
+ code: 'ERR_ASSERTION',
+ actual: [ [ 'vikunja:5/1', undefined, undefined ] ],
+ expected: [ [ 'vikunja:5/1', null, null ] ],
+ operator: 'deepStrictEqual',
+ diff: 'simple'
+ }