Brand board revisions, the five dashboard mockups with their verify
script and evidence, the slice 1 S5 design note (SLICE1-VIEWS.md, rev 3
plus the S2c section 10 update), the Console mockup of inbox, tasks,
agents and trail (mockups/slice1/) and checks/slice1-verify.mjs (16
checks; check 16 pinned to bus at d27042fa). Also the CHAT-03 progress
log and the 2026-09-26 dirty-tree manifest. Design and evidence only;
no package code. Row 40 (#1522), committed on its own at Sage's
direction, 2026-10-09.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
27 KiB
Slice 1 S5: WebUI views for inbox, tasks, agents and trails (design note)
Dewey, 2026-10-04. Row 40, #1522. Design only: no code, no commit. S5 follows S4, so nothing here starts the build.
Revision 2, same day: lead decision 56 (c9c1699a) answered Q1 to Q5 and
accepted all five recommendations. Sections 1, 3.2, 3.5, 5 and 7 now say
what was decided. Section 8 is a new finding for the Q1 module. The
mockup is in mockups/slice1/, checked by checks/slice1-verify.mjs.
Its fixtures follow schema v3a, and the verify script loads every row
into that schema.
Revision 3, same day. Three things changed:
- Schema v3b (179ffe35, lead decision 60) landed. It adds the
task_currentview and buildstasks_openon it (lead decision 59). The fixture check now loads v3b and reads current state from those two views. It agrees with the page on every task, #42 included. Section 8 records the ruling. - The mockup is reshaped to the Q1 module (Sage's Q1 shapes).
data.jsholds raw v3b rows written the way the candidate broker writes them.mockups/slice1/q1.jsports the four reader verbs, andreads(S)inapp.jsbuilds the views only from what those verbs return. A new check runs Rocko's own candidate reader over the same rows and compares. Section 1 and section 7 describe this. - Inbox rows now carry
authorization {action, target, approvalChoice}from thedecision.raiseevent, and the views show it (section 3.1). What the page needs and no Q1 verb returns is labelled on the page and listed for Rocko in section 9.
Sources, pinned by sha256 prefix:
- brief
docs/plans/2026-10-04_slice-1.md(655e1290, commit43c48d7a), S5 section and the shared rules; - data model note (948b94ce), addendum A (0b36acb0), addendum B (14e747f5). B wins over A, and A wins over the note;
- schema v3
agents/darkwing/work/slice1-proto/schema-v3.sql(084d0a07, commit7ed83178, lead decision 55), replaced by schema v3aschema-v3a.sql(d55e41fd), which S2 builds on, then by schema v3bschema-v3b.sql(179ffe35, lead decision 60). The mockup follows v3b; packages/busas committed ind27042fa(S2c, row 44; S2 was38828a2c, S2b4afab552): broker.mjs 51e03b33, store.mjs e69cb395, views.mjs 446b6da4;- PRD draft 0.4
docs/prd/mosaic-stack.md(d907705a): REQ-WEB-1, REQ-DEC-1 to 4, REQ-CLI-1, REQ-EVT-1; round 3, 7B; - DEFERRED.md, "CHAT-03 I1 round 2 follow-ups, for the I3 brief".
Not read on purpose: today's queue. The views follow the data model.
1. What the WebUI reads, and through what
REQ-WEB-1 says the WebUI shows the same data as the CLI. The S4 brief says
every CLI command reads through the broker and never opens bus.sqlite.
The WebUI follows the same rule:
- The browser never talks to the broker. The WebUI server (
packages/webui) calls the broker's read verbs over the local socket and serves JSON to the page, the way it proxies the board today. - The WebUI never opens
bus.sqlite, the business file or a token file. Bucket titles, bot names and launch limits come from the broker, which already loaded them. - The WebUI adds no write route for slice 1 data (section 6).
One set of read functions serves both renderers, so the two can't drift.
Lead decision 56, Q1: the module lives in packages/bus, Rocko owns it,
and both the CLI and the WebUI import it. The CLI formats text and the
WebUI server serves JSON. The module, packages/bus/src/views.mjs, is a
thin client: inbox, tasks, agents and trail(subject), each
one broker call. The mockup's mockups/slice1/q1.js stands in for those
four calls over fixture rows, and the verify script checks it returns
what the broker at commit d27042fa returns. reads(S) in mockups/slice1/app.js
builds the view objects from the stand-in alone. A second object,
missing(S), holds what no verb returns. Each entry names its source,
and every renderer that shows one labels it "not in the Q1 module".
When Rocko pins an example fixture, it replaces data.js.
2. Routes
Console keeps its shell, its palettes and its keyboard rules. The left column gains a section list above the project tree. Hash routes work with the back button, as the mockups did.
| Route | View | Main source |
|---|---|---|
#/ |
Control board (unchanged) | the board scan |
#/inbox |
Decisions routed to the human, open first | decisions, decision_events |
#/inbox/<id> |
One decision, its options and its history | same, plus supersedes chain |
#/tasks |
Tasks by state | latest task_snapshots per task, task.* events |
#/tasks/<ref> |
One task and its trail | same, plus the trail (section 3.4) |
#/agents |
Role instances, holders and live sessions | business file via broker, role_claims, session.* events |
#/trail/task/<ref>, #/trail/decision/<id> |
The ordered trail | events, decisions, messages, deliveries |
#/conversation/... |
CHAT-02 history and CHAT-03 drive (existing) | the board and packages/conversation |
A task ref in a URL is vikunja:<project>/<task>, encoded. The #12 a
person sees in Vikunja is display only (addendum A section 1).
3. The views
Every view keeps the Console states: loading, empty, error with the last good data kept and a warning, and refusal with the reason. All agent-authored text (questions, option text, recommendations, message bodies, task titles and descriptions) is shown as text under the CHAT-02 rules: Markdown stays as source, and controls and bidi marks show as visible symbols.
3.1 Inbox
- Rows: decisions with
route_to = 'human'and no closing row (resolved,withdrawn,expired). Blocking first (theurgent_inboxview), then oldest first. The note's rule is "gated first, then oldest", and every human-routed decision is gated, soblockingis the useful split. - Each row: question, class and action, raised by role and run, age,
task_refandrequirement_refas links, a "blocking" badge, and whether a DM went out (adeliveriesrow with transportdiscord-dm). - Detail: the options as a numbered list of key and text, the
recommendation marked, the
seenhistory, and earlier versions throughsupersedes, each shownwithdrawn. - Approval context, from the row's
authorization: the card and the detail say "Choosing “” approves on .", and the approving option carries an "approves " badge. The approving choice need not be the recommendation: in the fixture, the conflict on #48 recommendsqueue, andvikunjaapprovestask.close. A decision without a recorded raise says no option is marked as approving. - Resolution stays in the CLI (REQ-DEC-3). The detail shows the exact
command,
mosaic decide <id> <key>, one per option, with a Copy button. Copy puts text on the clipboard and does nothing else. There is no Resolve button, disabled or otherwise. - A second tab, "Routed to roles", lists open cross-role decisions by
arbiter (
pm,cto). It's read-only too. Closed decisions get a "Recent" list: choice, by, via and time open.
3.2 Tasks
- Rows: the latest snapshot of each task in the business, grouped by the
five bucket titles (
todo,in-progress,in-review,blocked,done).doneis collapsed and shows the last 20. - Columns:
#indexand title, assignee (bot name), requirement id (fromtask.created, not the description line), due date, priority, percent done, queue row if the task has one, and the time of the last change. - Markers, each linked to its event:
- "changed in Vikunja" for a task in
task_external_changes. A task the broker never wrote is always in that view (addendum B section 5), so the marker reads "not created by the stack" when noselfsnapshot exists; - "conflict" for an unresolved
task.conflict, linked to the decision raised to the PM; - "missing" with the reason (
moved,not-found,no-access) for a tombstone, and the new project id formoved.
- "changed in Vikunja" for a task in
- Freshness: a line under the heading, "Vikunja last read
- Which snapshot to show: not always the latest one. Section 8.
- "Open in Vikunja" links to the task on
tracker.baseUrl. It leaves Console and changes nothing.
3.3 Agents
- One row per role instance in the business file (
pm,cto,coder,reviewerin slice 1): definition, harness, model. - Holder: the latest
role_claimsrow for the instance. Aclaimshows the holder run, harness, address and time held.releaseandrevokeshow "not held" and who released or revoked it; a revoke links its decision. - A claim from a T3 seat carries "advisory" (decision 46, note 6.3).
- Live sessions:
session.launchedrows with no matchingsession.ended, with model family andlaunched_by. Above the table, live counts against the business file's limits, for example "Opus 2 of 4, Sonnet 1 of 4", and the launch state from thelaunch_stateview ("launching allowed" or "launching revoked at - Credentials: per instance and service, the latest of
credential.expiring,credential.expiredandcredential.changed. Service names and dates only. There is never a value or a path. - Today's board seats stay on
#/. The Agents view is about roles. A seat that holds a role links to its board row.
3.4 Trail
The S4 brief lists the order: request, decisions, launches, task changes, review, close. The trail view is a single time-ordered list with one row per event and a filter by kind. Each row says what happened, which role and run did it, and links to the object.
For a task, the rows I can join from schema v3:
- events with
subject= the task ref (task.created,.assigned,.state,.closed,.changed.external,.conflict,.missing); - decisions with
task_ref= the task ref, and theirdecision_events; - messages that cite one of those decisions, with their deliveries;
review.requestedandreview.verdict, if their subject is the task ref or its queue row.
What I couldn't join from the schema alone: which launch worked on the
task, and which human.input was the original request. Q3 was decided:
task.created names the request in body.request, and the broker's
trail follows it to the human.input and its message. Launches come in
through the runs seen on the trail's rows. For a decision, the trail is the decision, its events, the
messages that cite it and the events whose body cites it.
3.5 Conversations and CHAT-03
The brief puts two I3 follow-ups into S5, because the live PM session needs them:
- the seal covers the engine command: the I3 entry point takes no engine
command or
preArgsfrom config, so only the pinned Pi starts; - an explicit engine environment: the engine starts with an allow-listed
environment, not
process.env. That matches addendum A section 4, which starts every agent with an allow-listed environment.
Lead decision 56, Q5, adds two more of the DEFERRED items to S5: clear
escalating after a throw, and handle takeover plus Enter arriving in one
chunk. Both sit in files S5 already changes. The test that members are
listed only under freeze stays deferred.
Which session Console drives depends on S6. The PM moves off T3 there, and
the stack owns its session. Console shows the PM's conversation from
whatever S6 records, through packages/conversation.
4. Gate E
Round 3, 7B: Gate E is shown during this step, not separately. The brief: an interactive demonstration with all seats, then Jason's workday ruling. Live cutover still needs its own approval.
A draft script, to be fixed once S4 and S6 exist:
- Jason opens Console. The board shows the seats, and Agents shows the four role instances with their holders.
- He sends the PM a one-sentence request from the CLI, then watches the task appear in Tasks with its requirement id.
- A gated decision reaches the inbox and his DM. He reads it in Console,
copies the
mosaic decidecommand and runs it in a terminal. The inbox row closes on the next refresh. - He follows the task's trail to review and close.
- He drives a conversation through CHAT-03 and takes control, then hands it back.
- He uses Console for the rest of his workday and rules on it.
Evidence: browser runs against a fixture broker with recorded data, and the live run's screen captures. Model calls during the demonstration are Jason's to start. I don't run them for discovery.
5. Questions, now decided
Lead decision 56 (c9c1699a) accepted all five recommendations below. Each
answer is quoted after the question; the original text stays for the
record.
Q1. One read layer for CLI and WebUI. Recommendation: S4 exposes the
inbox, tasks, agents and trail reads as functions over the broker client,
in a module packages/webui can import. The CLI formats text and the
WebUI serves JSON. For Rocko, through Sage.
Decided: one read module in packages/bus, owned by Rocko, imported by
the CLI and the WebUI.
Q2. Freshness of task data. A quiet poll writes no snapshot (addendum B section 5), so the database can't say when Vikunja was last read, or whether the poller is failing. Recommendation: the broker answers a status read with the last successful board read and cursor read per project, and the last poll error. S3 owns the poller and S2 the read verb. For Darkwing and Rocko. Decided: the broker status read goes in S3, Darkwing's.
Q3. Trail join keys. session.launched and human.input carry no
task ref in the schema. Recommendation: put the task ref in subject for
any event about one task, including session.launched when the launch is
for a task, and human.input when it creates one. That is a body and
subject rule, not a new kind, so the closed kind list holds. For Rocko and
Darkwing.
Decided: the task ref goes in subject for any event about one task.
task.create names the human.input that asked for it. Darkwing picks
whether a trigger or the broker enforces it. Schema v3a does it with
triggers: events_task_subject for every task.* event, and
events_task_created_body, which requires request (the human.input
id) and requirement in the task.created body. For session.launched
and the other kinds, the broker sets subject.
My original Q3 text was wrong on one point. A human.input can't carry
the task ref, because the task doesn't exist when the request is
recorded. The join runs the other way: the task trail takes the
human.input named in task.created.request. The mockup does that, and
a check confirms that #41's trail shows its own request and not another
task's.
Q4. seen from the browser. decision_events allows op = 'seen',
and the note lists webui under via. Recommendation: the WebUI writes
nothing in slice 1, not even seen. Every broker write is stamped from a
launch record or comes from the CLI outside a run, and an unauthenticated
loopback page is neither. seen from the WebUI comes with Pocket ID. For
Sage.
Decided: the WebUI writes nothing in slice 1. It shows the mosaic decide command to copy.
Q5. I3 follow-ups beyond the brief's two. Recommendation: fix
escalating and the takeover-plus-Enter case in S5, since both sit in
files S5 already owns. Keep the freeze test as a follow-up. For Sage.
Decided: as recommended (section 3.5).
6. Boundaries
- No write route for decisions, tasks, claims, messages or events. Copy is clipboard only.
- The page and server stay loopback-only with the existing checks (Host, Origin, body limits, no CORS). The new routes are GET only.
- No token value, token path or business-file path reaches the page. Credential rows show the service and dates.
- Agent-authored text is inert.
- No model calls. Tests use a fixture broker.
7. Before S4 lands
- Done: a Console mockup of the four views,
mockups/slice1/.checks/slice1-verify.mjschecks it at eight widths, ten palettes in three modes, and covers inert text, read-only behaviour, the inbox, tasks, agents and trail rules, the loading/empty/failed/refused states, the keyboard and long text. - Done: fixtures as raw schema v3b rows (
data.js), written in the broker's order and with its millisecond timestamps. The Store refuses anyatorread_atthat isn't the 24-charactertoISOString()form; the schema doesn't check that, so the fixture had to change. - Check 15 inserts every fixture row (104) into schema v3b (179ffe35) in
time order, so its CHECKs and triggers judge the fixture. It compares
urgent_inbox,task_external_changes,task_currentandtasks_openwith the page, and requires the "stale read" marks to be exactly the snapshot rowstask_currentskips (#42 only). - Check 16 extracts
packages/busfrom commitd27042fa(S2c, row 44; S2 was38828a2c, S2b4afab552) withgit archive, so working-tree edits don't count. It opens that Store and Broker on a temporary data root, inserts the same rows, binds a reader and requestsinbox,tasks,agentsand 21 trails (every task, decision and message). The results must equalq1.json the same rows: 203 rows, equal, with broker.mjs 51e03b33, store.mjs e69cb395 and schema.sql 179ffe35. It checks the reader only: rows go in by SQL, not through the broker's writers, so it can't tell whether the broker would write them (section 10). It writes nothing outside its temporary directory, which it removes. Run from a copy outside the repository, it needsDEWEY_S1_REPO, or it records "not verified". - A mutation run in a scratch copy breaks one rule at a time, and each
check has to fail on the mutant it guards against. Round 3, 21
mutants, 20 killed on the first pass. The survivor reversed the
decision trail's order; check 8 only checked the task trail's order,
and now checks both. M5 (stale mark on the current row) failed check
15, not check 6 as I had expected; check 15 now owns that rule. After
the fix, 21 of 21 (
~/dewey-scratch/s1mut2/mutants.txt, scratch only). Report:evidence/slice1/report.json. - Next: switch
data.jsto Rocko's pinned example fixture when it arrives. Section 9's gaps decide what S5-B1 needs beyond the four verbs.
8. Finding: a stale poll read becomes the latest snapshot
Ruled: lead decision 59 (e153c3a3) takes the read-side fix and puts it
in the schema. The poller keeps writing the stale row as evidence. A v3b
amendment (Darkwing) adds a view of each task's current snapshot that
skips the stale poll row, and rebuilds tasks_open on that view. The
Q1 module, the CLI and the WebUI read that view and don't reimplement the
rule. When v3b lands, the fixture check moves to it and expects
tasks_open to match the page on every row, #42 included. The Tasks view
keeps the "stale read, not shown" marker in the snapshot list.
Landed: v3b (179ffe35, lead decision 60). Check 15 loads it and agrees
with the page on every task, #42 included. The page reads the current row
from the stand-in tasks verb, which reads task_current, and marks as
stale the snapshot rows after it. The original finding follows.
For Rocko (the Q1 module) and Darkwing (S3 poller), through Sage.
Addendum B section 5 writes a poll snapshot whenever its digest differs
from the task's latest snapshot, and ignores a stale read when it detects
external changes: task_external_changes requires
p.read_at > ls.at, so a board read that started before the broker's
own write doesn't count as a change in Vikunja. The next tick settles it.
The stale read is still stored, though, and it is now the latest
snapshot. Anything that shows "the latest snapshot per task", including
tasks_open and the Tasks view, shows the old bucket until the next tick.
Example in the fixtures, task #42:
- 14:59:40,
selfsnapshot: the broker moved #42 toin-progress.updatedstays 09:50, because a move between open columns doesn't change it (probe P3). - 14:59:41,
pollsnapshot,read_at14:59:39: the board read started before the move and still seestodo. Its digest differs, so it is written. - Latest snapshot:
todo. Not external, correctly. Shown astodo, wrongly.
The verify script confirms this against the real schema. After the
fixture is loaded into schema v3a, tasks_open gives todo for #42,
while the page shows in-progress. That is the only row where they
differ.
What the mockup does: when choosing the row to display, skip a poll
snapshot whose read_at is at or before the latest self snapshot's
at. The task's snapshot list still shows that row, marked "stale read,
not shown". The verify script checks #42 sits under in-progress and the
marker.
Recommendation: the Q1 module applies the same rule, and tasks_open gets
it too, so the CLI and the WebUI agree. The other fix is in the poller:
don't write a poll snapshot whose read_at is at or before the latest
self snapshot. That loses no information the external-change rule uses,
but it drops a row from the evidence, so I'd leave the choice to Darkwing.
9. What the views need that no Q1 verb returns
Ruled: lead decision 63 (Sage, 2026-10-05) sends this list to Rocko for S4. Session events go to S6 and credential events to S3. Whatever S4 doesn't add keeps its "not in the Q1 module" label in S5.
For Rocko, through Sage. The reader (d27042fa) has four verbs: inbox,
tasks, agents and trail(subject). The mockup builds everything it
can from those. The rest comes from missing(S) in q1.js, and the page
labels each one "not in the Q1 module". Suggestions are the smallest
change I can see; the choice is Rocko's.
Inbox and decisions:
- Decisions routed to roles, and closed decisions (the "Routed to roles"
tab and "Recent").
inboxreturns only open, human-routed decisions. Suggestion: an argument toinbox, or adecisionsverb. - Seen and DM state per inbox row. The page gets them with one trail read
per decision. Suggestion:
seen_atanddm_aton the inbox row. - The forward link from a withdrawn decision to the one that replaced it.
The trail's decision query matches
idandtask_refonly. Suggestion: addOR supersedes = ?. - Approval context for a decision that isn't in the inbox. The page reads
it from the
decision.raiseevent in the trail, which works, but it is the same derivation#decisionViewalready does.
Tasks:
- Requirement, conflict, external-change fields, review and the missing
reason all need one trail read per task. Suggestion: an
externalflag fromtask_external_changesand the requirement on eachtasksrow. - Bucket titles, bot names and the tracker
baseUrl. They are in the business file and the kanban view, which the broker has loaded. - Freshness ("Vikunja last read"): the S3 status read, not built yet.
Agents:
- Role instances, limits and model families, from the business file.
- Launch state: the
launch_stateview exists in the schema; no verb returns it. - Live and ended sessions. Nothing writes
session.endedyet, andsession.launchedcarries no model and nolaunched_by. - Credential notices. Nothing writes credential events yet.
agentsreturns only roles whose latest row is a claim, so the view can't say who released or revoked a role. Suggestion: return the latest row per role and let the reader filter.
10. Finding: S2b stopped an agent message citing its decision; S2c restores it
Ruled: lead decision 65 (dd35a0ab) took none of the options below as
written. For a within-role message.send, decision is a citation: the
broker checks the decision exists and stores it in messages.decision,
but doesn't class-match it, doesn't consume it, and leaves it out of the
action.allowed event. For a send that isn't within-role, it stays the
authority. messages.decision keeps its "about this decision" meaning,
and messages 101 and 102 stay valid. S4 still sends and records the
Discord DM. The fix is row 44, S2c (#1526, Rocko, Darkwing reviewing).
Landed: S2c is commit d27042fa (row 44 done at queue rev 157), and it
restores messages 101 and 102: the broker at d27042fa writes both. The
probe below, rerun on d27042fa, shows a within-role citing send is
written, the decision is stored and not consumed (a second citing
message is written too), an unknown decision refuses with
decision-not-found, and the send's action.allowed event is {action
message.send, class within-role, target human} with no decision.
No action.allowed row outside decision.raise names the decision. The
fixture now carries that event 1 ms before each of 101 and 102, and
check 16 is pinned to d27042fa. Check 16 still inserts by SQL, so a
broker-written fixture with a citing DECISION message remains the
regression test it can't give. The original finding follows.
For Rocko and Sage. Found 2026-10-05 while repinning check 16 to S2b.
message.send takes decision?, and the broker stores it in
messages.decision. Up to S2 (38828a2c), a role with message.send in
its within-role authority returned before the decision was looked at, so
decision worked as a citation: the PM could send the human a DECISION
message about an open gated decision. S2b (4afab552) passes every agent
send through #consumeAuthority, which now checks a supplied decision
even for a within-role action. The decision's class and action must
match message.send, so a citation of a role.launch decision refuses
with decision-mismatch.
Probe (~/dewey-scratch/s2b-probe.*/probe.mjs, both commits extracted
with git archive, PM with message.send within role, an open blocking
role.launch decision routed to the human):
38828a2c |
4afab552 |
d27042fa (S2c) |
|
|---|---|---|---|
PM DECISION message with decision: <id> |
written | refused, decision-mismatch |
written; decision stored, not consumed |
the same message without decision |
written | written, plus action.allowed {action message.send, class within-role, target human} with subject human |
written, plus the same action.allowed |
| citing an unknown decision | refused, decision-not-found |
refused, decision-not-found |
refused, decision-not-found |
What depends on the citation:
- the trail's decision query collects
messages WHERE decision = ?, and deliveries through those messages; - the inbox's "DM sent" state, which comes from a
discord-dmdelivery of a message citing the decision (section 3.1). The fixture's messages 101 and 102 are those DMs, and the broker would now refuse both.
After S2b, a non-null messages.decision can only name a decision that
approved message.send itself, and that decision is used up by the
send. The column now means "the decision that authorized this message".
The data model and the views read it as "the decision this message is
about".
Options, for Rocko and Sage:
- A separate citation argument (for example
about), stored apart from the authorizing decision. That needs a schema column or an event. - The Discord connector sends the DM from the decision itself, not from an agent message, and records its own delivery. The views then read "DM sent" from that record.
- Keep S2b's rule, and drop "DM sent" and decision messages from the views.
My recommendation is 2. REQ-DEC-4 and the slice 1 brief's S4 section
already give the outbound DM for a blocking gated decision to S4,
"through the existing connector (#1509)", so the PM doesn't need to send
one. It also leaves message.send's decision meaning one thing. S4
then has to record the DM somewhere the views can read it; Darkwing's
prototype has a digest.sent event with a transport for the digest
side. Until a ruling, the fixture keeps messages
101 and 102 as they are, and this section marks them as rows the broker
at 4afab552 would refuse. (Ruled above: they stay, and S2c restores
them.)